fraim-framework 2.0.126 → 2.0.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/src/ai-hub/catalog.js +280 -44
  2. package/dist/src/ai-hub/desktop-main.js +2 -2
  3. package/dist/src/ai-hub/hosts.js +384 -10
  4. package/dist/src/ai-hub/server.js +255 -9
  5. package/dist/src/cli/commands/add-ide.js +4 -3
  6. package/dist/src/cli/commands/first-run.js +61 -0
  7. package/dist/src/cli/commands/hub.js +4 -4
  8. package/dist/src/cli/commands/init-project.js +8 -4
  9. package/dist/src/cli/commands/setup.js +4 -3
  10. package/dist/src/cli/commands/sync.js +32 -6
  11. package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
  12. package/dist/src/cli/fraim.js +2 -0
  13. package/dist/src/cli/mcp/ide-formats.js +29 -1
  14. package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
  15. package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
  16. package/dist/src/cli/setup/ide-detector.js +32 -1
  17. package/dist/src/cli/setup/ide-global-integration.js +5 -1
  18. package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
  19. package/dist/src/cli/setup/mcp-config-generator.js +12 -1
  20. package/dist/src/cli/utils/agent-adapters.js +10 -0
  21. package/dist/src/core/utils/git-utils.js +14 -6
  22. package/dist/src/first-run/install-state.js +70 -0
  23. package/dist/src/first-run/server.js +158 -0
  24. package/dist/src/first-run/session-service.js +746 -0
  25. package/dist/src/first-run/types.js +97 -0
  26. package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
  27. package/dist/src/local-mcp-server/stdio-server.js +41 -9
  28. package/package.json +3 -1
  29. package/public/ai-hub/index.html +149 -102
  30. package/public/ai-hub/script.js +1154 -271
  31. package/public/ai-hub/styles.css +753 -450
  32. package/public/first-run/error-frame.js +89 -0
  33. package/public/first-run/index.html +35 -0
  34. package/public/first-run/script.js +417 -0
  35. package/public/first-run/styles.css +386 -0
