fraim-framework 2.0.126 → 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.
- package/dist/src/ai-hub/catalog.js +280 -44
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/hosts.js +384 -10
- package/dist/src/ai-hub/server.js +255 -9
- package/dist/src/cli/commands/add-ide.js +4 -3
- package/dist/src/cli/commands/first-run.js +61 -0
- package/dist/src/cli/commands/hub.js +4 -4
- package/dist/src/cli/commands/init-project.js +8 -4
- package/dist/src/cli/commands/setup.js +4 -3
- package/dist/src/cli/commands/sync.js +32 -6
- package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +29 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
- package/dist/src/cli/setup/ide-detector.js +32 -1
- package/dist/src/cli/setup/ide-global-integration.js +5 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +10 -0
- package/dist/src/core/utils/git-utils.js +14 -6
- package/dist/src/first-run/install-state.js +70 -0
- package/dist/src/first-run/server.js +158 -0
- package/dist/src/first-run/session-service.js +746 -0
- package/dist/src/first-run/types.js +97 -0
- package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
- package/dist/src/local-mcp-server/stdio-server.js +41 -9
- package/package.json +3 -1
- package/public/ai-hub/index.html +149 -102
- package/public/ai-hub/script.js +1154 -271
- package/public/ai-hub/styles.css +753 -450
- package/public/first-run/error-frame.js +89 -0
- package/public/first-run/index.html +35 -0
- package/public/first-run/script.js +417 -0
- package/public/first-run/styles.css +386 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R6.1 — unified error-frame renderer.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a single function `renderErrorFrame(frame, onAction)` which returns
|
|
5
|
+
* a DOM Node containing exactly:
|
|
6
|
+
* - "What we tried" sentence
|
|
7
|
+
* - "What happened" verbatim stderr (last 12 lines, with a Show-full toggle
|
|
8
|
+
* that surfaces the rest on demand)
|
|
9
|
+
* - Up to three actions in fixed order (Retry / Try alternative / Skip and continue)
|
|
10
|
+
*
|
|
11
|
+
* The same renderer must be lifted into public/ai-hub/ in the v2 follow-up
|
|
12
|
+
* (issue #355, "Hub-side error-frame adoption") so bootstrap and Hub share
|
|
13
|
+
* one frame across surfaces.
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
function escapeText(text) {
|
|
19
|
+
if (typeof text !== 'string') return '';
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncateToLastLines(text, lineCount) {
|
|
24
|
+
if (typeof text !== 'string') return { visible: '', hidden: '' };
|
|
25
|
+
const lines = text.split(/\r?\n/);
|
|
26
|
+
if (lines.length <= lineCount) {
|
|
27
|
+
return { visible: text, hidden: '' };
|
|
28
|
+
}
|
|
29
|
+
const visible = lines.slice(-lineCount).join('\n');
|
|
30
|
+
const hidden = lines.slice(0, -lineCount).join('\n');
|
|
31
|
+
return { visible, hidden };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderErrorFrame(frame, onAction) {
|
|
35
|
+
const root = document.createElement('div');
|
|
36
|
+
root.className = 'error-frame';
|
|
37
|
+
root.setAttribute('data-testid', 'error-frame');
|
|
38
|
+
root.setAttribute('role', 'alert');
|
|
39
|
+
|
|
40
|
+
const whatTried = document.createElement('div');
|
|
41
|
+
whatTried.className = 'what-tried';
|
|
42
|
+
whatTried.setAttribute('data-testid', 'error-what-tried');
|
|
43
|
+
whatTried.textContent = escapeText(frame.whatTried || '');
|
|
44
|
+
root.appendChild(whatTried);
|
|
45
|
+
|
|
46
|
+
const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
|
|
47
|
+
const whatHappened = document.createElement('pre');
|
|
48
|
+
whatHappened.className = 'what-happened';
|
|
49
|
+
whatHappened.setAttribute('data-testid', 'error-what-happened');
|
|
50
|
+
whatHappened.textContent = visible;
|
|
51
|
+
root.appendChild(whatHappened);
|
|
52
|
+
|
|
53
|
+
if (hidden) {
|
|
54
|
+
const showFull = document.createElement('button');
|
|
55
|
+
showFull.type = 'button';
|
|
56
|
+
showFull.className = 'show-full';
|
|
57
|
+
showFull.setAttribute('data-testid', 'error-show-full');
|
|
58
|
+
showFull.textContent = 'Show full output';
|
|
59
|
+
showFull.addEventListener('click', () => {
|
|
60
|
+
whatHappened.textContent = (hidden + '\n' + visible).trimEnd();
|
|
61
|
+
showFull.remove();
|
|
62
|
+
});
|
|
63
|
+
root.appendChild(showFull);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const actions = Array.isArray(frame.actions) ? frame.actions.slice(0, 3) : [];
|
|
67
|
+
const actionRow = document.createElement('div');
|
|
68
|
+
actionRow.className = 'actions';
|
|
69
|
+
for (const action of actions) {
|
|
70
|
+
const btn = document.createElement('button');
|
|
71
|
+
btn.type = 'button';
|
|
72
|
+
btn.className = 'action';
|
|
73
|
+
btn.setAttribute('data-testid', 'error-action');
|
|
74
|
+
btn.setAttribute('data-action-id', action.id);
|
|
75
|
+
btn.setAttribute('data-variant', action.variant || 'secondary');
|
|
76
|
+
btn.textContent = action.label || action.id;
|
|
77
|
+
btn.addEventListener('click', () => {
|
|
78
|
+
if (typeof onAction === 'function') {
|
|
79
|
+
onAction(action);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
actionRow.appendChild(btn);
|
|
83
|
+
}
|
|
84
|
+
root.appendChild(actionRow);
|
|
85
|
+
return root;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.FraimErrorFrame = { render: renderErrorFrame };
|
|
89
|
+
})();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>FRAIM Setup</title>
|
|
7
|
+
<link rel="stylesheet" href="/first-run/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="page">
|
|
11
|
+
<header class="page-header">
|
|
12
|
+
<h1>Set up FRAIM</h1>
|
|
13
|
+
<p class="lede" id="lede">We're getting your machine ready. Sit tight — we'll only need you for one step.</p>
|
|
14
|
+
</header>
|
|
15
|
+
|
|
16
|
+
<section class="card">
|
|
17
|
+
<ul class="checklist" id="checklist" data-testid="setup-checklist" aria-label="FRAIM setup checklist">
|
|
18
|
+
<!-- Skeleton rows shown until the async session-load completes.
|
|
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">·</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="agent" data-row-status="pending"><span class="icon" aria-hidden="true">·</span><span class="label">AI agent</span><span class="verb" data-testid="row-verb">checking…</span></li>
|
|
23
|
+
<li class="row skeleton-row" data-row-id="agent-login" data-row-status="pending"><span class="icon" aria-hidden="true">·</span><span class="label">Sign in</span><span class="verb" data-testid="row-verb">checking…</span></li>
|
|
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>
|
|
25
|
+
</ul>
|
|
26
|
+
<div class="actions">
|
|
27
|
+
<button id="primary-button" class="primary-button" data-testid="primary-button">Set up FRAIM</button>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="status" id="status" role="status" aria-live="polite"></p>
|
|
30
|
+
</section>
|
|
31
|
+
</main>
|
|
32
|
+
<script src="/first-run/error-frame.js"></script>
|
|
33
|
+
<script src="/first-run/script.js"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
@@ -0,0 +1,417 @@
|
|
|
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
|
+
}());
|