fraim 2.0.129 → 2.0.131
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.
- package/dist/src/ai-hub/catalog.js +7 -4
- package/dist/src/cli/commands/init-project.js +55 -121
- package/dist/src/cli/commands/setup.js +68 -43
- package/dist/src/cli/commands/workspace-config.js +31 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-global-integration.js +19 -0
- package/dist/src/cli/setup/user-level-sync.js +5 -0
- package/dist/src/cli/utils/project-bootstrap.js +3 -3
- package/dist/src/core/fraim-config-contract.js +145 -0
- package/dist/src/core/fraim-config-schema.generated.js +296 -0
- package/dist/src/core/utils/setup-preferences.js +41 -0
- package/dist/src/first-run/server.js +59 -4
- package/dist/src/first-run/session-service.js +267 -364
- package/dist/src/first-run/types.js +10 -21
- package/dist/src/local-mcp-server/stdio-server.js +28 -29
- package/dist/src/local-mcp-server/usage-collector.js +3 -0
- package/index.js +1 -1
- package/package.json +7 -5
- package/public/ai-hub/script.js +187 -1
- package/public/first-run/error-frame.js +100 -100
- package/public/first-run/index.html +5 -6
- package/public/first-run/script.js +275 -238
- package/public/first-run/styles.css +603 -395
|
@@ -43,9 +43,9 @@ const path_1 = __importDefault(require("path"));
|
|
|
43
43
|
const crypto_1 = __importDefault(require("crypto"));
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const ide_detector_1 = require("../cli/setup/ide-detector");
|
|
46
|
+
const ide_global_integration_1 = require("../cli/setup/ide-global-integration");
|
|
46
47
|
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
47
48
|
const setup_1 = require("../cli/commands/setup");
|
|
48
|
-
const init_project_1 = require("../cli/commands/init-project");
|
|
49
49
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
50
50
|
const types_1 = require("./types");
|
|
51
51
|
Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
|
|
@@ -54,31 +54,23 @@ function getFakeStateMode() {
|
|
|
54
54
|
if (process.env.FRAIM_FIRST_RUN_FAKE !== '1')
|
|
55
55
|
return null;
|
|
56
56
|
const explicit = process.env.FRAIM_FIRST_RUN_FAKE_STATE;
|
|
57
|
-
if (explicit === 'all-pending' || explicit === 'all-detected' || explicit === 'agent-install-fails') {
|
|
57
|
+
if (explicit === 'all-pending' || explicit === 'all-detected' || explicit === 'agent-install-fails' || explicit === 'no-agents') {
|
|
58
58
|
return explicit;
|
|
59
59
|
}
|
|
60
60
|
return 'default';
|
|
61
61
|
}
|
|
62
|
-
function
|
|
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) {
|
|
62
|
+
function commandVersion(command, extraBinDir) {
|
|
75
63
|
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
76
64
|
const args = process.platform === 'win32'
|
|
77
65
|
? ['/d', '/s', '/c', `${command} --version`]
|
|
78
66
|
: ['--version'];
|
|
67
|
+
const env = extraBinDir
|
|
68
|
+
? { ...process.env, PATH: `${extraBinDir}${path_1.default.delimiter}${process.env.PATH || ''}` }
|
|
69
|
+
: undefined;
|
|
79
70
|
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
80
71
|
encoding: 'utf8',
|
|
81
72
|
timeout: 5000,
|
|
73
|
+
...(env ? { env } : {}),
|
|
82
74
|
});
|
|
83
75
|
if (result.status !== 0)
|
|
84
76
|
return null;
|
|
@@ -89,6 +81,78 @@ function ensureOutputDirs() {
|
|
|
89
81
|
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
90
82
|
fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
|
|
91
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns the directory that contains node/npm/npx executables for FRAIM's
|
|
86
|
+
* portable Node installation.
|
|
87
|
+
*
|
|
88
|
+
* - Mac/Linux: ~/.fraim/node/bin (standard Unix layout)
|
|
89
|
+
* - Windows: ~/.fraim/node/node-v<version>-win-x64/ if extracted, else
|
|
90
|
+
* ~/.fraim/node/ as fallback (executables live at the root on Windows)
|
|
91
|
+
*/
|
|
92
|
+
function getFraimNodeBinPath() {
|
|
93
|
+
const nodeRoot = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
94
|
+
if (process.platform === 'win32') {
|
|
95
|
+
if (fs_1.default.existsSync(nodeRoot)) {
|
|
96
|
+
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
97
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
98
|
+
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
99
|
+
if (extractedDir) {
|
|
100
|
+
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return nodeRoot;
|
|
104
|
+
}
|
|
105
|
+
return path_1.default.join(nodeRoot, 'bin');
|
|
106
|
+
}
|
|
107
|
+
// Prepend the portable Node bin dir to process PATH once at module load so
|
|
108
|
+
// every spawnSync call (detection, login probe, change-agent) finds binaries
|
|
109
|
+
// installed there without needing per-call path overrides.
|
|
110
|
+
(function bootstrapFraimNodeBin() {
|
|
111
|
+
const fraimNodeBin = getFraimNodeBinPath();
|
|
112
|
+
const current = process.env.PATH || '';
|
|
113
|
+
if (!current.split(path_1.default.delimiter).includes(fraimNodeBin)) {
|
|
114
|
+
process.env.PATH = `${fraimNodeBin}${path_1.default.delimiter}${current}`;
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
function persistShellPath() {
|
|
118
|
+
const fraimNodeBin = getFraimNodeBinPath();
|
|
119
|
+
const marker = '# FRAIM managed binaries';
|
|
120
|
+
const exportLine = 'export PATH="$HOME/.fraim/node/bin:$PATH"';
|
|
121
|
+
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
122
|
+
if (process.platform === 'win32') {
|
|
123
|
+
const escapedBin = fraimNodeBin.replace(/'/g, "''");
|
|
124
|
+
// Guard pattern matches both the versioned subdirectory and the root fallback.
|
|
125
|
+
const psCmd = [
|
|
126
|
+
`$bin = '${escapedBin}'`,
|
|
127
|
+
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
128
|
+
`if ($cur -notlike "*$bin*") { [Environment]::SetEnvironmentVariable('PATH', "$bin;$cur", 'User') }`,
|
|
129
|
+
].join('; ');
|
|
130
|
+
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const profiles = [
|
|
134
|
+
path_1.default.join(os_1.default.homedir(), '.zshrc'),
|
|
135
|
+
path_1.default.join(os_1.default.homedir(), '.bash_profile'),
|
|
136
|
+
path_1.default.join(os_1.default.homedir(), '.bashrc'),
|
|
137
|
+
];
|
|
138
|
+
// macOS defaults to zsh since Catalina — create .zshrc if it doesn't exist.
|
|
139
|
+
if (process.platform === 'darwin' && !fs_1.default.existsSync(profiles[0])) {
|
|
140
|
+
fs_1.default.writeFileSync(profiles[0], stanza, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
for (const profile of profiles) {
|
|
143
|
+
try {
|
|
144
|
+
if (!fs_1.default.existsSync(profile))
|
|
145
|
+
continue;
|
|
146
|
+
const content = fs_1.default.readFileSync(profile, 'utf8');
|
|
147
|
+
if (content.includes(marker))
|
|
148
|
+
continue;
|
|
149
|
+
fs_1.default.appendFileSync(profile, stanza, 'utf8');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore unreadable or missing profiles.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
92
156
|
function getNextPromptPath() {
|
|
93
157
|
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install', 'next-prompt.txt');
|
|
94
158
|
}
|
|
@@ -99,16 +163,46 @@ function appendInstallLog(line) {
|
|
|
99
163
|
ensureOutputDirs();
|
|
100
164
|
fs_1.default.appendFileSync(getInstallLogPath(), `[${new Date().toISOString()}] ${line}${os_1.default.EOL}`);
|
|
101
165
|
}
|
|
166
|
+
function runProcess(command, args, env) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
|
|
169
|
+
const child = (0, child_process_1.spawn)(executable, args, {
|
|
170
|
+
env: { ...process.env, ...(env || {}) },
|
|
171
|
+
shell: false,
|
|
172
|
+
});
|
|
173
|
+
let stdout = '';
|
|
174
|
+
let stderr = '';
|
|
175
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
176
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
177
|
+
child.on('error', reject);
|
|
178
|
+
child.on('close', (code) => {
|
|
179
|
+
if (code === 0) {
|
|
180
|
+
resolve({ stdout, stderr });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
reject(new Error(`${command} ${args.join(' ')} failed with code ${code}\n${stdout}${stderr}`.trim()));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
102
187
|
function findAgentOption(agentId) {
|
|
103
188
|
return types_1.FIRST_RUN_AGENT_OPTIONS.find((option) => option.id === agentId);
|
|
104
189
|
}
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
190
|
+
function normalizeRows(rows) {
|
|
191
|
+
const existingById = new Map(rows.map((row) => [row.id, row]));
|
|
192
|
+
return (0, types_1.createInitialRows)().map((canonical) => ({
|
|
193
|
+
...canonical,
|
|
194
|
+
...(existingById.get(canonical.id) || {}),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
function buildConfiguredSurfaces() {
|
|
198
|
+
const ides = (0, ide_detector_1.detectInstalledIDEs)();
|
|
199
|
+
const hints = (0, ide_global_integration_1.describeOnboardingInvocationSurfaces)(ides);
|
|
200
|
+
return ides.map((ide, index) => ({
|
|
201
|
+
id: ide.configType,
|
|
202
|
+
name: ide.name,
|
|
203
|
+
invocationHint: hints[index] || '/fraim onboard this project',
|
|
204
|
+
status: 'configured',
|
|
205
|
+
}));
|
|
112
206
|
}
|
|
113
207
|
class FirstRunSessionService {
|
|
114
208
|
constructor(options) {
|
|
@@ -120,6 +214,7 @@ class FirstRunSessionService {
|
|
|
120
214
|
['npm error code EACCES', 'npm error syscall mkdir'].join('\n');
|
|
121
215
|
this.requestToken = crypto_1.default.randomUUID();
|
|
122
216
|
this.state = options.resume ? (0, install_state_1.loadFirstRunState)() || (0, install_state_1.createInitialFirstRunState)(options.key) : (0, install_state_1.createInitialFirstRunState)(options.key);
|
|
217
|
+
this.state.rows = normalizeRows(this.state.rows);
|
|
123
218
|
if (options.projectRoot) {
|
|
124
219
|
this.state.workspacePath = path_1.default.resolve(options.projectRoot);
|
|
125
220
|
(0, install_state_1.saveFirstRunState)(this.state);
|
|
@@ -131,7 +226,7 @@ class FirstRunSessionService {
|
|
|
131
226
|
getRow(rowId) {
|
|
132
227
|
const row = this.state.rows.find((r) => r.id === rowId);
|
|
133
228
|
if (!row) {
|
|
134
|
-
throw new Error(`Row ${rowId} not found in state
|
|
229
|
+
throw new Error(`Row ${rowId} not found in state — initial rows must include all canonical IDs`);
|
|
135
230
|
}
|
|
136
231
|
return row;
|
|
137
232
|
}
|
|
@@ -141,13 +236,13 @@ class FirstRunSessionService {
|
|
|
141
236
|
}
|
|
142
237
|
setRowError(row, whatTried, whatHappened, actions, hint) {
|
|
143
238
|
row.status = 'error';
|
|
144
|
-
row.verb = 'failed
|
|
239
|
+
row.verb = 'failed — see below';
|
|
145
240
|
row.errorFrame = { whatTried, whatHappened, actions, ...(hint ? { hint } : {}) };
|
|
146
241
|
}
|
|
147
242
|
/**
|
|
148
243
|
* Classify a thrown init/sync error into a non-tech-friendly hint.
|
|
149
244
|
* The verbatim stderr is preserved separately in `whatHappened` per
|
|
150
|
-
* R6.3
|
|
245
|
+
* R6.3 — the hint is additive context, not a replacement.
|
|
151
246
|
*/
|
|
152
247
|
classifyInitError(detail) {
|
|
153
248
|
if (/status code 401|Unauthorized|401\b/i.test(detail)) {
|
|
@@ -157,13 +252,13 @@ class FirstRunSessionService {
|
|
|
157
252
|
return "The FRAIM registry refused this key for this project. Check that your install key has access to the registry you're syncing from.";
|
|
158
253
|
}
|
|
159
254
|
if (/status code 5\d\d/i.test(detail) || /5\d\d\b/.test(detail)) {
|
|
160
|
-
return 'The FRAIM registry returned a server error. This is usually transient
|
|
255
|
+
return 'The FRAIM registry returned a server error. This is usually transient — wait a minute and click Retry. If it keeps failing, the registry may be down.';
|
|
161
256
|
}
|
|
162
257
|
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|getaddrinfo|network/i.test(detail)) {
|
|
163
258
|
return "Couldn't reach the FRAIM registry. Check your internet connection (or if you're testing locally, that the local FRAIM server is running) and click Retry.";
|
|
164
259
|
}
|
|
165
260
|
if (/Global FRAIM setup not found/i.test(detail)) {
|
|
166
|
-
return 'FRAIM\'s global config is missing. This usually means the agent step didn\'t complete cleanly
|
|
261
|
+
return 'FRAIM\'s global config is missing. This usually means the agent step didn\'t complete cleanly — go back and re-run the AI agent row.';
|
|
167
262
|
}
|
|
168
263
|
return undefined;
|
|
169
264
|
}
|
|
@@ -172,7 +267,7 @@ class FirstRunSessionService {
|
|
|
172
267
|
this.applyFakeStateOnLoad(this.fakeMode);
|
|
173
268
|
return;
|
|
174
269
|
}
|
|
175
|
-
// Real detection
|
|
270
|
+
// Real detection — populate row statuses from `command -v` style probes.
|
|
176
271
|
const nodeRow = this.getRow('node');
|
|
177
272
|
const nodeVer = commandVersion('node');
|
|
178
273
|
if (nodeVer) {
|
|
@@ -193,53 +288,47 @@ class FirstRunSessionService {
|
|
|
193
288
|
gitRow.status = 'pending';
|
|
194
289
|
gitRow.verb = "we'll install";
|
|
195
290
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
loginRow.status = 'ok';
|
|
211
|
-
loginRow.verb = 'Signed in (verified)';
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
loginRow.status = 'pending';
|
|
215
|
-
loginRow.verb = "you'll sign in after install";
|
|
216
|
-
}
|
|
217
|
-
const projectRow = this.getRow('project');
|
|
218
|
-
if (this.state.workspacePath && fs_1.default.existsSync(this.state.workspacePath)) {
|
|
219
|
-
projectRow.status = 'ok';
|
|
220
|
-
projectRow.verb = this.state.workspacePath;
|
|
291
|
+
const fraimRow = this.getRow('fraim');
|
|
292
|
+
if (commandVersion('npx') !== null) {
|
|
293
|
+
// Only mark ok if setup has previously completed (config.json is written after autoConfigureMCP).
|
|
294
|
+
// Without this, the wizard skips runFraimRow entirely and autoConfigureMCP never runs for IDEs.
|
|
295
|
+
const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
|
|
296
|
+
if (fs_1.default.existsSync(globalConfigPath)) {
|
|
297
|
+
fraimRow.status = 'ok';
|
|
298
|
+
fraimRow.verb = 'ready';
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
fraimRow.status = 'pending';
|
|
302
|
+
fraimRow.verb = "we'll configure your IDEs";
|
|
303
|
+
}
|
|
304
|
+
persistShellPath();
|
|
221
305
|
}
|
|
222
306
|
else {
|
|
223
|
-
|
|
224
|
-
|
|
307
|
+
fraimRow.status = 'pending';
|
|
308
|
+
fraimRow.verb = "we'll set up FRAIM";
|
|
225
309
|
}
|
|
310
|
+
this.updateAgentSummaryRow();
|
|
226
311
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Also accept matches via the IDE detector for desktop variants.
|
|
235
|
-
const ides = (0, ide_detector_1.detectInstalledIDEs)();
|
|
236
|
-
for (const option of types_1.FIRST_RUN_AGENT_OPTIONS) {
|
|
237
|
-
const ide = ides.find((entry) => entry.aliases?.some((a) => option.detectAliases.includes(a)));
|
|
238
|
-
if (ide) {
|
|
239
|
-
return { id: option.id, label: option.label, versionLine: null };
|
|
240
|
-
}
|
|
312
|
+
updateAgentSummaryRow() {
|
|
313
|
+
const agentRow = this.getRow('agent');
|
|
314
|
+
const surfaces = this.fakeMode === 'no-agents' ? [] : buildConfiguredSurfaces();
|
|
315
|
+
if (surfaces.length > 0) {
|
|
316
|
+
agentRow.status = 'ok';
|
|
317
|
+
agentRow.verb = `${surfaces.length} AI Employee${surfaces.length === 1 ? '' : 's'} ready`;
|
|
318
|
+
agentRow.detail = surfaces.map((surface) => surface.name).join(', ');
|
|
241
319
|
}
|
|
242
|
-
|
|
320
|
+
else {
|
|
321
|
+
agentRow.status = 'manual-required';
|
|
322
|
+
agentRow.verb = "let's recruit AI Employees";
|
|
323
|
+
delete agentRow.detail;
|
|
324
|
+
}
|
|
325
|
+
this.state.setupResult = {
|
|
326
|
+
mode: 'conversational',
|
|
327
|
+
configuredSurfaces: surfaces,
|
|
328
|
+
failedSurfaces: [],
|
|
329
|
+
detectedSurfaceCount: surfaces.length,
|
|
330
|
+
completedAt: new Date().toISOString(),
|
|
331
|
+
};
|
|
243
332
|
}
|
|
244
333
|
applyFakeStateOnLoad(mode) {
|
|
245
334
|
const setStatus = (rowId, status, verb, detail) => {
|
|
@@ -252,33 +341,63 @@ class FirstRunSessionService {
|
|
|
252
341
|
if (mode === 'all-pending') {
|
|
253
342
|
setStatus('node', 'pending', "we'll install");
|
|
254
343
|
setStatus('git', 'pending', "we'll install");
|
|
255
|
-
setStatus('
|
|
256
|
-
setStatus('agent
|
|
257
|
-
|
|
344
|
+
setStatus('fraim', 'pending', "we'll install");
|
|
345
|
+
setStatus('agent', 'pending', "we'll check for AI Employees");
|
|
346
|
+
delete this.state.setupResult;
|
|
258
347
|
return;
|
|
259
348
|
}
|
|
260
349
|
if (mode === 'all-detected') {
|
|
261
350
|
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
262
351
|
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
263
|
-
setStatus('
|
|
264
|
-
setStatus('agent
|
|
265
|
-
|
|
352
|
+
setStatus('fraim', 'ok', 'fraim-framework detected');
|
|
353
|
+
setStatus('agent', 'ok', '1 AI Employee ready', 'Claude Code');
|
|
354
|
+
this.state.setupResult = {
|
|
355
|
+
mode: 'conversational',
|
|
356
|
+
configuredSurfaces: [
|
|
357
|
+
{ id: 'claude-code', name: 'Claude Code', invocationHint: 'Claude Code: /fraim onboard this project', status: 'configured' },
|
|
358
|
+
],
|
|
359
|
+
failedSurfaces: [],
|
|
360
|
+
detectedSurfaceCount: 1,
|
|
361
|
+
completedAt: new Date().toISOString(),
|
|
362
|
+
};
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (mode === 'no-agents') {
|
|
366
|
+
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
367
|
+
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
368
|
+
setStatus('fraim', 'ok', 'fraim-framework detected');
|
|
369
|
+
setStatus('agent', 'manual-required', "let's recruit AI Employees");
|
|
370
|
+
this.state.setupResult = {
|
|
371
|
+
mode: 'conversational',
|
|
372
|
+
configuredSurfaces: [],
|
|
373
|
+
failedSurfaces: [],
|
|
374
|
+
detectedSurfaceCount: 0,
|
|
375
|
+
completedAt: new Date().toISOString(),
|
|
376
|
+
};
|
|
266
377
|
return;
|
|
267
378
|
}
|
|
268
379
|
if (mode === 'agent-install-fails') {
|
|
269
380
|
setStatus('node', 'ok', 'v20.11.1 installed');
|
|
270
381
|
setStatus('git', 'ok', '2.45 installed');
|
|
271
|
-
setStatus('
|
|
272
|
-
setStatus('agent
|
|
273
|
-
|
|
382
|
+
setStatus('fraim', 'ok', 'fraim-framework installed');
|
|
383
|
+
setStatus('agent', 'pending', "we'll check for AI Employees");
|
|
384
|
+
delete this.state.setupResult;
|
|
274
385
|
return;
|
|
275
386
|
}
|
|
276
|
-
// 'default' fake mode
|
|
387
|
+
// 'default' fake mode — infra rows all ok.
|
|
277
388
|
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
278
389
|
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
279
|
-
setStatus('
|
|
280
|
-
setStatus('agent
|
|
281
|
-
|
|
390
|
+
setStatus('fraim', 'ok', 'fake-mode fraim installed');
|
|
391
|
+
setStatus('agent', 'ok', 'fake-mode AI Employee ready', 'Claude Code');
|
|
392
|
+
this.state.setupResult = {
|
|
393
|
+
mode: 'conversational',
|
|
394
|
+
configuredSurfaces: [
|
|
395
|
+
{ id: 'claude-code', name: 'Claude Code', invocationHint: 'Claude Code: /fraim onboard this project', status: 'configured' },
|
|
396
|
+
],
|
|
397
|
+
failedSurfaces: [],
|
|
398
|
+
detectedSurfaceCount: 1,
|
|
399
|
+
completedAt: new Date().toISOString(),
|
|
400
|
+
};
|
|
282
401
|
}
|
|
283
402
|
getRequestToken() {
|
|
284
403
|
return this.requestToken;
|
|
@@ -326,16 +445,14 @@ class FirstRunSessionService {
|
|
|
326
445
|
return await this.runNodeRow();
|
|
327
446
|
case 'git':
|
|
328
447
|
return await this.runGitRow();
|
|
448
|
+
case 'fraim':
|
|
449
|
+
return await this.runFraimRow();
|
|
329
450
|
case 'agent':
|
|
330
451
|
return await this.runAgentRow(request);
|
|
331
|
-
case 'agent-login':
|
|
332
|
-
return await this.runAgentLoginRow(request);
|
|
333
|
-
case 'project':
|
|
334
|
-
return await this.runProjectRow(request);
|
|
335
452
|
}
|
|
336
453
|
}
|
|
337
454
|
catch (error) {
|
|
338
|
-
// Last-ditch safety net
|
|
455
|
+
// Last-ditch safety net — every row's runner is supposed to surface
|
|
339
456
|
// its own R6 frame via setRowError before throwing, but if anything
|
|
340
457
|
// slips past (e.g. a sync layer that calls process.exit and we narrowly
|
|
341
458
|
// converted it to throw), we still surface a frame here rather than
|
|
@@ -343,7 +460,7 @@ class FirstRunSessionService {
|
|
|
343
460
|
// user is never wedged on a non-recoverable failure of one row.
|
|
344
461
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
345
462
|
row.status = 'error';
|
|
346
|
-
row.verb = 'failed
|
|
463
|
+
row.verb = 'failed — see below';
|
|
347
464
|
row.errorFrame = {
|
|
348
465
|
whatTried: `Running ${row.label} step`,
|
|
349
466
|
whatHappened: message,
|
|
@@ -396,250 +513,83 @@ class FirstRunSessionService {
|
|
|
396
513
|
this.persist();
|
|
397
514
|
return this.respond('git not detected.', false);
|
|
398
515
|
}
|
|
399
|
-
async
|
|
400
|
-
const row = this.getRow('
|
|
401
|
-
// If the user picked an alternative through the error frame, switch and continue.
|
|
402
|
-
if (request.errorActionId === 'alternative' && request.alternativeAgentId) {
|
|
403
|
-
this.state.agentId = request.alternativeAgentId;
|
|
404
|
-
row.verb = pendingVerbForAgent(this.state.agentId);
|
|
405
|
-
}
|
|
406
|
-
if (request.errorActionId === 'skip') {
|
|
407
|
-
row.status = 'manual-required';
|
|
408
|
-
row.verb = 'you said you\'ll set up the agent yourself';
|
|
409
|
-
// If the user skipped agent install, sign-in is also their problem —
|
|
410
|
-
// there's no agent to sign into. Mark agent-login manual-required so
|
|
411
|
-
// the auto-progress loop doesn't try to probe a nonexistent CLI on
|
|
412
|
-
// the next Continue click.
|
|
413
|
-
const loginRow = this.getRow('agent-login');
|
|
414
|
-
if (loginRow.status !== 'ok') {
|
|
415
|
-
loginRow.status = 'manual-required';
|
|
416
|
-
loginRow.verb = "you'll handle sign-in when you set up the agent";
|
|
417
|
-
}
|
|
418
|
-
this.persist();
|
|
419
|
-
return this.respond('Agent setup deferred.', true);
|
|
420
|
-
}
|
|
421
|
-
const option = findAgentOption(this.state.agentId);
|
|
422
|
-
if (!option) {
|
|
423
|
-
this.setRowError(row, 'Looking up the AI agent install command', `Unknown agent id: ${this.state.agentId}`, [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
|
|
424
|
-
this.persist();
|
|
425
|
-
return this.respond('Unknown agent.', false);
|
|
426
|
-
}
|
|
427
|
-
// Fake mode short-circuit — never call real OS commands. The whole point
|
|
428
|
-
// of fake mode is hermetic, deterministic test rendering.
|
|
429
|
-
if (this.fakeMode === 'agent-install-fails') {
|
|
430
|
-
this.setRowError(row, `Installing ${option.label} via npm install -g ${option.installPackage}`, this.fakeStderr, this.buildAgentInstallActions(this.state.agentId));
|
|
431
|
-
this.persist();
|
|
432
|
-
return this.respond('Fake-mode agent install failed.', false);
|
|
433
|
-
}
|
|
516
|
+
async runFraimRow() {
|
|
517
|
+
const row = this.getRow('fraim');
|
|
434
518
|
if (this.fakeMode) {
|
|
435
519
|
row.status = 'ok';
|
|
436
|
-
row.verb =
|
|
437
|
-
this.persist();
|
|
438
|
-
return this.respond('Fake-mode agent installed.', true);
|
|
439
|
-
}
|
|
440
|
-
// Real path: detect first — if already installed we skip install entirely.
|
|
441
|
-
const existingVer = commandVersion(option.launchCommand);
|
|
442
|
-
if (existingVer) {
|
|
443
|
-
row.status = 'ok';
|
|
444
|
-
row.verb = `${option.label} ${existingVer} detected`;
|
|
445
|
-
row.detail = existingVer;
|
|
520
|
+
row.verb = 'FRAIM ready (fake-mode)';
|
|
446
521
|
this.persist();
|
|
447
|
-
return this.respond(
|
|
522
|
+
return this.respond('Fake-mode fraim ok.', true);
|
|
448
523
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
524
|
+
try {
|
|
525
|
+
if (!commandVersion('fraim')) {
|
|
526
|
+
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
527
|
+
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
528
|
+
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
529
|
+
this.persist();
|
|
530
|
+
await runProcess('npm', ['install', '-g', 'fraim-framework@latest'], { npm_config_prefix: prefix });
|
|
531
|
+
}
|
|
532
|
+
persistShellPath();
|
|
533
|
+
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
534
|
+
if (detectedIDEs.length > 0) {
|
|
535
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
|
|
536
|
+
}
|
|
537
|
+
// Write config.json after autoConfigureMCP so detectRowsOnLoad can use its
|
|
538
|
+
// existence as a signal that IDE setup completed. Writing it before would cause
|
|
539
|
+
// the wizard to mark this row 'ok' on retry before IDEs are actually configured.
|
|
540
|
+
(0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
|
|
541
|
+
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../cli/setup/user-level-sync')));
|
|
542
|
+
await syncUserLevelArtifacts();
|
|
543
|
+
const { installSlashCommands, installGlobalRules } = await Promise.resolve().then(() => __importStar(require('../cli/setup/ide-global-integration')));
|
|
544
|
+
await installSlashCommands();
|
|
545
|
+
await installGlobalRules();
|
|
458
546
|
row.status = 'ok';
|
|
459
|
-
row.verb =
|
|
460
|
-
row.
|
|
461
|
-
this.persist();
|
|
462
|
-
return this.respond(`${option.label} installed.`, true);
|
|
463
|
-
}
|
|
464
|
-
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));
|
|
465
|
-
this.persist();
|
|
466
|
-
return this.respond(`${option.label} install failed.`, false);
|
|
467
|
-
}
|
|
468
|
-
buildAgentInstallActions(currentAgentId) {
|
|
469
|
-
const others = types_1.FIRST_RUN_AGENT_OPTIONS.filter((option) => option.id !== currentAgentId);
|
|
470
|
-
const actions = [
|
|
471
|
-
{ id: 'retry', label: 'Retry', variant: 'primary' },
|
|
472
|
-
];
|
|
473
|
-
if (others.length > 0) {
|
|
474
|
-
actions.push({
|
|
475
|
-
id: 'alternative',
|
|
476
|
-
label: `Try ${others[0].label} instead`,
|
|
477
|
-
variant: 'secondary',
|
|
478
|
-
alternativeAgentId: others[0].id,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
actions.push({ id: 'skip', label: 'Skip and continue', variant: 'ghost' });
|
|
482
|
-
return actions;
|
|
483
|
-
}
|
|
484
|
-
async runAgentLoginRow(request) {
|
|
485
|
-
const row = this.getRow('agent-login');
|
|
486
|
-
if (request.errorActionId === 'skip') {
|
|
487
|
-
row.status = 'manual-required';
|
|
488
|
-
row.verb = "you'll sign in later from the Hub";
|
|
489
|
-
this.persist();
|
|
490
|
-
return this.respond('Agent login deferred.', true);
|
|
491
|
-
}
|
|
492
|
-
const option = findAgentOption(this.state.agentId);
|
|
493
|
-
if (!option) {
|
|
494
|
-
this.setRowError(row, 'Resolving agent login command', `Unknown agent id: ${this.state.agentId}`, [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
|
|
547
|
+
row.verb = 'Ready.';
|
|
548
|
+
delete row.streamOutput;
|
|
495
549
|
this.persist();
|
|
496
|
-
return this.respond('
|
|
550
|
+
return this.respond('FRAIM is ready.', true);
|
|
497
551
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
row
|
|
552
|
+
catch (error) {
|
|
553
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
554
|
+
this.setRowError(row, 'Setting up FRAIM on this machine', detail, [
|
|
555
|
+
{ id: 'retry', label: 'Retry', variant: 'primary' },
|
|
556
|
+
{ id: 'skip', label: 'Skip and continue', variant: 'ghost' },
|
|
557
|
+
]);
|
|
501
558
|
this.persist();
|
|
502
|
-
return this.respond('
|
|
559
|
+
return this.respond('FRAIM setup failed.', false);
|
|
503
560
|
}
|
|
504
|
-
// First, see whether the agent is already authenticated.
|
|
505
|
-
const probe = this.probeAgentReady(this.state.agentId);
|
|
506
|
-
if (probe.ok) {
|
|
507
|
-
row.status = 'ok';
|
|
508
|
-
row.verb = 'Signed in (verified)';
|
|
509
|
-
this.persist();
|
|
510
|
-
return this.respond(`${option.label} signed in.`, true);
|
|
511
|
-
}
|
|
512
|
-
// Not authenticated yet — surface as manual-required with a clear message.
|
|
513
|
-
// We do NOT auto-spawn `claude login` from here because OAuth flows differ
|
|
514
|
-
// by agent and need the user's actual browser. The client renders a
|
|
515
|
-
// "Sign in to Claude in the new tab" prompt and polls back via Retry.
|
|
516
|
-
row.status = 'manual-required';
|
|
517
|
-
row.verb = `Sign in to ${option.label} (then click Retry)`;
|
|
518
|
-
row.manualMessage = `Open a terminal and run \`${option.loginCommand}\`. We'll detect when you're signed in.`;
|
|
519
|
-
this.persist();
|
|
520
|
-
return this.respond(`Awaiting ${option.label} login.`, true);
|
|
521
561
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return { ok: this.fakeMode !== 'agent-install-fails', output: 'fake' };
|
|
525
|
-
const option = findAgentOption(agentId);
|
|
526
|
-
if (!option)
|
|
527
|
-
return { ok: false, output: 'unknown agent' };
|
|
528
|
-
if (agentId === 'codex') {
|
|
529
|
-
const result = (0, child_process_1.spawnSync)('codex', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'], {
|
|
530
|
-
cwd: this.state.workspacePath || process.cwd(),
|
|
531
|
-
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
532
|
-
encoding: 'utf8',
|
|
533
|
-
timeout: 60000,
|
|
534
|
-
});
|
|
535
|
-
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
536
|
-
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
537
|
-
}
|
|
538
|
-
if (agentId === 'claude-code') {
|
|
539
|
-
const result = (0, child_process_1.spawnSync)('claude', ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'], {
|
|
540
|
-
cwd: this.state.workspacePath || process.cwd(),
|
|
541
|
-
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
542
|
-
encoding: 'utf8',
|
|
543
|
-
timeout: 60000,
|
|
544
|
-
});
|
|
545
|
-
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
546
|
-
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
547
|
-
}
|
|
548
|
-
// Gemini probe is `--help` based until Gemini ships a stronger non-interactive path.
|
|
549
|
-
const result = (0, child_process_1.spawnSync)('gemini', ['--help'], {
|
|
550
|
-
cwd: this.state.workspacePath || process.cwd(),
|
|
551
|
-
encoding: 'utf8',
|
|
552
|
-
timeout: 30000,
|
|
553
|
-
});
|
|
554
|
-
return { ok: result.status === 0, output: `${result.stdout || ''}${result.stderr || ''}`.trim() };
|
|
555
|
-
}
|
|
556
|
-
async runProjectRow(request) {
|
|
557
|
-
const row = this.getRow('project');
|
|
558
|
-
// Skip-and-continue contract: a user who explicitly skips this row chose
|
|
559
|
-
// to set the project up themselves later. Mark manual-required so the
|
|
560
|
-
// primary-button derivation can route to Open Hub via the skip-path rule.
|
|
562
|
+
async runAgentRow(request) {
|
|
563
|
+
const row = this.getRow('agent');
|
|
561
564
|
if (request.errorActionId === 'skip') {
|
|
562
565
|
row.status = 'manual-required';
|
|
563
|
-
row.verb = "
|
|
564
|
-
this.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
row.verb = 'pick a folder where FRAIM should work';
|
|
566
|
+
row.verb = "let's recruit AI Employees";
|
|
567
|
+
this.state.setupResult = {
|
|
568
|
+
mode: 'conversational',
|
|
569
|
+
configuredSurfaces: [],
|
|
570
|
+
failedSurfaces: [],
|
|
571
|
+
detectedSurfaceCount: 0,
|
|
572
|
+
completedAt: new Date().toISOString(),
|
|
573
|
+
};
|
|
572
574
|
this.persist();
|
|
573
|
-
return this.respond('
|
|
574
|
-
}
|
|
575
|
-
const resolvedPath = path_1.default.resolve(projectPath);
|
|
576
|
-
fs_1.default.mkdirSync(resolvedPath, { recursive: true });
|
|
577
|
-
if (commandExists('git')) {
|
|
578
|
-
try {
|
|
579
|
-
(0, child_process_1.execSync)('git rev-parse --git-dir', { cwd: resolvedPath, stdio: 'ignore' });
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
try {
|
|
583
|
-
(0, child_process_1.execSync)('git init', { cwd: resolvedPath, stdio: 'ignore' });
|
|
584
|
-
}
|
|
585
|
-
catch (gitError) {
|
|
586
|
-
// Surface but don't fail the row — init-project will still work.
|
|
587
|
-
appendInstallLog(`git init failed at ${resolvedPath}: ${gitError instanceof Error ? gitError.message : 'unknown'}`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
575
|
+
return this.respond('AI Employee recruiting deferred.', true);
|
|
590
576
|
}
|
|
591
|
-
|
|
592
|
-
// Configure FRAIM globals + agent MCP wiring before init-project runs.
|
|
593
|
-
(0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
|
|
594
|
-
const selectedAgent = (0, ide_detector_1.findIDEByName)(this.state.agentId);
|
|
595
|
-
const targetNames = selectedAgent ? [selectedAgent.name] : undefined;
|
|
596
|
-
try {
|
|
597
|
-
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, targetNames, {});
|
|
598
|
-
}
|
|
599
|
-
catch (mcpError) {
|
|
600
|
-
appendInstallLog(`autoConfigureMCP non-fatal error: ${mcpError instanceof Error ? mcpError.message : 'unknown'}`);
|
|
601
|
-
}
|
|
602
|
-
// Pass failHard:'throw' so a sync 401 / network error / missing-key
|
|
603
|
-
// bubbles back here instead of calling process.exit(1) and killing the
|
|
604
|
-
// FRE server. The catch below converts it into the R6 error frame.
|
|
605
|
-
try {
|
|
606
|
-
await (0, init_project_1.runInitProject)({ projectRoot: resolvedPath, failHard: 'throw' });
|
|
607
|
-
}
|
|
608
|
-
catch (initError) {
|
|
609
|
-
const detail = initError instanceof Error ? initError.message : 'Unknown init error';
|
|
610
|
-
// Skip is intentionally omitted on the project row — without a
|
|
611
|
-
// project there is no Hub to open, so deferring this row would
|
|
612
|
-
// leave the user wedged. Retry is always offered; the hint
|
|
613
|
-
// translates the verbatim error (e.g. "Request failed with
|
|
614
|
-
// status code 401") into something a non-tech user can act on.
|
|
615
|
-
const hint = this.classifyInitError(detail);
|
|
616
|
-
this.setRowError(row, `Initializing FRAIM in ${resolvedPath}`, detail, [{ id: 'retry', label: 'Retry', variant: 'primary' }], hint);
|
|
617
|
-
appendInstallLog(`project-init-failed ${resolvedPath}: ${detail}`);
|
|
618
|
-
this.persist();
|
|
619
|
-
return this.respond(`Initialization failed: ${detail}`, false);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
this.state.workspacePath = resolvedPath;
|
|
623
|
-
row.status = 'ok';
|
|
624
|
-
row.verb = resolvedPath;
|
|
577
|
+
this.updateAgentSummaryRow();
|
|
625
578
|
this.persist();
|
|
626
|
-
|
|
627
|
-
return this.respond(
|
|
579
|
+
const count = this.state.setupResult?.detectedSurfaceCount || 0;
|
|
580
|
+
return this.respond(count > 0 ? 'AI Employees are ready.' : 'No AI Employees found.', true);
|
|
628
581
|
}
|
|
629
582
|
/**
|
|
630
|
-
* Update the current agent selection (inline `Change
|
|
631
|
-
* the
|
|
583
|
+
* Update the current agent selection (inline `Change…` picker).
|
|
584
|
+
* Records the preference for future recruiting workflows; does not run any installs.
|
|
632
585
|
*/
|
|
633
586
|
changeAgent(req) {
|
|
634
587
|
if (req.customAgent) {
|
|
635
588
|
this.state.customAgent = { name: req.customAgent.name, invocationPrefix: req.customAgent.invocationPrefix };
|
|
636
589
|
const agentRow = this.getRow('agent');
|
|
637
|
-
|
|
638
|
-
agentRow.status = 'ok';
|
|
590
|
+
agentRow.status = 'manual-required';
|
|
639
591
|
agentRow.verb = `Custom CLI recorded: ${req.customAgent.name}`;
|
|
640
|
-
agentRow.detail =
|
|
641
|
-
loginRow.status = 'manual-required';
|
|
642
|
-
loginRow.verb = `Sign in to ${req.customAgent.name} yourself`;
|
|
592
|
+
agentRow.detail = 'Run npx fraim add-ide after installing it.';
|
|
643
593
|
this.persist();
|
|
644
594
|
appendInstallLog(`custom-agent recorded ${req.customAgent.name}`);
|
|
645
595
|
return this.respond(`Custom agent "${req.customAgent.name}" recorded.`, true);
|
|
@@ -651,53 +601,9 @@ class FirstRunSessionService {
|
|
|
651
601
|
if (!option) {
|
|
652
602
|
return this.respond(`Unknown agent: ${req.agentId}`, false);
|
|
653
603
|
}
|
|
654
|
-
const previousAgentId = this.state.agentId;
|
|
655
604
|
this.state.agentId = req.agentId;
|
|
656
605
|
delete this.state.customAgent;
|
|
657
|
-
|
|
658
|
-
const loginRow = this.getRow('agent-login');
|
|
659
|
-
// If the user actually switched to a different agent, the row state from
|
|
660
|
-
// the previous agent is now stale — even if it was `ok`. The new agent
|
|
661
|
-
// has not been detected, installed, verified, or signed in. Re-detect
|
|
662
|
-
// the new agent and reset both rows so the next run executes against
|
|
663
|
-
// the new selection rather than silently keeping the previous verbs.
|
|
664
|
-
if (previousAgentId !== req.agentId) {
|
|
665
|
-
this.clearRowError(agentRow);
|
|
666
|
-
const detectedVer = this.fakeMode ? null : commandVersion(option.launchCommand);
|
|
667
|
-
if (detectedVer) {
|
|
668
|
-
agentRow.status = 'ok';
|
|
669
|
-
agentRow.verb = `${option.label} ${detectedVer} detected`;
|
|
670
|
-
agentRow.detail = detectedVer;
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
agentRow.status = 'pending';
|
|
674
|
-
agentRow.verb = pendingVerbForAgent(req.agentId);
|
|
675
|
-
delete agentRow.detail;
|
|
676
|
-
}
|
|
677
|
-
// Login state never carries across agents.
|
|
678
|
-
this.clearRowError(loginRow);
|
|
679
|
-
const loginReady = agentRow.status === 'ok' && this.probeAgentReady(req.agentId).ok;
|
|
680
|
-
if (loginReady) {
|
|
681
|
-
loginRow.status = 'ok';
|
|
682
|
-
loginRow.verb = 'Signed in (verified)';
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
loginRow.status = 'pending';
|
|
686
|
-
loginRow.verb = "you'll sign in after install";
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
// Same agent re-selected — only normalize verbs if rows were not yet ok.
|
|
691
|
-
if (agentRow.status !== 'ok') {
|
|
692
|
-
agentRow.verb = pendingVerbForAgent(req.agentId);
|
|
693
|
-
this.clearRowError(agentRow);
|
|
694
|
-
agentRow.status = 'pending';
|
|
695
|
-
}
|
|
696
|
-
if (loginRow.status !== 'ok') {
|
|
697
|
-
loginRow.verb = "you'll sign in after install";
|
|
698
|
-
loginRow.status = 'pending';
|
|
699
|
-
}
|
|
700
|
-
}
|
|
606
|
+
this.updateAgentSummaryRow();
|
|
701
607
|
this.persist();
|
|
702
608
|
appendInstallLog(`agent-changed ${req.agentId}`);
|
|
703
609
|
return this.respond(`Selected ${option.label}.`, true);
|
|
@@ -713,29 +619,26 @@ class FirstRunSessionService {
|
|
|
713
619
|
/**
|
|
714
620
|
* Start the Hub server for the chosen project and open the user's browser
|
|
715
621
|
* to it. Returns the Hub URL so the client can show a clear "we're at <url>"
|
|
716
|
-
* message instead of a CLI hand-off
|
|
622
|
+
* message instead of a CLI hand-off — the whole point of v1 is to never
|
|
717
623
|
* leave the user typing a command. The single-file launcher binary in v2
|
|
718
624
|
* (#355) replaces this in-process spawn with a durable tray-icon launcher.
|
|
719
625
|
*/
|
|
720
626
|
async openHub() {
|
|
721
|
-
if (!this.state.workspacePath) {
|
|
722
|
-
return { ok: false, message: 'Pick a project folder before opening the Hub.' };
|
|
723
|
-
}
|
|
724
627
|
if (this.fakeMode) {
|
|
725
|
-
// Tests don't actually want a Hub server running
|
|
726
|
-
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub
|
|
628
|
+
// Tests don't actually want a Hub server running — just confirm intent.
|
|
629
|
+
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/?firstRun=true' };
|
|
727
630
|
}
|
|
728
631
|
try {
|
|
729
632
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../ai-hub/server')));
|
|
730
633
|
const port = await findAvailablePort(43091);
|
|
731
634
|
const hubServer = new AiHubServer({ projectPath: this.state.workspacePath });
|
|
732
635
|
await hubServer.start(port);
|
|
733
|
-
const hubUrl = `http://127.0.0.1:${port}/ai-hub
|
|
636
|
+
const hubUrl = `http://127.0.0.1:${port}/ai-hub/?firstRun=true`;
|
|
734
637
|
this.openBrowser(hubUrl);
|
|
735
638
|
appendInstallLog(`hub-opened ${hubUrl}`);
|
|
736
639
|
return {
|
|
737
640
|
ok: true,
|
|
738
|
-
message: `Hub is open at ${hubUrl}. From now on, run \`npx fraim-framework@latest hub --browser\` to launch it again
|
|
641
|
+
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).`,
|
|
739
642
|
hubUrl,
|
|
740
643
|
};
|
|
741
644
|
}
|
|
@@ -765,7 +668,7 @@ class FirstRunSessionService {
|
|
|
765
668
|
(0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
766
669
|
}
|
|
767
670
|
catch {
|
|
768
|
-
// Best-effort
|
|
671
|
+
// Best-effort — fall through; the URL is already in the response so the
|
|
769
672
|
// client surfaces it for the user.
|
|
770
673
|
}
|
|
771
674
|
}
|