fraim-framework 2.0.151 → 2.0.152
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/hosts.js +16 -7
- package/dist/src/ai-hub/server.js +7 -1
- package/dist/src/cli/commands/test-mcp.js +171 -0
- package/dist/src/cli/setup/first-run.js +242 -0
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +174 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +67 -67
- package/public/ai-hub/script.js +374 -365
- package/public/ai-hub/styles.css +582 -565
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +667 -667
- package/public/first-run/styles.css +73 -73
|
@@ -1,667 +1,667 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const CHECKLIST_EL = document.getElementById('checklist');
|
|
5
|
-
const PRIMARY_BUTTON = document.getElementById('primary-button');
|
|
6
|
-
const STATUS_EL = document.getElementById('status');
|
|
7
|
-
const LEDE_EL = document.getElementById('lede');
|
|
8
|
-
|
|
9
|
-
const state = {
|
|
10
|
-
session: null,
|
|
11
|
-
runningRowId: null,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function setStatus(text, tone) {
|
|
15
|
-
STATUS_EL.textContent = text || '';
|
|
16
|
-
if (tone) STATUS_EL.setAttribute('data-tone', tone);
|
|
17
|
-
else STATUS_EL.removeAttribute('data-tone');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function setHeader(title, lede) {
|
|
21
|
-
const h1El = document.querySelector('.page-header h1');
|
|
22
|
-
if (h1El) h1El.textContent = title;
|
|
23
|
-
if (LEDE_EL) LEDE_EL.textContent = lede;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function applyHeading(rows) {
|
|
27
|
-
if (!LEDE_EL) return;
|
|
28
|
-
const complete = rows.every((r) => r.status === 'ok' || r.status === 'manual-required') && rows.some((r) => r.status === 'ok');
|
|
29
|
-
if (complete) {
|
|
30
|
-
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
31
|
-
if (setupResult && setupResult.detectedSurfaceCount > 0) {
|
|
32
|
-
LEDE_EL.textContent = 'The following AI agents are now ready to function as your AI Employees.';
|
|
33
|
-
} else {
|
|
34
|
-
LEDE_EL.textContent = "You do not seem to have any AI agents on this machine.";
|
|
35
|
-
}
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const errored = rows.find((r) => r.status === 'error');
|
|
39
|
-
if (errored) {
|
|
40
|
-
LEDE_EL.textContent = "Something did not go through. Pick the right next step below.";
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (rows.some((r) => r.status === 'ok')) {
|
|
44
|
-
LEDE_EL.textContent = 'FRAIM is finishing setup on this machine.';
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
LEDE_EL.textContent = 'We are getting your machine ready for FRAIM.';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function api(path, method, body) {
|
|
51
|
-
const headers = {};
|
|
52
|
-
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
53
|
-
if (state.session && state.session.requestToken) {
|
|
54
|
-
headers['x-fraim-first-run-token'] = state.session.requestToken;
|
|
55
|
-
}
|
|
56
|
-
const response = await fetch(path, {
|
|
57
|
-
method: method || 'GET',
|
|
58
|
-
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
59
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
60
|
-
});
|
|
61
|
-
if (response.status === 204) return null;
|
|
62
|
-
let json = null;
|
|
63
|
-
try {
|
|
64
|
-
json = await response.json();
|
|
65
|
-
} catch (e) {
|
|
66
|
-
throw new Error(`Server returned invalid JSON (status ${response.status}).`);
|
|
67
|
-
}
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
|
|
70
|
-
}
|
|
71
|
-
return json;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function loadSession() {
|
|
75
|
-
state.session = await api('/api/first-run/session');
|
|
76
|
-
render();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function setSessionFromActionResponse(actionResp) {
|
|
80
|
-
if (!state.session) return;
|
|
81
|
-
state.session.state = actionResp.state;
|
|
82
|
-
state.session.rows = actionResp.rows;
|
|
83
|
-
state.session.primaryButtonLabel = actionResp.primaryButtonLabel;
|
|
84
|
-
state.session.currentAgentId = actionResp.state.agentId;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function makeIcon(status) {
|
|
88
|
-
const icon = document.createElement('span');
|
|
89
|
-
icon.className = 'icon';
|
|
90
|
-
icon.setAttribute('aria-hidden', 'true');
|
|
91
|
-
icon.textContent =
|
|
92
|
-
status === 'ok' ? '✓' :
|
|
93
|
-
status === 'in-progress' ? '...' :
|
|
94
|
-
status === 'manual-required' ? '!' :
|
|
95
|
-
status === 'error' ? '!' :
|
|
96
|
-
'.';
|
|
97
|
-
return icon;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderRow(row) {
|
|
101
|
-
const li = document.createElement('li');
|
|
102
|
-
li.className = 'row';
|
|
103
|
-
li.setAttribute('data-row-id', row.id);
|
|
104
|
-
li.setAttribute('data-row-status', row.status);
|
|
105
|
-
|
|
106
|
-
li.appendChild(makeIcon(row.status));
|
|
107
|
-
|
|
108
|
-
const label = document.createElement('span');
|
|
109
|
-
label.className = 'label';
|
|
110
|
-
label.textContent = row.label;
|
|
111
|
-
li.appendChild(label);
|
|
112
|
-
|
|
113
|
-
const verb = document.createElement('span');
|
|
114
|
-
verb.className = 'verb';
|
|
115
|
-
verb.setAttribute('data-testid', 'row-verb');
|
|
116
|
-
verb.textContent = row.verb || '';
|
|
117
|
-
li.appendChild(verb);
|
|
118
|
-
|
|
119
|
-
if (row.detail) {
|
|
120
|
-
const detail = document.createElement('span');
|
|
121
|
-
detail.className = 'detail';
|
|
122
|
-
detail.setAttribute('data-testid', 'row-detail');
|
|
123
|
-
detail.textContent = row.detail;
|
|
124
|
-
li.appendChild(detail);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (row.streamOutput) {
|
|
128
|
-
const stream = document.createElement('pre');
|
|
129
|
-
stream.className = 'row-stream';
|
|
130
|
-
stream.setAttribute('data-testid', 'row-stream');
|
|
131
|
-
stream.textContent = row.streamOutput;
|
|
132
|
-
li.appendChild(stream);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (row.manualMessage) {
|
|
136
|
-
const message = document.createElement('div');
|
|
137
|
-
message.className = 'manual-message';
|
|
138
|
-
message.textContent = row.manualMessage;
|
|
139
|
-
li.appendChild(message);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (row.status === 'error' && row.errorFrame && window.FraimErrorFrame) {
|
|
143
|
-
const frame = window.FraimErrorFrame.render(row.errorFrame, (action) => onErrorAction(row.id, action));
|
|
144
|
-
li.appendChild(frame);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return li;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderSetupSummary() {
|
|
151
|
-
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
152
|
-
if (!setupResult) return null;
|
|
153
|
-
|
|
154
|
-
const li = document.createElement('li');
|
|
155
|
-
li.className = 'setup-result';
|
|
156
|
-
li.setAttribute('data-testid', 'setup-result');
|
|
157
|
-
|
|
158
|
-
const title = document.createElement('strong');
|
|
159
|
-
title.textContent = setupResult.detectedSurfaceCount > 0
|
|
160
|
-
? 'The following AI agents are now ready to function as your AI Employees'
|
|
161
|
-
: "You do not seem to have any AI agents on this machine";
|
|
162
|
-
li.appendChild(title);
|
|
163
|
-
|
|
164
|
-
if (setupResult.detectedSurfaceCount > 0) {
|
|
165
|
-
const list = document.createElement('ul');
|
|
166
|
-
list.className = 'surface-list';
|
|
167
|
-
for (const surface of setupResult.configuredSurfaces || []) {
|
|
168
|
-
const item = document.createElement('li');
|
|
169
|
-
item.textContent = surface.name;
|
|
170
|
-
list.appendChild(item);
|
|
171
|
-
}
|
|
172
|
-
li.appendChild(list);
|
|
173
|
-
} else {
|
|
174
|
-
const copy = document.createElement('p');
|
|
175
|
-
copy.textContent = "Let's recruit some agents who can function as your AI Employees.";
|
|
176
|
-
li.appendChild(copy);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return li;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function runRow(rowId, extraBody) {
|
|
183
|
-
if (state.runningRowId) return;
|
|
184
|
-
state.runningRowId = rowId;
|
|
185
|
-
PRIMARY_BUTTON.disabled = true;
|
|
186
|
-
setStatus(`Running ${rowId}...`);
|
|
187
|
-
try {
|
|
188
|
-
const resp = await api(`/api/first-run/rows/${rowId}/run`, 'POST', extraBody || {});
|
|
189
|
-
setSessionFromActionResponse(resp);
|
|
190
|
-
setStatus(resp.message, resp.ok ? null : 'error');
|
|
191
|
-
} catch (err) {
|
|
192
|
-
setStatus(err.message, 'error');
|
|
193
|
-
} finally {
|
|
194
|
-
state.runningRowId = null;
|
|
195
|
-
PRIMARY_BUTTON.disabled = false;
|
|
196
|
-
render();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function onErrorAction(rowId, action) {
|
|
201
|
-
const body = { errorActionId: action.id };
|
|
202
|
-
if (action.id === 'alternative' && action.alternativeAgentId) {
|
|
203
|
-
body.alternativeAgentId = action.alternativeAgentId;
|
|
204
|
-
}
|
|
205
|
-
await runRow(rowId, body);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function onPrimaryClick() {
|
|
209
|
-
if (!state.session) return;
|
|
210
|
-
|
|
211
|
-
if (state.session.primaryButtonLabel === 'Get Started') {
|
|
212
|
-
PRIMARY_BUTTON.disabled = true;
|
|
213
|
-
const setupResult = state.session.state ? state.session.state.setupResult : null;
|
|
214
|
-
if (setupResult && setupResult.detectedSurfaceCount === 0) {
|
|
215
|
-
renderRecruitAgents();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
try {
|
|
219
|
-
const ideData = await api('/api/first-run/ide-commands');
|
|
220
|
-
renderStartWorking(ideData ? ideData.commands : []);
|
|
221
|
-
} catch (_err) {
|
|
222
|
-
renderStartWorking([]);
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const next = state.session.rows.find((r) => r.status === 'pending');
|
|
228
|
-
if (!next) return;
|
|
229
|
-
await runRow(next.id);
|
|
230
|
-
|
|
231
|
-
while (state.session) {
|
|
232
|
-
const blockingError = state.session.rows.find((r) => r.status === 'error');
|
|
233
|
-
if (blockingError) break;
|
|
234
|
-
const nextRow = state.session.rows.find((r) => r.status === 'pending');
|
|
235
|
-
if (!nextRow) break;
|
|
236
|
-
await runRow(nextRow.id);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function renderRecruitAgents() {
|
|
241
|
-
CHECKLIST_EL.className = 'selection-container recruit-container';
|
|
242
|
-
CHECKLIST_EL.innerHTML = '';
|
|
243
|
-
PRIMARY_BUTTON.style.display = 'none';
|
|
244
|
-
setHeader('Recruit AI Employees', 'Choose how you want to add AI Employees to this machine.');
|
|
245
|
-
|
|
246
|
-
const agentOptions = (state.session && state.session.agentOptions) ? state.session.agentOptions : [
|
|
247
|
-
{ id: 'claude-code', label: 'Claude Code' },
|
|
248
|
-
{ id: 'codex', label: 'Codex' },
|
|
249
|
-
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
const AGENT_DESCS = {
|
|
253
|
-
'claude-code': 'Install Claude Code and connect it to FRAIM.',
|
|
254
|
-
'codex': 'Install Codex and connect it to FRAIM.',
|
|
255
|
-
'gemini-cli': 'Install Gemini CLI and connect it to FRAIM.',
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
for (const opt of agentOptions) {
|
|
259
|
-
const li = document.createElement('li');
|
|
260
|
-
const card = document.createElement('div');
|
|
261
|
-
card.className = 'user-type-card recruit-card';
|
|
262
|
-
card.setAttribute('data-agent-id', opt.id);
|
|
263
|
-
|
|
264
|
-
const title = document.createElement('strong');
|
|
265
|
-
title.className = 'card-title';
|
|
266
|
-
title.textContent = opt.label;
|
|
267
|
-
const desc = document.createElement('p');
|
|
268
|
-
desc.className = 'card-desc';
|
|
269
|
-
desc.textContent = AGENT_DESCS[opt.id] || ('Install ' + opt.label + ' and connect it to FRAIM.');
|
|
270
|
-
card.appendChild(title);
|
|
271
|
-
card.appendChild(desc);
|
|
272
|
-
|
|
273
|
-
const installBtn = document.createElement('button');
|
|
274
|
-
installBtn.type = 'button';
|
|
275
|
-
installBtn.className = 'btn btn-secondary btn-block';
|
|
276
|
-
installBtn.textContent = 'Install';
|
|
277
|
-
installBtn.setAttribute('data-testid', 'install-' + opt.id);
|
|
278
|
-
installBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
279
|
-
card.appendChild(installBtn);
|
|
280
|
-
|
|
281
|
-
li.appendChild(card);
|
|
282
|
-
CHECKLIST_EL.appendChild(li);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const byoaLi = document.createElement('li');
|
|
286
|
-
const byoaCard = document.createElement('div');
|
|
287
|
-
byoaCard.className = 'user-type-card recruit-card';
|
|
288
|
-
const byoaTitle = document.createElement('strong');
|
|
289
|
-
byoaTitle.className = 'card-title';
|
|
290
|
-
byoaTitle.textContent = 'Bring Your Own Agent';
|
|
291
|
-
const byoaDesc = document.createElement('p');
|
|
292
|
-
byoaDesc.className = 'card-desc';
|
|
293
|
-
byoaDesc.textContent = 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.';
|
|
294
|
-
const byoaNote = document.createElement('p');
|
|
295
|
-
byoaNote.className = 'card-desc';
|
|
296
|
-
byoaNote.textContent = 'When you are done installing your agent, run:';
|
|
297
|
-
const byoaCmd = document.createElement('div');
|
|
298
|
-
byoaCmd.className = 'cmd-block';
|
|
299
|
-
byoaCmd.textContent = 'npx fraim add-ide';
|
|
300
|
-
byoaCard.appendChild(byoaTitle);
|
|
301
|
-
byoaCard.appendChild(byoaDesc);
|
|
302
|
-
byoaCard.appendChild(byoaNote);
|
|
303
|
-
byoaCard.appendChild(byoaCmd);
|
|
304
|
-
byoaLi.appendChild(byoaCard);
|
|
305
|
-
CHECKLIST_EL.appendChild(byoaLi);
|
|
306
|
-
|
|
307
|
-
const continueLi = document.createElement('li');
|
|
308
|
-
const continueBtn = document.createElement('button');
|
|
309
|
-
continueBtn.type = 'button';
|
|
310
|
-
continueBtn.className = 'btn btn-primary btn-block';
|
|
311
|
-
continueBtn.textContent = 'Continue without AI Agent';
|
|
312
|
-
continueBtn.addEventListener('click', async () => {
|
|
313
|
-
try {
|
|
314
|
-
const ideData = await api('/api/first-run/ide-commands');
|
|
315
|
-
renderStartWorking(ideData ? ideData.commands : []);
|
|
316
|
-
} catch (_err) {
|
|
317
|
-
renderStartWorking([]);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
continueLi.appendChild(continueBtn);
|
|
321
|
-
CHECKLIST_EL.appendChild(continueLi);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function renderAgentInstallFlow(opt) {
|
|
325
|
-
CHECKLIST_EL.className = 'selection-container recruit-container';
|
|
326
|
-
CHECKLIST_EL.innerHTML = '';
|
|
327
|
-
setHeader('Install ' + opt.label, 'Setting up ' + opt.label + ' on this machine...');
|
|
328
|
-
|
|
329
|
-
const statusLi = document.createElement('li');
|
|
330
|
-
const statusDiv = document.createElement('div');
|
|
331
|
-
statusDiv.className = 'install-status';
|
|
332
|
-
statusDiv.setAttribute('data-testid', 'agent-install-status');
|
|
333
|
-
statusDiv.textContent = 'Installing ' + opt.label + '...';
|
|
334
|
-
statusLi.appendChild(statusDiv);
|
|
335
|
-
CHECKLIST_EL.appendChild(statusLi);
|
|
336
|
-
|
|
337
|
-
(async () => {
|
|
338
|
-
try {
|
|
339
|
-
const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
|
|
340
|
-
if (!result || !result.ok) {
|
|
341
|
-
statusDiv.textContent = (result && result.message) ? result.message : 'Install failed.';
|
|
342
|
-
statusDiv.setAttribute('data-tone', 'error');
|
|
343
|
-
const actions = document.createElement('div');
|
|
344
|
-
actions.className = 'install-actions';
|
|
345
|
-
const retryBtn = document.createElement('button');
|
|
346
|
-
retryBtn.type = 'button';
|
|
347
|
-
retryBtn.className = 'btn btn-primary';
|
|
348
|
-
retryBtn.textContent = 'Retry';
|
|
349
|
-
retryBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
350
|
-
const backBtn = document.createElement('button');
|
|
351
|
-
backBtn.type = 'button';
|
|
352
|
-
backBtn.className = 'btn btn-ghost';
|
|
353
|
-
backBtn.textContent = 'Choose a different agent';
|
|
354
|
-
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
355
|
-
actions.appendChild(retryBtn);
|
|
356
|
-
actions.appendChild(backBtn);
|
|
357
|
-
statusLi.appendChild(actions);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
statusDiv.textContent = opt.label + ' installed successfully!';
|
|
361
|
-
renderAgentLoginStep(opt, result.loginCommand, result.loginHint, statusLi);
|
|
362
|
-
} catch (err) {
|
|
363
|
-
statusDiv.textContent = err.message || 'Install failed.';
|
|
364
|
-
statusDiv.setAttribute('data-tone', 'error');
|
|
365
|
-
const backBtn = document.createElement('button');
|
|
366
|
-
backBtn.type = 'button';
|
|
367
|
-
backBtn.className = 'btn btn-ghost btn-block';
|
|
368
|
-
backBtn.textContent = 'Choose a different agent';
|
|
369
|
-
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
370
|
-
statusLi.appendChild(backBtn);
|
|
371
|
-
}
|
|
372
|
-
})();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function renderAgentLoginStep(opt, loginCommand, loginHint, parentLi) {
|
|
376
|
-
const hintEl = document.createElement('p');
|
|
377
|
-
hintEl.className = 'install-hint';
|
|
378
|
-
hintEl.textContent = loginHint || ('Sign in to ' + opt.label + ' to activate it.');
|
|
379
|
-
parentLi.appendChild(hintEl);
|
|
380
|
-
|
|
381
|
-
const signInBtn = document.createElement('button');
|
|
382
|
-
signInBtn.type = 'button';
|
|
383
|
-
signInBtn.className = 'btn btn-primary btn-block';
|
|
384
|
-
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
385
|
-
signInBtn.addEventListener('click', async () => {
|
|
386
|
-
signInBtn.disabled = true;
|
|
387
|
-
signInBtn.textContent = 'Opening terminal...';
|
|
388
|
-
try {
|
|
389
|
-
const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
|
|
390
|
-
signInBtn.style.display = 'none';
|
|
391
|
-
renderAgentReadyCheck(opt, result && result.message, parentLi);
|
|
392
|
-
} catch (err) {
|
|
393
|
-
signInBtn.disabled = false;
|
|
394
|
-
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
395
|
-
setStatus(err.message, 'error');
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
parentLi.appendChild(signInBtn);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function renderAgentReadyCheck(opt, loginMessage, parentLi) {
|
|
402
|
-
const msgEl = document.createElement('p');
|
|
403
|
-
msgEl.className = 'install-hint';
|
|
404
|
-
msgEl.textContent = loginMessage || ('Complete sign-in in the terminal, then click Check if Ready.');
|
|
405
|
-
parentLi.appendChild(msgEl);
|
|
406
|
-
|
|
407
|
-
const checkBtn = document.createElement('button');
|
|
408
|
-
checkBtn.type = 'button';
|
|
409
|
-
checkBtn.className = 'btn btn-primary btn-block';
|
|
410
|
-
checkBtn.textContent = 'Check if Ready';
|
|
411
|
-
|
|
412
|
-
// Declare skipEl before the click handler so it can be hidden on success.
|
|
413
|
-
const skipEl = document.createElement('p');
|
|
414
|
-
const skipLink = document.createElement('button');
|
|
415
|
-
skipLink.type = 'button';
|
|
416
|
-
skipLink.className = 'text-button';
|
|
417
|
-
skipLink.textContent = 'Skip for now — I will sign in later';
|
|
418
|
-
skipLink.addEventListener('click', async () => {
|
|
419
|
-
try {
|
|
420
|
-
const ideData = await api('/api/first-run/ide-commands');
|
|
421
|
-
renderStartWorking(ideData ? ideData.commands : []);
|
|
422
|
-
} catch (_err) {
|
|
423
|
-
renderStartWorking([]);
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
skipEl.appendChild(skipLink);
|
|
427
|
-
|
|
428
|
-
checkBtn.addEventListener('click', async () => {
|
|
429
|
-
checkBtn.disabled = true;
|
|
430
|
-
checkBtn.textContent = 'Checking...';
|
|
431
|
-
try {
|
|
432
|
-
const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
|
|
433
|
-
if (result && result.ready) {
|
|
434
|
-
checkBtn.style.display = 'none';
|
|
435
|
-
skipEl.style.display = 'none';
|
|
436
|
-
msgEl.textContent = opt.label + ' is ready!';
|
|
437
|
-
setHeader(opt.label + ' is ready!', 'You can now open the Hub.');
|
|
438
|
-
renderOpenHubButton(parentLi);
|
|
439
|
-
} else {
|
|
440
|
-
checkBtn.disabled = false;
|
|
441
|
-
checkBtn.textContent = 'Check if Ready';
|
|
442
|
-
setStatus((result && result.message) || (opt.label + ' not detected yet. Complete sign-in and try again.'), 'error');
|
|
443
|
-
}
|
|
444
|
-
} catch (err) {
|
|
445
|
-
checkBtn.disabled = false;
|
|
446
|
-
checkBtn.textContent = 'Check if Ready';
|
|
447
|
-
setStatus(err.message, 'error');
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
parentLi.appendChild(checkBtn);
|
|
452
|
-
parentLi.appendChild(skipEl);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function renderOpenHubButton(parentLi) {
|
|
456
|
-
const openHubBtn = document.createElement('button');
|
|
457
|
-
openHubBtn.type = 'button';
|
|
458
|
-
openHubBtn.className = 'btn btn-primary btn-block';
|
|
459
|
-
openHubBtn.textContent = 'Open Hub';
|
|
460
|
-
openHubBtn.addEventListener('click', async () => {
|
|
461
|
-
openHubBtn.disabled = true;
|
|
462
|
-
setStatus('Opening Hub...');
|
|
463
|
-
try {
|
|
464
|
-
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
465
|
-
} catch (_err) { /* non-fatal */ }
|
|
466
|
-
try {
|
|
467
|
-
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
468
|
-
if (openResp && openResp.message) setStatus(openResp.message);
|
|
469
|
-
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
470
|
-
else openHubBtn.disabled = false;
|
|
471
|
-
} catch (err) {
|
|
472
|
-
openHubBtn.disabled = false;
|
|
473
|
-
setStatus(err.message, 'error');
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
parentLi.appendChild(openHubBtn);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function renderStartWorking(ideCommands) {
|
|
480
|
-
CHECKLIST_EL.className = 'selection-container start-container';
|
|
481
|
-
CHECKLIST_EL.innerHTML = '';
|
|
482
|
-
PRIMARY_BUTTON.style.display = 'none';
|
|
483
|
-
setHeader('Start in your IDE', 'Recommended: fully restart your favorite IDE, open your project there, and let FRAIM run in the surface you already use.');
|
|
484
|
-
|
|
485
|
-
const ideLi = document.createElement('li');
|
|
486
|
-
const ideCard = document.createElement('div');
|
|
487
|
-
ideCard.className = 'user-type-card user-type-card--featured user-type-card--ide-default';
|
|
488
|
-
ideCard.appendChild(cardHeader('</>', 'In my IDE', 'Recommended. Restart Claude Code, Cursor, Codex, or your favorite AI IDE before you continue.'));
|
|
489
|
-
ideCard.appendChild(cardEyebrow('Recommended default'));
|
|
490
|
-
const ideNote = document.createElement('p');
|
|
491
|
-
ideNote.className = 'route-note';
|
|
492
|
-
ideNote.textContent = 'This keeps FRAIM inside the tool you already work in and avoids the extra layer of the Hub.';
|
|
493
|
-
ideCard.appendChild(ideNote);
|
|
494
|
-
const ideBtn = document.createElement('button');
|
|
495
|
-
ideBtn.type = 'button';
|
|
496
|
-
ideBtn.className = 'btn btn-primary btn-block';
|
|
497
|
-
ideBtn.textContent = 'Continue in my IDE';
|
|
498
|
-
ideBtn.addEventListener('click', async () => {
|
|
499
|
-
try {
|
|
500
|
-
await api('/api/first-run/set-preference', 'POST', { choice: 'ide' });
|
|
501
|
-
} catch (_err) { /* non-fatal */ }
|
|
502
|
-
renderIdeCommandDisplay(ideCommands);
|
|
503
|
-
});
|
|
504
|
-
ideCard.appendChild(ideBtn);
|
|
505
|
-
ideLi.appendChild(ideCard);
|
|
506
|
-
CHECKLIST_EL.appendChild(ideLi);
|
|
507
|
-
|
|
508
|
-
const hubLi = document.createElement('li');
|
|
509
|
-
const hubCard = document.createElement('div');
|
|
510
|
-
hubCard.className = 'user-type-card user-type-card--compact user-type-card--alpha';
|
|
511
|
-
hubCard.appendChild(cardHeader('α', 'AI Hub Alpha', 'Early testers only'));
|
|
512
|
-
hubCard.appendChild(cardEyebrow('Optional fallback'));
|
|
513
|
-
const hubNote = document.createElement('p');
|
|
514
|
-
hubNote.className = 'route-note route-note--compact';
|
|
515
|
-
hubNote.textContent = 'Use the Hub only if you specifically want the experimental browser shell.';
|
|
516
|
-
hubCard.appendChild(hubNote);
|
|
517
|
-
const hubBtn = document.createElement('button');
|
|
518
|
-
hubBtn.type = 'button';
|
|
519
|
-
hubBtn.className = 'btn btn-secondary';
|
|
520
|
-
hubBtn.textContent = 'Open AI Hub Alpha';
|
|
521
|
-
hubBtn.addEventListener('click', async () => {
|
|
522
|
-
try {
|
|
523
|
-
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
524
|
-
} catch (_err) { /* non-fatal */ }
|
|
525
|
-
try {
|
|
526
|
-
hubBtn.disabled = true;
|
|
527
|
-
setStatus('Opening Hub...');
|
|
528
|
-
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
529
|
-
if (openResp && openResp.needsAgentSetup) {
|
|
530
|
-
renderRecruitAgents();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (openResp && openResp.message) setStatus(openResp.message);
|
|
534
|
-
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
535
|
-
else hubBtn.disabled = false;
|
|
536
|
-
} catch (err) {
|
|
537
|
-
hubBtn.disabled = false;
|
|
538
|
-
setStatus(err.message, 'error');
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
hubCard.appendChild(hubBtn);
|
|
542
|
-
hubLi.appendChild(hubCard);
|
|
543
|
-
CHECKLIST_EL.appendChild(hubLi);
|
|
544
|
-
|
|
545
|
-
setTimeout(() => ideBtn.focus(), 0);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function cardHeader(iconText, titleText, descText) {
|
|
549
|
-
const header = document.createElement('div');
|
|
550
|
-
header.className = 'card-header';
|
|
551
|
-
const icon = document.createElement('div');
|
|
552
|
-
icon.className = 'card-icon';
|
|
553
|
-
icon.textContent = iconText;
|
|
554
|
-
const text = document.createElement('div');
|
|
555
|
-
const title = document.createElement('strong');
|
|
556
|
-
title.className = 'card-title';
|
|
557
|
-
title.textContent = titleText;
|
|
558
|
-
const desc = document.createElement('p');
|
|
559
|
-
desc.className = 'card-desc';
|
|
560
|
-
desc.textContent = descText;
|
|
561
|
-
text.appendChild(title);
|
|
562
|
-
text.appendChild(desc);
|
|
563
|
-
header.appendChild(icon);
|
|
564
|
-
header.appendChild(text);
|
|
565
|
-
return header;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function cardEyebrow(text) {
|
|
569
|
-
const eyebrow = document.createElement('span');
|
|
570
|
-
eyebrow.className = 'card-eyebrow';
|
|
571
|
-
eyebrow.textContent = text;
|
|
572
|
-
return eyebrow;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function renderIdeCommandDisplay(commands) {
|
|
576
|
-
CHECKLIST_EL.className = 'selection-container';
|
|
577
|
-
CHECKLIST_EL.innerHTML = '';
|
|
578
|
-
setHeader('Restart your favorite IDE', 'Fully restart your IDE first so it picks up the new FRAIM setup. Then open your project there and type:');
|
|
579
|
-
|
|
580
|
-
const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
|
|
581
|
-
for (const cmd of cmdList) {
|
|
582
|
-
const li = document.createElement('li');
|
|
583
|
-
const row = document.createElement('div');
|
|
584
|
-
row.className = 'command-row';
|
|
585
|
-
const block = document.createElement('div');
|
|
586
|
-
block.className = 'cmd-block';
|
|
587
|
-
block.textContent = cmd;
|
|
588
|
-
const copyBtn = document.createElement('button');
|
|
589
|
-
copyBtn.type = 'button';
|
|
590
|
-
copyBtn.className = 'btn btn-secondary';
|
|
591
|
-
copyBtn.textContent = 'Copy';
|
|
592
|
-
copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
|
|
593
|
-
copyBtn.addEventListener('click', async () => {
|
|
594
|
-
try {
|
|
595
|
-
await navigator.clipboard.writeText(cmd);
|
|
596
|
-
copyBtn.textContent = 'Copied';
|
|
597
|
-
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
598
|
-
} catch (_err) {
|
|
599
|
-
// Fallback for environments where clipboard API is unavailable.
|
|
600
|
-
try {
|
|
601
|
-
const ta = document.createElement('textarea');
|
|
602
|
-
ta.value = cmd;
|
|
603
|
-
ta.style.position = 'fixed';
|
|
604
|
-
ta.style.opacity = '0';
|
|
605
|
-
document.body.appendChild(ta);
|
|
606
|
-
ta.select();
|
|
607
|
-
document.execCommand('copy');
|
|
608
|
-
document.body.removeChild(ta);
|
|
609
|
-
copyBtn.textContent = 'Copied';
|
|
610
|
-
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
611
|
-
} catch (_e) {
|
|
612
|
-
copyBtn.textContent = 'Copy failed';
|
|
613
|
-
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
row.appendChild(block);
|
|
618
|
-
row.appendChild(copyBtn);
|
|
619
|
-
li.appendChild(row);
|
|
620
|
-
CHECKLIST_EL.appendChild(li);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const switchLi = document.createElement('li');
|
|
624
|
-
const switchLink = document.createElement('button');
|
|
625
|
-
switchLink.type = 'button';
|
|
626
|
-
switchLink.className = 'text-button';
|
|
627
|
-
switchLink.textContent = 'Prefer AI Hub Alpha instead';
|
|
628
|
-
switchLink.addEventListener('click', async () => {
|
|
629
|
-
try {
|
|
630
|
-
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
631
|
-
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
632
|
-
if (openResp && openResp.needsAgentSetup) {
|
|
633
|
-
renderRecruitAgents();
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
637
|
-
} catch (err) {
|
|
638
|
-
setStatus(err.message, 'error');
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
switchLi.appendChild(switchLink);
|
|
642
|
-
CHECKLIST_EL.appendChild(switchLi);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function render() {
|
|
646
|
-
if (!state.session) return;
|
|
647
|
-
CHECKLIST_EL.className = 'checklist';
|
|
648
|
-
PRIMARY_BUTTON.style.display = '';
|
|
649
|
-
const { rows, primaryButtonLabel } = state.session;
|
|
650
|
-
CHECKLIST_EL.innerHTML = '';
|
|
651
|
-
for (const row of rows) {
|
|
652
|
-
CHECKLIST_EL.appendChild(renderRow(row));
|
|
653
|
-
}
|
|
654
|
-
if (primaryButtonLabel === 'Get Started') {
|
|
655
|
-
const summary = renderSetupSummary();
|
|
656
|
-
if (summary) CHECKLIST_EL.appendChild(summary);
|
|
657
|
-
}
|
|
658
|
-
PRIMARY_BUTTON.textContent = primaryButtonLabel || 'Set up FRAIM';
|
|
659
|
-
applyHeading(rows);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
PRIMARY_BUTTON.addEventListener('click', onPrimaryClick);
|
|
663
|
-
|
|
664
|
-
loadSession().catch((err) => {
|
|
665
|
-
setStatus(err.message || 'Could not load first-run.', 'error');
|
|
666
|
-
});
|
|
667
|
-
}());
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const CHECKLIST_EL = document.getElementById('checklist');
|
|
5
|
+
const PRIMARY_BUTTON = document.getElementById('primary-button');
|
|
6
|
+
const STATUS_EL = document.getElementById('status');
|
|
7
|
+
const LEDE_EL = document.getElementById('lede');
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
session: null,
|
|
11
|
+
runningRowId: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function setStatus(text, tone) {
|
|
15
|
+
STATUS_EL.textContent = text || '';
|
|
16
|
+
if (tone) STATUS_EL.setAttribute('data-tone', tone);
|
|
17
|
+
else STATUS_EL.removeAttribute('data-tone');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setHeader(title, lede) {
|
|
21
|
+
const h1El = document.querySelector('.page-header h1');
|
|
22
|
+
if (h1El) h1El.textContent = title;
|
|
23
|
+
if (LEDE_EL) LEDE_EL.textContent = lede;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyHeading(rows) {
|
|
27
|
+
if (!LEDE_EL) return;
|
|
28
|
+
const complete = rows.every((r) => r.status === 'ok' || r.status === 'manual-required') && rows.some((r) => r.status === 'ok');
|
|
29
|
+
if (complete) {
|
|
30
|
+
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
31
|
+
if (setupResult && setupResult.detectedSurfaceCount > 0) {
|
|
32
|
+
LEDE_EL.textContent = 'The following AI agents are now ready to function as your AI Employees.';
|
|
33
|
+
} else {
|
|
34
|
+
LEDE_EL.textContent = "You do not seem to have any AI agents on this machine.";
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const errored = rows.find((r) => r.status === 'error');
|
|
39
|
+
if (errored) {
|
|
40
|
+
LEDE_EL.textContent = "Something did not go through. Pick the right next step below.";
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (rows.some((r) => r.status === 'ok')) {
|
|
44
|
+
LEDE_EL.textContent = 'FRAIM is finishing setup on this machine.';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
LEDE_EL.textContent = 'We are getting your machine ready for FRAIM.';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function api(path, method, body) {
|
|
51
|
+
const headers = {};
|
|
52
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
53
|
+
if (state.session && state.session.requestToken) {
|
|
54
|
+
headers['x-fraim-first-run-token'] = state.session.requestToken;
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(path, {
|
|
57
|
+
method: method || 'GET',
|
|
58
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
59
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
60
|
+
});
|
|
61
|
+
if (response.status === 204) return null;
|
|
62
|
+
let json = null;
|
|
63
|
+
try {
|
|
64
|
+
json = await response.json();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error(`Server returned invalid JSON (status ${response.status}).`);
|
|
67
|
+
}
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
|
|
70
|
+
}
|
|
71
|
+
return json;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadSession() {
|
|
75
|
+
state.session = await api('/api/first-run/session');
|
|
76
|
+
render();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setSessionFromActionResponse(actionResp) {
|
|
80
|
+
if (!state.session) return;
|
|
81
|
+
state.session.state = actionResp.state;
|
|
82
|
+
state.session.rows = actionResp.rows;
|
|
83
|
+
state.session.primaryButtonLabel = actionResp.primaryButtonLabel;
|
|
84
|
+
state.session.currentAgentId = actionResp.state.agentId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeIcon(status) {
|
|
88
|
+
const icon = document.createElement('span');
|
|
89
|
+
icon.className = 'icon';
|
|
90
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
91
|
+
icon.textContent =
|
|
92
|
+
status === 'ok' ? '✓' :
|
|
93
|
+
status === 'in-progress' ? '...' :
|
|
94
|
+
status === 'manual-required' ? '!' :
|
|
95
|
+
status === 'error' ? '!' :
|
|
96
|
+
'.';
|
|
97
|
+
return icon;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderRow(row) {
|
|
101
|
+
const li = document.createElement('li');
|
|
102
|
+
li.className = 'row';
|
|
103
|
+
li.setAttribute('data-row-id', row.id);
|
|
104
|
+
li.setAttribute('data-row-status', row.status);
|
|
105
|
+
|
|
106
|
+
li.appendChild(makeIcon(row.status));
|
|
107
|
+
|
|
108
|
+
const label = document.createElement('span');
|
|
109
|
+
label.className = 'label';
|
|
110
|
+
label.textContent = row.label;
|
|
111
|
+
li.appendChild(label);
|
|
112
|
+
|
|
113
|
+
const verb = document.createElement('span');
|
|
114
|
+
verb.className = 'verb';
|
|
115
|
+
verb.setAttribute('data-testid', 'row-verb');
|
|
116
|
+
verb.textContent = row.verb || '';
|
|
117
|
+
li.appendChild(verb);
|
|
118
|
+
|
|
119
|
+
if (row.detail) {
|
|
120
|
+
const detail = document.createElement('span');
|
|
121
|
+
detail.className = 'detail';
|
|
122
|
+
detail.setAttribute('data-testid', 'row-detail');
|
|
123
|
+
detail.textContent = row.detail;
|
|
124
|
+
li.appendChild(detail);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (row.streamOutput) {
|
|
128
|
+
const stream = document.createElement('pre');
|
|
129
|
+
stream.className = 'row-stream';
|
|
130
|
+
stream.setAttribute('data-testid', 'row-stream');
|
|
131
|
+
stream.textContent = row.streamOutput;
|
|
132
|
+
li.appendChild(stream);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (row.manualMessage) {
|
|
136
|
+
const message = document.createElement('div');
|
|
137
|
+
message.className = 'manual-message';
|
|
138
|
+
message.textContent = row.manualMessage;
|
|
139
|
+
li.appendChild(message);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (row.status === 'error' && row.errorFrame && window.FraimErrorFrame) {
|
|
143
|
+
const frame = window.FraimErrorFrame.render(row.errorFrame, (action) => onErrorAction(row.id, action));
|
|
144
|
+
li.appendChild(frame);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return li;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderSetupSummary() {
|
|
151
|
+
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
152
|
+
if (!setupResult) return null;
|
|
153
|
+
|
|
154
|
+
const li = document.createElement('li');
|
|
155
|
+
li.className = 'setup-result';
|
|
156
|
+
li.setAttribute('data-testid', 'setup-result');
|
|
157
|
+
|
|
158
|
+
const title = document.createElement('strong');
|
|
159
|
+
title.textContent = setupResult.detectedSurfaceCount > 0
|
|
160
|
+
? 'The following AI agents are now ready to function as your AI Employees'
|
|
161
|
+
: "You do not seem to have any AI agents on this machine";
|
|
162
|
+
li.appendChild(title);
|
|
163
|
+
|
|
164
|
+
if (setupResult.detectedSurfaceCount > 0) {
|
|
165
|
+
const list = document.createElement('ul');
|
|
166
|
+
list.className = 'surface-list';
|
|
167
|
+
for (const surface of setupResult.configuredSurfaces || []) {
|
|
168
|
+
const item = document.createElement('li');
|
|
169
|
+
item.textContent = surface.name;
|
|
170
|
+
list.appendChild(item);
|
|
171
|
+
}
|
|
172
|
+
li.appendChild(list);
|
|
173
|
+
} else {
|
|
174
|
+
const copy = document.createElement('p');
|
|
175
|
+
copy.textContent = "Let's recruit some agents who can function as your AI Employees.";
|
|
176
|
+
li.appendChild(copy);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return li;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function runRow(rowId, extraBody) {
|
|
183
|
+
if (state.runningRowId) return;
|
|
184
|
+
state.runningRowId = rowId;
|
|
185
|
+
PRIMARY_BUTTON.disabled = true;
|
|
186
|
+
setStatus(`Running ${rowId}...`);
|
|
187
|
+
try {
|
|
188
|
+
const resp = await api(`/api/first-run/rows/${rowId}/run`, 'POST', extraBody || {});
|
|
189
|
+
setSessionFromActionResponse(resp);
|
|
190
|
+
setStatus(resp.message, resp.ok ? null : 'error');
|
|
191
|
+
} catch (err) {
|
|
192
|
+
setStatus(err.message, 'error');
|
|
193
|
+
} finally {
|
|
194
|
+
state.runningRowId = null;
|
|
195
|
+
PRIMARY_BUTTON.disabled = false;
|
|
196
|
+
render();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function onErrorAction(rowId, action) {
|
|
201
|
+
const body = { errorActionId: action.id };
|
|
202
|
+
if (action.id === 'alternative' && action.alternativeAgentId) {
|
|
203
|
+
body.alternativeAgentId = action.alternativeAgentId;
|
|
204
|
+
}
|
|
205
|
+
await runRow(rowId, body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function onPrimaryClick() {
|
|
209
|
+
if (!state.session) return;
|
|
210
|
+
|
|
211
|
+
if (state.session.primaryButtonLabel === 'Get Started') {
|
|
212
|
+
PRIMARY_BUTTON.disabled = true;
|
|
213
|
+
const setupResult = state.session.state ? state.session.state.setupResult : null;
|
|
214
|
+
if (setupResult && setupResult.detectedSurfaceCount === 0) {
|
|
215
|
+
renderRecruitAgents();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
220
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
221
|
+
} catch (_err) {
|
|
222
|
+
renderStartWorking([]);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const next = state.session.rows.find((r) => r.status === 'pending');
|
|
228
|
+
if (!next) return;
|
|
229
|
+
await runRow(next.id);
|
|
230
|
+
|
|
231
|
+
while (state.session) {
|
|
232
|
+
const blockingError = state.session.rows.find((r) => r.status === 'error');
|
|
233
|
+
if (blockingError) break;
|
|
234
|
+
const nextRow = state.session.rows.find((r) => r.status === 'pending');
|
|
235
|
+
if (!nextRow) break;
|
|
236
|
+
await runRow(nextRow.id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderRecruitAgents() {
|
|
241
|
+
CHECKLIST_EL.className = 'selection-container recruit-container';
|
|
242
|
+
CHECKLIST_EL.innerHTML = '';
|
|
243
|
+
PRIMARY_BUTTON.style.display = 'none';
|
|
244
|
+
setHeader('Recruit AI Employees', 'Choose how you want to add AI Employees to this machine.');
|
|
245
|
+
|
|
246
|
+
const agentOptions = (state.session && state.session.agentOptions) ? state.session.agentOptions : [
|
|
247
|
+
{ id: 'claude-code', label: 'Claude Code' },
|
|
248
|
+
{ id: 'codex', label: 'Codex' },
|
|
249
|
+
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const AGENT_DESCS = {
|
|
253
|
+
'claude-code': 'Install Claude Code and connect it to FRAIM.',
|
|
254
|
+
'codex': 'Install Codex and connect it to FRAIM.',
|
|
255
|
+
'gemini-cli': 'Install Gemini CLI and connect it to FRAIM.',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
for (const opt of agentOptions) {
|
|
259
|
+
const li = document.createElement('li');
|
|
260
|
+
const card = document.createElement('div');
|
|
261
|
+
card.className = 'user-type-card recruit-card';
|
|
262
|
+
card.setAttribute('data-agent-id', opt.id);
|
|
263
|
+
|
|
264
|
+
const title = document.createElement('strong');
|
|
265
|
+
title.className = 'card-title';
|
|
266
|
+
title.textContent = opt.label;
|
|
267
|
+
const desc = document.createElement('p');
|
|
268
|
+
desc.className = 'card-desc';
|
|
269
|
+
desc.textContent = AGENT_DESCS[opt.id] || ('Install ' + opt.label + ' and connect it to FRAIM.');
|
|
270
|
+
card.appendChild(title);
|
|
271
|
+
card.appendChild(desc);
|
|
272
|
+
|
|
273
|
+
const installBtn = document.createElement('button');
|
|
274
|
+
installBtn.type = 'button';
|
|
275
|
+
installBtn.className = 'btn btn-secondary btn-block';
|
|
276
|
+
installBtn.textContent = 'Install';
|
|
277
|
+
installBtn.setAttribute('data-testid', 'install-' + opt.id);
|
|
278
|
+
installBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
279
|
+
card.appendChild(installBtn);
|
|
280
|
+
|
|
281
|
+
li.appendChild(card);
|
|
282
|
+
CHECKLIST_EL.appendChild(li);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const byoaLi = document.createElement('li');
|
|
286
|
+
const byoaCard = document.createElement('div');
|
|
287
|
+
byoaCard.className = 'user-type-card recruit-card';
|
|
288
|
+
const byoaTitle = document.createElement('strong');
|
|
289
|
+
byoaTitle.className = 'card-title';
|
|
290
|
+
byoaTitle.textContent = 'Bring Your Own Agent';
|
|
291
|
+
const byoaDesc = document.createElement('p');
|
|
292
|
+
byoaDesc.className = 'card-desc';
|
|
293
|
+
byoaDesc.textContent = 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.';
|
|
294
|
+
const byoaNote = document.createElement('p');
|
|
295
|
+
byoaNote.className = 'card-desc';
|
|
296
|
+
byoaNote.textContent = 'When you are done installing your agent, run:';
|
|
297
|
+
const byoaCmd = document.createElement('div');
|
|
298
|
+
byoaCmd.className = 'cmd-block';
|
|
299
|
+
byoaCmd.textContent = 'npx fraim add-ide';
|
|
300
|
+
byoaCard.appendChild(byoaTitle);
|
|
301
|
+
byoaCard.appendChild(byoaDesc);
|
|
302
|
+
byoaCard.appendChild(byoaNote);
|
|
303
|
+
byoaCard.appendChild(byoaCmd);
|
|
304
|
+
byoaLi.appendChild(byoaCard);
|
|
305
|
+
CHECKLIST_EL.appendChild(byoaLi);
|
|
306
|
+
|
|
307
|
+
const continueLi = document.createElement('li');
|
|
308
|
+
const continueBtn = document.createElement('button');
|
|
309
|
+
continueBtn.type = 'button';
|
|
310
|
+
continueBtn.className = 'btn btn-primary btn-block';
|
|
311
|
+
continueBtn.textContent = 'Continue without AI Agent';
|
|
312
|
+
continueBtn.addEventListener('click', async () => {
|
|
313
|
+
try {
|
|
314
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
315
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
316
|
+
} catch (_err) {
|
|
317
|
+
renderStartWorking([]);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
continueLi.appendChild(continueBtn);
|
|
321
|
+
CHECKLIST_EL.appendChild(continueLi);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderAgentInstallFlow(opt) {
|
|
325
|
+
CHECKLIST_EL.className = 'selection-container recruit-container';
|
|
326
|
+
CHECKLIST_EL.innerHTML = '';
|
|
327
|
+
setHeader('Install ' + opt.label, 'Setting up ' + opt.label + ' on this machine...');
|
|
328
|
+
|
|
329
|
+
const statusLi = document.createElement('li');
|
|
330
|
+
const statusDiv = document.createElement('div');
|
|
331
|
+
statusDiv.className = 'install-status';
|
|
332
|
+
statusDiv.setAttribute('data-testid', 'agent-install-status');
|
|
333
|
+
statusDiv.textContent = 'Installing ' + opt.label + '...';
|
|
334
|
+
statusLi.appendChild(statusDiv);
|
|
335
|
+
CHECKLIST_EL.appendChild(statusLi);
|
|
336
|
+
|
|
337
|
+
(async () => {
|
|
338
|
+
try {
|
|
339
|
+
const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
|
|
340
|
+
if (!result || !result.ok) {
|
|
341
|
+
statusDiv.textContent = (result && result.message) ? result.message : 'Install failed.';
|
|
342
|
+
statusDiv.setAttribute('data-tone', 'error');
|
|
343
|
+
const actions = document.createElement('div');
|
|
344
|
+
actions.className = 'install-actions';
|
|
345
|
+
const retryBtn = document.createElement('button');
|
|
346
|
+
retryBtn.type = 'button';
|
|
347
|
+
retryBtn.className = 'btn btn-primary';
|
|
348
|
+
retryBtn.textContent = 'Retry';
|
|
349
|
+
retryBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
350
|
+
const backBtn = document.createElement('button');
|
|
351
|
+
backBtn.type = 'button';
|
|
352
|
+
backBtn.className = 'btn btn-ghost';
|
|
353
|
+
backBtn.textContent = 'Choose a different agent';
|
|
354
|
+
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
355
|
+
actions.appendChild(retryBtn);
|
|
356
|
+
actions.appendChild(backBtn);
|
|
357
|
+
statusLi.appendChild(actions);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
statusDiv.textContent = opt.label + ' installed successfully!';
|
|
361
|
+
renderAgentLoginStep(opt, result.loginCommand, result.loginHint, statusLi);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
statusDiv.textContent = err.message || 'Install failed.';
|
|
364
|
+
statusDiv.setAttribute('data-tone', 'error');
|
|
365
|
+
const backBtn = document.createElement('button');
|
|
366
|
+
backBtn.type = 'button';
|
|
367
|
+
backBtn.className = 'btn btn-ghost btn-block';
|
|
368
|
+
backBtn.textContent = 'Choose a different agent';
|
|
369
|
+
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
370
|
+
statusLi.appendChild(backBtn);
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderAgentLoginStep(opt, loginCommand, loginHint, parentLi) {
|
|
376
|
+
const hintEl = document.createElement('p');
|
|
377
|
+
hintEl.className = 'install-hint';
|
|
378
|
+
hintEl.textContent = loginHint || ('Sign in to ' + opt.label + ' to activate it.');
|
|
379
|
+
parentLi.appendChild(hintEl);
|
|
380
|
+
|
|
381
|
+
const signInBtn = document.createElement('button');
|
|
382
|
+
signInBtn.type = 'button';
|
|
383
|
+
signInBtn.className = 'btn btn-primary btn-block';
|
|
384
|
+
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
385
|
+
signInBtn.addEventListener('click', async () => {
|
|
386
|
+
signInBtn.disabled = true;
|
|
387
|
+
signInBtn.textContent = 'Opening terminal...';
|
|
388
|
+
try {
|
|
389
|
+
const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
|
|
390
|
+
signInBtn.style.display = 'none';
|
|
391
|
+
renderAgentReadyCheck(opt, result && result.message, parentLi);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
signInBtn.disabled = false;
|
|
394
|
+
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
395
|
+
setStatus(err.message, 'error');
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
parentLi.appendChild(signInBtn);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function renderAgentReadyCheck(opt, loginMessage, parentLi) {
|
|
402
|
+
const msgEl = document.createElement('p');
|
|
403
|
+
msgEl.className = 'install-hint';
|
|
404
|
+
msgEl.textContent = loginMessage || ('Complete sign-in in the terminal, then click Check if Ready.');
|
|
405
|
+
parentLi.appendChild(msgEl);
|
|
406
|
+
|
|
407
|
+
const checkBtn = document.createElement('button');
|
|
408
|
+
checkBtn.type = 'button';
|
|
409
|
+
checkBtn.className = 'btn btn-primary btn-block';
|
|
410
|
+
checkBtn.textContent = 'Check if Ready';
|
|
411
|
+
|
|
412
|
+
// Declare skipEl before the click handler so it can be hidden on success.
|
|
413
|
+
const skipEl = document.createElement('p');
|
|
414
|
+
const skipLink = document.createElement('button');
|
|
415
|
+
skipLink.type = 'button';
|
|
416
|
+
skipLink.className = 'text-button';
|
|
417
|
+
skipLink.textContent = 'Skip for now — I will sign in later';
|
|
418
|
+
skipLink.addEventListener('click', async () => {
|
|
419
|
+
try {
|
|
420
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
421
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
422
|
+
} catch (_err) {
|
|
423
|
+
renderStartWorking([]);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
skipEl.appendChild(skipLink);
|
|
427
|
+
|
|
428
|
+
checkBtn.addEventListener('click', async () => {
|
|
429
|
+
checkBtn.disabled = true;
|
|
430
|
+
checkBtn.textContent = 'Checking...';
|
|
431
|
+
try {
|
|
432
|
+
const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
|
|
433
|
+
if (result && result.ready) {
|
|
434
|
+
checkBtn.style.display = 'none';
|
|
435
|
+
skipEl.style.display = 'none';
|
|
436
|
+
msgEl.textContent = opt.label + ' is ready!';
|
|
437
|
+
setHeader(opt.label + ' is ready!', 'You can now open the Hub.');
|
|
438
|
+
renderOpenHubButton(parentLi);
|
|
439
|
+
} else {
|
|
440
|
+
checkBtn.disabled = false;
|
|
441
|
+
checkBtn.textContent = 'Check if Ready';
|
|
442
|
+
setStatus((result && result.message) || (opt.label + ' not detected yet. Complete sign-in and try again.'), 'error');
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
checkBtn.disabled = false;
|
|
446
|
+
checkBtn.textContent = 'Check if Ready';
|
|
447
|
+
setStatus(err.message, 'error');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
parentLi.appendChild(checkBtn);
|
|
452
|
+
parentLi.appendChild(skipEl);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderOpenHubButton(parentLi) {
|
|
456
|
+
const openHubBtn = document.createElement('button');
|
|
457
|
+
openHubBtn.type = 'button';
|
|
458
|
+
openHubBtn.className = 'btn btn-primary btn-block';
|
|
459
|
+
openHubBtn.textContent = 'Open Hub';
|
|
460
|
+
openHubBtn.addEventListener('click', async () => {
|
|
461
|
+
openHubBtn.disabled = true;
|
|
462
|
+
setStatus('Opening Hub...');
|
|
463
|
+
try {
|
|
464
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
465
|
+
} catch (_err) { /* non-fatal */ }
|
|
466
|
+
try {
|
|
467
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
468
|
+
if (openResp && openResp.message) setStatus(openResp.message);
|
|
469
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
470
|
+
else openHubBtn.disabled = false;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
openHubBtn.disabled = false;
|
|
473
|
+
setStatus(err.message, 'error');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
parentLi.appendChild(openHubBtn);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function renderStartWorking(ideCommands) {
|
|
480
|
+
CHECKLIST_EL.className = 'selection-container start-container';
|
|
481
|
+
CHECKLIST_EL.innerHTML = '';
|
|
482
|
+
PRIMARY_BUTTON.style.display = 'none';
|
|
483
|
+
setHeader('Start in your IDE', 'Recommended: fully restart your favorite IDE, open your project there, and let FRAIM run in the surface you already use.');
|
|
484
|
+
|
|
485
|
+
const ideLi = document.createElement('li');
|
|
486
|
+
const ideCard = document.createElement('div');
|
|
487
|
+
ideCard.className = 'user-type-card user-type-card--featured user-type-card--ide-default';
|
|
488
|
+
ideCard.appendChild(cardHeader('</>', 'In my IDE', 'Recommended. Restart Claude Code, Cursor, Codex, or your favorite AI IDE before you continue.'));
|
|
489
|
+
ideCard.appendChild(cardEyebrow('Recommended default'));
|
|
490
|
+
const ideNote = document.createElement('p');
|
|
491
|
+
ideNote.className = 'route-note';
|
|
492
|
+
ideNote.textContent = 'This keeps FRAIM inside the tool you already work in and avoids the extra layer of the Hub.';
|
|
493
|
+
ideCard.appendChild(ideNote);
|
|
494
|
+
const ideBtn = document.createElement('button');
|
|
495
|
+
ideBtn.type = 'button';
|
|
496
|
+
ideBtn.className = 'btn btn-primary btn-block';
|
|
497
|
+
ideBtn.textContent = 'Continue in my IDE';
|
|
498
|
+
ideBtn.addEventListener('click', async () => {
|
|
499
|
+
try {
|
|
500
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'ide' });
|
|
501
|
+
} catch (_err) { /* non-fatal */ }
|
|
502
|
+
renderIdeCommandDisplay(ideCommands);
|
|
503
|
+
});
|
|
504
|
+
ideCard.appendChild(ideBtn);
|
|
505
|
+
ideLi.appendChild(ideCard);
|
|
506
|
+
CHECKLIST_EL.appendChild(ideLi);
|
|
507
|
+
|
|
508
|
+
const hubLi = document.createElement('li');
|
|
509
|
+
const hubCard = document.createElement('div');
|
|
510
|
+
hubCard.className = 'user-type-card user-type-card--compact user-type-card--alpha';
|
|
511
|
+
hubCard.appendChild(cardHeader('α', 'AI Hub Alpha', 'Early testers only'));
|
|
512
|
+
hubCard.appendChild(cardEyebrow('Optional fallback'));
|
|
513
|
+
const hubNote = document.createElement('p');
|
|
514
|
+
hubNote.className = 'route-note route-note--compact';
|
|
515
|
+
hubNote.textContent = 'Use the Hub only if you specifically want the experimental browser shell.';
|
|
516
|
+
hubCard.appendChild(hubNote);
|
|
517
|
+
const hubBtn = document.createElement('button');
|
|
518
|
+
hubBtn.type = 'button';
|
|
519
|
+
hubBtn.className = 'btn btn-secondary';
|
|
520
|
+
hubBtn.textContent = 'Open AI Hub Alpha';
|
|
521
|
+
hubBtn.addEventListener('click', async () => {
|
|
522
|
+
try {
|
|
523
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
524
|
+
} catch (_err) { /* non-fatal */ }
|
|
525
|
+
try {
|
|
526
|
+
hubBtn.disabled = true;
|
|
527
|
+
setStatus('Opening Hub...');
|
|
528
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
529
|
+
if (openResp && openResp.needsAgentSetup) {
|
|
530
|
+
renderRecruitAgents();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (openResp && openResp.message) setStatus(openResp.message);
|
|
534
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
535
|
+
else hubBtn.disabled = false;
|
|
536
|
+
} catch (err) {
|
|
537
|
+
hubBtn.disabled = false;
|
|
538
|
+
setStatus(err.message, 'error');
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
hubCard.appendChild(hubBtn);
|
|
542
|
+
hubLi.appendChild(hubCard);
|
|
543
|
+
CHECKLIST_EL.appendChild(hubLi);
|
|
544
|
+
|
|
545
|
+
setTimeout(() => ideBtn.focus(), 0);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function cardHeader(iconText, titleText, descText) {
|
|
549
|
+
const header = document.createElement('div');
|
|
550
|
+
header.className = 'card-header';
|
|
551
|
+
const icon = document.createElement('div');
|
|
552
|
+
icon.className = 'card-icon';
|
|
553
|
+
icon.textContent = iconText;
|
|
554
|
+
const text = document.createElement('div');
|
|
555
|
+
const title = document.createElement('strong');
|
|
556
|
+
title.className = 'card-title';
|
|
557
|
+
title.textContent = titleText;
|
|
558
|
+
const desc = document.createElement('p');
|
|
559
|
+
desc.className = 'card-desc';
|
|
560
|
+
desc.textContent = descText;
|
|
561
|
+
text.appendChild(title);
|
|
562
|
+
text.appendChild(desc);
|
|
563
|
+
header.appendChild(icon);
|
|
564
|
+
header.appendChild(text);
|
|
565
|
+
return header;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cardEyebrow(text) {
|
|
569
|
+
const eyebrow = document.createElement('span');
|
|
570
|
+
eyebrow.className = 'card-eyebrow';
|
|
571
|
+
eyebrow.textContent = text;
|
|
572
|
+
return eyebrow;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderIdeCommandDisplay(commands) {
|
|
576
|
+
CHECKLIST_EL.className = 'selection-container';
|
|
577
|
+
CHECKLIST_EL.innerHTML = '';
|
|
578
|
+
setHeader('Restart your favorite IDE', 'Fully restart your IDE first so it picks up the new FRAIM setup. Then open your project there and type:');
|
|
579
|
+
|
|
580
|
+
const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
|
|
581
|
+
for (const cmd of cmdList) {
|
|
582
|
+
const li = document.createElement('li');
|
|
583
|
+
const row = document.createElement('div');
|
|
584
|
+
row.className = 'command-row';
|
|
585
|
+
const block = document.createElement('div');
|
|
586
|
+
block.className = 'cmd-block';
|
|
587
|
+
block.textContent = cmd;
|
|
588
|
+
const copyBtn = document.createElement('button');
|
|
589
|
+
copyBtn.type = 'button';
|
|
590
|
+
copyBtn.className = 'btn btn-secondary';
|
|
591
|
+
copyBtn.textContent = 'Copy';
|
|
592
|
+
copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
|
|
593
|
+
copyBtn.addEventListener('click', async () => {
|
|
594
|
+
try {
|
|
595
|
+
await navigator.clipboard.writeText(cmd);
|
|
596
|
+
copyBtn.textContent = 'Copied';
|
|
597
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
598
|
+
} catch (_err) {
|
|
599
|
+
// Fallback for environments where clipboard API is unavailable.
|
|
600
|
+
try {
|
|
601
|
+
const ta = document.createElement('textarea');
|
|
602
|
+
ta.value = cmd;
|
|
603
|
+
ta.style.position = 'fixed';
|
|
604
|
+
ta.style.opacity = '0';
|
|
605
|
+
document.body.appendChild(ta);
|
|
606
|
+
ta.select();
|
|
607
|
+
document.execCommand('copy');
|
|
608
|
+
document.body.removeChild(ta);
|
|
609
|
+
copyBtn.textContent = 'Copied';
|
|
610
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
611
|
+
} catch (_e) {
|
|
612
|
+
copyBtn.textContent = 'Copy failed';
|
|
613
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
row.appendChild(block);
|
|
618
|
+
row.appendChild(copyBtn);
|
|
619
|
+
li.appendChild(row);
|
|
620
|
+
CHECKLIST_EL.appendChild(li);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const switchLi = document.createElement('li');
|
|
624
|
+
const switchLink = document.createElement('button');
|
|
625
|
+
switchLink.type = 'button';
|
|
626
|
+
switchLink.className = 'text-button';
|
|
627
|
+
switchLink.textContent = 'Prefer AI Hub Alpha instead';
|
|
628
|
+
switchLink.addEventListener('click', async () => {
|
|
629
|
+
try {
|
|
630
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
631
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
632
|
+
if (openResp && openResp.needsAgentSetup) {
|
|
633
|
+
renderRecruitAgents();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
setStatus(err.message, 'error');
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
switchLi.appendChild(switchLink);
|
|
642
|
+
CHECKLIST_EL.appendChild(switchLi);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function render() {
|
|
646
|
+
if (!state.session) return;
|
|
647
|
+
CHECKLIST_EL.className = 'checklist';
|
|
648
|
+
PRIMARY_BUTTON.style.display = '';
|
|
649
|
+
const { rows, primaryButtonLabel } = state.session;
|
|
650
|
+
CHECKLIST_EL.innerHTML = '';
|
|
651
|
+
for (const row of rows) {
|
|
652
|
+
CHECKLIST_EL.appendChild(renderRow(row));
|
|
653
|
+
}
|
|
654
|
+
if (primaryButtonLabel === 'Get Started') {
|
|
655
|
+
const summary = renderSetupSummary();
|
|
656
|
+
if (summary) CHECKLIST_EL.appendChild(summary);
|
|
657
|
+
}
|
|
658
|
+
PRIMARY_BUTTON.textContent = primaryButtonLabel || 'Set up FRAIM';
|
|
659
|
+
applyHeading(rows);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
PRIMARY_BUTTON.addEventListener('click', onPrimaryClick);
|
|
663
|
+
|
|
664
|
+
loadSession().catch((err) => {
|
|
665
|
+
setStatus(err.message || 'Could not load first-run.', 'error');
|
|
666
|
+
});
|
|
667
|
+
}());
|