Skip to content
Draft
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
43 changes: 43 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { hideBin } from 'yargs/helpers';
import { VERSION } from './src/lib/version.js';

const WIZARD_VERSION = VERSION;

Check warning on line 9 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value

const NODE_VERSION_RANGE = '>=18.17.0';

Expand Down Expand Up @@ -187,7 +187,7 @@
const { startPlayground } = await import(
'./src/ui/tui/playground/start-playground.js'
);
(startPlayground as (version: string) => void)(WIZARD_VERSION);

Check warning on line 190 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
})();
} else {
// Interactive TTY: launch the Ink TUI
Expand All @@ -198,7 +198,7 @@
'./src/lib/wizard-session.js'
);

const tui = startTUI(WIZARD_VERSION);

Check warning on line 201 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`

// Build session from CLI args and attach to store
const session = buildSession({
Expand Down Expand Up @@ -320,10 +320,10 @@
// Feature discovery — deterministic scan of package.json deps
try {
const { readFileSync } = await import('fs');
const pkgPath = require('path').join(installDir, 'package.json');

Check warning on line 323 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe call of an `any` typed value

Check warning on line 323 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .join on an `any` value

Check warning on line 323 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));

Check warning on line 324 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `PathOrFileDescriptor`

Check warning on line 324 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
const allDeps = {

Check warning on line 325 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
...pkg.dependencies,

Check warning on line 326 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .dependencies on an `any` value
...pkg.devDependencies,
};
const depNames = Object.keys(allDeps);
Expand Down Expand Up @@ -504,6 +504,49 @@
.demandCommand(1, 'You must specify a subcommand (add or remove)')
.help();
})
.command(
'setup-revenue-analytics',
'Set up Stripe revenue analytics with PostHog',
(yargs) => {
return yargs.options({
'install-dir': {
describe:
'Directory of the project to set up\nenv: POSTHOG_WIZARD_INSTALL_DIR',
type: 'string',
},
});
},
(argv) => {
const options = { ...argv };

void (async () => {
// Always use LoggingUI for this subcommand (simple linear flow)
setUI(new LoggingUI());

const { readApiKeyFromEnv } = await import(
'./src/utils/env-api-key.js'
);
const { buildSession } = await import('./src/lib/wizard-session.js');
const { runSetupRevenueAnalytics } = await import(
'./src/setup-revenue-analytics/index.js'
);

const apiKey =
(options.apiKey as string | undefined) || readApiKeyFromEnv();

const session = buildSession({
debug: options.debug as boolean | undefined,
installDir: options.installDir as string | undefined,
ci: options.ci as boolean | undefined,
localMcp: options.localMcp as boolean | undefined,
apiKey,
projectId: options.projectId as string | undefined,
});

await runSetupRevenueAnalytics(session);
})();
},
)
.help()
.alias('help', 'h')
.version()
Expand Down
1 change: 0 additions & 1 deletion src/lib/agent-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,6 @@ function handleSDKMessage(
const statusMatch = block.text.match(statusRegex);
if (statusMatch) {
const statusText = statusMatch[1].trim();
getUI().pushStatus(statusText);
spinner.message(statusText);
}
}
Expand Down
151 changes: 151 additions & 0 deletions src/setup-revenue-analytics/__tests__/language-detection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import {
detectLanguageFromFiles,
languageFromIntegration,
} from '../language-detection';
import { Integration } from '../../lib/constants';

describe('languageFromIntegration', () => {
const cases: [Integration, string | null][] = [
[Integration.nextjs, 'node'],
[Integration.nuxt, 'node'],
[Integration.vue, 'node'],
[Integration.reactRouter, 'node'],
[Integration.tanstackStart, 'node'],
[Integration.tanstackRouter, 'node'],
[Integration.reactNative, 'node'],
[Integration.angular, 'node'],
[Integration.astro, 'node'],
[Integration.sveltekit, 'node'],
[Integration.javascript_web, 'node'],
[Integration.javascriptNode, 'node'],
[Integration.django, 'python'],
[Integration.flask, 'python'],
[Integration.fastapi, 'python'],
[Integration.python, 'python'],
[Integration.laravel, 'php'],
[Integration.rails, 'ruby'],
[Integration.ruby, 'ruby'],
[Integration.swift, null],
[Integration.android, null],
];

test.each(cases)('%s → %s', (integration, expected) => {
expect(languageFromIntegration(integration)).toBe(expected);
});
});

describe('detectLanguageFromFiles', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wizard-lang-'));
});

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

test('detects node from package.json', async () => {
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
expect(await detectLanguageFromFiles(tmpDir)).toBe('node');
});

test('detects python from requirements.txt', async () => {
fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'stripe==5.0.0');
expect(await detectLanguageFromFiles(tmpDir)).toBe('python');
});

test('detects python from pyproject.toml', async () => {
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]');
expect(await detectLanguageFromFiles(tmpDir)).toBe('python');
});

test('detects ruby from Gemfile', async () => {
fs.writeFileSync(
path.join(tmpDir, 'Gemfile'),
"source 'https://rubygems.org'",
);
expect(await detectLanguageFromFiles(tmpDir)).toBe('ruby');
});

test('detects php from composer.json', async () => {
fs.writeFileSync(path.join(tmpDir, 'composer.json'), '{}');
expect(await detectLanguageFromFiles(tmpDir)).toBe('php');
});

test('detects go from go.mod', async () => {
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module myapp');
expect(await detectLanguageFromFiles(tmpDir)).toBe('go');
});

test('detects java from build.gradle', async () => {
fs.writeFileSync(path.join(tmpDir, 'build.gradle'), 'plugins {}');
expect(await detectLanguageFromFiles(tmpDir)).toBe('java');
});

test('detects java from pom.xml', async () => {
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
expect(await detectLanguageFromFiles(tmpDir)).toBe('java');
});

test('detects dotnet from .csproj', async () => {
fs.writeFileSync(path.join(tmpDir, 'MyApp.csproj'), '<Project></Project>');
expect(await detectLanguageFromFiles(tmpDir)).toBe('dotnet');
});

test('detects dotnet from .sln', async () => {
fs.writeFileSync(
path.join(tmpDir, 'MyApp.sln'),
'Microsoft Visual Studio Solution',
);
expect(await detectLanguageFromFiles(tmpDir)).toBe('dotnet');
});

test('returns null for empty directory', async () => {
expect(await detectLanguageFromFiles(tmpDir)).toBeNull();
});

test('returns first match when multiple indicators present', async () => {
// node (package.json) comes before python in indicator order
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'stripe');
expect(await detectLanguageFromFiles(tmpDir)).toBe('node');
});

describe('monorepo support', () => {
test('detects node from subdirectory package.json', async () => {
fs.mkdirSync(path.join(tmpDir, 'backend'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'backend', 'package.json'), '{}');
expect(await detectLanguageFromFiles(tmpDir)).toBe('node');
});

test('detects python from subdirectory requirements.txt', async () => {
fs.mkdirSync(path.join(tmpDir, 'server'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, 'server', 'requirements.txt'),
'django',
);
expect(await detectLanguageFromFiles(tmpDir)).toBe('python');
});

test('detects ruby from nested Gemfile', async () => {
fs.mkdirSync(path.join(tmpDir, 'api'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'api', 'Gemfile'), "gem 'rails'");
expect(await detectLanguageFromFiles(tmpDir)).toBe('ruby');
});

test('ignores node_modules subdirectories', async () => {
fs.mkdirSync(path.join(tmpDir, 'node_modules', 'stripe'), {
recursive: true,
});
fs.writeFileSync(
path.join(tmpDir, 'node_modules', 'stripe', 'package.json'),
'{}',
);
expect(await detectLanguageFromFiles(tmpDir)).toBeNull();
});
});
});
Loading
Loading