Skip to content
Merged
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 .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pnpm-lock.yaml
dist/
node_modules/
CHANGELOG.md
33 changes: 20 additions & 13 deletions src/integrations/claude/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createInterface } from 'node:readline';
import type { OnboardingOpts, OnboardingCallbacks } from '../types.js';
import { STATUS_PREFIX } from '../constants.js';
import { type StreamEvent, extractTextLines } from '../stream-json.js';
import { spawnErrorMessage } from '../utils.js';

export function runOnboarding(
opts: OnboardingOpts,
Expand All @@ -12,18 +13,24 @@ export function runOnboarding(
? { ...globalThis.process.env, CONFIDENCE_ACCESS_TOKEN: opts.token }
: undefined;

const child = spawn(
'claude',
['--print', '--output-format', 'stream-json', '--verbose', opts.prompt],
{
cwd: opts.projectDir,
timeout: 300000,
stdio: ['ignore', 'pipe', 'pipe'],
env,
},
);
let child: ChildProcess;
try {
child = spawn(
'claude',
['--print', '--output-format', 'stream-json', '--verbose', opts.prompt],
{
cwd: opts.projectDir,
timeout: 600_000,
stdio: ['ignore', 'pipe', 'pipe'],
env,
},
);
} catch (err) {
callbacks.onError(spawnErrorMessage('claude', err as NodeJS.ErrnoException));
return null;
}

if (!child?.stdout) return null;
if (!child.stdout) return null;

const allLines: string[] = [];
let stderrBuf = '';
Expand Down Expand Up @@ -66,9 +73,9 @@ export function runOnboarding(
callbacks.onComplete(allLines);
});

child.on('error', (err: Error) => {
child.on('error', (err: NodeJS.ErrnoException) => {
rl.close();
callbacks.onError(err.message);
callbacks.onError(spawnErrorMessage('claude', err));
});

return child;
Expand Down
25 changes: 16 additions & 9 deletions src/integrations/codex/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type ChildProcess, spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import type { OnboardingOpts, OnboardingCallbacks } from '../types.js';
import { STATUS_PREFIX } from '../constants.js';
import { spawnErrorMessage } from '../utils.js';

type CodexEvent = {
type: string;
Expand All @@ -19,16 +20,22 @@ export function runOnboarding(
? { ...globalThis.process.env, CONFIDENCE_ACCESS_TOKEN: opts.token }
: undefined;

const child = spawn('codex', ['exec', '--json', '--sandbox', 'workspace-write', '-'], {
cwd: opts.projectDir,
timeout: 300000,
stdio: ['pipe', 'pipe', 'pipe'],
env,
});
let child: ChildProcess;
try {
child = spawn('codex', ['exec', '--json', '--sandbox', 'workspace-write', '-'], {
cwd: opts.projectDir,
timeout: 600_000,
stdio: ['pipe', 'pipe', 'pipe'],
env,
});
} catch (err) {
callbacks.onError(spawnErrorMessage('codex', err as NodeJS.ErrnoException));
return null;
}

child.stdin?.end(opts.prompt);

if (!child?.stdout) return null;
if (!child.stdout) return null;

const allLines: string[] = [];
let stderrBuf = '';
Expand Down Expand Up @@ -74,9 +81,9 @@ export function runOnboarding(
callbacks.onComplete(allLines);
});

child.on('error', (err: Error) => {
child.on('error', (err: NodeJS.ErrnoException) => {
rl.close();
callbacks.onError(err.message);
callbacks.onError(spawnErrorMessage('codex', err));
});

return child;
Expand Down
26 changes: 16 additions & 10 deletions src/integrations/cursor/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ChildProcess, spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import type { OnboardingOpts, OnboardingCallbacks } from '../types.js';
import { type StreamEvent, extractTextLines } from '../stream-json.js';
import { normalizeStatusLine } from '../utils.js';
import { normalizeStatusLine, spawnErrorMessage } from '../utils.js';

export function runOnboarding(
opts: OnboardingOpts,
Expand All @@ -21,14 +21,20 @@ export function runOnboarding(
'--auto-review',
opts.prompt,
];
const child = spawn('cursor', args, {
cwd: opts.projectDir,
timeout: 300000,
stdio: ['ignore', 'pipe', 'pipe'],
env,
});
let child: ChildProcess;
try {
child = spawn('cursor', args, {
cwd: opts.projectDir,
timeout: 600_000,
stdio: ['ignore', 'pipe', 'pipe'],
env,
});
} catch (err) {
callbacks.onError(spawnErrorMessage('cursor', err as NodeJS.ErrnoException));
return null;
}

if (!child?.stdout) return null;
if (!child.stdout) return null;

const allLines: string[] = [];
let stderrBuf = '';
Expand Down Expand Up @@ -69,9 +75,9 @@ export function runOnboarding(
callbacks.onComplete(allLines);
});

child.on('error', (err: Error) => {
child.on('error', (err: NodeJS.ErrnoException) => {
rl.close();
callbacks.onError(err.message);
callbacks.onError(spawnErrorMessage('cursor', err));
});

return child;
Expand Down
7 changes: 7 additions & 0 deletions src/integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ import { STATUS_PREFIX } from './constants.js';
export function normalizeStatusLine(line: string) {
return line.startsWith(STATUS_PREFIX) ? line.slice(STATUS_PREFIX.length) : line;
}

export function spawnErrorMessage(bin: string, err: NodeJS.ErrnoException): string {
if (err.code === 'ENOEXEC' || err.code === 'ENOENT') {
return `${bin} CLI not found or not executable. Make sure it is installed and on your PATH.`;
}
return err.message;
}
18 changes: 9 additions & 9 deletions src/lib/onboarding-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,27 @@ export function buildOnboardingPrompt({
const steps = new StepCounter(isEmptyProject ? 2 : 1);
const tools = buildToolVars(ide);

const includingFlags = goal === 'feature-flags' || goal === 'all';
const includingRecording = goal === 'session-recording' || goal === 'all';
const usingSkill = includingFlags && hasPlugins;
const withFlags = goal === 'feature-flags' || goal === 'all';
const withRecordings = goal === 'session-recording' || goal === 'all';
const viaSkill = withFlags && hasPlugins;

const sections = [
preamble(framework, projectDir, isEmptyProject, goal),
preflight(tools),
addIf(isEmptyProject, () => scaffold(framework, steps.next())),

usingSkill
viaSkill
? [integrateViaSkill(framework, steps.next(), isEmptyProject, ide)]
: [
addIf(includingFlags, () => determineSDK(framework, steps.next(), tools)),
addIf(includingFlags, () => resolveClient(framework, steps.next(), tools)),
addIf(includingFlags, () =>
addIf(withFlags, () => determineSDK(framework, steps.next(), tools)),
addIf(withFlags, () => resolveClient(framework, steps.next(), tools)),
addIf(withFlags, () =>
integrateSDK(steps.next(), steps.current - 2, isEmptyProject, tools),
),
],

addIf(includingRecording, () => determineRecordingSDK(framework, steps.next(), tools)),
addIf(includingRecording, () => integrateRecording(steps.next(), isEmptyProject)),
addIf(withRecordings, () => determineRecordingSDK(framework, steps.next(), tools)),
addIf(withRecordings, () => integrateRecording(steps.next(), isEmptyProject)),

...migrations.map((m) => migrateFlags(m, steps.next())),

Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboarding-prompt/integrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function integrateViaSkill(
isEmptyProject: boolean,
ide: ChosenIde,
): string {
const needsReactGotchas = /react|nextjs|next/i.test(framework);

return loadStep('integrate-via-skill.md', {
STEP: step,
FRAMEWORK: framework,
Expand All @@ -26,6 +28,9 @@ export function integrateViaSkill(
INSERTION_HINT: isEmptyProject
? 'For fresh scaffolds, use the scaffold\'s default heading or welcome text as the insertion point — the "aha" moment works just as well on boilerplate. Demonstrate at least two use cases (e.g. a gradual rollout for a heading change and a kill switch for a feature section).'
: 'Read the top 2–3 candidate files and pick the best one: a single visible string or component, no complex conditionals already wrapping it, in a file the user will recognize.',

FLAG_GUIDANCE: isEmptyProject ? FLAG_GUIDANCE_EMPTY : FLAG_GUIDANCE_EXISTING,
REACT_GOTCHAS: needsReactGotchas ? REACT_GOTCHAS : '',
});
}

Expand Down
14 changes: 11 additions & 3 deletions src/lib/onboarding-prompt/steps/integrate-via-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Print "STATUS: Analyzing project for flag integration points..."

The detected framework is **{{FRAMEWORK}}**.

Read `{{SKILLS_DIR}}/analyze-project/SKILL.md` as a **methodology reference** — use it for what to analyze, how to identify flag candidates, which SDK to pick, and how to create and wire flags. Ignore its output formatting entirely: no step tracker, no EDUCATE blocks, no AskUserQuestion calls.

Execute the skill's workflow automatically, without pausing for user input:
Expand All @@ -13,7 +15,7 @@ Execute the skill's workflow automatically, without pausing for user input:

{{INSERTION_HINT}}

**Output format — use this instead of anything in the skill:**
**Output format — use this instead of the skill's formatting:**

The only user-visible output is STATUS-prefixed lines (~60 chars max). No step tracker boxes, no EDUCATE blocks, no headers, no AskUserQuestion. Print STATUS lines before each phase and periodically within longer phases:

Expand All @@ -27,5 +29,11 @@ The only user-visible output is STATUS-prefixed lines (~60 chars max). No step t
- After wiring each flag: "STATUS: Integrated flag: <flag-name>"
- "STATUS: Verifying project builds..."

Read the client secret from CONFIDENCE_CLIENT_SECRET env var in all generated code.
Use the OpenFeature API with local resolve where supported. Access flag values via dot notation: `flag-name.property`.
**Guardrails — apply regardless of what the skill says:**

- Read the client secret from CONFIDENCE_CLIENT_SECRET env var in all generated code — never hardcode it. Write the secret to `.env` and ensure `.env` is listed in `.gitignore`.
- Use the OpenFeature API with local resolve where supported. Access flag values via dot notation: `flag-name.property`.
- Set up the evaluation context with a stable `targeting_key` (user ID, session ID, or anonymous ID) and any attributes the app already has (`country`, `plan`, `device`). Don't fabricate attributes.
- Use only SDK APIs from the docs integration guide — do not improvise method names or signatures from memory.
{{FLAG_GUIDANCE}}
{{REACT_GOTCHAS}}