Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__tests__/mcp-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// suite's assertions, stubbed only so the mocked module still satisfies
// the real module's exports.
reportableDiscoveredFeatures: () => undefined,
reportablePosthogSdkDetected: () => undefined,
}));
vi.mock('@ui/tui/start-tui', () => ({
startTUI: mockStartTUIMcp,
Expand Down Expand Up @@ -69,13 +70,13 @@
});

test('starts the TUI with the McpAdd program id', async () => {
mcpAddCommand.handler!(makeArgv());

Check warning on line 73 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockStartTUIMcp).toHaveBeenCalledWith(expect.any(String), 'mcp-add');
});

test('passes --local through as localMcp', async () => {
mcpAddCommand.handler!(makeArgv({ local: true }));

Check warning on line 79 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockBuildSessionMcp).toHaveBeenCalledWith(
expect.objectContaining({ localMcp: true }),
Expand All @@ -83,7 +84,7 @@
});

test('passes --api-key through to buildSession', async () => {
mcpAddCommand.handler!(makeArgv({ apiKey: 'phx_from_flag' }));

Check warning on line 87 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockBuildSessionMcp).toHaveBeenCalledWith(
expect.objectContaining({ apiKey: 'phx_from_flag' }),
Expand All @@ -92,7 +93,7 @@

test('falls back to readApiKeyFromEnv when --api-key is omitted', async () => {
mockReadApiKeyFromEnvMcp.mockReturnValueOnce('phx_from_env');
mcpAddCommand.handler!(makeArgv());

Check warning on line 96 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockBuildSessionMcp).toHaveBeenCalledWith(
expect.objectContaining({ apiKey: 'phx_from_env' }),
Expand All @@ -100,7 +101,7 @@
});

test('parses --features into a trimmed array', async () => {
mcpAddCommand.handler!(makeArgv({ features: 'flags, errors , logs' }));

Check warning on line 104 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockBuildSessionMcp).toHaveBeenCalledWith(
expect.objectContaining({ mcpFeatures: ['flags', 'errors', 'logs'] }),
Expand Down Expand Up @@ -131,7 +132,7 @@
});

test('starts the TUI with the McpRemove program id', async () => {
mcpRemoveCommand.handler!(makeArgv());

Check warning on line 135 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockStartTUIMcp).toHaveBeenCalledWith(
expect.any(String),
Expand All @@ -140,7 +141,7 @@
});

test('passes --local through as localMcp', async () => {
mcpRemoveCommand.handler!(makeArgv({ local: true }));

Check warning on line 144 in src/__tests__/mcp-cli.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
await flush();
expect(mockBuildSessionMcp).toHaveBeenCalledWith(
expect.objectContaining({ localMcp: true }),
Expand Down
71 changes: 71 additions & 0 deletions src/lib/programs/__tests__/posthog-integration-detect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { detectExistingPostHog } from '@lib/programs/posthog-integration/detect';

function makeTmpDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ph-detect-'));
}

function writePackageJson(
dir: string,
pkg: {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
} = {},
): void {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg));
}

describe('detectExistingPosthog', () => {
let tmpDir: string;
let setPosthogSdkDetected: ReturnType<typeof vi.fn>;

beforeEach(() => {
tmpDir = makeTmpDir();
setPosthogSdkDetected = vi.fn();
});

afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));

const run = (dir: string) =>
detectExistingPostHog({ setPosthogSdkDetected }, dir);

it('reports false when no package.json exists', () => {
run(tmpDir);
expect(setPosthogSdkDetected).toHaveBeenCalledWith(false);
});

it('reports false when dependencies have no PostHog SDK', () => {
writePackageJson(tmpDir, { dependencies: { react: '^19.0.0' } });
run(tmpDir);
expect(setPosthogSdkDetected).toHaveBeenCalledWith(false);
});

it('reports true for posthog-js in dependencies', () => {
writePackageJson(tmpDir, { dependencies: { 'posthog-js': '^1.0.0' } });
run(tmpDir);
expect(setPosthogSdkDetected).toHaveBeenCalledWith(true);
});

it('reports true for posthog-node in devDependencies', () => {
writePackageJson(tmpDir, { devDependencies: { 'posthog-node': '^4.0.0' } });
run(tmpDir);
expect(setPosthogSdkDetected).toHaveBeenCalledWith(true);
});

it('reports true for a PostHog SDK in a nested monorepo package', () => {
writePackageJson(tmpDir, { dependencies: {} });
writePackageJson(path.join(tmpDir, 'apps', 'web'), {
dependencies: { 'posthog-js': '^1.0.0' },
});
run(tmpDir);
expect(setPosthogSdkDetected).toHaveBeenCalledWith(true);
});

it('does not throw and reports false for an invalid install dir', () => {
expect(() => run('/nonexistent/path')).not.toThrow();
expect(setPosthogSdkDetected).toHaveBeenCalledWith(false);
});
});
52 changes: 51 additions & 1 deletion src/lib/programs/__tests__/program-registry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
PROGRAM_REGISTRY,
agentSkillConfig,
getCommandPath,
getLaunchablePrograms,
getProgramConfig,
getSubcommandPrograms,
} from '@lib/programs/program-registry';
Expand Down Expand Up @@ -34,14 +36,62 @@ describe('getSubcommandPrograms', () => {
const subcommands = getSubcommandPrograms();
const commands = subcommands.map((c) => c.command);

expect(commands).toContain('integrate');
expect(commands).toContain('revenue-analytics');
for (const config of subcommands) {
expect(config.command).toBeTruthy();
}
});
});

