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
2 changes: 1 addition & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -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).
[
Expand Down
32 changes: 31 additions & 1 deletion src/steps/finish.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
93 changes: 38 additions & 55 deletions src/steps/preflight.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 ───────────────────────────────────────

/**
Expand Down
18 changes: 17 additions & 1 deletion test/preflight.test.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading