From 333043f0befe585e66252d205793ed0255cbecdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Negr=C3=B3n?= Date: Sun, 8 Feb 2026 18:23:45 -0400 Subject: [PATCH 1/4] feat: update pre-commit config for python projects --- package.json | 1 + pnpm-lock.yaml | 16 +- src/lib/agent-runner.ts | 22 ++ .../update-pre-commit-config.test.ts | 236 ++++++++++++++++++ src/steps/index.ts | 1 + src/steps/update-pre-commit-config.ts | 160 ++++++++++++ 6 files changed, 430 insertions(+), 6 deletions(-) create mode 100644 src/steps/__tests__/update-pre-commit-config.test.ts create mode 100644 src/steps/update-pre-commit-config.ts diff --git a/package.json b/package.json index 58a5b3a9..468c16e9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "uuid": "^11.1.0", "xcode": "3.0.1", "xml-js": "^1.6.11", + "yaml": "^2.8.2", "yargs": "^16.2.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a8c9e5..f785fed6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: xml-js: specifier: ^1.6.11 version: 1.6.11 + yaml: + specifier: ^2.8.2 + version: 2.8.2 yargs: specifier: ^16.2.0 version: 16.2.0 @@ -1651,11 +1654,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -2813,9 +2817,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@20.2.9: @@ -5052,7 +5056,7 @@ snapshots: micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.7.1 + yaml: 2.8.2 transitivePeerDependencies: - supports-color @@ -5732,7 +5736,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.7.1: {} + yaml@2.8.2: {} yargs-parser@20.2.9: {} diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index c0fdf439..fb959c79 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -31,7 +31,9 @@ import * as semver from 'semver'; import { addMCPServerToClientsStep, uploadEnvironmentVariablesStep, + updatePreCommitConfigStep, } from '../steps'; +import { Integration } from './constants'; import { checkAnthropicStatusWithPrompt } from '../utils/anthropic-status'; import { enableDebugLogs } from '../utils/debug'; @@ -304,6 +306,23 @@ Please report this error to: ${chalk.cyan('wizard@posthog.com')}`; ci: options.ci, }); + // Update pre-commit config for Python frameworks + const pythonIntegrations = [ + Integration.django, + Integration.flask, + Integration.fastapi, + Integration.python, + ]; + + let preCommitUpdated = false; + if (pythonIntegrations.includes(config.metadata.integration)) { + const preCommitResult = await updatePreCommitConfigStep({ + installDir: options.installDir, + integration: config.metadata.integration, + }); + preCommitUpdated = preCommitResult.updated; + } + // Build outro message const continueUrl = options.signup ? `${getCloudUrlFromRegion(cloudRegion)}/products?source=wizard` @@ -317,6 +336,9 @@ Please report this error to: ${chalk.cyan('wizard@posthog.com')}`; uploadedEnvVars.length > 0 ? `Uploaded environment variables to your hosting provider` : '', + preCommitUpdated + ? `Updated .pre-commit-config.yaml with posthog dependency` + : '', ].filter(Boolean); const nextSteps = [ diff --git a/src/steps/__tests__/update-pre-commit-config.test.ts b/src/steps/__tests__/update-pre-commit-config.test.ts new file mode 100644 index 00000000..b29c9f9b --- /dev/null +++ b/src/steps/__tests__/update-pre-commit-config.test.ts @@ -0,0 +1,236 @@ +import * as fs from 'fs'; +import path from 'path'; +import { updatePreCommitConfigStep } from '../update-pre-commit-config'; +import { Integration } from '../../lib/constants'; + +jest.mock('fs', () => ({ + existsSync: jest.fn(), + promises: { + readFile: jest.fn(), + writeFile: jest.fn(), + }, +})); + +jest.mock('../../utils/analytics', () => ({ + analytics: { + capture: jest.fn(), + setTag: jest.fn(), + }, +})); + +jest.mock('../../utils/clack', () => ({ + log: { + warn: jest.fn(), + success: jest.fn(), + }, +})); + +describe('updatePreCommitConfigStep', () => { + const mockOptions = { + installDir: '/test/project', + integration: Integration.django, + }; + + const existsSyncMock = fs.existsSync as jest.Mock; + const readFileMock = fs.promises.readFile as jest.Mock; + const writeFileMock = fs.promises.writeFile as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('config file discovery', () => { + it('returns updated: false when no config file exists', async () => { + existsSyncMock.mockReturnValue(false); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + }); + + it.each([ + ['.pre-commit-config.yaml', '.pre-commit-config.yaml'], + ['.pre-commit-config.yml', '.pre-commit-config.yml'], + ])('reads %s when it exists', async (existingFile, expectedFile) => { + existsSyncMock.mockImplementation((p: string) => + p.endsWith(existingFile), + ); + readFileMock.mockResolvedValue('repos: []'); + + await updatePreCommitConfigStep(mockOptions); + + expect(readFileMock).toHaveBeenCalledWith( + path.join('/test/project', expectedFile), + 'utf8', + ); + }); + + it('prefers .yaml over .yml when both exist', async () => { + existsSyncMock.mockReturnValue(true); // Both exist + readFileMock.mockResolvedValue('repos: []'); + + await updatePreCommitConfigStep(mockOptions); + + expect(readFileMock).toHaveBeenCalledWith( + path.join('/test/project', '.pre-commit-config.yaml'), + 'utf8', + ); + }); + }); + + describe('adding posthog dependency', () => { + beforeEach(() => { + existsSyncMock.mockReturnValue(true); + writeFileMock.mockResolvedValue(undefined); + }); + + it.each(['mypy', 'pyright', 'pytype'])( + 'adds posthog to %s hook', + async (hookId) => { + readFileMock.mockResolvedValue(` +repos: + - repo: https://example.com/${hookId} + hooks: + - id: ${hookId} +`); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: true }); + const writtenContent = writeFileMock.mock.calls[0][1] as string; + expect(writtenContent).toContain('posthog'); + }, + ); + + it('adds posthog to all type-checking hooks when multiple exist', async () => { + readFileMock.mockResolvedValue(` +repos: + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy + - repo: https://github.com/microsoft/pyright + hooks: + - id: pyright +`); + + await updatePreCommitConfigStep(mockOptions); + + const writtenContent = writeFileMock.mock.calls[0][1] as string; + expect(writtenContent.match(/posthog/g)).toHaveLength(2); + }); + + it('preserves existing additional_dependencies', async () => { + readFileMock.mockResolvedValue(` +repos: + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy + additional_dependencies: + - django-stubs + - types-requests +`); + + await updatePreCommitConfigStep(mockOptions); + + const writtenContent = writeFileMock.mock.calls[0][1] as string; + expect(writtenContent).toContain('django-stubs'); + expect(writtenContent).toContain('types-requests'); + expect(writtenContent).toContain('posthog'); + }); + }); + + describe('idempotency', () => { + beforeEach(() => { + existsSyncMock.mockReturnValue(true); + }); + + it.each([ + 'posthog', + 'posthog==3.0.0', + 'posthog>=2.0.0', + 'posthog<=3.0.0', + 'posthog~=2.0', + 'posthog!=1.0.0', + 'posthog[sentry]', + ])( + 'does not modify when "%s" already in dependencies', + async (existingDep) => { + readFileMock.mockResolvedValue(` +repos: + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy + additional_dependencies: + - ${existingDep} +`); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + expect(writeFileMock).not.toHaveBeenCalled(); + }, + ); + }); + + describe('configs without type-checking hooks', () => { + beforeEach(() => { + existsSyncMock.mockReturnValue(true); + }); + + it.each([ + ['no repos key', 'default_language_version:\n python: python3'], + ['empty repos', 'repos: []'], + ['repos without hooks', 'repos:\n - repo: local'], + [ + 'only non-type-checking hooks', + `repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + hooks: + - id: trailing-whitespace`, + ], + ])('returns updated: false for %s', async (_desc, content) => { + readFileMock.mockResolvedValue(content); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + expect(writeFileMock).not.toHaveBeenCalled(); + }); + }); + + describe('error handling', () => { + beforeEach(() => { + existsSyncMock.mockReturnValue(true); + }); + + it('returns updated: false on read error', async () => { + readFileMock.mockRejectedValue(new Error('Permission denied')); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + }); + + it('returns updated: false on invalid YAML', async () => { + readFileMock.mockResolvedValue('invalid: yaml: content: ['); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + }); + + it('returns updated: false on write error', async () => { + readFileMock.mockResolvedValue(` +repos: + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy +`); + writeFileMock.mockRejectedValue(new Error('Disk full')); + + const result = await updatePreCommitConfigStep(mockOptions); + + expect(result).toEqual({ updated: false }); + }); + }); +}); diff --git a/src/steps/index.ts b/src/steps/index.ts index 9761ee9d..d20e1211 100644 --- a/src/steps/index.ts +++ b/src/steps/index.ts @@ -3,3 +3,4 @@ export * from './run-prettier'; export * from './add-or-update-environment-variables'; export * from './add-mcp-server-to-clients'; export * from './upload-environment-variables'; +export * from './update-pre-commit-config'; diff --git a/src/steps/update-pre-commit-config.ts b/src/steps/update-pre-commit-config.ts new file mode 100644 index 00000000..a3b71274 --- /dev/null +++ b/src/steps/update-pre-commit-config.ts @@ -0,0 +1,160 @@ +import * as fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import { parse, stringify } from 'yaml'; +import type { Integration } from '../lib/constants'; +import { analytics } from '../utils/analytics'; +import clack from '../utils/clack'; +import { traceStep } from '../telemetry'; + +const TYPE_CHECKING_HOOK_IDS = ['mypy', 'pyright', 'pytype']; +const CONFIG_FILENAMES = ['.pre-commit-config.yaml', '.pre-commit-config.yml']; +// Matches 'posthog' with any version specifier (==, >=, <=, ~=, !=, etc.) or extras [...] +const POSTHOG_DEP_PATTERN = /^posthog([=<>~![]|$)/; + +interface PreCommitHook { + id: string; + additional_dependencies?: string[]; + [key: string]: unknown; +} + +interface PreCommitRepo { + repo: string; + hooks?: PreCommitHook[]; + [key: string]: unknown; +} + +interface PreCommitConfig { + repos?: PreCommitRepo[]; + [key: string]: unknown; +} + +function findConfigFile(installDir: string): string | null { + for (const filename of CONFIG_FILENAMES) { + const candidate = path.join(installDir, filename); + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +function hasTypeCheckingHooks(config: PreCommitConfig): boolean { + if (!config.repos || !Array.isArray(config.repos)) { + return false; + } + return config.repos.some((repo) => + repo.hooks?.some((hook) => TYPE_CHECKING_HOOK_IDS.includes(hook.id)), + ); +} + +export async function updatePreCommitConfigStep({ + installDir, + integration, +}: { + installDir: string; + integration: Integration; +}): Promise<{ updated: boolean }> { + return traceStep('update-pre-commit-config', async () => { + const configPath = findConfigFile(installDir); + + if (!configPath) { + return { updated: false }; + } + + const configFilename = path.basename(configPath); + + let content: string; + try { + content = await fs.promises.readFile(configPath, 'utf8'); + } catch (error) { + clack.log.warn(`Could not read ${chalk.bold.cyan(configFilename)}`); + analytics.capture('wizard interaction', { + action: 'failed to read pre-commit config', + integration, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return { updated: false }; + } + + let config: PreCommitConfig; + try { + config = parse(content) as PreCommitConfig; + } catch (error) { + clack.log.warn(`Could not parse ${chalk.bold.cyan(configFilename)}`); + analytics.capture('wizard interaction', { + action: 'failed to parse pre-commit config', + integration, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return { updated: false }; + } + + if (!config.repos || !Array.isArray(config.repos)) { + return { updated: false }; + } + + let modified = false; + + for (const repo of config.repos) { + if (!repo.hooks || !Array.isArray(repo.hooks)) { + continue; + } + + for (const hook of repo.hooks) { + if (!TYPE_CHECKING_HOOK_IDS.includes(hook.id)) { + continue; + } + + if (!hook.additional_dependencies) { + hook.additional_dependencies = []; + } + + const hasPosthog = hook.additional_dependencies.some((dep) => + POSTHOG_DEP_PATTERN.test(dep), + ); + + if (!hasPosthog) { + hook.additional_dependencies.push('posthog'); + modified = true; + } + } + } + + if (!modified) { + if (hasTypeCheckingHooks(config)) { + clack.log.success( + `${chalk.bold.cyan( + configFilename, + )} already has posthog in type-checking hook dependencies`, + ); + } + return { updated: false }; + } + + try { + await fs.promises.writeFile(configPath, stringify(config), 'utf8'); + } catch (error) { + clack.log.warn(`Could not write ${chalk.bold.cyan(configFilename)}`); + analytics.capture('wizard interaction', { + action: 'failed to write pre-commit config', + integration, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return { updated: false }; + } + + analytics.capture('wizard interaction', { + action: 'updated pre-commit config', + integration, + }); + + clack.log.success( + `Added posthog to additional_dependencies in ${chalk.bold.cyan( + configFilename, + )}`, + ); + + return { updated: true }; + }); +} From 8107afe6814da749d5e534203132ff6b790d6f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Negr=C3=B3n?= Date: Sun, 8 Feb 2026 18:49:51 -0400 Subject: [PATCH 2/4] fix: use AST approach to try and preserve yaml formatting --- .../update-pre-commit-config.test.ts | 127 ++++++++++++++++++ src/steps/update-pre-commit-config.ts | 84 +++++------- 2 files changed, 163 insertions(+), 48 deletions(-) diff --git a/src/steps/__tests__/update-pre-commit-config.test.ts b/src/steps/__tests__/update-pre-commit-config.test.ts index b29c9f9b..e9eb886a 100644 --- a/src/steps/__tests__/update-pre-commit-config.test.ts +++ b/src/steps/__tests__/update-pre-commit-config.test.ts @@ -233,4 +233,131 @@ repos: expect(result).toEqual({ updated: false }); }); }); + + describe('format preservation', () => { + beforeEach(() => { + existsSyncMock.mockReturnValue(true); + writeFileMock.mockResolvedValue(undefined); + }); + + it('preserves flow-style arrays', async () => { + const input = `repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + hooks: + - id: check-added-large-files + args: ['--maxkb=1000'] + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy + args: [--ignore-missing-imports, --disallow-untyped-defs] +`; + readFileMock.mockResolvedValue(input); + + await updatePreCommitConfigStep(mockOptions); + + const output = writeFileMock.mock.calls[0][1] as string; + expect(output).toContain("args: ['--maxkb=1000']"); + expect(output).toContain( + 'args: [--ignore-missing-imports, --disallow-untyped-defs]', + ); + }); + + it('preserves blank lines between repos', async () => { + const input = `repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + hooks: + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy +`; + readFileMock.mockResolvedValue(input); + + await updatePreCommitConfigStep(mockOptions); + + const output = writeFileMock.mock.calls[0][1] as string; + expect(output).toContain('trailing-whitespace\n\n - repo:'); + }); + + it('preserves quoted strings', async () => { + const input = `repos: + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.19.1" + hooks: + - id: mypy +`; + readFileMock.mockResolvedValue(input); + + await updatePreCommitConfigStep(mockOptions); + + const output = writeFileMock.mock.calls[0][1] as string; + expect(output).toContain('rev: "v1.19.1"'); + }); + + it('preserves flow-style additional_dependencies and appends posthog', async () => { + const input = `repos: + - repo: https://github.com/pre-commit/mirrors-mypy + hooks: + - id: mypy + additional_dependencies: [django-stubs, types-requests] +`; + readFileMock.mockResolvedValue(input); + + await updatePreCommitConfigStep(mockOptions); + + const output = writeFileMock.mock.calls[0][1] as string; + expect(output).toContain('additional_dependencies:'); + expect(output).toContain('django-stubs'); + expect(output).toContain('types-requests'); + expect(output).toContain('posthog'); + }); + + it('preserves complex real-world config formatting', async () => { + const input = `repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + args: ['--maxkb=1000'] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 + hooks: + - id: ruff-check + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.19.1" + hooks: + - id: mypy + args: [--ignore-missing-imports] + additional_dependencies: + [ + django-stubs==5.2.8, + djangorestframework-stubs==3.14.5, + ] + + - repo: local + hooks: + - id: commitizen-check + stages: [commit-msg] +`; + readFileMock.mockResolvedValue(input); + + await updatePreCommitConfigStep(mockOptions); + + const output = writeFileMock.mock.calls[0][1] as string; + + // Verify key formatting elements are preserved + expect(output).toContain("args: ['--maxkb=1000']"); + expect(output).toContain('rev: "v1.19.1"'); + expect(output).toContain('args: [--ignore-missing-imports]'); + expect(output).toContain('stages: [commit-msg]'); + // Verify blank lines between repos are preserved + expect(output).toContain('ruff-check\n\n - repo:'); + // Verify posthog was added + expect(output).toContain('posthog'); + }); + }); }); diff --git a/src/steps/update-pre-commit-config.ts b/src/steps/update-pre-commit-config.ts index a3b71274..2f842981 100644 --- a/src/steps/update-pre-commit-config.ts +++ b/src/steps/update-pre-commit-config.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import path from 'path'; import chalk from 'chalk'; -import { parse, stringify } from 'yaml'; +import { parseDocument, isSeq, isMap, YAMLSeq } from 'yaml'; import type { Integration } from '../lib/constants'; import { analytics } from '../utils/analytics'; import clack from '../utils/clack'; @@ -12,23 +12,6 @@ const CONFIG_FILENAMES = ['.pre-commit-config.yaml', '.pre-commit-config.yml']; // Matches 'posthog' with any version specifier (==, >=, <=, ~=, !=, etc.) or extras [...] const POSTHOG_DEP_PATTERN = /^posthog([=<>~![]|$)/; -interface PreCommitHook { - id: string; - additional_dependencies?: string[]; - [key: string]: unknown; -} - -interface PreCommitRepo { - repo: string; - hooks?: PreCommitHook[]; - [key: string]: unknown; -} - -interface PreCommitConfig { - repos?: PreCommitRepo[]; - [key: string]: unknown; -} - function findConfigFile(installDir: string): string | null { for (const filename of CONFIG_FILENAMES) { const candidate = path.join(installDir, filename); @@ -39,15 +22,6 @@ function findConfigFile(installDir: string): string | null { return null; } -function hasTypeCheckingHooks(config: PreCommitConfig): boolean { - if (!config.repos || !Array.isArray(config.repos)) { - return false; - } - return config.repos.some((repo) => - repo.hooks?.some((hook) => TYPE_CHECKING_HOOK_IDS.includes(hook.id)), - ); -} - export async function updatePreCommitConfigStep({ installDir, integration, @@ -77,9 +51,9 @@ export async function updatePreCommitConfigStep({ return { updated: false }; } - let config: PreCommitConfig; + let doc; try { - config = parse(content) as PreCommitConfig; + doc = parseDocument(content); } catch (error) { clack.log.warn(`Could not parse ${chalk.bold.cyan(configFilename)}`); analytics.capture('wizard interaction', { @@ -90,39 +64,52 @@ export async function updatePreCommitConfigStep({ return { updated: false }; } - if (!config.repos || !Array.isArray(config.repos)) { + const repos = doc.get('repos'); + if (!isSeq(repos)) { return { updated: false }; } let modified = false; + let hasTypeCheckingHooks = false; - for (const repo of config.repos) { - if (!repo.hooks || !Array.isArray(repo.hooks)) { - continue; - } + for (const repo of repos.items) { + if (!isMap(repo)) continue; - for (const hook of repo.hooks) { - if (!TYPE_CHECKING_HOOK_IDS.includes(hook.id)) { - continue; - } + const hooks = repo.get('hooks'); + if (!isSeq(hooks)) continue; - if (!hook.additional_dependencies) { - hook.additional_dependencies = []; - } + for (const hook of hooks.items) { + if (!isMap(hook)) continue; - const hasPosthog = hook.additional_dependencies.some((dep) => - POSTHOG_DEP_PATTERN.test(dep), - ); + const hookId = String(hook.get('id') ?? ''); + if (!TYPE_CHECKING_HOOK_IDS.includes(hookId)) continue; + + hasTypeCheckingHooks = true; + + const deps = hook.get('additional_dependencies'); + + if (isSeq(deps)) { + // Check if posthog already exists + const hasPosthog = deps.items.some((item) => + POSTHOG_DEP_PATTERN.test(String(item)), + ); - if (!hasPosthog) { - hook.additional_dependencies.push('posthog'); + if (!hasPosthog) { + deps.add('posthog'); + modified = true; + } + } else if (deps === undefined || deps === null) { + // No additional_dependencies - create new sequence + const newDeps = new YAMLSeq(); + newDeps.add('posthog'); + hook.set('additional_dependencies', newDeps); modified = true; } } } if (!modified) { - if (hasTypeCheckingHooks(config)) { + if (hasTypeCheckingHooks) { clack.log.success( `${chalk.bold.cyan( configFilename, @@ -133,7 +120,8 @@ export async function updatePreCommitConfigStep({ } try { - await fs.promises.writeFile(configPath, stringify(config), 'utf8'); + const output = doc.toString({ flowCollectionPadding: false }); + await fs.promises.writeFile(configPath, output, 'utf8'); } catch (error) { clack.log.warn(`Could not write ${chalk.bold.cyan(configFilename)}`); analytics.capture('wizard interaction', { From a6ea9cdd028294e29dd8e92a722962571201a5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Negr=C3=B3n?= Date: Sun, 8 Feb 2026 19:11:27 -0400 Subject: [PATCH 3/4] Update src/lib/agent-runner.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/agent-runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index fb959c79..311846a0 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -337,7 +337,7 @@ Please report this error to: ${chalk.cyan('wizard@posthog.com')}`; ? `Uploaded environment variables to your hosting provider` : '', preCommitUpdated - ? `Updated .pre-commit-config.yaml with posthog dependency` + ? `Updated pre-commit config with posthog dependency` : '', ].filter(Boolean); From 3bc6b01a4a60a39c7299e33bf47adbbf90cceab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Negr=C3=B3n?= Date: Mon, 9 Feb 2026 13:43:34 -0400 Subject: [PATCH 4/4] fix: copilot review suggestions; false success log for unrecognized deps format --- src/lib/agent-runner.ts | 7 ++----- src/steps/update-pre-commit-config.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts index 311846a0..597e7f0a 100644 --- a/src/lib/agent-runner.ts +++ b/src/lib/agent-runner.ts @@ -17,7 +17,7 @@ import { } from '../utils/clack-utils'; import type { PackageDotJson } from '../utils/package-json'; import { analytics } from '../utils/analytics'; -import { WIZARD_INTERACTION_EVENT_NAME } from './constants'; +import { WIZARD_INTERACTION_EVENT_NAME, Integration } from './constants'; import clack from '../utils/clack'; import { initializeAgent, @@ -33,7 +33,6 @@ import { uploadEnvironmentVariablesStep, updatePreCommitConfigStep, } from '../steps'; -import { Integration } from './constants'; import { checkAnthropicStatusWithPrompt } from '../utils/anthropic-status'; import { enableDebugLogs } from '../utils/debug'; @@ -336,9 +335,7 @@ Please report this error to: ${chalk.cyan('wizard@posthog.com')}`; uploadedEnvVars.length > 0 ? `Uploaded environment variables to your hosting provider` : '', - preCommitUpdated - ? `Updated pre-commit config with posthog dependency` - : '', + preCommitUpdated ? `Updated pre-commit config with posthog dependency` : '', ].filter(Boolean); const nextSteps = [ diff --git a/src/steps/update-pre-commit-config.ts b/src/steps/update-pre-commit-config.ts index 2f842981..15e527c9 100644 --- a/src/steps/update-pre-commit-config.ts +++ b/src/steps/update-pre-commit-config.ts @@ -70,7 +70,7 @@ export async function updatePreCommitConfigStep({ } let modified = false; - let hasTypeCheckingHooks = false; + let alreadyHasPosthog = false; for (const repo of repos.items) { if (!isMap(repo)) continue; @@ -84,8 +84,6 @@ export async function updatePreCommitConfigStep({ const hookId = String(hook.get('id') ?? ''); if (!TYPE_CHECKING_HOOK_IDS.includes(hookId)) continue; - hasTypeCheckingHooks = true; - const deps = hook.get('additional_dependencies'); if (isSeq(deps)) { @@ -97,6 +95,8 @@ export async function updatePreCommitConfigStep({ if (!hasPosthog) { deps.add('posthog'); modified = true; + } else { + alreadyHasPosthog = true; } } else if (deps === undefined || deps === null) { // No additional_dependencies - create new sequence @@ -104,12 +104,18 @@ export async function updatePreCommitConfigStep({ newDeps.add('posthog'); hook.set('additional_dependencies', newDeps); modified = true; + } else { + clack.log.warn( + `Unexpected additional_dependencies format in ${chalk.bold.cyan( + hookId, + )} hook, skipping`, + ); } } } if (!modified) { - if (hasTypeCheckingHooks) { + if (alreadyHasPosthog) { clack.log.success( `${chalk.bold.cyan( configFilename,