// A nested program is only reachable through its parent's word.
describe('getCommandPath', () => {
const subcommand = (id: string) =>
getSubcommandPrograms().find((config) => config.id === id)!;

it('reaches a nested program through its parent', () => {
expect(getCommandPath(subcommand('web-analytics-doctor'))).toBe(
'audit web-analytics',
);
});

it('leaves a top-level program alone', () => {
expect(getCommandPath(subcommand('revenue-analytics-setup'))).toBe(
'revenue-analytics',
);
});
});

describe('getLaunchablePrograms', () => {
// The list is curated, so an id that stops matching drops its row in silence.
it("offers the intro's programs, in order, all resolving", () => {
expect(getLaunchablePrograms().map((config) => config.id)).toEqual([
'self-driving',
'error-tracking-upload-source-maps',
'warehouse-source',
'audit',
'posthog-doctor',
'mcp-analytics',
'replay-vision',
'ai-observability',
'metrics',
'revenue-analytics-setup',
]);
});

// A row wider than the terminal stops the whole block from centering.
it('keeps every row inside an 80-column terminal', () => {
const COMMAND_COLUMN = 21;
const MARKER_PREFIX = 2;
const BUDGET = 80 - COMMAND_COLUMN - MARKER_PREFIX;

const tooLong = getLaunchablePrograms()
.filter((config) => config.description.length > BUDGET)
.map((config) => `${config.id} (${config.description.length})`);

expect(tooLong).toEqual([]);
});
});

