fraim 2.0.167 → 2.0.168
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 +28 -14
- package/dist/src/ai-hub/server.js +10 -403
- package/dist/src/cli/commands/init-project.js +1 -98
- package/dist/src/cli/commands/manager.js +40 -0
- package/dist/src/cli/commands/sync.js +17 -21
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/utils/github-workflow-sync.js +12 -146
- package/dist/src/cli/utils/manager-pack-sync.js +188 -0
- package/dist/src/cli/utils/manager-publish.js +76 -0
- package/dist/src/cli/utils/user-config.js +20 -0
- package/dist/src/core/fraim-config-schema.generated.js +85 -10
- package/dist/src/core/manager-pack.js +26 -0
- package/dist/src/first-run/install-state.js +1 -0
- package/dist/src/first-run/server.js +9 -0
- package/dist/src/first-run/session-service.js +117 -23
- package/dist/src/first-run/types.js +2 -5
- package/dist/src/local-mcp-server/learning-context-builder.js +45 -8
- package/dist/src/local-mcp-server/stdio-server.js +28 -0
- package/index.js +1 -1
- package/package.json +1 -2
- package/public/ai-hub/index.html +0 -81
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
- package/public/ai-hub/script.js +3 -219
- package/public/ai-hub/styles.css +8 -36
- package/public/first-run/index.html +1 -1
- package/public/first-run/script.js +459 -530
- package/public/first-run/styles.css +288 -73
- package/public/portfolio/ashley.html +1 -1
- package/public/portfolio/casey.html +1 -1
- package/public/portfolio/celia.html +1 -1
- package/public/portfolio/gautam.html +1 -1
- package/public/portfolio/hari.html +1 -1
- package/public/portfolio/maestro.html +1 -1
- package/public/portfolio/mandy.html +1 -1
- package/public/portfolio/pam.html +6 -6
- package/public/portfolio/qasm.html +1 -1
- package/public/portfolio/sade.html +1 -1
- package/public/portfolio/sam.html +1 -1
- package/public/portfolio/swen.html +6 -6
- package/dist/src/ai-hub/word-sideload.js +0 -95
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/config/ai-manager-hiring.js +0 -121
- package/dist/src/config/compat.js +0 -16
- package/dist/src/config/feature-flags.js +0 -25
- package/dist/src/config/persona-capability-bundles.js +0 -273
- package/dist/src/config/persona-hiring.js +0 -270
- package/dist/src/config/portfolio-slug-overrides.js +0 -17
- package/dist/src/config/pricing.js +0 -37
- package/dist/src/config/stripe.js +0 -43
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
|
@@ -8,9 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
const state = {
|
|
10
10
|
session: null,
|
|
11
|
-
|
|
11
|
+
activeStep: 'prereqs',
|
|
12
|
+
configureInFlight: false,
|
|
13
|
+
configureError: null,
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
const STEP_LABELS = {
|
|
17
|
+
prereqs: 'Prerequisites',
|
|
18
|
+
agents: 'Local Agents',
|
|
19
|
+
configure: 'Configure FRAIM',
|
|
20
|
+
use: 'Use FRAIM',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const STEP_ORDER = ['prereqs', 'agents', 'configure', 'use'];
|
|
24
|
+
|
|
14
25
|
function setStatus(text, tone) {
|
|
15
26
|
STATUS_EL.textContent = text || '';
|
|
16
27
|
if (tone) STATUS_EL.setAttribute('data-tone', tone);
|
|
@@ -23,643 +34,561 @@
|
|
|
23
34
|
if (LEDE_EL) LEDE_EL.textContent = lede;
|
|
24
35
|
}
|
|
25
36
|
|
|
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 = 'Your FRAIM employees are ready to work. Here are the surfaces available to them.';
|
|
33
|
-
} else {
|
|
34
|
-
LEDE_EL.textContent = "No execution surfaces found. Your FRAIM employees need at least one agent (Claude Code, Codex, etc.) to work in.";
|
|
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
37
|
async function api(path, method, body) {
|
|
51
38
|
const headers = {};
|
|
52
39
|
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
|
-
}
|
|
40
|
+
if (state.session && state.session.requestToken) headers['x-fraim-first-run-token'] = state.session.requestToken;
|
|
56
41
|
const response = await fetch(path, {
|
|
57
42
|
method: method || 'GET',
|
|
58
43
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
59
44
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
60
45
|
});
|
|
61
46
|
if (response.status === 204) return null;
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
47
|
+
const json = await response.json();
|
|
48
|
+
if (!response.ok) throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
|
|
71
49
|
return json;
|
|
72
50
|
}
|
|
73
51
|
|
|
74
|
-
async function loadSession() {
|
|
75
|
-
state.session = await api('/api/first-run/session');
|
|
76
|
-
render();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
52
|
function setSessionFromActionResponse(actionResp) {
|
|
80
|
-
if (!state.session) return;
|
|
53
|
+
if (!state.session || !actionResp) return;
|
|
81
54
|
state.session.state = actionResp.state;
|
|
82
55
|
state.session.rows = actionResp.rows;
|
|
83
56
|
state.session.primaryButtonLabel = actionResp.primaryButtonLabel;
|
|
84
57
|
state.session.currentAgentId = actionResp.state.agentId;
|
|
85
58
|
}
|
|
86
59
|
|
|
87
|
-
function
|
|
88
|
-
|
|
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;
|
|
60
|
+
function row(id) {
|
|
61
|
+
return (state.session && state.session.rows || []).find((r) => r.id === id) || null;
|
|
98
62
|
}
|
|
99
63
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
64
|
+
function requiredPrereqsReady() {
|
|
65
|
+
const node = row('node');
|
|
66
|
+
return Boolean(node && node.status === 'ok');
|
|
67
|
+
}
|
|
134
68
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
69
|
+
function readyAgents() {
|
|
70
|
+
const installs = state.session && state.session.state ? state.session.state.agentInstalls || {} : {};
|
|
71
|
+
return Object.entries(installs)
|
|
72
|
+
.filter(([, entry]) => entry && entry.status === 'ready')
|
|
73
|
+
.map(([id, entry]) => ({ id, label: entry.label }));
|
|
74
|
+
}
|
|
141
75
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
76
|
+
function configuredSurfaces() {
|
|
77
|
+
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
78
|
+
return setupResult && Array.isArray(setupResult.configuredSurfaces) ? setupResult.configuredSurfaces : [];
|
|
79
|
+
}
|
|
146
80
|
|
|
147
|
-
|
|
81
|
+
function isAgentReady(opt) {
|
|
82
|
+
const installState = state.session && state.session.state && state.session.state.agentInstalls
|
|
83
|
+
? state.session.state.agentInstalls[opt.id]
|
|
84
|
+
: null;
|
|
85
|
+
if (installState && installState.status === 'ready') return true;
|
|
86
|
+
return configuredSurfaces().some((surface) => surface && (surface.id === opt.id || surface.name === opt.label));
|
|
148
87
|
}
|
|
149
88
|
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
? 'Your FRAIM employees are ready to work in these surfaces'
|
|
161
|
-
: 'No execution surfaces found';
|
|
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 = "No execution surfaces found. Install Claude Code, Codex, or another agent to work in.";
|
|
176
|
-
li.appendChild(copy);
|
|
89
|
+
function readyAgentLabels() {
|
|
90
|
+
const labels = new Set();
|
|
91
|
+
for (const agent of readyAgents()) labels.add(agent.label);
|
|
92
|
+
for (const surface of configuredSurfaces()) {
|
|
93
|
+
if (surface && surface.name) labels.add(surface.name);
|
|
177
94
|
}
|
|
95
|
+
return Array.from(labels);
|
|
96
|
+
}
|
|
178
97
|
|
|
179
|
-
|
|
98
|
+
function detectedAgentCount() {
|
|
99
|
+
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
100
|
+
return (setupResult && setupResult.detectedSurfaceCount) || readyAgents().length;
|
|
180
101
|
}
|
|
181
102
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
}
|
|
103
|
+
function configureReady() {
|
|
104
|
+
const fraim = row('fraim');
|
|
105
|
+
return Boolean(fraim && fraim.status === 'ok' && detectedAgentCount() > 0);
|
|
198
106
|
}
|
|
199
107
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
await runRow(rowId, body);
|
|
108
|
+
function chooseActiveStep() {
|
|
109
|
+
if (!requiredPrereqsReady()) return 'prereqs';
|
|
110
|
+
if (detectedAgentCount() < 1) return 'agents';
|
|
111
|
+
if (!configureReady()) return 'configure';
|
|
112
|
+
return 'use';
|
|
206
113
|
}
|
|
207
114
|
|
|
208
|
-
|
|
209
|
-
if (
|
|
115
|
+
function iconFor(status) {
|
|
116
|
+
if (status === 'ok') return '✓';
|
|
117
|
+
if (status === 'in-progress') return '...';
|
|
118
|
+
if (status === 'manual-required' || status === 'error') return '!';
|
|
119
|
+
return '.';
|
|
120
|
+
}
|
|
210
121
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
122
|
+
function renderShell(renderPane) {
|
|
123
|
+
CHECKLIST_EL.className = 'setup-shell';
|
|
124
|
+
CHECKLIST_EL.innerHTML = '';
|
|
125
|
+
PRIMARY_BUTTON.style.display = 'none';
|
|
126
|
+
setHeader('Set up FRAIM', 'Complete these steps in order. FRAIM configures itself after a local AI agent is ready.');
|
|
127
|
+
|
|
128
|
+
const steps = document.createElement('div');
|
|
129
|
+
steps.className = 'setup-steps';
|
|
130
|
+
steps.setAttribute('data-testid', 'setup-stepper');
|
|
131
|
+
|
|
132
|
+
for (const step of STEP_ORDER) {
|
|
133
|
+
const btn = document.createElement('button');
|
|
134
|
+
btn.type = 'button';
|
|
135
|
+
btn.className = 'setup-step';
|
|
136
|
+
btn.setAttribute('data-step', step);
|
|
137
|
+
btn.setAttribute('aria-current', state.activeStep === step ? 'step' : 'false');
|
|
138
|
+
btn.setAttribute('data-status', stepStatus(step));
|
|
139
|
+
btn.disabled = (step === 'configure' && detectedAgentCount() < 1) || (step === 'use' && !configureReady());
|
|
140
|
+
btn.addEventListener('click', () => {
|
|
141
|
+
if (btn.disabled) return;
|
|
142
|
+
state.activeStep = step;
|
|
143
|
+
render();
|
|
144
|
+
});
|
|
145
|
+
const dot = document.createElement('span');
|
|
146
|
+
dot.className = 'step-dot';
|
|
147
|
+
dot.textContent = stepStatus(step) === 'done' ? '✓' : String(STEP_ORDER.indexOf(step) + 1);
|
|
148
|
+
const label = document.createElement('span');
|
|
149
|
+
label.textContent = STEP_LABELS[step];
|
|
150
|
+
btn.appendChild(dot);
|
|
151
|
+
btn.appendChild(label);
|
|
152
|
+
steps.appendChild(btn);
|
|
225
153
|
}
|
|
226
154
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
155
|
+
const pane = document.createElement('div');
|
|
156
|
+
pane.className = 'setup-pane';
|
|
157
|
+
pane.setAttribute('data-testid', 'setup-pane');
|
|
158
|
+
renderPane(pane);
|
|
230
159
|
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
160
|
+
CHECKLIST_EL.appendChild(steps);
|
|
161
|
+
CHECKLIST_EL.appendChild(pane);
|
|
238
162
|
}
|
|
239
163
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
164
|
+
function stepStatus(step) {
|
|
165
|
+
if (step === 'prereqs') return requiredPrereqsReady() ? 'done' : 'active';
|
|
166
|
+
if (step === 'agents') return detectedAgentCount() > 0 ? 'done' : 'active';
|
|
167
|
+
if (step === 'configure') return configureReady() ? 'done' : (detectedAgentCount() > 0 ? 'active' : 'locked');
|
|
168
|
+
if (step === 'use') return configureReady() ? 'active' : 'locked';
|
|
169
|
+
return 'active';
|
|
170
|
+
}
|
|
245
171
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
];
|
|
172
|
+
function renderPrereqs(pane) {
|
|
173
|
+
const h = document.createElement('h2');
|
|
174
|
+
h.textContent = 'Prerequisites';
|
|
175
|
+
pane.appendChild(h);
|
|
251
176
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
177
|
+
const list = document.createElement('ul');
|
|
178
|
+
list.className = 'row-list';
|
|
179
|
+
for (const id of ['node', 'git']) {
|
|
180
|
+
const r = row(id);
|
|
181
|
+
if (!r) continue;
|
|
182
|
+
const li = document.createElement('li');
|
|
183
|
+
li.className = 'row';
|
|
184
|
+
li.setAttribute('data-row-id', r.id);
|
|
185
|
+
li.setAttribute('data-row-status', r.status);
|
|
186
|
+
li.innerHTML = `<span class="icon" aria-hidden="true">${iconFor(r.status)}</span><span class="label"></span><span class="verb" data-testid="row-verb"></span>`;
|
|
187
|
+
li.querySelector('.label').textContent = r.label;
|
|
188
|
+
li.querySelector('.verb').textContent = r.verb || '';
|
|
189
|
+
list.appendChild(li);
|
|
190
|
+
}
|
|
191
|
+
pane.appendChild(list);
|
|
192
|
+
|
|
193
|
+
const btn = button(requiredPrereqsReady() ? 'Next' : 'Check prerequisites', 'primary');
|
|
194
|
+
btn.setAttribute('data-testid', 'check-prereqs');
|
|
195
|
+
btn.addEventListener('click', async () => {
|
|
196
|
+
btn.disabled = true;
|
|
197
|
+
try {
|
|
198
|
+
for (const id of ['node', 'git']) {
|
|
199
|
+
const r = row(id);
|
|
200
|
+
if (r && r.status !== 'ok') setSessionFromActionResponse(await api(`/api/first-run/rows/${id}/run`, 'POST', {}));
|
|
201
|
+
}
|
|
202
|
+
state.activeStep = 'agents';
|
|
203
|
+
setStatus('Prerequisites are ready.');
|
|
204
|
+
} catch (err) {
|
|
205
|
+
setStatus(err.message, 'error');
|
|
206
|
+
} finally {
|
|
207
|
+
btn.disabled = false;
|
|
208
|
+
render();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
pane.appendChild(btn);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderAgents(pane) {
|
|
215
|
+
const h = document.createElement('h2');
|
|
216
|
+
h.textContent = 'Locally installed AI agents';
|
|
217
|
+
pane.appendChild(h);
|
|
218
|
+
|
|
219
|
+
const p = document.createElement('p');
|
|
220
|
+
p.className = 'pane-copy';
|
|
221
|
+
p.textContent = detectedAgentCount() > 0
|
|
222
|
+
? 'At least one local AI agent is ready. You can add more agents or continue to configure FRAIM.'
|
|
223
|
+
: 'No problem, we will install AI agents next.';
|
|
224
|
+
pane.appendChild(p);
|
|
225
|
+
|
|
226
|
+
const readyLabels = readyAgentLabels();
|
|
227
|
+
if (readyLabels.length > 0) {
|
|
228
|
+
const readyList = document.createElement('div');
|
|
229
|
+
readyList.className = 'ready-strip';
|
|
230
|
+
readyList.setAttribute('data-testid', 'ready-agents');
|
|
231
|
+
readyList.textContent = `Already installed: ${readyLabels.join(', ')}`;
|
|
232
|
+
pane.appendChild(readyList);
|
|
233
|
+
} else {
|
|
234
|
+
const readyList = document.createElement('div');
|
|
235
|
+
readyList.className = 'ready-strip ready-strip--muted';
|
|
236
|
+
readyList.setAttribute('data-testid', 'ready-agents');
|
|
237
|
+
readyList.textContent = 'Already installed: none detected yet';
|
|
238
|
+
pane.appendChild(readyList);
|
|
239
|
+
}
|
|
257
240
|
|
|
241
|
+
const grid = document.createElement('div');
|
|
242
|
+
grid.className = 'agent-grid';
|
|
243
|
+
const agentOptions = state.session.agentOptions || [];
|
|
258
244
|
for (const opt of agentOptions) {
|
|
259
|
-
const li = document.createElement('li');
|
|
260
245
|
const card = document.createElement('div');
|
|
261
246
|
card.className = 'user-type-card recruit-card';
|
|
262
247
|
card.setAttribute('data-agent-id', opt.id);
|
|
263
|
-
|
|
248
|
+
card.setAttribute('data-testid', `agent-card-${opt.id}`);
|
|
249
|
+
const agentReady = isAgentReady(opt);
|
|
264
250
|
const title = document.createElement('strong');
|
|
265
251
|
title.className = 'card-title';
|
|
266
252
|
title.textContent = opt.label;
|
|
267
253
|
const desc = document.createElement('p');
|
|
268
254
|
desc.className = 'card-desc';
|
|
269
|
-
desc.textContent =
|
|
255
|
+
desc.textContent = agentReady
|
|
256
|
+
? 'Already installed and ready for FRAIM.'
|
|
257
|
+
: `Available to set up: install, sign in, and verify ${opt.label}.`;
|
|
258
|
+
const action = button(agentReady ? 'Ready' : 'Set up', 'secondary');
|
|
259
|
+
action.disabled = agentReady;
|
|
260
|
+
action.setAttribute('data-testid', `install-${opt.id}`);
|
|
261
|
+
action.addEventListener('click', () => openAgentModal(opt));
|
|
270
262
|
card.appendChild(title);
|
|
271
263
|
card.appendChild(desc);
|
|
272
|
-
|
|
273
|
-
|
|
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);
|
|
264
|
+
card.appendChild(action);
|
|
265
|
+
grid.appendChild(card);
|
|
283
266
|
}
|
|
284
267
|
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
})();
|
|
268
|
+
const byoa = document.createElement('div');
|
|
269
|
+
byoa.className = 'user-type-card recruit-card byoa-card';
|
|
270
|
+
byoa.innerHTML = '<strong class="card-title">Bring Your Own Agent</strong><p class="card-desc">Use Cursor, Windsurf, Kiro, VS Code, or another supported local AI tool. If you install a new agent later and want FRAIM to use it, run this command from your project.</p><div class="cmd-block">npx fraim add-ide</div>';
|
|
271
|
+
grid.appendChild(byoa);
|
|
272
|
+
pane.appendChild(grid);
|
|
273
|
+
|
|
274
|
+
const done = button('Next', 'primary');
|
|
275
|
+
done.setAttribute('data-testid', 'done-installing-agents');
|
|
276
|
+
done.disabled = detectedAgentCount() < 1;
|
|
277
|
+
done.addEventListener('click', () => { state.activeStep = 'configure'; render(); });
|
|
278
|
+
pane.appendChild(done);
|
|
279
|
+
|
|
280
|
+
if (detectedAgentCount() < 1) {
|
|
281
|
+
const locked = document.createElement('p');
|
|
282
|
+
locked.className = 'locked-note';
|
|
283
|
+
locked.textContent = 'Configure FRAIM stays locked until one local AI agent is ready.';
|
|
284
|
+
pane.appendChild(locked);
|
|
285
|
+
}
|
|
373
286
|
}
|
|
374
287
|
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
395
|
-
setStatus(err.message, 'error');
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
parentLi.appendChild(signInBtn);
|
|
288
|
+
async function configureFraim() {
|
|
289
|
+
if (state.configureInFlight) return;
|
|
290
|
+
state.configureInFlight = true;
|
|
291
|
+
state.configureError = null;
|
|
292
|
+
render();
|
|
293
|
+
setStatus('Configuring FRAIM for your local AI agents...');
|
|
294
|
+
try {
|
|
295
|
+
const resp = await api('/api/first-run/done-recruiting', 'POST', {});
|
|
296
|
+
setSessionFromActionResponse(resp);
|
|
297
|
+
setStatus(resp.message, resp.ok ? null : 'error');
|
|
298
|
+
state.activeStep = 'configure';
|
|
299
|
+
state.configureError = resp && resp.ok ? null : (resp && resp.message ? resp.message : 'FRAIM setup did not complete.');
|
|
300
|
+
} catch (err) {
|
|
301
|
+
state.configureError = err.message;
|
|
302
|
+
setStatus(state.configureError, 'error');
|
|
303
|
+
} finally {
|
|
304
|
+
state.configureInFlight = false;
|
|
305
|
+
render();
|
|
306
|
+
}
|
|
399
307
|
}
|
|
400
308
|
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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);
|
|
309
|
+
function renderConfigure(pane) {
|
|
310
|
+
const h = document.createElement('h2');
|
|
311
|
+
h.textContent = 'Configure FRAIM';
|
|
312
|
+
pane.appendChild(h);
|
|
313
|
+
|
|
314
|
+
if (detectedAgentCount() < 1) {
|
|
315
|
+
const p = document.createElement('p');
|
|
316
|
+
p.className = 'pane-copy';
|
|
317
|
+
p.textContent = 'A local AI agent is required before FRAIM setup can finish.';
|
|
318
|
+
pane.appendChild(p);
|
|
319
|
+
const back = button('Choose a local agent', 'primary');
|
|
320
|
+
back.addEventListener('click', () => { state.activeStep = 'agents'; render(); });
|
|
321
|
+
pane.appendChild(back);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
427
324
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
checkBtn.disabled = false;
|
|
446
|
-
checkBtn.textContent = 'Check if Ready';
|
|
447
|
-
setStatus(err.message, 'error');
|
|
325
|
+
if (!configureReady()) {
|
|
326
|
+
const p = document.createElement('p');
|
|
327
|
+
p.className = 'pane-copy';
|
|
328
|
+
p.textContent = state.configureError
|
|
329
|
+
? 'FRAIM setup needs attention before you continue.'
|
|
330
|
+
: 'FRAIM is configuring MCP and local setup files for every ready agent.';
|
|
331
|
+
pane.appendChild(p);
|
|
332
|
+
const status = document.createElement('div');
|
|
333
|
+
status.className = state.configureError ? 'ready-strip ready-strip--error' : 'ready-strip';
|
|
334
|
+
status.textContent = state.configureError || 'Configuring FRAIM...';
|
|
335
|
+
pane.appendChild(status);
|
|
336
|
+
if (state.configureError) {
|
|
337
|
+
const retry = button('Try again', 'primary');
|
|
338
|
+
retry.addEventListener('click', configureFraim);
|
|
339
|
+
pane.appendChild(retry);
|
|
340
|
+
} else if (!state.configureInFlight) {
|
|
341
|
+
window.setTimeout(configureFraim, 0);
|
|
448
342
|
}
|
|
449
|
-
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
450
345
|
|
|
451
|
-
|
|
452
|
-
|
|
346
|
+
const success = document.createElement('div');
|
|
347
|
+
success.className = 'ready-strip';
|
|
348
|
+
success.textContent = 'FRAIM setup succeeded for your ready local AI agents.';
|
|
349
|
+
pane.appendChild(success);
|
|
350
|
+
const next = button('Next', 'primary');
|
|
351
|
+
next.addEventListener('click', () => { state.activeStep = 'use'; render(); });
|
|
352
|
+
pane.appendChild(next);
|
|
453
353
|
}
|
|
454
354
|
|
|
455
|
-
function
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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);
|
|
355
|
+
function renderUseFraim(pane) {
|
|
356
|
+
const h = document.createElement('h2');
|
|
357
|
+
h.textContent = 'Use FRAIM';
|
|
358
|
+
pane.appendChild(h);
|
|
359
|
+
|
|
360
|
+
const p = document.createElement('p');
|
|
361
|
+
p.className = 'pane-copy';
|
|
362
|
+
p.textContent = 'FRAIM is set up to use your ready local AI agents. Choose where you want to work.';
|
|
363
|
+
pane.appendChild(p);
|
|
364
|
+
renderStartWorkingChoices(pane);
|
|
477
365
|
}
|
|
478
366
|
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
|
|
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.');
|
|
367
|
+
function renderStartWorkingChoices(pane) {
|
|
368
|
+
const choices = document.createElement('div');
|
|
369
|
+
choices.className = 'choice-grid';
|
|
484
370
|
|
|
485
|
-
const ideLi = document.createElement('li');
|
|
486
371
|
const ideCard = document.createElement('div');
|
|
487
|
-
ideCard.className = 'user-type-card user-type-card--featured
|
|
488
|
-
ideCard.
|
|
489
|
-
|
|
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';
|
|
372
|
+
ideCard.className = 'user-type-card user-type-card--featured';
|
|
373
|
+
ideCard.innerHTML = '<strong class="card-title">In my IDE</strong><p class="card-desc">Use FRAIM inside Claude Code, Codex, Cursor, or the local agent tool you already use.</p>';
|
|
374
|
+
const ideBtn = button('Continue in my IDE', 'primary');
|
|
498
375
|
ideBtn.addEventListener('click', async () => {
|
|
499
|
-
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
renderIdeCommandDisplay(ideCommands);
|
|
376
|
+
try { await api('/api/first-run/set-preference', 'POST', { choice: 'ide' }); } catch (_) {}
|
|
377
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
378
|
+
renderIdeCommandDisplay(ideData ? ideData.commands : []);
|
|
503
379
|
});
|
|
504
380
|
ideCard.appendChild(ideBtn);
|
|
505
|
-
ideLi.appendChild(ideCard);
|
|
506
|
-
CHECKLIST_EL.appendChild(ideLi);
|
|
507
381
|
|
|
508
|
-
const hubLi = document.createElement('li');
|
|
509
382
|
const hubCard = document.createElement('div');
|
|
510
|
-
hubCard.className = 'user-type-card
|
|
511
|
-
hubCard.
|
|
512
|
-
|
|
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';
|
|
383
|
+
hubCard.className = 'user-type-card';
|
|
384
|
+
hubCard.innerHTML = '<strong class="card-title">In FRAIM Hub</strong><p class="card-desc">Open the Company, Manager, and Projects shell.</p>';
|
|
385
|
+
const hubBtn = button('Open FRAIM Hub', 'secondary');
|
|
521
386
|
hubBtn.addEventListener('click', async () => {
|
|
522
|
-
try {
|
|
523
|
-
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
524
|
-
} catch (_err) { /* non-fatal */ }
|
|
387
|
+
try { await api('/api/first-run/set-preference', 'POST', { choice: 'hub' }); } catch (_) {}
|
|
525
388
|
try {
|
|
526
389
|
hubBtn.disabled = true;
|
|
527
390
|
setStatus('Opening Hub...');
|
|
528
391
|
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
529
392
|
if (openResp && openResp.needsAgentSetup) {
|
|
530
|
-
|
|
393
|
+
state.activeStep = 'agents';
|
|
394
|
+
render();
|
|
395
|
+
setStatus(openResp.message, 'error');
|
|
531
396
|
return;
|
|
532
397
|
}
|
|
533
|
-
if (openResp && openResp.message) setStatus(openResp.message);
|
|
534
398
|
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
535
|
-
else hubBtn.disabled = false;
|
|
536
399
|
} catch (err) {
|
|
537
400
|
hubBtn.disabled = false;
|
|
538
401
|
setStatus(err.message, 'error');
|
|
539
402
|
}
|
|
540
403
|
});
|
|
541
404
|
hubCard.appendChild(hubBtn);
|
|
542
|
-
|
|
543
|
-
|
|
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;
|
|
405
|
+
choices.appendChild(ideCard);
|
|
406
|
+
choices.appendChild(hubCard);
|
|
407
|
+
pane.appendChild(choices);
|
|
573
408
|
}
|
|
574
409
|
|
|
575
410
|
function renderIdeCommandDisplay(commands) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
try {
|
|
411
|
+
renderShell((pane) => {
|
|
412
|
+
const h = document.createElement('h2');
|
|
413
|
+
h.textContent = 'Continue in your IDE';
|
|
414
|
+
pane.appendChild(h);
|
|
415
|
+
const copy = document.createElement('p');
|
|
416
|
+
copy.className = 'pane-copy';
|
|
417
|
+
copy.textContent = 'Restart your IDE so it picks up FRAIM setup, then run:';
|
|
418
|
+
pane.appendChild(copy);
|
|
419
|
+
const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
|
|
420
|
+
for (const cmd of cmdList) {
|
|
421
|
+
const row = document.createElement('div');
|
|
422
|
+
row.className = 'command-row';
|
|
423
|
+
const block = document.createElement('div');
|
|
424
|
+
block.className = 'cmd-block';
|
|
425
|
+
block.textContent = cmd;
|
|
426
|
+
const copyBtn = button('Copy', 'secondary');
|
|
427
|
+
copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
|
|
428
|
+
copyBtn.addEventListener('click', async () => {
|
|
595
429
|
await navigator.clipboard.writeText(cmd);
|
|
596
430
|
copyBtn.textContent = 'Copied';
|
|
597
431
|
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
}
|
|
432
|
+
});
|
|
433
|
+
row.appendChild(block);
|
|
434
|
+
row.appendChild(copyBtn);
|
|
435
|
+
pane.appendChild(row);
|
|
436
|
+
}
|
|
437
|
+
const back = button('Back to choices', 'secondary');
|
|
438
|
+
back.setAttribute('data-testid', 'back-to-use-fraim');
|
|
439
|
+
back.addEventListener('click', () => { state.activeStep = 'use'; render(); });
|
|
440
|
+
pane.appendChild(back);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
622
443
|
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
444
|
+
function openAgentModal(opt) {
|
|
445
|
+
const overlay = document.createElement('div');
|
|
446
|
+
overlay.className = 'modal-backdrop';
|
|
447
|
+
overlay.setAttribute('data-testid', 'agent-install-modal');
|
|
448
|
+
const modal = document.createElement('div');
|
|
449
|
+
modal.className = 'modal';
|
|
450
|
+
modal.setAttribute('role', 'dialog');
|
|
451
|
+
modal.setAttribute('aria-modal', 'true');
|
|
452
|
+
modal.innerHTML = `<h2>Set up ${escapeHtml(opt.label)}</h2><p class="install-status" data-testid="agent-install-status">Ready to set up ${escapeHtml(opt.label)}.</p><p class="modal-help">Setup installs the CLI, opens sign-in, and verifies the agent is available before FRAIM uses it.</p>`;
|
|
453
|
+
const status = modal.querySelector('[data-testid="agent-install-status"]');
|
|
454
|
+
const actions = document.createElement('div');
|
|
455
|
+
actions.className = 'install-actions';
|
|
456
|
+
|
|
457
|
+
const install = button('Set up', 'primary');
|
|
458
|
+
const signIn = button(`Sign In to ${opt.label}`, 'secondary');
|
|
459
|
+
const check = button('Check readiness', 'secondary');
|
|
460
|
+
const close = button('Close', 'ghost');
|
|
461
|
+
signIn.disabled = true;
|
|
462
|
+
check.disabled = true;
|
|
463
|
+
close.addEventListener('click', () => { overlay.remove(); render(); });
|
|
464
|
+
|
|
465
|
+
install.addEventListener('click', async () => {
|
|
466
|
+
install.disabled = true;
|
|
467
|
+
status.textContent = `Setting up ${opt.label}...`;
|
|
629
468
|
try {
|
|
630
|
-
await api('/api/first-run/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
469
|
+
const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
|
|
470
|
+
if (!result || !result.ok) throw new Error(result && result.message ? result.message : 'Setup failed.');
|
|
471
|
+
status.textContent = `${opt.label} setup started. Sign in next.`;
|
|
472
|
+
signIn.disabled = false;
|
|
473
|
+
await loadSession(false);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
install.disabled = false;
|
|
476
|
+
install.textContent = 'Retry';
|
|
477
|
+
status.textContent = err.message;
|
|
478
|
+
status.setAttribute('data-tone', 'error');
|
|
479
|
+
showAgentInstallError(modal, opt, err.message, install, close);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
signIn.addEventListener('click', async () => {
|
|
484
|
+
signIn.disabled = true;
|
|
485
|
+
status.textContent = 'Opening terminal for sign-in...';
|
|
486
|
+
try {
|
|
487
|
+
const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
|
|
488
|
+
status.textContent = result && result.message ? result.message : 'Complete sign-in, then check readiness.';
|
|
489
|
+
check.disabled = false;
|
|
490
|
+
} catch (err) {
|
|
491
|
+
signIn.disabled = false;
|
|
492
|
+
status.textContent = err.message;
|
|
493
|
+
status.setAttribute('data-tone', 'error');
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
check.addEventListener('click', async () => {
|
|
498
|
+
check.disabled = true;
|
|
499
|
+
status.textContent = 'Checking that the CLI is installed, signed in, and available on PATH...';
|
|
500
|
+
try {
|
|
501
|
+
const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
|
|
502
|
+
if (result && result.ready) {
|
|
503
|
+
status.textContent = `${opt.label} is ready!`;
|
|
504
|
+
await loadSession(false);
|
|
505
|
+
setTimeout(() => { overlay.remove(); state.activeStep = 'agents'; render(); }, 250);
|
|
634
506
|
return;
|
|
635
507
|
}
|
|
636
|
-
|
|
508
|
+
status.textContent = result && result.message ? result.message : `${opt.label} is not ready yet.`;
|
|
509
|
+
status.setAttribute('data-tone', 'error');
|
|
510
|
+
check.disabled = false;
|
|
637
511
|
} catch (err) {
|
|
638
|
-
|
|
512
|
+
status.textContent = err.message;
|
|
513
|
+
status.setAttribute('data-tone', 'error');
|
|
514
|
+
check.disabled = false;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
actions.appendChild(install);
|
|
519
|
+
actions.appendChild(signIn);
|
|
520
|
+
actions.appendChild(check);
|
|
521
|
+
actions.appendChild(close);
|
|
522
|
+
modal.appendChild(actions);
|
|
523
|
+
overlay.appendChild(modal);
|
|
524
|
+
document.body.appendChild(overlay);
|
|
525
|
+
install.focus();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function showAgentInstallError(modal, opt, message, retryButton, closeButton) {
|
|
529
|
+
const existing = modal.querySelector('[data-testid="error-frame"]');
|
|
530
|
+
if (existing) existing.remove();
|
|
531
|
+
if (!window.FraimErrorFrame || typeof window.FraimErrorFrame.render !== 'function') return;
|
|
532
|
+
const frame = window.FraimErrorFrame.render({
|
|
533
|
+
whatTried: `We tried to set up ${opt.label}.`,
|
|
534
|
+
whatHappened: message,
|
|
535
|
+
actions: [
|
|
536
|
+
{ id: 'retry', label: 'Retry', variant: 'primary' },
|
|
537
|
+
{ id: 'alternative', label: 'Choose another agent', variant: 'secondary' },
|
|
538
|
+
{ id: 'manual', label: 'Manual setup help', variant: 'ghost' },
|
|
539
|
+
],
|
|
540
|
+
}, (action) => {
|
|
541
|
+
if (action.id === 'retry') {
|
|
542
|
+
retryButton.click();
|
|
543
|
+
} else if (action.id === 'alternative') {
|
|
544
|
+
closeButton.click();
|
|
545
|
+
} else if (action.id === 'manual') {
|
|
546
|
+
const status = modal.querySelector('[data-testid="agent-install-status"]');
|
|
547
|
+
if (status) {
|
|
548
|
+
status.textContent = 'Run npx fraim add-ide for manual setup, then return here when your local agent is ready.';
|
|
549
|
+
status.removeAttribute('data-tone');
|
|
550
|
+
}
|
|
639
551
|
}
|
|
640
552
|
});
|
|
641
|
-
|
|
642
|
-
|
|
553
|
+
modal.appendChild(frame);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function button(text, variant) {
|
|
557
|
+
const btn = document.createElement('button');
|
|
558
|
+
btn.type = 'button';
|
|
559
|
+
btn.className = variant === 'primary' ? 'btn btn-primary' : variant === 'ghost' ? 'btn btn-ghost' : 'btn btn-secondary';
|
|
560
|
+
btn.textContent = text;
|
|
561
|
+
return btn;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function escapeHtml(value) {
|
|
565
|
+
return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function loadSession(shouldRender) {
|
|
569
|
+
state.session = await api('/api/first-run/session');
|
|
570
|
+
if (!state.session.state.agentInstalls) state.session.state.agentInstalls = {};
|
|
571
|
+
if (shouldRender !== false) {
|
|
572
|
+
state.activeStep = chooseActiveStep();
|
|
573
|
+
render();
|
|
574
|
+
}
|
|
643
575
|
}
|
|
644
576
|
|
|
645
577
|
function render() {
|
|
646
578
|
if (!state.session) return;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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);
|
|
579
|
+
if (!STEP_ORDER.includes(state.activeStep)) state.activeStep = chooseActiveStep();
|
|
580
|
+
renderShell((pane) => {
|
|
581
|
+
if (state.activeStep === 'prereqs') renderPrereqs(pane);
|
|
582
|
+
else if (state.activeStep === 'agents') renderAgents(pane);
|
|
583
|
+
else if (state.activeStep === 'configure') renderConfigure(pane);
|
|
584
|
+
else renderUseFraim(pane);
|
|
585
|
+
});
|
|
660
586
|
}
|
|
661
587
|
|
|
662
|
-
PRIMARY_BUTTON.addEventListener('click',
|
|
588
|
+
PRIMARY_BUTTON.addEventListener('click', () => {
|
|
589
|
+
state.activeStep = chooseActiveStep();
|
|
590
|
+
render();
|
|
591
|
+
});
|
|
663
592
|
|
|
664
593
|
loadSession().catch((err) => {
|
|
665
594
|
setStatus(err.message || 'Could not load first-run.', 'error');
|