fraim-framework 2.0.127 → 2.0.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,361 +1,417 @@
1
- (function () {
2
- const stepOrder = ['welcome', 'prereqs', 'agent', 'configure', 'project', 'launch', 'finish'];
3
- const stepLabels = {
4
- welcome: 'Welcome',
5
- prereqs: 'System prerequisites',
6
- agent: 'AI agent',
7
- configure: 'Configure FRAIM',
8
- project: 'Project folder',
9
- launch: 'Open your AI',
10
- finish: 'Your first prompt',
11
- };
12
-
13
- const state = {
14
- session: null,
15
- selectedAgentId: null,
16
- };
17
-
18
- const content = document.getElementById('content');
19
- const statusEl = document.getElementById('status');
20
- const detailsEl = document.getElementById('details');
21
- const stepsEl = document.getElementById('steps');
22
-
23
- function escapeHtml(value) {
24
- return String(value)
25
- .replace(/&/g, '&')
26
- .replace(/</g, '&lt;')
27
- .replace(/>/g, '&gt;')
28
- .replace(/"/g, '&quot;')
29
- .replace(/'/g, '&#39;');
30
- }
31
-
32
- function setStatus(message, error) {
33
- statusEl.textContent = message;
34
- statusEl.classList.toggle('error', Boolean(error));
35
- }
36
-
37
- function setDetails(text) {
38
- if (!text) {
39
- detailsEl.hidden = true;
40
- detailsEl.textContent = '';
41
- return;
42
- }
43
- detailsEl.hidden = false;
44
- detailsEl.textContent = text;
45
- }
46
-
47
- function firstIncompleteStep(session) {
48
- for (const stepId of stepOrder) {
49
- const status = session.state.stepStates[stepId];
50
- if (status !== 'complete') {
51
- return stepId;
52
- }
53
- }
54
- return 'finish';
55
- }
56
-
57
- function renderSteps(session) {
58
- const active = firstIncompleteStep(session);
59
- stepsEl.innerHTML = stepOrder.map((stepId) => {
60
- const status = session.state.stepStates[stepId];
61
- const activeClass = stepId === active ? ' active' : '';
62
- return `<li class="step${activeClass}" data-step="${stepId}"><strong>${stepLabels[stepId]}</strong><span>${status}</span></li>`;
63
- }).join('');
64
- }
65
-
66
- async function api(path, method, body) {
67
- const headers = {};
68
- if (body) {
69
- headers['Content-Type'] = 'application/json';
70
- }
71
- if (state.session && state.session.requestToken) {
72
- headers['x-fraim-first-run-token'] = state.session.requestToken;
73
- }
74
-
75
- const response = await fetch(path, {
76
- method: method || 'GET',
77
- headers: Object.keys(headers).length > 0 ? headers : undefined,
78
- body: body ? JSON.stringify(body) : undefined,
79
- });
80
-
81
- if (response.status === 204) {
82
- return null;
83
- }
84
-
85
- const json = await response.json();
86
- if (!response.ok) {
87
- throw new Error(json.error || 'Request failed.');
88
- }
89
- return json;
90
- }
91
-
92
- async function loadSession() {
93
- state.session = await api('/api/first-run/session');
94
- if (!state.selectedAgentId && state.session.state.selectedAgentId) {
95
- state.selectedAgentId = state.session.state.selectedAgentId;
96
- }
97
- render();
98
- }
99
-
100
- function renderWelcome() {
101
- return `
102
- <h2>One guided setup</h2>
103
- <p class="lede">This flow uses your FRAIM key, writes FRAIM config where it belongs, initializes your project, and gives you a clean handoff prompt.</p>
104
- <div class="actions">
105
- <button id="start-prereqs">Get started</button>
106
- </div>
107
- `;
108
- }
109
-
110
- function renderPrereqs() {
111
- return `
112
- <h2>System prerequisites</h2>
113
- <p class="lede">We’ll confirm that Node.js, npx, and git are callable before moving into agent setup.</p>
114
- <div class="grid">
115
- <article class="stat"><h3>Node.js</h3><p>Required for FRAIM CLI orchestration.</p></article>
116
- <article class="stat"><h3>npx</h3><p>Used to invoke the packaged FRAIM runtime.</p></article>
117
- <article class="stat"><h3>git</h3><p>Required for project initialization and issue-based workflows.</p></article>
118
- </div>
119
- <div class="actions">
120
- <button id="run-prereqs">Run checks</button>
121
- </div>
122
- `;
123
- }
124
-
125
- function renderAgents(session) {
126
- const cards = session.agents.map((agent) => {
127
- const selected = state.selectedAgentId === agent.id ? ' style="border-color: var(--accent); background: var(--accent-2);"' : '';
128
- const actionLabel = agent.detected ? `Use ${agent.label}` : `Choose ${agent.label}`;
129
- return `
130
- <article class="agent-card"${selected}>
131
- <h3>${agent.label}</h3>
132
- <p>${agent.detected ? 'Detected on this machine.' : 'Not detected yet. You can still choose it and follow the vendor login/install flow.'}</p>
133
- <p style="margin-top:10px">${agent.detail}</p>
134
- <div class="actions">
135
- <button class="select-agent" data-agent="${agent.id}">${actionLabel}</button>
136
- </div>
137
- </article>
138
- `;
139
- }).join('');
140
-
141
- return `
142
- <h2>Choose your AI agent</h2>
143
- <p class="lede">Pick the agent you want FRAIM to guide for your first run. Subscription/vendor-owned login remains outside FRAIM.</p>
144
- <div class="grid">${cards}</div>
145
- `;
146
- }
147
-
148
- function renderConfigure() {
149
- return `
150
- <h2>Configure FRAIM</h2>
151
- <p class="lede">This writes your FRAIM global config and updates supported agent config on the machine where possible.</p>
152
- <div class="actions">
153
- <button id="configure-fraim">Write config</button>
154
- </div>
155
- `;
156
- }
157
-
158
- function renderProject(session) {
159
- const projectPath = escapeHtml(session.state.workspacePath || '');
160
- return `
161
- <h2>Pick a project folder</h2>
162
- <p class="lede">Choose an existing folder or create a new one. FRAIM will initialize git when needed and run <code>fraim init-project</code> there.</p>
163
- <label for="project-path">Project path</label>
164
- <div class="path-row">
165
- <input id="project-path" type="text" value="${projectPath}" placeholder="C:\\Projects\\my-project or /Users/you/Projects/my-project">
166
- <button class="secondary" id="pick-folder">Browse</button>
167
- </div>
168
- <div class="actions">
169
- <button id="init-project">Initialize project</button>
170
- </div>
171
- `;
172
- }
173
-
174
- function renderLaunch(session) {
175
- const launchCommand = session.state.lastLaunchCommand
176
- ? `<p><strong>Launch command:</strong> <code>${escapeHtml(session.state.lastLaunchCommand)}</code></p>`
177
- : '';
178
- return `
179
- <h2>Open your AI</h2>
180
- <p class="lede">FRAIM will attempt a best-effort launch, then run a deterministic probe where the selected CLI supports it.</p>
181
- ${launchCommand}
182
- <div class="actions">
183
- <button id="launch-agent">Launch and verify</button>
184
- </div>
185
- `;
186
- }
187
-
188
- function renderFinish(session) {
189
- const prompt = escapeHtml(session.prompt || 'Onboard this project');
190
- const resourcesUrl = escapeHtml(session.state.resourcesUrl);
191
- return `
192
- <h2>Your first prompt</h2>
193
- <p class="lede">Copy this into your agent if it is not already open. FRAIM also writes it to your local install artifact for later recovery.</p>
194
- <div class="prompt" id="prompt-box">${prompt}</div>
195
- <div class="actions">
196
- <button class="secondary" id="copy-prompt">Copy prompt</button>
197
- <button id="finish-flow">Done</button>
198
- </div>
199
- <p style="margin-top:18px"><a href="${resourcesUrl}" target="_blank" rel="noreferrer">Open FRAIM resources</a></p>
200
- `;
201
- }
202
-
203
- function render() {
204
- const session = state.session;
205
- if (!session) {
206
- return;
207
- }
208
-
209
- renderSteps(session);
210
- const active = firstIncompleteStep(session);
211
- if (active === 'prereqs' && session.state.stepStates.welcome === 'complete') {
212
- content.innerHTML = renderPrereqs();
213
- } else if (active === 'agent') {
214
- content.innerHTML = renderAgents(session);
215
- } else if (active === 'configure') {
216
- content.innerHTML = renderConfigure();
217
- } else if (active === 'project') {
218
- content.innerHTML = renderProject(session);
219
- } else if (active === 'launch') {
220
- content.innerHTML = renderLaunch(session);
221
- } else if (active === 'finish') {
222
- content.innerHTML = renderFinish(session);
223
- } else {
224
- content.innerHTML = renderWelcome();
225
- }
226
- bindActions();
227
- }
228
-
229
- function bindActions() {
230
- const startPrereqs = document.getElementById('start-prereqs');
231
- if (startPrereqs) {
232
- startPrereqs.addEventListener('click', async function () {
233
- state.session.state.stepStates.welcome = 'complete';
234
- state.session.state.stepStates.prereqs = 'running';
235
- render();
236
- });
237
- }
238
-
239
- const runPrereqs = document.getElementById('run-prereqs');
240
- if (runPrereqs) {
241
- runPrereqs.addEventListener('click', async function () {
242
- setStatus('Checking prerequisites…');
243
- try {
244
- const result = await api('/api/first-run/prereqs', 'POST');
245
- setStatus(result.message, !result.ok);
246
- setDetails('');
247
- await loadSession();
248
- } catch (error) {
249
- setStatus(error.message, true);
250
- }
251
- });
252
- }
253
-
254
- document.querySelectorAll('.select-agent').forEach(function (button) {
255
- button.addEventListener('click', async function () {
256
- const agentId = this.getAttribute('data-agent');
257
- setStatus('Saving agent selection…');
258
- try {
259
- const result = await api('/api/first-run/agents/select', 'POST', { agentId: agentId });
260
- state.selectedAgentId = agentId;
261
- setStatus(result.message);
262
- setDetails(result.launchCommand ? `Vendor login command: ${result.launchCommand}` : '');
263
- await loadSession();
264
- } catch (error) {
265
- setStatus(error.message, true);
266
- }
267
- });
268
- });
269
-
270
- const configureFraim = document.getElementById('configure-fraim');
271
- if (configureFraim) {
272
- configureFraim.addEventListener('click', async function () {
273
- setStatus('Writing FRAIM config…');
274
- try {
275
- const result = await api('/api/first-run/configure', 'POST');
276
- setStatus(result.message, !result.ok);
277
- setDetails('');
278
- await loadSession();
279
- } catch (error) {
280
- setStatus(error.message, true);
281
- }
282
- });
283
- }
284
-
285
- const pickFolder = document.getElementById('pick-folder');
286
- if (pickFolder) {
287
- pickFolder.addEventListener('click', async function () {
288
- try {
289
- const picked = await api('/api/first-run/project-path/pick', 'POST');
290
- if (picked && picked.path) {
291
- document.getElementById('project-path').value = picked.path;
292
- }
293
- } catch (error) {
294
- setStatus(error.message, true);
295
- }
296
- });
297
- }
298
-
299
- const initProject = document.getElementById('init-project');
300
- if (initProject) {
301
- initProject.addEventListener('click', async function () {
302
- const projectPath = document.getElementById('project-path').value.trim();
303
- if (!projectPath) {
304
- setStatus('Project path is required.', true);
305
- return;
306
- }
307
- setStatus('Initializing project…');
308
- try {
309
- const result = await api('/api/first-run/project', 'POST', { projectPath: projectPath, initializeGit: true });
310
- setStatus(result.message, !result.ok);
311
- setDetails('');
312
- await loadSession();
313
- } catch (error) {
314
- setStatus(error.message, true);
315
- }
316
- });
317
- }
318
-
319
- const launchAgent = document.getElementById('launch-agent');
320
- if (launchAgent) {
321
- launchAgent.addEventListener('click', async function () {
322
- setStatus('Launching agent and running probe…');
323
- try {
324
- const result = await api('/api/first-run/launch', 'POST');
325
- setStatus(result.message, !result.ok);
326
- setDetails(result.output || result.launchCommand || '');
327
- await loadSession();
328
- } catch (error) {
329
- setStatus(error.message, true);
330
- }
331
- });
332
- }
333
-
334
- const copyPrompt = document.getElementById('copy-prompt');
335
- if (copyPrompt) {
336
- copyPrompt.addEventListener('click', async function () {
337
- const prompt = document.getElementById('prompt-box').textContent || '';
338
- await navigator.clipboard.writeText(prompt);
339
- setStatus('Prompt copied.');
340
- });
341
- }
342
-
343
- const finishFlow = document.getElementById('finish-flow');
344
- if (finishFlow) {
345
- finishFlow.addEventListener('click', async function () {
346
- setStatus('Writing final prompt artifact…');
347
- try {
348
- const result = await api('/api/first-run/finish', 'POST');
349
- setStatus(result.message);
350
- setDetails('');
351
- } catch (error) {
352
- setStatus(error.message, true);
353
- }
354
- });
355
- }
356
- }
357
-
358
- loadSession().catch(function (error) {
359
- setStatus(error.message || 'Could not load first-run.', true);
360
- });
361
- }());
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
+ activeAgentPickerRowId: null,
12
+ runningRowId: null,
13
+ };
14
+
15
+ function setStatus(text, tone) {
16
+ STATUS_EL.textContent = text || '';
17
+ if (tone) {
18
+ STATUS_EL.setAttribute('data-tone', tone);
19
+ } else {
20
+ STATUS_EL.removeAttribute('data-tone');
21
+ }
22
+ }
23
+
24
+ function applyHeading(rows) {
25
+ if (!LEDE_EL) return;
26
+ if (rows.every((r) => r.status === 'ok')) {
27
+ LEDE_EL.textContent = 'You\'re ready. Open the Hub to get started.';
28
+ return;
29
+ }
30
+ const errored = rows.find((r) => r.status === 'error');
31
+ if (errored) {
32
+ LEDE_EL.textContent = 'Something didn\'t go through. Pick the right next step below.';
33
+ return;
34
+ }
35
+ const onlyProjectLeft = rows.filter((r) => r.status !== 'ok').every((r) => r.id === 'project');
36
+ if (onlyProjectLeft) {
37
+ LEDE_EL.textContent = 'Almost there — pick a project folder where FRAIM should work.';
38
+ return;
39
+ }
40
+ if (rows.some((r) => r.status === 'ok')) {
41
+ LEDE_EL.textContent = 'Some pieces are already on your machine. We\'re finishing the rest now.';
42
+ return;
43
+ }
44
+ LEDE_EL.textContent = 'We\'re getting your machine ready. Sit tight — we\'ll only need you for one step.';
45
+ }
46
+
47
+ async function api(path, method, body) {
48
+ const headers = {};
49
+ if (body !== undefined) headers['Content-Type'] = 'application/json';
50
+ if (state.session && state.session.requestToken) {
51
+ headers['x-fraim-first-run-token'] = state.session.requestToken;
52
+ }
53
+ const response = await fetch(path, {
54
+ method: method || 'GET',
55
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
56
+ body: body !== undefined ? JSON.stringify(body) : undefined,
57
+ });
58
+ if (response.status === 204) return null;
59
+ let json = null;
60
+ try {
61
+ json = await response.json();
62
+ } catch (e) {
63
+ throw new Error(`Server returned invalid JSON (status ${response.status}).`);
64
+ }
65
+ if (!response.ok) {
66
+ throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
67
+ }
68
+ return json;
69
+ }
70
+
71
+ async function loadSession() {
72
+ state.session = await api('/api/first-run/session');
73
+ render();
74
+ }
75
+
76
+ function setSessionFromActionResponse(actionResp) {
77
+ // Action responses include rows + primaryButtonLabel + state but not
78
+ // every session-level field. Merge into the held session view.
79
+ if (!state.session) return;
80
+ state.session.state = actionResp.state;
81
+ state.session.rows = actionResp.rows;
82
+ state.session.primaryButtonLabel = actionResp.primaryButtonLabel;
83
+ state.session.currentAgentId = actionResp.state.agentId;
84
+ }
85
+
86
+ function findRow(rowId) {
87
+ return state.session.rows.find((row) => row.id === rowId);
88
+ }
89
+
90
+ function makeIcon(status) {
91
+ const icon = document.createElement('span');
92
+ icon.className = 'icon';
93
+ icon.setAttribute('aria-hidden', 'true');
94
+ icon.textContent =
95
+ status === 'ok' ? '✓' :
96
+ status === 'in-progress' ? '⠋' :
97
+ status === 'manual-required' ? '!' :
98
+ status === 'error' ? '!' :
99
+ '·';
100
+ return icon;
101
+ }
102
+
103
+ function renderRow(row) {
104
+ const li = document.createElement('li');
105
+ li.className = 'row';
106
+ li.setAttribute('data-row-id', row.id);
107
+ li.setAttribute('data-row-status', row.status);
108
+
109
+ li.appendChild(makeIcon(row.status));
110
+
111
+ const label = document.createElement('span');
112
+ label.className = 'label';
113
+ label.textContent = row.label;
114
+ li.appendChild(label);
115
+
116
+ const verb = document.createElement('span');
117
+ verb.className = 'verb';
118
+ verb.setAttribute('data-testid', 'row-verb');
119
+ verb.textContent = row.verb || '';
120
+ li.appendChild(verb);
121
+
122
+ if (row.detail) {
123
+ const detail = document.createElement('span');
124
+ detail.className = 'detail';
125
+ detail.setAttribute('data-testid', 'row-detail');
126
+ detail.textContent = row.detail;
127
+ li.appendChild(detail);
128
+ }
129
+
130
+ // Inline `Change…` link on the agent row
131
+ if (row.id === 'agent') {
132
+ const change = document.createElement('button');
133
+ change.type = 'button';
134
+ change.className = 'change-link';
135
+ change.setAttribute('data-testid', 'change-agent');
136
+ change.textContent = 'Change…';
137
+ change.addEventListener('click', () => toggleAgentPicker(row.id));
138
+ li.appendChild(change);
139
+ if (state.activeAgentPickerRowId === row.id) {
140
+ li.appendChild(renderAgentPicker());
141
+ }
142
+ }
143
+
144
+ // Project picker affordance for the project row
145
+ if (row.id === 'project' && row.status !== 'ok') {
146
+ const picker = document.createElement('div');
147
+ picker.className = 'project-picker';
148
+ picker.setAttribute('data-testid', 'project-picker');
149
+ const input = document.createElement('input');
150
+ input.type = 'text';
151
+ input.placeholder = 'C:\\Projects\\my-project or /Users/you/Projects/my-project';
152
+ input.value = state.session.state.workspacePath || '';
153
+ input.id = 'project-path-input';
154
+ const browse = document.createElement('button');
155
+ browse.type = 'button';
156
+ browse.className = 'browse';
157
+ browse.textContent = 'Browse';
158
+ browse.addEventListener('click', async () => {
159
+ try {
160
+ const picked = await api('/api/first-run/project-path/pick', 'POST');
161
+ if (picked && picked.path) input.value = picked.path;
162
+ } catch (err) {
163
+ setStatus(err.message, 'error');
164
+ }
165
+ });
166
+ picker.appendChild(input);
167
+ picker.appendChild(browse);
168
+ li.appendChild(picker);
169
+
170
+ // Preflight summary — show the user what FRAIM will do to their machine
171
+ // before they click Continue. Without this, a non-tech user has no way
172
+ // to know we're about to write to ~/.claude.json, drop GitHub workflow
173
+ // files, and call the registry. The "no surprises" half of the
174
+ // non-tech-user invariant lives here.
175
+ const preflight = document.createElement('div');
176
+ preflight.className = 'preflight-summary';
177
+ preflight.setAttribute('data-testid', 'preflight-summary');
178
+ const heading = document.createElement('div');
179
+ heading.className = 'preflight-heading';
180
+ heading.textContent = 'When you click Continue, FRAIM will:';
181
+ preflight.appendChild(heading);
182
+ const items = [
183
+ 'Configure your AI agent so it can talk to FRAIM (a backup of your existing config is saved first).',
184
+ 'Create a fraim/ folder inside the project you picked, with the workflow templates.',
185
+ 'If the project has a GitHub remote, install the FRAIM GitHub Action and labels.',
186
+ 'Download the latest FRAIM jobs and skills from the registry (uses your install key).',
187
+ ];
188
+ const ul = document.createElement('ul');
189
+ ul.className = 'preflight-list';
190
+ for (const text of items) {
191
+ const item = document.createElement('li');
192
+ item.textContent = text;
193
+ ul.appendChild(item);
194
+ }
195
+ preflight.appendChild(ul);
196
+ li.appendChild(preflight);
197
+ }
198
+
199
+ if (row.streamOutput) {
200
+ const stream = document.createElement('pre');
201
+ stream.className = 'row-stream';
202
+ stream.setAttribute('data-testid', 'row-stream');
203
+ stream.textContent = row.streamOutput;
204
+ li.appendChild(stream);
205
+ }
206
+
207
+ if (row.manualMessage) {
208
+ const message = document.createElement('div');
209
+ message.className = 'manual-message';
210
+ message.textContent = row.manualMessage;
211
+ li.appendChild(message);
212
+ }
213
+
214
+ if (row.status === 'error' && row.errorFrame && window.FraimErrorFrame) {
215
+ const frame = window.FraimErrorFrame.render(row.errorFrame, (action) => onErrorAction(row.id, action));
216
+ li.appendChild(frame);
217
+ }
218
+
219
+ return li;
220
+ }
221
+
222
+ function renderAgentPicker() {
223
+ const wrap = document.createElement('div');
224
+ wrap.className = 'agent-picker';
225
+ wrap.setAttribute('data-testid', 'agent-picker');
226
+
227
+ const header = document.createElement('div');
228
+ header.textContent = 'Pick the AI agent FRAIM should set up:';
229
+ header.style.color = 'var(--muted)';
230
+ header.style.fontSize = '13px';
231
+ wrap.appendChild(header);
232
+
233
+ const optionsWrap = document.createElement('div');
234
+ optionsWrap.className = 'options';
235
+ for (const option of state.session.agentOptions) {
236
+ const btn = document.createElement('button');
237
+ btn.type = 'button';
238
+ btn.className = 'option';
239
+ btn.setAttribute('data-agent-id', option.id);
240
+ btn.setAttribute('aria-pressed', String(option.id === state.session.currentAgentId));
241
+ btn.textContent = option.label;
242
+ btn.addEventListener('click', async () => {
243
+ try {
244
+ const resp = await api('/api/first-run/agent/change', 'POST', { agentId: option.id });
245
+ setSessionFromActionResponse(resp);
246
+ state.activeAgentPickerRowId = null;
247
+ render();
248
+ setStatus(resp.message || `Selected ${option.label}.`);
249
+ } catch (err) {
250
+ setStatus(err.message, 'error');
251
+ }
252
+ });
253
+ optionsWrap.appendChild(btn);
254
+ }
255
+ wrap.appendChild(optionsWrap);
256
+
257
+ const advanced = document.createElement('details');
258
+ const summary = document.createElement('summary');
259
+ summary.textContent = 'Advanced: I have a different CLI';
260
+ advanced.appendChild(summary);
261
+ const warning = document.createElement('div');
262
+ warning.className = 'custom-warning';
263
+ warning.textContent = 'FRAIM will not auto-invoke unknown CLIs in v1. You will need to invoke this CLI manually from the Hub.';
264
+ advanced.appendChild(warning);
265
+ const form = document.createElement('div');
266
+ form.className = 'custom-form';
267
+ const nameInput = document.createElement('input');
268
+ nameInput.type = 'text';
269
+ nameInput.placeholder = 'CLI name (e.g. my-agent)';
270
+ const prefixInput = document.createElement('input');
271
+ prefixInput.type = 'text';
272
+ prefixInput.placeholder = 'Invocation prefix (e.g. $my-agent run)';
273
+ const submit = document.createElement('button');
274
+ submit.type = 'button';
275
+ submit.textContent = 'Use this CLI';
276
+ submit.addEventListener('click', async () => {
277
+ const name = nameInput.value.trim();
278
+ const invocationPrefix = prefixInput.value.trim();
279
+ if (!name) {
280
+ setStatus('Custom CLI name is required.', 'error');
281
+ return;
282
+ }
283
+ try {
284
+ const resp = await api('/api/first-run/agent/change', 'POST', { customAgent: { name, invocationPrefix } });
285
+ setSessionFromActionResponse(resp);
286
+ state.activeAgentPickerRowId = null;
287
+ render();
288
+ setStatus(resp.message);
289
+ } catch (err) {
290
+ setStatus(err.message, 'error');
291
+ }
292
+ });
293
+ form.appendChild(nameInput);
294
+ form.appendChild(prefixInput);
295
+ form.appendChild(submit);
296
+ advanced.appendChild(form);
297
+ wrap.appendChild(advanced);
298
+
299
+ return wrap;
300
+ }
301
+
302
+ function toggleAgentPicker(rowId) {
303
+ state.activeAgentPickerRowId = state.activeAgentPickerRowId === rowId ? null : rowId;
304
+ render();
305
+ }
306
+
307
+ async function runRow(rowId, extraBody) {
308
+ if (state.runningRowId) return;
309
+ state.runningRowId = rowId;
310
+ PRIMARY_BUTTON.disabled = true;
311
+ setStatus(`Running ${rowId}…`);
312
+ try {
313
+ const body = extraBody || {};
314
+ if (rowId === 'project') {
315
+ const input = document.getElementById('project-path-input');
316
+ if (input && input.value.trim()) {
317
+ body.projectPath = input.value.trim();
318
+ }
319
+ }
320
+ const resp = await api(`/api/first-run/rows/${rowId}/run`, 'POST', body);
321
+ setSessionFromActionResponse(resp);
322
+ setStatus(resp.message, resp.ok ? null : 'error');
323
+ } catch (err) {
324
+ setStatus(err.message, 'error');
325
+ } finally {
326
+ state.runningRowId = null;
327
+ PRIMARY_BUTTON.disabled = false;
328
+ render();
329
+ }
330
+ }
331
+
332
+ async function onErrorAction(rowId, action) {
333
+ const body = { errorActionId: action.id };
334
+ if (action.id === 'alternative' && action.alternativeAgentId) {
335
+ body.alternativeAgentId = action.alternativeAgentId;
336
+ }
337
+ await runRow(rowId, body);
338
+ }
339
+
340
+ async function onPrimaryClick() {
341
+ if (!state.session) return;
342
+ const rows = state.session.rows;
343
+ const projectRow = rows.find((r) => r.id === 'project');
344
+ const projectOk = projectRow && projectRow.status === 'ok';
345
+ const allOk = rows.every((r) => r.status === 'ok');
346
+ // Open-Hub gate: either everything is genuinely ok, or the user has
347
+ // explicitly chosen to handle the remaining steps themselves
348
+ // (Skip-and-continue manual-required) and the project is ready.
349
+ const skipPathDone = projectOk && rows.every((r) => r.status === 'ok' || r.status === 'manual-required');
350
+
351
+ if (allOk || skipPathDone) {
352
+ try {
353
+ const finishResp = await api('/api/first-run/finish', 'POST');
354
+ setStatus(finishResp.message);
355
+ const openResp = await api('/api/first-run/open-hub', 'POST');
356
+ if (openResp && openResp.message) setStatus(openResp.message);
357
+ } catch (err) {
358
+ setStatus(err.message, 'error');
359
+ }
360
+ return;
361
+ }
362
+ // Find the first PENDING row (not just non-ok). Manual-required rows
363
+ // represent steps the user has chosen to handle themselves (skip on
364
+ // agent) or steps that are inherently manual (project pick) — both
365
+ // should not be silently re-run on a Continue click.
366
+ let next = rows.find((r) => r.status === 'pending' && r.id !== 'project');
367
+ if (!next) {
368
+ // No pending non-project rows. If the project row still needs a path,
369
+ // focus the input. Otherwise run project to finalize, or fall through
370
+ // to "open hub" if nothing remains.
371
+ const projectRow = rows.find((r) => r.id === 'project');
372
+ if (projectRow && projectRow.status !== 'ok') {
373
+ next = projectRow;
374
+ }
375
+ }
376
+ if (!next) return;
377
+ if (next.id === 'project') {
378
+ const input = document.getElementById('project-path-input');
379
+ if (input && !input.value.trim()) {
380
+ input.focus();
381
+ setStatus('Pick a project folder to continue.');
382
+ return;
383
+ }
384
+ }
385
+ await runRow(next.id);
386
+
387
+ // Auto-progress: keep running pending non-project rows until one errors
388
+ // or surfaces a manual-required state that isn't the project row (e.g.
389
+ // the agent-login row asking for a vendor sign-in). The project row is
390
+ // always manual by design and does not block the auto-progress loop.
391
+ while (state.session) {
392
+ const blockingError = state.session.rows.find((r) => r.status === 'error');
393
+ const blockingManual = state.session.rows.find((r) => r.status === 'manual-required' && r.id !== 'project');
394
+ if (blockingError || blockingManual) break;
395
+ const nextRow = state.session.rows.find((r) => r.status === 'pending' && r.id !== 'project');
396
+ if (!nextRow) break;
397
+ await runRow(nextRow.id);
398
+ }
399
+ }
400
+
401
+ function render() {
402
+ if (!state.session) return;
403
+ const { rows, primaryButtonLabel } = state.session;
404
+ CHECKLIST_EL.innerHTML = '';
405
+ for (const row of rows) {
406
+ CHECKLIST_EL.appendChild(renderRow(row));
407
+ }
408
+ PRIMARY_BUTTON.textContent = primaryButtonLabel || 'Set up FRAIM';
409
+ applyHeading(rows);
410
+ }
411
+
412
+ PRIMARY_BUTTON.addEventListener('click', onPrimaryClick);
413
+
414
+ loadSession().catch((err) => {
415
+ setStatus(err.message || 'Could not load first-run.', 'error');
416
+ });
417
+ }());