describe('parentCommand nesting', () => {
it('nests web-analytics-doctor under the audit command', () => {
const webAnalytics = getProgramConfig('web-analytics-doctor');
Expand Down
3 changes: 1 addition & 2 deletions src/lib/programs/audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ const baseConfig = createSkillProgram({
skillId: 'audit',
command: 'audit',
id: 'audit',
description:
'Audit an existing PostHog integration for correctness and best practices',
description: 'Audit and improve your PostHog setup',
integrationLabel: 'audit',
customPrompt:
'Run a comprehensive audit of the existing PostHog integration. Follow the skill program steps in order. Do not modify any project files — only create the final audit report.',
Expand Down
7 changes: 6 additions & 1 deletion src/lib/programs/events-audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ export { SETUP_REPORT_FILE };

const DOCS_URL = 'https://posthog.com/docs/product-analytics/best-practices';

/**
* No CLI word of its own since the audit family took over: `wizard audit
* events` is the live path, and it resolves to the context-mill `audit-events`
* skill (whose id AuditRunScreen keys its slides on), not to this config.
* Registered so its id stays resolvable; nothing dispatches to it today.
*/
export const eventsAuditConfig: ProgramConfig = {
command: 'events-audit',
description: 'Audit PostHog event tracking in this project',
id: 'events-audit',
skillId: 'events-audit',
Expand Down
2 changes: 1 addition & 1 deletion src/lib/programs/mcp-analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const mcpAnalyticsConfig = createSkillProgram({
skillId: 'mcp-analytics',
command: 'mcp-analytics',
id: 'mcp-analytics',
description: 'Add PostHog MCP analytics to your MCP server',
description: 'Add PostHog MCP Analytics to your MCP server',
integrationLabel: 'mcp-analytics',
customPrompt:
"Instrument this project's MCP server with PostHog MCP analytics. Run the " +
Expand Down
3 changes: 1 addition & 2 deletions src/lib/programs/posthog-doctor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { POSTHOG_DOCTOR_PROGRAM } from './steps.js';

export const posthogDoctorConfig: ProgramConfig = {
command: 'doctor',
description:
'Diagnose your PostHog project for configuration issues and setup warnings',
description: 'Diagnose your PostHog project setup',
id: 'posthog-doctor',
requiresAi: false,
steps: POSTHOG_DOCTOR_PROGRAM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function makeCtx(session: WizardSession): ProgramReadyContext {
},
setFrameworkConfig: vi.fn(),
setDetectedFramework: vi.fn(),
setPosthogSdkDetected: vi.fn(),
setSkillId: vi.fn(),
setUnsupportedVersion: vi.fn(),
addDiscoveredFeature: vi.fn(),
Expand Down
19 changes: 19 additions & 0 deletions src/lib/programs/posthog-integration/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
DETECTED_WAREHOUSE_SOURCES_KEY,
getDetectedWarehouseSources,
} from '@lib/programs/warehouse-source/detect';
import { findPackageJsons } from '@lib/programs/shared/package-scanning';

export async function detectPostHogIntegration(
ctx: ProgramReadyContext,
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function detectPostHogIntegration(
}

detectWarehouseSourcesForSuggestion(ctx, installDir);
detectExistingPostHog(ctx, installDir);

ctx.setDetectionComplete();
}
Expand Down Expand Up @@ -251,3 +253,20 @@ export function reportWarehouseSourcesDetected(

return true;
}

/** Dependency-level signal, not a verified install. A failed scan reports false. */
export function detectExistingPostHog(
ctx: Pick<ProgramReadyContext, 'setPosthogSdkDetected'>,
installDir: string,
): void {
try {
const pkgJsons = findPackageJsons(installDir);
ctx.setPosthogSdkDetected(pkgJsons.some((p) => p.posthogSdks.length > 0));
} catch (error) {
analytics.captureException(
error instanceof Error ? error : new Error(String(error)),
{ step: 'detectExistingPosthog' },
);
ctx.setPosthogSdkDetected(false);
}
}
1 change: 0 additions & 1 deletion src/lib/programs/posthog-integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export const SETUP_REPORT_FILE = 'posthog-setup-report.md';
export { EVENT_PLAN_FILE } from './constants.js';

export const posthogIntegrationConfig: ProgramConfig = {
command: 'integrate',
description: 'Set up PostHog SDK integration',
id: 'posthog-integration',
agentFlow: 'integration-v2',
Expand Down
29 changes: 29 additions & 0 deletions src/lib/programs/program-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,32 @@ export function getSubcommandPrograms(): SubcommandProgram[] {
(c): c is SubcommandProgram => c.command != null,
);
}

/** What a user types to reach the program. Nested ones go through its parent. */
export function getCommandPath(config: SubcommandProgram): string {
return config.parentCommand
? `${config.parentCommand} ${config.command}`
: config.command;
}

/** What the intro offers, in order. Curated: no config field ranks these. */
const INTRO_PROGRAMS = [
'self-driving',
'error-tracking-upload-source-maps',
'warehouse-source',
'audit',
'posthog-doctor',
'mcp-analytics',
'replay-vision',
'ai-observability',
'metrics',
'revenue-analytics-setup',
];

/** The programs the intro can hand off to, in the order it lists them. */
export function getLaunchablePrograms(): SubcommandProgram[] {
const byId = new Map(getSubcommandPrograms().map((c) => [c.id, c]));
return INTRO_PROGRAMS.map((id) => byId.get(id)).filter(
(config): config is SubcommandProgram => config != null,
);
}
1 change: 1 addition & 0 deletions src/lib/programs/program-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface ProgramReadyContext {
}) => void;
readonly addDiscoveredFeature: (feature: DiscoveredFeature) => void;
readonly setDetectionComplete: () => void;
readonly setPosthogSdkDetected: (detected: boolean) => void;
}

export interface ProgramStep {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/programs/replay-vision/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const base = createSkillProgram({
skillId: 'replay-vision-setup',
command: 'replay-vision',
id: 'replay-vision',
description: 'Set up PostHog Replay vision scanners for your product',
description: 'Set up PostHog Replay Vision scanners for your product',
integrationLabel: 'replay-vision',
customPrompt:
'Set up PostHog Replay vision. Run the `replay-vision` skill end-to-end: ' +
Expand Down
2 changes: 1 addition & 1 deletion src/lib/programs/revenue-analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getContentBlocks } from './content/index.js';

export const revenueAnalyticsConfig: ProgramConfig = {
command: 'revenue-analytics',
description: 'Set up PostHog revenue analytics (e.g. Stripe integration)',
description: 'Set up PostHog for Revenue Analytics',
id: 'revenue-analytics-setup',
skillId: 'revenue-analytics-setup',
steps: REVENUE_ANALYTICS_PROGRAM,
Expand Down
3 changes: 1 addition & 2 deletions src/lib/programs/warehouse-source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ function buildPrompt(session: WizardSession): string {

export const warehouseSourceConfig: ProgramConfig = {
command: 'warehouse',
description:
'Detect and connect a data warehouse source (Postgres, Stripe, …)',
description: 'Detect and connect Data Warehouse sources',
id: 'warehouse-source',
skillId: 'data-warehouse-source-setup',
steps: WAREHOUSE_SOURCE_PROGRAM,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/runners/run-non-interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ export function runNonInteractive(
},
addDiscoveredFeature: () => undefined,
setDetectionComplete: () => undefined,
setPosthogSdkDetected: (detected: boolean) => {
session.posthogSdkDetected = detected;
},
};
for (const step of config.steps) {
if (step.onReady) {
Expand Down
Loading
Loading