diff --git a/DOCS.md b/DOCS.md index 2c8e00d..fa429ac 100644 --- a/DOCS.md +++ b/DOCS.md @@ -184,7 +184,7 @@ prelude, then the tail for the chosen mode). | # | Step | File | Notes | | -- | ----- | ------- | ----- | | 1 | Access — sign in (optional) | `steps/auth.js` | Valid session → "Welcome back"; otherwise offers the device-flow login. Declining sets **guest mode** (§2) | -| 2 | Claude Code | `steps/preflight.js` | Blocks if missing/logged out | +| 2 | Engines — Claude Code and Codex | `steps/preflight.js` | Status only — NEVER blocks. Missing engine → install/login guidance and the run continues; the final summary repeats the roster with the exact commands | | 3 | Name and folder | `steps/project.js` | `.` installs in the current folder | | 4 | **How to start — mode + stack path** | `steps/mode.js` | Sets `ctx.mode` + `ctx.stackPath`; agent-files conflict policy asked here too | | 5 | Your stack — layer by layer | `steps/stack.js` | Only the `custom` path asks; may switch `ctx.mode` to `full` (never for guests) | diff --git a/README.md b/README.md index 5103b68..8b2122e 100644 --- a/README.md +++ b/README.md @@ -229,10 +229,12 @@ the agent harness (skills, commands, gates) and the FIA runtime: - An **active [Impactus Academy](https://www.impactus.academy) enrollment** for the templates + automated pipeline (optional: without it the installer delivers the harness + agent only) -- **Claude Code** installed and logged in with a Claude **Pro/Max** - subscription -- For FIA's Codex roles: a **ChatGPT Plus/Pro** subscription (login at the - end via `/login openai-codex` in Pi) +- Recommended, **not required** (the installer only warns and keeps going): + **Claude Code** with a Claude **Pro/Max** subscription, and/or — for FIA's + Codex roles — a **ChatGPT Plus/Pro** subscription (login at the end via + `/login openai-codex` in Pi). With neither, everything still installs; you + get the best results with one of these, and other providers/models can be + added later inside Pi with `/login`. Everything runs inside these subscriptions — no API keys, no per-token billing. diff --git a/src/main.js b/src/main.js index 5707c1a..e542fec 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,7 @@ import * as ui from './lib/ui.js'; import { initLog, getLogPath, logLine, redactSecret } from './lib/log.js'; import { run } from './lib/proc.js'; import { ensureAuthenticated } from './steps/auth.js'; -import { ensureClaudeCode, ensureCliTools, ensureFiaAuth } from './steps/preflight.js'; +import { checkEngines, ensureCliTools, ensureFiaAuth } from './steps/preflight.js'; import { promptProject, fetchTemplate, installTemplate, addMcps } from './steps/project.js'; import { resolveTemplateId } from './steps/template.js'; import { stepEnabled } from './lib/pipeline.js'; @@ -95,7 +95,7 @@ export async function main(flags = {}) { // the folder is prepared and the harness is merged into it. const preludeSteps = [ ['Access — sign in to the community (optional)', () => ensureAuthenticated(ctx)], - ['Prerequisite: Claude Code', () => ensureClaudeCode(flags)], + ['Engines — Claude Code and Codex (status only, never blocks)', () => checkEngines(ctx)], ['Project — target folder (the project name is the folder name)', async () => Object.assign(ctx, await promptProject(flags))], ['How to start — ready-made template, your own stack, or decide later', () => selectInstallMode(ctx)], // Only the "build my stack" path asks here; the others pass straight through. @@ -122,8 +122,8 @@ export async function main(flags = {}) { () => ensureCliTools({ vercel: ctx.decisions?.deploy === true, ghAuth: ctx.decisions?.push === true, flags }), 'core', ], - // FIA subscriptions: Claude was already validated in the prelude - // (ensureClaudeCode); here we only make sure Pi is installed/updated — the + // FIA subscriptions: the engines were probed in the prelude (checkEngines, + // informational only); here we only make sure Pi is installed/updated — the // Codex login is the user's last step, AFTER the install finishes (the // final notes explain it). [ diff --git a/src/steps/finish.js b/src/steps/finish.js index 1ad85f9..2e0a137 100644 --- a/src/steps/finish.js +++ b/src/steps/finish.js @@ -2,8 +2,10 @@ import { existsSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { join } from 'node:path'; import { COMMUNITY } from '../config.js'; -import { run, runInherit } from '../lib/proc.js'; +import { has, run, runInherit } from '../lib/proc.js'; import { piCodexReady } from '../lib/pi-auth.js'; +import { osKind } from '../lib/platform.js'; +import { CLAUDE_INSTALL_HINT } from './preflight.js'; import { STACK_CATEGORIES, STACK_LATER } from '../stack-catalog.js'; import { relToCwd, STATE_MARKER } from './project.js'; import * as ui from '../lib/ui.js'; @@ -149,6 +151,34 @@ export async function finish(ctx) { 'All set ✅', ); + // Engines roster: ONE panel, at the end, with the state of each engine and + // the exact command for whatever is missing. Presence is probed NOW — not + // reused from the preflight snapshot — because the user may have installed + // Claude Code in another terminal while this run was going. Nothing here is + // required: the professional decides which subscriptions to use. + const claudeReady = await has('claude'); + const codexReady = piCodexReady(); + ui.note( + [ + claudeReady + ? '✅ Claude Code (Claude Pro/Max) — installed. Not logged in yet? Run `claude` once and finish in the browser.' + : `○ Claude Code (Claude Pro/Max) — not installed.\n Install: ${CLAUDE_INSTALL_HINT[osKind()]}\n Then run \`claude\` once to log in.`, + ctx.fiaInstalled + ? codexReady + ? '✅ Codex (ChatGPT Plus/Pro, via Pi) — logged in.' + : `○ Codex (ChatGPT Plus/Pro, via Pi) — login pending.\n Log in: run \`${agentCmd(ctx)}\` and type /login openai-codex.` + : null, + '', + 'Neither is mandatory. Agents give the best results with Claude and/or Codex,', + ctx.fiaInstalled + ? 'but Pi also accepts other providers/models — type /login inside Pi to see them.' + : 'but you can also work through Cursor or add engines later at any time.', + ] + .filter(Boolean) + .join('\n'), + 'Engines — who runs your agents', + ); + // Integrations report: what the keys step left ACTIVE and what is still // pending (with the keys — webhooks included — created via API). if (ctx.serviceReport?.length) { diff --git a/src/steps/preflight.js b/src/steps/preflight.js index 6e24917..404e178 100644 --- a/src/steps/preflight.js +++ b/src/steps/preflight.js @@ -1,64 +1,65 @@ import process from 'node:process'; -import { existsSync, statSync } from 'node:fs'; -import { homedir } from 'node:os'; -import { join } from 'node:path'; +import { existsSync } from 'node:fs'; import { has, run, runInherit } from '../lib/proc.js'; import { ensurePiReady, piCodexReady } from '../lib/pi-auth.js'; import { osKind, detectPackageManagers } from '../lib/platform.js'; import * as ui from '../lib/ui.js'; -const CLAUDE_INSTALL_HINT = { +export const CLAUDE_INSTALL_HINT = { mac: 'curl -fsSL https://claude.ai/install.sh | bash (or: brew install --cask claude-code)', linux: 'curl -fsSL https://claude.ai/install.sh | bash', windows: 'irm https://claude.ai/install.ps1 | iex (or: winget install Anthropic.ClaudeCode)', }; -// ── Gate 0: Claude Code (checked first of all) ────────────────────────────── +// ── Engines: Claude Code and Codex (status only — NEVER blocks) ───────────── +// +// The installer itself never runs an agent. `claude` is only used to register +// MCPs (`claude mcp add`, which degrades to a manual note when absent) and the +// Codex login has always been the user's last step, inside Pi, AFTER the +// install. The engines belong to the professional, not to the installer — a +// missing one produces guidance and moves on; the final summary (finish.js) +// shows the same roster with the exact commands. There is deliberately no +// login probe for `claude`: no heuristic is reliable, and the `claude` CLI +// walks the user through its own login on first run anyway. -export async function ensureClaudeCode(flags = {}) { - ui.step('Checking Claude Code…'); - if (!(await has('claude'))) { - ui.error('Claude Code not found.'); +export async function checkEngines(ctx = {}) { + ui.step('Checking the engines (Claude Code and Codex)…'); + const claude = await has('claude'); + const codex = piCodexReady(); + ctx.engines = { claude, codex }; + + if (claude) { + ui.success('Claude Code found.'); + } else { + ui.warn('Claude Code not found — optional: the install continues without it.'); + ui.info(`To install later: ${CLAUDE_INSTALL_HINT[osKind()]} (then run \`claude\` once to log in)`); + } + if (codex) { + ui.success('Codex login found (Pi).'); + } else { + ui.info('Codex login not done yet — normal: it is the last step, inside Pi (/login openai-codex).'); + } + + if (!claude && !codex) { ui.note( [ - 'Install Claude Code and log in before continuing:', - '', - ` ${CLAUDE_INSTALL_HINT[osKind()]}`, + 'Neither Claude Code nor a Codex login was found. Nothing stops here —', + 'the engines are used by the agents AFTER the install, never by the installer.', '', - 'Then log in by running: claude (finish in the browser)', - '', - 'Run this installer again when you are done.', + 'You will get the best results with one of these subscriptions, but you can', + 'also log in to other providers/models later, inside Pi, with /login.', + 'The final summary shows the exact commands for every option.', ].join('\n'), - 'Claude Code is required', + 'No engine yet — the install continues', ); - process.exit(1); } - - // No reliable scriptable login gate for `claude` — best-effort heuristic. - if (!(await detectClaudeLogin())) { - ui.warn("I couldn't confirm whether you are logged in to Claude Code."); - const ok = flags.yes - ? true // non-interactive mode assumes the login was done - : await ui.confirm({ - message: 'Have you already logged in to Claude Code (ran `claude` and authenticated)?', - initialValue: true, - }); - if (!ok) { - ui.note( - 'Run `claude`, finish the login in the browser and run this installer again.', - 'Log in to Claude Code', - ); - process.exit(1); - } - } - ui.success('Claude Code ready.'); } /** * Ensure the Pi CLI (install/update) when FIA will be installed. NO login * here: the Codex `/login` is the user's last step, AFTER the install * finishes — opening Pi mid-install invited a Ctrl+C that killed the stamp - * halfway. Claude Code is checked separately in ensureClaudeCode. + * halfway. Claude Code is probed separately in checkEngines (status only). */ export async function ensureFiaAuth(flags = {}) { if (flags.skipFia || flags.fia === false) { @@ -74,24 +75,6 @@ export async function ensureFiaAuth(flags = {}) { } } -async function detectClaudeLogin() { - if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) return true; - const credFile = join(homedir(), '.claude', '.credentials.json'); - try { - if (existsSync(credFile) && statSync(credFile).size > 0) return true; - } catch { - /* ignore */ - } - if (osKind() === 'mac') { - // Claude Code stores its OAuth token in the login keychain. The service - // name has varied across versions, so probe the known candidates. - for (const service of ['Claude Code-credentials', 'Claude Code', 'claude-code']) { - if ((await run('security', ['find-generic-password', '-s', service])).ok) return true; - } - } - return false; -} - // ── CLIs: Git, GitHub CLI, Vercel CLI ─────────────────────────────────────── /** diff --git a/test/preflight.test.js b/test/preflight.test.js index 7104964..b001555 100644 --- a/test/preflight.test.js +++ b/test/preflight.test.js @@ -1,6 +1,22 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { installPlan } from '../src/steps/preflight.js'; +import { checkEngines, installPlan } from '../src/steps/preflight.js'; + +// The engines probe is informational by contract: with NOTHING on PATH it must +// still resolve (no prompt, no process.exit) and record the status in ctx. +// A reintroduced gate would hang or kill this run — that's the guard. +test('checkEngines: never blocks — resolves and records status even with no engine on PATH', async () => { + const originalPath = process.env.PATH; + process.env.PATH = ''; + const ctx = {}; + try { + await checkEngines(ctx); + } finally { + process.env.PATH = originalPath; + } + assert.equal(ctx.engines.claude, false); + assert.equal(typeof ctx.engines.codex, 'boolean'); +}); // `installPlan` depends on the OS via osKind() (reads process.platform), so we // pin the platform in each case.