@@ -0,0 +1,746 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.FIRST_RUN_ROW_IDS = exports.FirstRunSessionService = void 0;
40
+ const fs_1 = __importDefault(require("fs"));
41
+ const os_1 = __importDefault(require("os"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const crypto_1 = __importDefault(require("crypto"));
44
+ const child_process_1 = require("child_process");
45
+ const ide_detector_1 = require("../cli/setup/ide-detector");
46
+ const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
47
+ const setup_1 = require("../cli/commands/setup");
48
+ const init_project_1 = require("../cli/commands/init-project");
49
+ const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
50
+ const types_1 = require("./types");
51
+ Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
52
+ const install_state_1 = require("./install-state");
53
+ function getFakeStateMode() {
54
+ if (process.env.FRAIM_FIRST_RUN_FAKE !== '1')
55
+ return null;
56
+ const explicit = process.env.FRAIM_FIRST_RUN_FAKE_STATE;
57
+ if (explicit === 'all-pending' || explicit === 'all-detected' || explicit === 'agent-install-fails') {
58
+ return explicit;
59
+ }
60
+ return 'default';
61
+ }
62
+ function commandExists(command) {
63
+ const executable = process.platform === 'win32' ? 'cmd.exe' : command;
64
+ const args = process.platform === 'win32'
65
+ ? ['/d', '/s', '/c', `${command} --version`]
66
+ : ['--version'];
67
+ const result = (0, child_process_1.spawnSync)(executable, args, {
68
+ encoding: 'utf8',
69
+ stdio: ['ignore', 'ignore', 'ignore'],
70
+ shell: false,
71
+ });
72
+ return result.status === 0;
73
+ }
74
+ function commandVersion(command) {
75
+ const executable = process.platform === 'win32' ? 'cmd.exe' : command;
76
+ const args = process.platform === 'win32'
77
+ ? ['/d', '/s', '/c', `${command} --version`]
78
+ : ['--version'];
79
+ const result = (0, child_process_1.spawnSync)(executable, args, {
80
+ encoding: 'utf8',
81
+ timeout: 5000,
82
+ });
83
+ if (result.status !== 0)
84
+ return null;
85
+ const text = `${result.stdout || ''}${result.stderr || ''}`.split(/\r?\n/)[0]?.trim() || null;
86
+ return text;
87
+ }
88
+ function ensureOutputDirs() {
89
+ fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
90
+ fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
91
+ }
92
+ function getNextPromptPath() {
93
+ return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install', 'next-prompt.txt');
94
+ }
95
+ function getInstallLogPath() {
96
+ return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-log.txt');
97
+ }
98
+ function appendInstallLog(line) {
99
+ ensureOutputDirs();
100
+ fs_1.default.appendFileSync(getInstallLogPath(), `[${new Date().toISOString()}] ${line}${os_1.default.EOL}`);
101
+ }
102
+ function findAgentOption(agentId) {
103
+ return types_1.FIRST_RUN_AGENT_OPTIONS.find((option) => option.id === agentId);
104
+ }
105
+ function pendingVerbForAgent(agentId) {
106
+ const option = findAgentOption(agentId);
107
+ if (!option)
108
+ return "we'll set up the recommended agent";
109
+ if (agentId === 'claude-code')
110
+ return "we'll set up Claude Code (recommended)";
111
+ return `we'll set up ${option.label}`;
112
+ }
113
+ class FirstRunSessionService {
114
+ constructor(options) {
115
+ this.key = options.key;
116
+ this.headless = options.headless === true;
117
+ this.fakeMode = getFakeStateMode();
118
+ this.fakeStderr =
119
+ process.env.FRAIM_FIRST_RUN_FAKE_STDERR ??
120
+ ['npm error code EACCES', 'npm error syscall mkdir'].join('\n');
121
+ this.requestToken = crypto_1.default.randomUUID();
122
+ this.state = options.resume ? (0, install_state_1.loadFirstRunState)() || (0, install_state_1.createInitialFirstRunState)(options.key) : (0, install_state_1.createInitialFirstRunState)(options.key);
123
+ if (options.projectRoot) {
124
+ this.state.workspacePath = path_1.default.resolve(options.projectRoot);
125
+ (0, install_state_1.saveFirstRunState)(this.state);
126
+ }
127
+ }
128
+ persist() {
129
+ (0, install_state_1.saveFirstRunState)(this.state);
130
+ }
131
+ getRow(rowId) {
132
+ const row = this.state.rows.find((r) => r.id === rowId);
133
+ if (!row) {
134
+ throw new Error(`Row ${rowId} not found in state — initial rows must include all canonical IDs`);
135
+ }
136
+ return row;
137
+ }
138
+ clearRowError(row) {
139
+ delete row.errorFrame;
140
+ delete row.streamOutput;
141
+ }
142
+ setRowError(row, whatTried, whatHappened, actions) {
143
+ row.status = 'error';
144
+ row.verb = 'failed — see below';
145
+ row.errorFrame = { whatTried, whatHappened, actions };
146
+ }
147
+ detectRowsOnLoad() {
148
+ if (this.fakeMode) {
149
+ this.applyFakeStateOnLoad(this.fakeMode);
150
+ return;
151
+ }
152
+ // Real detection — populate row statuses from `command -v` style probes.
153
+ const nodeRow = this.getRow('node');
154
+ const nodeVer = commandVersion('node');
155
+ if (nodeVer) {
156
+ nodeRow.status = 'ok';
157
+ nodeRow.verb = `${nodeVer} detected`;
158
+ }
159
+ else {
160
+ nodeRow.status = 'pending';
161
+ nodeRow.verb = "we'll install";
162
+ }
163
+ const gitRow = this.getRow('git');
164
+ const gitVer = commandVersion('git');
165
+ if (gitVer) {
166
+ gitRow.status = 'ok';
167
+ gitRow.verb = `${gitVer} detected`;
168
+ }
169
+ else {
170
+ gitRow.status = 'pending';
171
+ gitRow.verb = "we'll install";
172
+ }
173
+ const agentRow = this.getRow('agent');
174
+ const detected = this.detectAnyKnownAgent();
175
+ if (detected) {
176
+ this.state.agentId = detected.id;
177
+ agentRow.status = 'ok';
178
+ agentRow.verb = `${detected.label} detected`;
179
+ agentRow.detail = detected.versionLine ?? undefined;
180
+ }
181
+ else {
182
+ agentRow.status = 'pending';
183
+ agentRow.verb = pendingVerbForAgent(this.state.agentId);
184
+ }
185
+ const loginRow = this.getRow('agent-login');
186
+ if (agentRow.status === 'ok' && this.probeAgentReady(this.state.agentId).ok) {
187
+ loginRow.status = 'ok';
188
+ loginRow.verb = 'Signed in (verified)';
189
+ }
190
+ else {
191
+ loginRow.status = 'pending';
192
+ loginRow.verb = "you'll sign in after install";
193
+ }
194
+ const projectRow = this.getRow('project');
195
+ if (this.state.workspacePath && fs_1.default.existsSync(this.state.workspacePath)) {
196
+ projectRow.status = 'ok';
197
+ projectRow.verb = this.state.workspacePath;
198
+ }
199
+ else {
200
+ projectRow.status = 'manual-required';
201
+ projectRow.verb = 'pick a folder where FRAIM should work';
202
+ }
203
+ }
204
+ detectAnyKnownAgent() {
205
+ // Priority order matches FIRST_RUN_AGENT_OPTIONS ordering.
206
+ for (const option of types_1.FIRST_RUN_AGENT_OPTIONS) {
207
+ if (commandExists(option.launchCommand)) {
208
+ return { id: option.id, label: option.label, versionLine: commandVersion(option.launchCommand) };
209
+ }
210
+ }
211
+ // Also accept matches via the IDE detector for desktop variants.
212
+ const ides = (0, ide_detector_1.detectInstalledIDEs)();
213
+ for (const option of types_1.FIRST_RUN_AGENT_OPTIONS) {
214
+ const ide = ides.find((entry) => entry.aliases?.some((a) => option.detectAliases.includes(a)));
215
+ if (ide) {
216
+ return { id: option.id, label: option.label, versionLine: null };
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+ applyFakeStateOnLoad(mode) {
222
+ const setStatus = (rowId, status, verb, detail) => {
223
+ const row = this.getRow(rowId);
224
+ row.status = status;
225
+ row.verb = verb;
226
+ if (detail !== undefined)
227
+ row.detail = detail;
228
+ };
229
+ if (mode === 'all-pending') {
230
+ setStatus('node', 'pending', "we'll install");
231
+ setStatus('git', 'pending', "we'll install");
232
+ setStatus('agent', 'pending', pendingVerbForAgent(this.state.agentId));
233
+ setStatus('agent-login', 'pending', "you'll sign in after install");
234
+ setStatus('project', 'manual-required', 'pick a folder where FRAIM should work');
235
+ return;
236
+ }
237
+ if (mode === 'all-detected') {
238
+ setStatus('node', 'ok', 'v20.11.1 detected');
239
+ setStatus('git', 'ok', 'git version 2.45 detected');
240
+ setStatus('agent', 'ok', 'Claude Code v1.4.2 detected');
241
+ setStatus('agent-login', 'ok', 'Signed in (verified by claude -p probe)');
242
+ setStatus('project', 'manual-required', 'pick a folder where FRAIM should work');
243
+ return;
244
+ }
245
+ if (mode === 'agent-install-fails') {
246
+ setStatus('node', 'ok', 'v20.11.1 installed');
247
+ setStatus('git', 'ok', '2.45 installed');
248
+ setStatus('agent', 'pending', pendingVerbForAgent(this.state.agentId));
249
+ setStatus('agent-login', 'pending', "you'll sign in after install");
250
+ setStatus('project', 'manual-required', 'pick a folder where FRAIM should work');
251
+ return;
252
+ }
253
+ // 'default' fake mode — everything ok, project still requires a pick.
254
+ setStatus('node', 'ok', 'v20.11.1 detected');
255
+ setStatus('git', 'ok', 'git version 2.45 detected');
256
+ setStatus('agent', 'ok', 'fake-mode agent detected');
257
+ setStatus('agent-login', 'ok', 'fake-mode signed in');
258
+ setStatus('project', 'manual-required', 'pick a folder where FRAIM should work');
259
+ }
260
+ getRequestToken() {
261
+ return this.requestToken;
262
+ }
263
+ getSession() {
264
+ this.detectRowsOnLoad();
265
+ this.persist();
266
+ return {
267
+ state: this.state,
268
+ rows: this.state.rows,
269
+ primaryButtonLabel: (0, types_1.derivePrimaryButtonLabel)(this.state.rows),
270
+ prompt: types_1.FIRST_RUN_PROMPT,
271
+ headless: this.headless,
272
+ requestToken: this.requestToken,
273
+ agentOptions: types_1.FIRST_RUN_AGENT_OPTIONS,
274
+ currentAgentId: this.state.agentId,
275
+ };
276
+ }
277
+ respond(message, ok) {
278
+ return {
279
+ ok,
280
+ message,
281
+ state: this.state,
282
+ rows: this.state.rows,
283
+ primaryButtonLabel: (0, types_1.derivePrimaryButtonLabel)(this.state.rows),
284
+ };
285
+ }
286
+ /**
287
+ * Run a single row to completion (or to its next stable state). Idempotent.
288
+ * The client calls this for each row that is `pending`, in the canonical
289
+ * order, until every row is `ok` or surfaces an error.
290
+ */
291
+ async runRow(rowId, request = {}) {
292
+ const row = this.getRow(rowId);
293
+ if (row.status === 'ok') {
294
+ return this.respond(`${row.label} already ready.`, true);
295
+ }
296
+ this.clearRowError(row);
297
+ row.status = 'in-progress';
298
+ this.persist();
299
+ appendInstallLog(`row-run start ${rowId} status=${row.status}`);
300
+ try {
301
+ switch (rowId) {
302
+ case 'node':
303
+ return await this.runNodeRow();
304
+ case 'git':
305
+ return await this.runGitRow();
306
+ case 'agent':
307
+ return await this.runAgentRow(request);
308
+ case 'agent-login':
309
+ return await this.runAgentLoginRow(request);
310
+ case 'project':
311
+ return await this.runProjectRow(request);
312
+ }
313
+ }
314
+ catch (error) {
315
+ // Last-ditch safety net — every row's runner is supposed to surface
316
+ // its own R6 frame via setRowError before throwing, but if anything
317
+ // slips past (e.g. a sync layer that calls process.exit and we narrowly
318
+ // converted it to throw), we still surface a frame here rather than
319
+ // letting the request return an opaque 500. Skip is included so the
320
+ // user is never wedged on a non-recoverable failure of one row.
321
+ const message = error instanceof Error ? error.message : 'Unknown error';
322
+ row.status = 'error';
323
+ row.verb = 'failed — see below';
324
+ row.errorFrame = {
325
+ whatTried: `Running ${row.label} step`,
326
+ whatHappened: message,
327
+ actions: [
328
+ { id: 'retry', label: 'Retry', variant: 'primary' },
329
+ { id: 'skip', label: 'Skip and continue', variant: 'ghost' },
330
+ ],
331
+ };
332
+ this.persist();
333
+ return this.respond(`Row ${rowId} failed: ${message}`, false);
334
+ }
335
+ }
336
+ async runNodeRow() {
337
+ const row = this.getRow('node');
338
+ // Node is bootstrapped by the install template before the wizard starts.
339
+ // If node is missing here something is severely wrong; fall through to error.
340
+ const ver = commandVersion('node');
341
+ if (ver) {
342
+ row.status = 'ok';
343
+ row.verb = `${ver} installed`;
344
+ this.persist();
345
+ return this.respond(`Node detected (${ver}).`, true);
346
+ }
347
+ if (this.fakeMode) {
348
+ row.status = 'ok';
349
+ row.verb = 'v20.11.1 installed';
350
+ this.persist();
351
+ return this.respond('Fake-mode node ok.', true);
352
+ }
353
+ this.setRowError(row, 'Verifying Node.js installation', 'node --version returned a non-zero exit code or no output.\n\nThe installer template is expected to have bootstrapped Node before launching first-run.', [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
354
+ this.persist();
355
+ return this.respond('Node not detected.', false);
356
+ }
357
+ async runGitRow() {
358
+ const row = this.getRow('git');
359
+ const ver = commandVersion('git');
360
+ if (ver) {
361
+ row.status = 'ok';
362
+ row.verb = `${ver} installed`;
363
+ this.persist();
364
+ return this.respond(`git detected (${ver}).`, true);
365
+ }
366
+ if (this.fakeMode) {
367
+ row.status = 'ok';
368
+ row.verb = 'git version 2.45 installed';
369
+ this.persist();
370
+ return this.respond('Fake-mode git ok.', true);
371
+ }
372
+ this.setRowError(row, 'Verifying git installation', 'git --version returned a non-zero exit code or no output.\n\nOn macOS, run `xcode-select --install` to install the Command Line Developer Tools, then retry. On Windows, install Git for Windows from https://git-scm.com/download/win.', [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
373
+ this.persist();
374
+ return this.respond('git not detected.', false);
375
+ }
376
+ async runAgentRow(request) {
377
+ const row = this.getRow('agent');
378
+ // If the user picked an alternative through the error frame, switch and continue.
379
+ if (request.errorActionId === 'alternative' && request.alternativeAgentId) {
380
+ this.state.agentId = request.alternativeAgentId;
381
+ row.verb = pendingVerbForAgent(this.state.agentId);
382
+ }
383
+ if (request.errorActionId === 'skip') {
384
+ row.status = 'manual-required';
385
+ row.verb = 'you said you\'ll set up the agent yourself';
386
+ // If the user skipped agent install, sign-in is also their problem —
387
+ // there's no agent to sign into. Mark agent-login manual-required so
388
+ // the auto-progress loop doesn't try to probe a nonexistent CLI on
389
+ // the next Continue click.
390
+ const loginRow = this.getRow('agent-login');
391
+ if (loginRow.status !== 'ok') {
392
+ loginRow.status = 'manual-required';
393
+ loginRow.verb = "you'll handle sign-in when you set up the agent";
394
+ }
395
+ this.persist();
396
+ return this.respond('Agent setup deferred.', true);
397
+ }
398
+ const option = findAgentOption(this.state.agentId);
399
+ if (!option) {
400
+ this.setRowError(row, 'Looking up the AI agent install command', `Unknown agent id: ${this.state.agentId}`, [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
401
+ this.persist();
402
+ return this.respond('Unknown agent.', false);
403
+ }
404
+ // Fake mode short-circuit — never call real OS commands. The whole point
405
+ // of fake mode is hermetic, deterministic test rendering.
406
+ if (this.fakeMode === 'agent-install-fails') {
407
+ this.setRowError(row, `Installing ${option.label} via npm install -g ${option.installPackage}`, this.fakeStderr, this.buildAgentInstallActions(this.state.agentId));
408
+ this.persist();
409
+ return this.respond('Fake-mode agent install failed.', false);
410
+ }
411
+ if (this.fakeMode) {
412
+ row.status = 'ok';
413
+ row.verb = `${option.label} installed (fake-mode)`;
414
+ this.persist();
415
+ return this.respond('Fake-mode agent installed.', true);
416
+ }
417
+ // Real path: detect first — if already installed we skip install entirely.
418
+ const existingVer = commandVersion(option.launchCommand);
419
+ if (existingVer) {
420
+ row.status = 'ok';
421
+ row.verb = `${option.label} ${existingVer} detected`;
422
+ row.detail = existingVer;
423
+ this.persist();
424
+ return this.respond(`${option.label} already installed.`, true);
425
+ }
426
+ // Real install — npm install -g <package>. Stream output into row.streamOutput.
427
+ const result = (0, child_process_1.spawnSync)('npm', ['install', '-g', option.installPackage], {
428
+ encoding: 'utf8',
429
+ timeout: 300000,
430
+ });
431
+ const combined = `${result.stdout || ''}${result.stderr || ''}`.trim();
432
+ row.streamOutput = combined.slice(-4000);
433
+ if (result.status === 0) {
434
+ const ver = commandVersion(option.launchCommand) ?? 'installed';
435
+ row.status = 'ok';
436
+ row.verb = `${option.label} ${ver}`;
437
+ row.detail = ver;
438
+ this.persist();
439
+ return this.respond(`${option.label} installed.`, true);
440
+ }
441
+ this.setRowError(row, `Installing ${option.label} via npm install -g ${option.installPackage}`, combined || `npm install -g ${option.installPackage} exited ${result.status}`, this.buildAgentInstallActions(this.state.agentId));
442
+ this.persist();
443
+ return this.respond(`${option.label} install failed.`, false);
444
+ }
445
+ buildAgentInstallActions(currentAgentId) {
446
+ const others = types_1.FIRST_RUN_AGENT_OPTIONS.filter((option) => option.id !== currentAgentId);
447
+ const actions = [
448
+ { id: 'retry', label: 'Retry', variant: 'primary' },
449
+ ];
450
+ if (others.length > 0) {
451
+ actions.push({
452
+ id: 'alternative',
453
+ label: `Try ${others[0].label} instead`,
454
+ variant: 'secondary',
455
+ alternativeAgentId: others[0].id,
456
+ });
457
+ }
458
+ actions.push({ id: 'skip', label: 'Skip and continue', variant: 'ghost' });
459
+ return actions;
460
+ }
461
+ async runAgentLoginRow(request) {
462
+ const row = this.getRow('agent-login');
463
+ if (request.errorActionId === 'skip') {
464
+ row.status = 'manual-required';
465
+ row.verb = "you'll sign in later from the Hub";
466
+ this.persist();
467
+ return this.respond('Agent login deferred.', true);
468
+ }
469
+ const option = findAgentOption(this.state.agentId);
470
+ if (!option) {
471
+ this.setRowError(row, 'Resolving agent login command', `Unknown agent id: ${this.state.agentId}`, [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
472
+ this.persist();
473
+ return this.respond('Unknown agent for login.', false);
474
+ }
475
+ if (this.fakeMode) {
476
+ row.status = 'ok';
477
+ row.verb = 'Signed in (fake-mode)';
478
+ this.persist();
479
+ return this.respond('Fake-mode signed in.', true);
480
+ }
481
+ // First, see whether the agent is already authenticated.
482
+ const probe = this.probeAgentReady(this.state.agentId);
483
+ if (probe.ok) {
484
+ row.status = 'ok';
485
+ row.verb = 'Signed in (verified)';
486
+ this.persist();
487
+ return this.respond(`${option.label} signed in.`, true);
488
+ }
489
+ // Not authenticated yet — surface as manual-required with a clear message.
490
+ // We do NOT auto-spawn `claude login` from here because OAuth flows differ
491
+ // by agent and need the user's actual browser. The client renders a
492
+ // "Sign in to Claude in the new tab" prompt and polls back via Retry.
493
+ row.status = 'manual-required';
494
+ row.verb = `Sign in to ${option.label} (then click Retry)`;
495
+ row.manualMessage = `Open a terminal and run \`${option.loginCommand}\`. We'll detect when you're signed in.`;
496
+ this.persist();
497
+ return this.respond(`Awaiting ${option.label} login.`, true);
498
+ }
499
+ probeAgentReady(agentId) {
500
+ if (this.fakeMode)
501
+ return { ok: this.fakeMode !== 'agent-install-fails', output: 'fake' };
502
+ const option = findAgentOption(agentId);
503
+ if (!option)
504
+ return { ok: false, output: 'unknown agent' };
505
+ if (agentId === 'codex') {
506
+ const result = (0, child_process_1.spawnSync)('codex', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'], {
507
+ cwd: this.state.workspacePath || process.cwd(),
508
+ input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
509
+ encoding: 'utf8',
510
+ timeout: 60000,
511
+ });
512
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
513
+ return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
514
+ }
515
+ if (agentId === 'claude-code') {
516
+ const result = (0, child_process_1.spawnSync)('claude', ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'], {
517
+ cwd: this.state.workspacePath || process.cwd(),
518
+ input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
519
+ encoding: 'utf8',
520
+ timeout: 60000,
521
+ });
522
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
523
+ return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
524
+ }
525
+ // Gemini probe is `--help` based until Gemini ships a stronger non-interactive path.
526
+ const result = (0, child_process_1.spawnSync)('gemini', ['--help'], {
527
+ cwd: this.state.workspacePath || process.cwd(),
528
+ encoding: 'utf8',
529
+ timeout: 30000,
530
+ });
531
+ return { ok: result.status === 0, output: `${result.stdout || ''}${result.stderr || ''}`.trim() };
532
+ }
533
+ async runProjectRow(request) {
534
+ const row = this.getRow('project');
535
+ // Skip-and-continue contract: a user who explicitly skips this row chose
536
+ // to set the project up themselves later. Mark manual-required so the
537
+ // primary-button derivation can route to Open Hub via the skip-path rule.
538
+ if (request.errorActionId === 'skip') {
539
+ row.status = 'manual-required';
540
+ row.verb = "you'll pick a project folder later";
541
+ this.clearRowError(row);
542
+ this.persist();
543
+ return this.respond('Project deferred.', true);
544
+ }
545
+ const projectPath = request.projectPath ?? this.state.workspacePath;
546
+ if (!projectPath) {
547
+ row.status = 'manual-required';
548
+ row.verb = 'pick a folder where FRAIM should work';
549
+ this.persist();
550
+ return this.respond('Project path required.', false);
551
+ }
552
+ const resolvedPath = path_1.default.resolve(projectPath);
553
+ fs_1.default.mkdirSync(resolvedPath, { recursive: true });
554
+ if (commandExists('git')) {
555
+ try {
556
+ (0, child_process_1.execSync)('git rev-parse --git-dir', { cwd: resolvedPath, stdio: 'ignore' });
557
+ }
558
+ catch {
559
+ try {
560
+ (0, child_process_1.execSync)('git init', { cwd: resolvedPath, stdio: 'ignore' });
561
+ }
562
+ catch (gitError) {
563
+ // Surface but don't fail the row — init-project will still work.
564
+ appendInstallLog(`git init failed at ${resolvedPath}: ${gitError instanceof Error ? gitError.message : 'unknown'}`);
565
+ }
566
+ }
567
+ }
568
+ if (!this.fakeMode) {
569
+ // Configure FRAIM globals + agent MCP wiring before init-project runs.
570
+ (0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
571
+ const selectedAgent = (0, ide_detector_1.findIDEByName)(this.state.agentId);
572
+ const targetNames = selectedAgent ? [selectedAgent.name] : undefined;
573
+ try {
574
+ await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, targetNames, {});
575
+ }
576
+ catch (mcpError) {
577
+ appendInstallLog(`autoConfigureMCP non-fatal error: ${mcpError instanceof Error ? mcpError.message : 'unknown'}`);
578
+ }
579
+ // Pass failHard:'throw' so a sync 401 / network error / missing-key
580
+ // bubbles back here instead of calling process.exit(1) and killing the
581
+ // FRE server. The catch below converts it into the R6 error frame.
582
+ try {
583
+ await (0, init_project_1.runInitProject)({ projectRoot: resolvedPath, failHard: 'throw' });
584
+ }
585
+ catch (initError) {
586
+ const detail = initError instanceof Error ? initError.message : 'Unknown init error';
587
+ // Skip is intentionally omitted on the project row — without a
588
+ // project there is no Hub to open, so deferring this row would
589
+ // leave the user wedged. Retry is always offered; if the failure
590
+ // is a 401 from remote sync, the verbatim message tells the user
591
+ // their API key is the problem and they need to re-run with a
592
+ // valid key.
593
+ this.setRowError(row, `Initializing FRAIM in ${resolvedPath}`, detail, [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
594
+ appendInstallLog(`project-init-failed ${resolvedPath}: ${detail}`);
595
+ this.persist();
596
+ return this.respond(`Initialization failed: ${detail}`, false);
597
+ }
598
+ }
599
+ this.state.workspacePath = resolvedPath;
600
+ row.status = 'ok';
601
+ row.verb = resolvedPath;
602
+ this.persist();
603
+ appendInstallLog(`project-initialized ${resolvedPath}`);
604
+ return this.respond(`Initialized FRAIM in ${resolvedPath}.`, true);
605
+ }
606
+ /**
607
+ * Update the current agent selection (inline `Change…` picker). Recomputes
608
+ * the agent and agent-login row verbs but does not run any installs.
609
+ */
610
+ changeAgent(req) {
611
+ if (req.customAgent) {
612
+ this.state.customAgent = { name: req.customAgent.name, invocationPrefix: req.customAgent.invocationPrefix };
613
+ const agentRow = this.getRow('agent');
614
+ const loginRow = this.getRow('agent-login');
615
+ agentRow.status = 'ok';
616
+ agentRow.verb = `Custom CLI recorded: ${req.customAgent.name}`;
617
+ agentRow.detail = `(advanced — Hub will not auto-invoke unknown CLIs in v1)`;
618
+ loginRow.status = 'manual-required';
619
+ loginRow.verb = `Sign in to ${req.customAgent.name} yourself`;
620
+ this.persist();
621
+ appendInstallLog(`custom-agent recorded ${req.customAgent.name}`);
622
+ return this.respond(`Custom agent "${req.customAgent.name}" recorded.`, true);
623
+ }
624
+ if (!req.agentId) {
625
+ return this.respond('agentId or customAgent required.', false);
626
+ }
627
+ const option = findAgentOption(req.agentId);
628
+ if (!option) {
629
+ return this.respond(`Unknown agent: ${req.agentId}`, false);
630
+ }
631
+ const previousAgentId = this.state.agentId;
632
+ this.state.agentId = req.agentId;
633
+ delete this.state.customAgent;
634
+ const agentRow = this.getRow('agent');
635
+ const loginRow = this.getRow('agent-login');
636
+ // If the user actually switched to a different agent, the row state from
637
+ // the previous agent is now stale — even if it was `ok`. The new agent
638
+ // has not been detected, installed, verified, or signed in. Re-detect
639
+ // the new agent and reset both rows so the next run executes against
640
+ // the new selection rather than silently keeping the previous verbs.
641
+ if (previousAgentId !== req.agentId) {
642
+ this.clearRowError(agentRow);
643
+ const detectedVer = this.fakeMode ? null : commandVersion(option.launchCommand);
644
+ if (detectedVer) {
645
+ agentRow.status = 'ok';
646
+ agentRow.verb = `${option.label} ${detectedVer} detected`;
647
+ agentRow.detail = detectedVer;
648
+ }
649
+ else {
650
+ agentRow.status = 'pending';
651
+ agentRow.verb = pendingVerbForAgent(req.agentId);
652
+ delete agentRow.detail;
653
+ }
654
+ // Login state never carries across agents.
655
+ this.clearRowError(loginRow);
656
+ const loginReady = agentRow.status === 'ok' && this.probeAgentReady(req.agentId).ok;
657
+ if (loginReady) {
658
+ loginRow.status = 'ok';
659
+ loginRow.verb = 'Signed in (verified)';
660
+ }
661
+ else {
662
+ loginRow.status = 'pending';
663
+ loginRow.verb = "you'll sign in after install";
664
+ }
665
+ }
666
+ else {
667
+ // Same agent re-selected — only normalize verbs if rows were not yet ok.
668
+ if (agentRow.status !== 'ok') {
669
+ agentRow.verb = pendingVerbForAgent(req.agentId);
670
+ this.clearRowError(agentRow);
671
+ agentRow.status = 'pending';
672
+ }
673
+ if (loginRow.status !== 'ok') {
674
+ loginRow.verb = "you'll sign in after install";
675
+ loginRow.status = 'pending';
676
+ }
677
+ }
678
+ this.persist();
679
+ appendInstallLog(`agent-changed ${req.agentId}`);
680
+ return this.respond(`Selected ${option.label}.`, true);
681
+ }
682
+ finish() {
683
+ ensureOutputDirs();
684
+ fs_1.default.writeFileSync(getNextPromptPath(), `${types_1.FIRST_RUN_PROMPT}${os_1.default.EOL}`, 'utf8');
685
+ this.state.nextPrompt = types_1.FIRST_RUN_PROMPT;
686
+ this.persist();
687
+ appendInstallLog('finish prompt-written');
688
+ return this.respond('Wrote the next prompt artifact and completed first-run.', true);
689
+ }
690
+ /**
691
+ * Start the Hub server for the chosen project and open the user's browser
692
+ * to it. Returns the Hub URL so the client can show a clear "we're at <url>"
693
+ * message instead of a CLI hand-off — the whole point of v1 is to never
694
+ * leave the user typing a command. The single-file launcher binary in v2
695
+ * (#355) replaces this in-process spawn with a durable tray-icon launcher.
696
+ */
697
+ async openHub() {
698
+ if (!this.state.workspacePath) {
699
+ return { ok: false, message: 'Pick a project folder before opening the Hub.' };
700
+ }
701
+ if (this.fakeMode) {
702
+ // Tests don't actually want a Hub server running — just confirm intent.
703
+ return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/' };
704
+ }
705
+ try {
706
+ const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../ai-hub/server')));
707
+ const port = await findAvailablePort(43091);
708
+ const hubServer = new AiHubServer({ projectPath: this.state.workspacePath });
709
+ await hubServer.start(port);
710
+ const hubUrl = `http://127.0.0.1:${port}/ai-hub/`;
711
+ this.openBrowser(hubUrl);
712
+ appendInstallLog(`hub-opened ${hubUrl}`);
713
+ return {
714
+ ok: true,
715
+ message: `Hub is open at ${hubUrl}. From now on, run \`npx fraim-framework@latest hub --browser\` to launch it again — the standalone launcher binary ships in v2 (#355).`,
716
+ hubUrl,
717
+ };
718
+ }
719
+ catch (error) {
720
+ const detail = error instanceof Error ? error.message : 'Unknown error';
721
+ appendInstallLog(`hub-open-failed ${detail}`);
722
+ return {
723
+ ok: false,
724
+ message: `Could not open the Hub automatically: ${detail}. Run \`npx fraim-framework@latest hub --browser\` from a terminal to open it manually.`,
725
+ };
726
+ }
727
+ }
728
+ openBrowser(url) {
729
+ try {
730
+ if (process.platform === 'win32') {
731
+ (0, child_process_1.spawn)('cmd.exe', ['/d', '/s', '/c', `start "" "${url}"`], { detached: true, stdio: 'ignore' }).unref();
732
+ return;
733
+ }
734
+ if (process.platform === 'darwin') {
735
+ (0, child_process_1.spawn)('open', [url], { detached: true, stdio: 'ignore' }).unref();
736
+ return;
737
+ }
738
+ (0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
739
+ }
740
+ catch {
741
+ // Best-effort — fall through; the URL is already in the response so the
742
+ // client surfaces it for the user.
743
+ }
744
+ }
745
+ }
746
+ exports.FirstRunSessionService = FirstRunSessionService;