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
|
@@ -10,18 +10,17 @@
|
|
|
10
10
|
<main class="page">
|
|
11
11
|
<header class="page-header">
|
|
12
12
|
<h1>Set up FRAIM</h1>
|
|
13
|
-
<p class="lede" id="lede">We
|
|
13
|
+
<p class="lede" id="lede">We are getting your machine ready for FRAIM.</p>
|
|
14
14
|
</header>
|
|
15
15
|
|
|
16
16
|
<section class="card">
|
|
17
17
|
<ul class="checklist" id="checklist" data-testid="setup-checklist" aria-label="FRAIM setup checklist">
|
|
18
18
|
<!-- Skeleton rows shown until the async session-load completes.
|
|
19
19
|
Replaced wholesale by script.js once /api/first-run/session returns. -->
|
|
20
|
-
<li class="row skeleton-row" data-row-id="node" data-row-status="pending"><span class="icon" aria-hidden="true"
|
|
21
|
-
<li class="row skeleton-row" data-row-id="git" data-row-status="pending"><span class="icon" aria-hidden="true"
|
|
22
|
-
<li class="row skeleton-row" data-row-id="
|
|
23
|
-
<li class="row skeleton-row" data-row-id="agent
|
|
24
|
-
<li class="row skeleton-row" data-row-id="project" data-row-status="pending"><span class="icon" aria-hidden="true">·</span><span class="label">Project folder</span><span class="verb" data-testid="row-verb">checking…</span></li>
|
|
20
|
+
<li class="row skeleton-row" data-row-id="node" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Node.js</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
21
|
+
<li class="row skeleton-row" data-row-id="git" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">git</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
22
|
+
<li class="row skeleton-row" data-row-id="fraim" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">FRAIM</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
23
|
+
<li class="row skeleton-row" data-row-id="agent" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">AI Employees</span><span class="verb" data-testid="row-verb">checking...</span></li>
|
|
25
24
|
</ul>
|
|
26
25
|
<div class="actions">
|
|
27
26
|
<button id="primary-button" class="primary-button" data-testid="primary-button">Set up FRAIM</button>
|
|
@@ -8,40 +8,43 @@
|
|
|
8
8
|
|
|
9
9
|
const state = {
|
|
10
10
|
session: null,
|
|
11
|
-
activeAgentPickerRowId: null,
|
|
12
11
|
runningRowId: null,
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
function setStatus(text, tone) {
|
|
16
15
|
STATUS_EL.textContent = text || '';
|
|
17
|
-
if (tone)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function applyHeading(rows) {
|
|
25
27
|
if (!LEDE_EL) return;
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|
|
28
36
|
return;
|
|
29
37
|
}
|
|
30
38
|
const errored = rows.find((r) => r.status === 'error');
|
|
31
39
|
if (errored) {
|
|
32
|
-
LEDE_EL.textContent =
|
|
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.';
|
|
40
|
+
LEDE_EL.textContent = "Something did not go through. Pick the right next step below.";
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
40
43
|
if (rows.some((r) => r.status === 'ok')) {
|
|
41
|
-
LEDE_EL.textContent = '
|
|
44
|
+
LEDE_EL.textContent = 'FRAIM is finishing setup on this machine.';
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
|
-
LEDE_EL.textContent = 'We
|
|
47
|
+
LEDE_EL.textContent = 'We are getting your machine ready for FRAIM.';
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
async function api(path, method, body) {
|
|
@@ -74,8 +77,6 @@
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function setSessionFromActionResponse(actionResp) {
|
|
77
|
-
// Action responses include rows + primaryButtonLabel + state but not
|
|
78
|
-
// every session-level field. Merge into the held session view.
|
|
79
80
|
if (!state.session) return;
|
|
80
81
|
state.session.state = actionResp.state;
|
|
81
82
|
state.session.rows = actionResp.rows;
|
|
@@ -83,20 +84,16 @@
|
|
|
83
84
|
state.session.currentAgentId = actionResp.state.agentId;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function findRow(rowId) {
|
|
87
|
-
return state.session.rows.find((row) => row.id === rowId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
87
|
function makeIcon(status) {
|
|
91
88
|
const icon = document.createElement('span');
|
|
92
89
|
icon.className = 'icon';
|
|
93
90
|
icon.setAttribute('aria-hidden', 'true');
|
|
94
91
|
icon.textContent =
|
|
95
92
|
status === 'ok' ? '✓' :
|
|
96
|
-
status === 'in-progress' ? '
|
|
93
|
+
status === 'in-progress' ? '...' :
|
|
97
94
|
status === 'manual-required' ? '!' :
|
|
98
95
|
status === 'error' ? '!' :
|
|
99
|
-
'
|
|
96
|
+
'.';
|
|
100
97
|
return icon;
|
|
101
98
|
}
|
|
102
99
|
|
|
@@ -127,75 +124,6 @@
|
|
|
127
124
|
li.appendChild(detail);
|
|
128
125
|
}
|
|
129
126
|
|
|
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
127
|
if (row.streamOutput) {
|
|
200
128
|
const stream = document.createElement('pre');
|
|
201
129
|
stream.className = 'row-stream';
|
|
@@ -219,105 +147,45 @@
|
|
|
219
147
|
return li;
|
|
220
148
|
}
|
|
221
149
|
|
|
222
|
-
function
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
wrap.setAttribute('data-testid', 'agent-picker');
|
|
150
|
+
function renderSetupSummary() {
|
|
151
|
+
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
152
|
+
if (!setupResult) return null;
|
|
226
153
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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');
|
|
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);
|
|
291
171
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return wrap;
|
|
300
|
-
}
|
|
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
|
+
}
|
|
301
178
|
|
|
302
|
-
|
|
303
|
-
state.activeAgentPickerRowId = state.activeAgentPickerRowId === rowId ? null : rowId;
|
|
304
|
-
render();
|
|
179
|
+
return li;
|
|
305
180
|
}
|
|
306
181
|
|
|
307
182
|
async function runRow(rowId, extraBody) {
|
|
308
183
|
if (state.runningRowId) return;
|
|
309
184
|
state.runningRowId = rowId;
|
|
310
185
|
PRIMARY_BUTTON.disabled = true;
|
|
311
|
-
setStatus(`Running ${rowId}
|
|
186
|
+
setStatus(`Running ${rowId}...`);
|
|
312
187
|
try {
|
|
313
|
-
const
|
|
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);
|
|
188
|
+
const resp = await api(`/api/first-run/rows/${rowId}/run`, 'POST', extraBody || {});
|
|
321
189
|
setSessionFromActionResponse(resp);
|
|
322
190
|
setStatus(resp.message, resp.ok ? null : 'error');
|
|
323
191
|
} catch (err) {
|
|
@@ -339,72 +207,252 @@
|
|
|
339
207
|
|
|
340
208
|
async function onPrimaryClick() {
|
|
341
209
|
if (!state.session) return;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (allOk || skipPathDone) {
|
|
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
|
+
}
|
|
352
218
|
try {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
} catch (err) {
|
|
358
|
-
setStatus(err.message, 'error');
|
|
219
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
220
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
221
|
+
} catch (_err) {
|
|
222
|
+
renderStartWorking([]);
|
|
359
223
|
}
|
|
360
224
|
return;
|
|
361
225
|
}
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
}
|
|
226
|
+
|
|
227
|
+
const next = state.session.rows.find((r) => r.status === 'pending');
|
|
376
228
|
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
229
|
await runRow(next.id);
|
|
386
230
|
|
|
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
231
|
while (state.session) {
|
|
392
232
|
const blockingError = state.session.rows.find((r) => r.status === 'error');
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const nextRow = state.session.rows.find((r) => r.status === 'pending' && r.id !== 'project');
|
|
233
|
+
if (blockingError) break;
|
|
234
|
+
const nextRow = state.session.rows.find((r) => r.status === 'pending');
|
|
396
235
|
if (!nextRow) break;
|
|
397
236
|
await runRow(nextRow.id);
|
|
398
237
|
}
|
|
399
238
|
}
|
|
400
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 options = [
|
|
247
|
+
{ title: 'Claude Code', desc: 'Install Claude Code and connect it to FRAIM.' },
|
|
248
|
+
{ title: 'Codex', desc: 'Install Codex and connect it to FRAIM.' },
|
|
249
|
+
{ title: 'Gemini CLI', desc: 'Install Gemini CLI and connect it to FRAIM.' },
|
|
250
|
+
{ title: 'Bring Your Own Agent', desc: 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.' },
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
for (const option of options) {
|
|
254
|
+
const li = document.createElement('li');
|
|
255
|
+
const card = document.createElement('div');
|
|
256
|
+
card.className = 'user-type-card recruit-card';
|
|
257
|
+
const title = document.createElement('strong');
|
|
258
|
+
title.className = 'card-title';
|
|
259
|
+
title.textContent = option.title;
|
|
260
|
+
const desc = document.createElement('p');
|
|
261
|
+
desc.className = 'card-desc';
|
|
262
|
+
desc.textContent = option.desc;
|
|
263
|
+
card.appendChild(title);
|
|
264
|
+
card.appendChild(desc);
|
|
265
|
+
|
|
266
|
+
if (option.title === 'Bring Your Own Agent') {
|
|
267
|
+
const note = document.createElement('p');
|
|
268
|
+
note.className = 'card-desc';
|
|
269
|
+
note.textContent = 'When you are done installing your agent, run:';
|
|
270
|
+
card.appendChild(note);
|
|
271
|
+
const cmd = document.createElement('div');
|
|
272
|
+
cmd.className = 'cmd-block';
|
|
273
|
+
cmd.textContent = 'npx fraim add-ide';
|
|
274
|
+
card.appendChild(cmd);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
li.appendChild(card);
|
|
278
|
+
CHECKLIST_EL.appendChild(li);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const continueLi = document.createElement('li');
|
|
282
|
+
const continueBtn = document.createElement('button');
|
|
283
|
+
continueBtn.type = 'button';
|
|
284
|
+
continueBtn.className = 'btn btn-primary btn-block';
|
|
285
|
+
continueBtn.textContent = 'Continue';
|
|
286
|
+
continueBtn.addEventListener('click', async () => {
|
|
287
|
+
try {
|
|
288
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
289
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
290
|
+
} catch (_err) {
|
|
291
|
+
renderStartWorking([]);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
continueLi.appendChild(continueBtn);
|
|
295
|
+
CHECKLIST_EL.appendChild(continueLi);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderStartWorking(ideCommands) {
|
|
299
|
+
CHECKLIST_EL.className = 'selection-container start-container';
|
|
300
|
+
CHECKLIST_EL.innerHTML = '';
|
|
301
|
+
PRIMARY_BUTTON.style.display = 'none';
|
|
302
|
+
setHeader('Get your AI Employees to start working', 'Choose where you want to work.');
|
|
303
|
+
|
|
304
|
+
const ideLi = document.createElement('li');
|
|
305
|
+
const ideCard = document.createElement('div');
|
|
306
|
+
ideCard.className = 'user-type-card';
|
|
307
|
+
ideCard.appendChild(cardHeader('</>', 'In my IDE', 'Claude Code, Cursor, Codex, or another AI tool'));
|
|
308
|
+
const ideBtn = document.createElement('button');
|
|
309
|
+
ideBtn.type = 'button';
|
|
310
|
+
ideBtn.className = 'btn btn-secondary btn-block';
|
|
311
|
+
ideBtn.textContent = 'Show command';
|
|
312
|
+
ideBtn.addEventListener('click', async () => {
|
|
313
|
+
try {
|
|
314
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'ide' });
|
|
315
|
+
} catch (_err) { /* non-fatal */ }
|
|
316
|
+
renderIdeCommandDisplay(ideCommands);
|
|
317
|
+
});
|
|
318
|
+
ideCard.appendChild(ideBtn);
|
|
319
|
+
ideLi.appendChild(ideCard);
|
|
320
|
+
CHECKLIST_EL.appendChild(ideLi);
|
|
321
|
+
|
|
322
|
+
const orLi = document.createElement('li');
|
|
323
|
+
orLi.className = 'route-or';
|
|
324
|
+
orLi.setAttribute('role', 'separator');
|
|
325
|
+
orLi.setAttribute('aria-hidden', 'true');
|
|
326
|
+
orLi.textContent = 'or';
|
|
327
|
+
CHECKLIST_EL.appendChild(orLi);
|
|
328
|
+
|
|
329
|
+
const hubLi = document.createElement('li');
|
|
330
|
+
const hubCard = document.createElement('div');
|
|
331
|
+
hubCard.className = 'user-type-card user-type-card--featured';
|
|
332
|
+
hubCard.appendChild(cardHeader('Hub', 'In FRAIM Hub', 'Browser-based launcher'));
|
|
333
|
+
const hubBtn = document.createElement('button');
|
|
334
|
+
hubBtn.type = 'button';
|
|
335
|
+
hubBtn.className = 'btn btn-primary btn-block';
|
|
336
|
+
hubBtn.textContent = 'Open Hub';
|
|
337
|
+
hubBtn.addEventListener('click', async () => {
|
|
338
|
+
try {
|
|
339
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
340
|
+
} catch (_err) { /* non-fatal */ }
|
|
341
|
+
try {
|
|
342
|
+
hubBtn.disabled = true;
|
|
343
|
+
setStatus('Opening Hub...');
|
|
344
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
345
|
+
if (openResp && openResp.message) setStatus(openResp.message);
|
|
346
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
hubBtn.disabled = false;
|
|
349
|
+
setStatus(err.message, 'error');
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
hubCard.appendChild(hubBtn);
|
|
353
|
+
hubLi.appendChild(hubCard);
|
|
354
|
+
CHECKLIST_EL.appendChild(hubLi);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function cardHeader(iconText, titleText, descText) {
|
|
358
|
+
const header = document.createElement('div');
|
|
359
|
+
header.className = 'card-header';
|
|
360
|
+
const icon = document.createElement('div');
|
|
361
|
+
icon.className = 'card-icon';
|
|
362
|
+
icon.textContent = iconText;
|
|
363
|
+
const text = document.createElement('div');
|
|
364
|
+
const title = document.createElement('strong');
|
|
365
|
+
title.className = 'card-title';
|
|
366
|
+
title.textContent = titleText;
|
|
367
|
+
const desc = document.createElement('p');
|
|
368
|
+
desc.className = 'card-desc';
|
|
369
|
+
desc.textContent = descText;
|
|
370
|
+
text.appendChild(title);
|
|
371
|
+
text.appendChild(desc);
|
|
372
|
+
header.appendChild(icon);
|
|
373
|
+
header.appendChild(text);
|
|
374
|
+
return header;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function renderIdeCommandDisplay(commands) {
|
|
378
|
+
CHECKLIST_EL.className = 'selection-container';
|
|
379
|
+
CHECKLIST_EL.innerHTML = '';
|
|
380
|
+
setHeader('Work in your IDE', 'Open your project in your AI tool and type:');
|
|
381
|
+
|
|
382
|
+
const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
|
|
383
|
+
for (const cmd of cmdList) {
|
|
384
|
+
const li = document.createElement('li');
|
|
385
|
+
const row = document.createElement('div');
|
|
386
|
+
row.className = 'command-row';
|
|
387
|
+
const block = document.createElement('div');
|
|
388
|
+
block.className = 'cmd-block';
|
|
389
|
+
block.textContent = cmd;
|
|
390
|
+
const copyBtn = document.createElement('button');
|
|
391
|
+
copyBtn.type = 'button';
|
|
392
|
+
copyBtn.className = 'btn btn-secondary';
|
|
393
|
+
copyBtn.textContent = 'Copy';
|
|
394
|
+
copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
|
|
395
|
+
copyBtn.addEventListener('click', async () => {
|
|
396
|
+
try {
|
|
397
|
+
await navigator.clipboard.writeText(cmd);
|
|
398
|
+
copyBtn.textContent = 'Copied';
|
|
399
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
400
|
+
} catch (_err) {
|
|
401
|
+
// Fallback for environments where clipboard API is unavailable.
|
|
402
|
+
try {
|
|
403
|
+
const ta = document.createElement('textarea');
|
|
404
|
+
ta.value = cmd;
|
|
405
|
+
ta.style.position = 'fixed';
|
|
406
|
+
ta.style.opacity = '0';
|
|
407
|
+
document.body.appendChild(ta);
|
|
408
|
+
ta.select();
|
|
409
|
+
document.execCommand('copy');
|
|
410
|
+
document.body.removeChild(ta);
|
|
411
|
+
copyBtn.textContent = 'Copied';
|
|
412
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
413
|
+
} catch (_e) {
|
|
414
|
+
copyBtn.textContent = 'Copy failed';
|
|
415
|
+
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
row.appendChild(block);
|
|
420
|
+
row.appendChild(copyBtn);
|
|
421
|
+
li.appendChild(row);
|
|
422
|
+
CHECKLIST_EL.appendChild(li);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const switchLi = document.createElement('li');
|
|
426
|
+
const switchLink = document.createElement('button');
|
|
427
|
+
switchLink.type = 'button';
|
|
428
|
+
switchLink.className = 'text-button';
|
|
429
|
+
switchLink.textContent = 'Switch to FRAIM Hub';
|
|
430
|
+
switchLink.addEventListener('click', async () => {
|
|
431
|
+
try {
|
|
432
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
433
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
434
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
setStatus(err.message, 'error');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
switchLi.appendChild(switchLink);
|
|
440
|
+
CHECKLIST_EL.appendChild(switchLi);
|
|
441
|
+
}
|
|
442
|
+
|
|
401
443
|
function render() {
|
|
402
444
|
if (!state.session) return;
|
|
445
|
+
CHECKLIST_EL.className = 'checklist';
|
|
446
|
+
PRIMARY_BUTTON.style.display = '';
|
|
403
447
|
const { rows, primaryButtonLabel } = state.session;
|
|
404
448
|
CHECKLIST_EL.innerHTML = '';
|
|
405
449
|
for (const row of rows) {
|
|
406
450
|
CHECKLIST_EL.appendChild(renderRow(row));
|
|
407
451
|
}
|
|
452
|
+
if (primaryButtonLabel === 'Get Started') {
|
|
453
|
+
const summary = renderSetupSummary();
|
|
454
|
+
if (summary) CHECKLIST_EL.appendChild(summary);
|
|
455
|
+
}
|
|
408
456
|
PRIMARY_BUTTON.textContent = primaryButtonLabel || 'Set up FRAIM';
|
|
409
457
|
applyHeading(rows);
|
|
410
458
|
}
|