fraim 2.0.128 → 2.0.130
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/first-run.js +14 -1
- package/dist/src/cli/commands/init-project.js +55 -121
- package/dist/src/cli/commands/setup.js +68 -43
- package/dist/src/cli/commands/sync.js +8 -1
- 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 +118 -18
- package/dist/src/first-run/session-service.js +282 -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 -89
- package/public/first-run/index.html +5 -6
- package/public/first-run/script.js +275 -227
- package/public/first-run/styles.css +603 -386
|
@@ -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
|
}
|
|
@@ -139,17 +234,40 @@ class FirstRunSessionService {
|
|
|
139
234
|
delete row.errorFrame;
|
|
140
235
|
delete row.streamOutput;
|
|
141
236
|
}
|
|
142
|
-
setRowError(row, whatTried, whatHappened, actions) {
|
|
237
|
+
setRowError(row, whatTried, whatHappened, actions, hint) {
|
|
143
238
|
row.status = 'error';
|
|
144
|
-
row.verb = 'failed
|
|
145
|
-
row.errorFrame = { whatTried, whatHappened, actions };
|
|
239
|
+
row.verb = 'failed — see below';
|
|
240
|
+
row.errorFrame = { whatTried, whatHappened, actions, ...(hint ? { hint } : {}) };
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Classify a thrown init/sync error into a non-tech-friendly hint.
|
|
244
|
+
* The verbatim stderr is preserved separately in `whatHappened` per
|
|
245
|
+
* R6.3 — the hint is additive context, not a replacement.
|
|
246
|
+
*/
|
|
247
|
+
classifyInitError(detail) {
|
|
248
|
+
if (/status code 401|Unauthorized|401\b/i.test(detail)) {
|
|
249
|
+
return "Your FRAIM install key was rejected by the registry. If your key expired, get a new one from your account page and re-run setup with the new key. If you're testing locally, set `FRAIM_LOCAL_SYNC=1` so the project row syncs from your local FRAIM server instead of the production registry.";
|
|
250
|
+
}
|
|
251
|
+
if (/status code 403|Forbidden/i.test(detail)) {
|
|
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.";
|
|
253
|
+
}
|
|
254
|
+
if (/status code 5\d\d/i.test(detail) || /5\d\d\b/.test(detail)) {
|
|
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.';
|
|
256
|
+
}
|
|
257
|
+
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|getaddrinfo|network/i.test(detail)) {
|
|
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.";
|
|
259
|
+
}
|
|
260
|
+
if (/Global FRAIM setup not found/i.test(detail)) {
|
|
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.';
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
146
264
|
}
|
|
147
265
|
detectRowsOnLoad() {
|
|
148
266
|
if (this.fakeMode) {
|
|
149
267
|
this.applyFakeStateOnLoad(this.fakeMode);
|
|
150
268
|
return;
|
|
151
269
|
}
|
|
152
|
-
// Real detection
|
|
270
|
+
// Real detection — populate row statuses from `command -v` style probes.
|
|
153
271
|
const nodeRow = this.getRow('node');
|
|
154
272
|
const nodeVer = commandVersion('node');
|
|
155
273
|
if (nodeVer) {
|
|
@@ -170,53 +288,38 @@ class FirstRunSessionService {
|
|
|
170
288
|
gitRow.status = 'pending';
|
|
171
289
|
gitRow.verb = "we'll install";
|
|
172
290
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
291
|
+
const fraimRow = this.getRow('fraim');
|
|
292
|
+
if (commandVersion('npx') !== null) {
|
|
293
|
+
fraimRow.status = 'ok';
|
|
294
|
+
fraimRow.verb = 'ready';
|
|
295
|
+
persistShellPath();
|
|
198
296
|
}
|
|
199
297
|
else {
|
|
200
|
-
|
|
201
|
-
|
|
298
|
+
fraimRow.status = 'pending';
|
|
299
|
+
fraimRow.verb = "we'll set up FRAIM";
|
|
202
300
|
}
|
|
301
|
+
this.updateAgentSummaryRow();
|
|
203
302
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
}
|
|
303
|
+
updateAgentSummaryRow() {
|
|
304
|
+
const agentRow = this.getRow('agent');
|
|
305
|
+
const surfaces = this.fakeMode === 'no-agents' ? [] : buildConfiguredSurfaces();
|
|
306
|
+
if (surfaces.length > 0) {
|
|
307
|
+
agentRow.status = 'ok';
|
|
308
|
+
agentRow.verb = `${surfaces.length} AI Employee${surfaces.length === 1 ? '' : 's'} ready`;
|
|
309
|
+
agentRow.detail = surfaces.map((surface) => surface.name).join(', ');
|
|
218
310
|
}
|
|
219
|
-
|
|
311
|
+
else {
|
|
312
|
+
agentRow.status = 'manual-required';
|
|
313
|
+
agentRow.verb = "let's recruit AI Employees";
|
|
314
|
+
delete agentRow.detail;
|
|
315
|
+
}
|
|
316
|
+
this.state.setupResult = {
|
|
317
|
+
mode: 'conversational',
|
|
318
|
+
configuredSurfaces: surfaces,
|
|
319
|
+
failedSurfaces: [],
|
|
320
|
+
detectedSurfaceCount: surfaces.length,
|
|
321
|
+
completedAt: new Date().toISOString(),
|
|
322
|
+
};
|
|
220
323
|
}
|
|
221
324
|
applyFakeStateOnLoad(mode) {
|
|
222
325
|
const setStatus = (rowId, status, verb, detail) => {
|
|
@@ -229,33 +332,63 @@ class FirstRunSessionService {
|
|
|
229
332
|
if (mode === 'all-pending') {
|
|
230
333
|
setStatus('node', 'pending', "we'll install");
|
|
231
334
|
setStatus('git', 'pending', "we'll install");
|
|
232
|
-
setStatus('
|
|
233
|
-
setStatus('agent
|
|
234
|
-
|
|
335
|
+
setStatus('fraim', 'pending', "we'll install");
|
|
336
|
+
setStatus('agent', 'pending', "we'll check for AI Employees");
|
|
337
|
+
delete this.state.setupResult;
|
|
235
338
|
return;
|
|
236
339
|
}
|
|
237
340
|
if (mode === 'all-detected') {
|
|
238
341
|
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
239
342
|
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
240
|
-
setStatus('
|
|
241
|
-
setStatus('agent
|
|
242
|
-
|
|
343
|
+
setStatus('fraim', 'ok', 'fraim-framework detected');
|
|
344
|
+
setStatus('agent', 'ok', '1 AI Employee ready', 'Claude Code');
|
|
345
|
+
this.state.setupResult = {
|
|
346
|
+
mode: 'conversational',
|
|
347
|
+
configuredSurfaces: [
|
|
348
|
+
{ id: 'claude-code', name: 'Claude Code', invocationHint: 'Claude Code: /fraim onboard this project', status: 'configured' },
|
|
349
|
+
],
|
|
350
|
+
failedSurfaces: [],
|
|
351
|
+
detectedSurfaceCount: 1,
|
|
352
|
+
completedAt: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (mode === 'no-agents') {
|
|
357
|
+
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
358
|
+
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
359
|
+
setStatus('fraim', 'ok', 'fraim-framework detected');
|
|
360
|
+
setStatus('agent', 'manual-required', "let's recruit AI Employees");
|
|
361
|
+
this.state.setupResult = {
|
|
362
|
+
mode: 'conversational',
|
|
363
|
+
configuredSurfaces: [],
|
|
364
|
+
failedSurfaces: [],
|
|
365
|
+
detectedSurfaceCount: 0,
|
|
366
|
+
completedAt: new Date().toISOString(),
|
|
367
|
+
};
|
|
243
368
|
return;
|
|
244
369
|
}
|
|
245
370
|
if (mode === 'agent-install-fails') {
|
|
246
371
|
setStatus('node', 'ok', 'v20.11.1 installed');
|
|
247
372
|
setStatus('git', 'ok', '2.45 installed');
|
|
248
|
-
setStatus('
|
|
249
|
-
setStatus('agent
|
|
250
|
-
|
|
373
|
+
setStatus('fraim', 'ok', 'fraim-framework installed');
|
|
374
|
+
setStatus('agent', 'pending', "we'll check for AI Employees");
|
|
375
|
+
delete this.state.setupResult;
|
|
251
376
|
return;
|
|
252
377
|
}
|
|
253
|
-
// 'default' fake mode
|
|
378
|
+
// 'default' fake mode — infra rows all ok.
|
|
254
379
|
setStatus('node', 'ok', 'v20.11.1 detected');
|
|
255
380
|
setStatus('git', 'ok', 'git version 2.45 detected');
|
|
256
|
-
setStatus('
|
|
257
|
-
setStatus('agent
|
|
258
|
-
|
|
381
|
+
setStatus('fraim', 'ok', 'fake-mode fraim installed');
|
|
382
|
+
setStatus('agent', 'ok', 'fake-mode AI Employee ready', 'Claude Code');
|
|
383
|
+
this.state.setupResult = {
|
|
384
|
+
mode: 'conversational',
|
|
385
|
+
configuredSurfaces: [
|
|
386
|
+
{ id: 'claude-code', name: 'Claude Code', invocationHint: 'Claude Code: /fraim onboard this project', status: 'configured' },
|
|
387
|
+
],
|
|
388
|
+
failedSurfaces: [],
|
|
389
|
+
detectedSurfaceCount: 1,
|
|
390
|
+
completedAt: new Date().toISOString(),
|
|
391
|
+
};
|
|
259
392
|
}
|
|
260
393
|
getRequestToken() {
|
|
261
394
|
return this.requestToken;
|
|
@@ -303,16 +436,14 @@ class FirstRunSessionService {
|
|
|
303
436
|
return await this.runNodeRow();
|
|
304
437
|
case 'git':
|
|
305
438
|
return await this.runGitRow();
|
|
439
|
+
case 'fraim':
|
|
440
|
+
return await this.runFraimRow();
|
|
306
441
|
case 'agent':
|
|
307
442
|
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
443
|
}
|
|
313
444
|
}
|
|
314
445
|
catch (error) {
|
|
315
|
-
// Last-ditch safety net
|
|
446
|
+
// Last-ditch safety net — every row's runner is supposed to surface
|
|
316
447
|
// its own R6 frame via setRowError before throwing, but if anything
|
|
317
448
|
// slips past (e.g. a sync layer that calls process.exit and we narrowly
|
|
318
449
|
// converted it to throw), we still surface a frame here rather than
|
|
@@ -320,7 +451,7 @@ class FirstRunSessionService {
|
|
|
320
451
|
// user is never wedged on a non-recoverable failure of one row.
|
|
321
452
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
322
453
|
row.status = 'error';
|
|
323
|
-
row.verb = 'failed
|
|
454
|
+
row.verb = 'failed — see below';
|
|
324
455
|
row.errorFrame = {
|
|
325
456
|
whatTried: `Running ${row.label} step`,
|
|
326
457
|
whatHappened: message,
|
|
@@ -373,250 +504,80 @@ class FirstRunSessionService {
|
|
|
373
504
|
this.persist();
|
|
374
505
|
return this.respond('git not detected.', false);
|
|
375
506
|
}
|
|
376
|
-
async
|
|
377
|
-
const row = this.getRow('
|
|
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
|
-
}
|
|
507
|
+
async runFraimRow() {
|
|
508
|
+
const row = this.getRow('fraim');
|
|
411
509
|
if (this.fakeMode) {
|
|
412
510
|
row.status = 'ok';
|
|
413
|
-
row.verb =
|
|
511
|
+
row.verb = 'FRAIM ready (fake-mode)';
|
|
414
512
|
this.persist();
|
|
415
|
-
return this.respond('Fake-mode
|
|
513
|
+
return this.respond('Fake-mode fraim ok.', true);
|
|
416
514
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
515
|
+
try {
|
|
516
|
+
if (!commandVersion('fraim')) {
|
|
517
|
+
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
518
|
+
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
519
|
+
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
520
|
+
this.persist();
|
|
521
|
+
await runProcess('npm', ['install', '-g', 'fraim-framework@latest'], { npm_config_prefix: prefix });
|
|
522
|
+
}
|
|
523
|
+
persistShellPath();
|
|
524
|
+
(0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
|
|
525
|
+
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
526
|
+
if (detectedIDEs.length > 0) {
|
|
527
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
|
|
528
|
+
}
|
|
529
|
+
const { syncUserLevelArtifacts } = await Promise.resolve().then(() => __importStar(require('../cli/setup/user-level-sync')));
|
|
530
|
+
await syncUserLevelArtifacts();
|
|
531
|
+
const { installSlashCommands, installGlobalRules } = await Promise.resolve().then(() => __importStar(require('../cli/setup/ide-global-integration')));
|
|
532
|
+
await installSlashCommands();
|
|
533
|
+
await installGlobalRules();
|
|
420
534
|
row.status = 'ok';
|
|
421
|
-
row.verb =
|
|
422
|
-
row.
|
|
535
|
+
row.verb = 'Ready.';
|
|
536
|
+
delete row.streamOutput;
|
|
423
537
|
this.persist();
|
|
424
|
-
return this.respond(
|
|
538
|
+
return this.respond('FRAIM is ready.', true);
|
|
425
539
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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;
|
|
540
|
+
catch (error) {
|
|
541
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
542
|
+
this.setRowError(row, 'Setting up FRAIM on this machine', detail, [
|
|
543
|
+
{ id: 'retry', label: 'Retry', variant: 'primary' },
|
|
544
|
+
{ id: 'skip', label: 'Skip and continue', variant: 'ghost' },
|
|
545
|
+
]);
|
|
438
546
|
this.persist();
|
|
439
|
-
return this.respond(
|
|
547
|
+
return this.respond('FRAIM setup failed.', false);
|
|
440
548
|
}
|
|
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
549
|
}
|
|
445
|
-
|
|
446
|
-
const
|
|
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.
|
|
550
|
+
async runAgentRow(request) {
|
|
551
|
+
const row = this.getRow('agent');
|
|
538
552
|
if (request.errorActionId === 'skip') {
|
|
539
553
|
row.status = 'manual-required';
|
|
540
|
-
row.verb = "
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
row.verb = 'pick a folder where FRAIM should work';
|
|
554
|
+
row.verb = "let's recruit AI Employees";
|
|
555
|
+
this.state.setupResult = {
|
|
556
|
+
mode: 'conversational',
|
|
557
|
+
configuredSurfaces: [],
|
|
558
|
+
failedSurfaces: [],
|
|
559
|
+
detectedSurfaceCount: 0,
|
|
560
|
+
completedAt: new Date().toISOString(),
|
|
561
|
+
};
|
|
549
562
|
this.persist();
|
|
550
|
-
return this.respond('
|
|
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
|
-
}
|
|
563
|
+
return this.respond('AI Employee recruiting deferred.', true);
|
|
598
564
|
}
|
|
599
|
-
this.
|
|
600
|
-
row.status = 'ok';
|
|
601
|
-
row.verb = resolvedPath;
|
|
565
|
+
this.updateAgentSummaryRow();
|
|
602
566
|
this.persist();
|
|
603
|
-
|
|
604
|
-
return this.respond(
|
|
567
|
+
const count = this.state.setupResult?.detectedSurfaceCount || 0;
|
|
568
|
+
return this.respond(count > 0 ? 'AI Employees are ready.' : 'No AI Employees found.', true);
|
|
605
569
|
}
|
|
606
570
|
/**
|
|
607
|
-
* Update the current agent selection (inline `Change
|
|
608
|
-
* the
|
|
571
|
+
* Update the current agent selection (inline `Change…` picker).
|
|
572
|
+
* Records the preference for future recruiting workflows; does not run any installs.
|
|
609
573
|
*/
|
|
610
574
|
changeAgent(req) {
|
|
611
575
|
if (req.customAgent) {
|
|
612
576
|
this.state.customAgent = { name: req.customAgent.name, invocationPrefix: req.customAgent.invocationPrefix };
|
|
613
577
|
const agentRow = this.getRow('agent');
|
|
614
|
-
|
|
615
|
-
agentRow.status = 'ok';
|
|
578
|
+
agentRow.status = 'manual-required';
|
|
616
579
|
agentRow.verb = `Custom CLI recorded: ${req.customAgent.name}`;
|
|
617
|
-
agentRow.detail =
|
|
618
|
-
loginRow.status = 'manual-required';
|
|
619
|
-
loginRow.verb = `Sign in to ${req.customAgent.name} yourself`;
|
|
580
|
+
agentRow.detail = 'Run npx fraim add-ide after installing it.';
|
|
620
581
|
this.persist();
|
|
621
582
|
appendInstallLog(`custom-agent recorded ${req.customAgent.name}`);
|
|
622
583
|
return this.respond(`Custom agent "${req.customAgent.name}" recorded.`, true);
|
|
@@ -628,53 +589,9 @@ class FirstRunSessionService {
|
|
|
628
589
|
if (!option) {
|
|
629
590
|
return this.respond(`Unknown agent: ${req.agentId}`, false);
|
|
630
591
|
}
|
|
631
|
-
const previousAgentId = this.state.agentId;
|
|
632
592
|
this.state.agentId = req.agentId;
|
|
633
593
|
delete this.state.customAgent;
|
|
634
|
-
|
|
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
|
-
}
|
|
594
|
+
this.updateAgentSummaryRow();
|
|
678
595
|
this.persist();
|
|
679
596
|
appendInstallLog(`agent-changed ${req.agentId}`);
|
|
680
597
|
return this.respond(`Selected ${option.label}.`, true);
|
|
@@ -690,29 +607,26 @@ class FirstRunSessionService {
|
|
|
690
607
|
/**
|
|
691
608
|
* Start the Hub server for the chosen project and open the user's browser
|
|
692
609
|
* 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
|
|
610
|
+
* message instead of a CLI hand-off — the whole point of v1 is to never
|
|
694
611
|
* leave the user typing a command. The single-file launcher binary in v2
|
|
695
612
|
* (#355) replaces this in-process spawn with a durable tray-icon launcher.
|
|
696
613
|
*/
|
|
697
614
|
async openHub() {
|
|
698
|
-
if (!this.state.workspacePath) {
|
|
699
|
-
return { ok: false, message: 'Pick a project folder before opening the Hub.' };
|
|
700
|
-
}
|
|
701
615
|
if (this.fakeMode) {
|
|
702
|
-
// Tests don't actually want a Hub server running
|
|
703
|
-
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub
|
|
616
|
+
// Tests don't actually want a Hub server running — just confirm intent.
|
|
617
|
+
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/?firstRun=true' };
|
|
704
618
|
}
|
|
705
619
|
try {
|
|
706
620
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../ai-hub/server')));
|
|
707
621
|
const port = await findAvailablePort(43091);
|
|
708
622
|
const hubServer = new AiHubServer({ projectPath: this.state.workspacePath });
|
|
709
623
|
await hubServer.start(port);
|
|
710
|
-
const hubUrl = `http://127.0.0.1:${port}/ai-hub
|
|
624
|
+
const hubUrl = `http://127.0.0.1:${port}/ai-hub/?firstRun=true`;
|
|
711
625
|
this.openBrowser(hubUrl);
|
|
712
626
|
appendInstallLog(`hub-opened ${hubUrl}`);
|
|
713
627
|
return {
|
|
714
628
|
ok: true,
|
|
715
|
-
message: `Hub is open at ${hubUrl}. From now on, run \`npx fraim-framework@latest hub --browser\` to launch it again
|
|
629
|
+
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
630
|
hubUrl,
|
|
717
631
|
};
|
|
718
632
|
}
|
|
@@ -728,7 +642,11 @@ class FirstRunSessionService {
|
|
|
728
642
|
openBrowser(url) {
|
|
729
643
|
try {
|
|
730
644
|
if (process.platform === 'win32') {
|
|
731
|
-
|
|
645
|
+
// `cmd.exe /c start "" "<url>"` mis-parses on some Windows hosts
|
|
646
|
+
// (cmd interprets the URL's `//` as a UNC path -> "Windows cannot
|
|
647
|
+
// find" popup). rundll32 + url.dll,FileProtocolHandler is the
|
|
648
|
+
// documented protocol-handler entry point and bypasses cmd entirely.
|
|
649
|
+
(0, child_process_1.spawn)('rundll32', ['url.dll,FileProtocolHandler', url], { detached: true, stdio: 'ignore' }).unref();
|
|
732
650
|
return;
|
|
733
651
|
}
|
|
734
652
|
if (process.platform === 'darwin') {
|
|
@@ -738,7 +656,7 @@ class FirstRunSessionService {
|
|
|
738
656
|
(0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
739
657
|
}
|
|
740
658
|
catch {
|
|
741
|
-
// Best-effort
|
|
659
|
+
// Best-effort — fall through; the URL is already in the response so the
|
|
742
660
|
// client surfaces it for the user.
|
|
743
661
|
}
|
|
744
662
|
}
|