diff --git a/.prettierignore b/.prettierignore index 0c09424..32405c6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ pnpm-lock.yaml dist/ node_modules/ +CHANGELOG.md diff --git a/src/integrations/claude/onboarding.ts b/src/integrations/claude/onboarding.ts index ca39886..d15fd24 100644 --- a/src/integrations/claude/onboarding.ts +++ b/src/integrations/claude/onboarding.ts @@ -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, @@ -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 = ''; @@ -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; diff --git a/src/integrations/codex/onboarding.ts b/src/integrations/codex/onboarding.ts index 6f30177..f231aca 100644 --- a/src/integrations/codex/onboarding.ts +++ b/src/integrations/codex/onboarding.ts @@ -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; @@ -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 = ''; @@ -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; diff --git a/src/integrations/cursor/onboarding.ts b/src/integrations/cursor/onboarding.ts index 55f15dc..fdc749f 100644 --- a/src/integrations/cursor/onboarding.ts +++ b/src/integrations/cursor/onboarding.ts @@ -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, @@ -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 = ''; @@ -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; diff --git a/src/integrations/utils.ts b/src/integrations/utils.ts index 35312f2..979030a 100644 --- a/src/integrations/utils.ts +++ b/src/integrations/utils.ts @@ -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; +} diff --git a/src/lib/onboarding-prompt/index.ts b/src/lib/onboarding-prompt/index.ts index bf3a2ff..49e61fc 100644 --- a/src/lib/onboarding-prompt/index.ts +++ b/src/lib/onboarding-prompt/index.ts @@ -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())), diff --git a/src/lib/onboarding-prompt/integrate.ts b/src/lib/onboarding-prompt/integrate.ts index c969996..948ad15 100644 --- a/src/lib/onboarding-prompt/integrate.ts +++ b/src/lib/onboarding-prompt/integrate.ts @@ -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, @@ -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 : '', }); } diff --git a/src/lib/onboarding-prompt/steps/integrate-via-skill.md b/src/lib/onboarding-prompt/steps/integrate-via-skill.md index e62d044..512f602 100644 --- a/src/lib/onboarding-prompt/steps/integrate-via-skill.md +++ b/src/lib/onboarding-prompt/steps/integrate-via-skill.md @@ -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: @@ -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: @@ -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: " - "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}}