fraim-framework 2.0.127 → 2.0.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/init-project.js +5 -1
- package/dist/src/cli/commands/sync.js +11 -4
- package/dist/src/first-run/install-state.js +7 -5
- package/dist/src/first-run/server.js +29 -24
- package/dist/src/first-run/session-service.js +638 -194
- package/dist/src/first-run/types.js +69 -12
- package/package.json +1 -1
- package/public/first-run/error-frame.js +89 -0
- package/public/first-run/index.html +35 -221
- package/public/first-run/script.js +417 -361
- package/public/first-run/styles.css +386 -0
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Setup checklist surface — the row-keyed model that replaces the prior
|
|
4
|
+
* linear step machine for issue #352 v1.
|
|
5
|
+
*
|
|
6
|
+
* The same surface serves every user. What changes between a fresh-machine
|
|
7
|
+
* run and an experienced-engineer run is the distribution of row statuses,
|
|
8
|
+
* not the surface itself. Per R2.0 acceptance, the DOM and CSS chrome are
|
|
9
|
+
* byte-identical between cases — only `data-row-status`, the verb-text
|
|
10
|
+
* node, and the primary-button label differ.
|
|
11
|
+
*/
|
|
2
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FIRST_RUN_AGENT_OPTIONS = exports.FIRST_RUN_RESOURCES_URL = exports.FIRST_RUN_PROMPT = void 0;
|
|
4
|
-
exports.
|
|
13
|
+
exports.FIRST_RUN_AGENT_OPTIONS = exports.FIRST_RUN_RESOURCES_URL = exports.FIRST_RUN_PROMPT = exports.FIRST_RUN_ROW_IDS = void 0;
|
|
14
|
+
exports.createInitialRows = createInitialRows;
|
|
15
|
+
exports.derivePrimaryButtonLabel = derivePrimaryButtonLabel;
|
|
16
|
+
exports.FIRST_RUN_ROW_IDS = [
|
|
17
|
+
'node',
|
|
18
|
+
'git',
|
|
19
|
+
'agent',
|
|
20
|
+
'agent-login',
|
|
21
|
+
'project',
|
|
22
|
+
];
|
|
5
23
|
exports.FIRST_RUN_PROMPT = 'Onboard this project';
|
|
6
24
|
exports.FIRST_RUN_RESOURCES_URL = 'https://fraimworks.ai/resources.html';
|
|
25
|
+
/**
|
|
26
|
+
* Recommended-agent priority order (R2.2).
|
|
27
|
+
* Tie-break: Claude Code first, then Codex, then Gemini.
|
|
28
|
+
*/
|
|
7
29
|
exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
8
30
|
{
|
|
9
31
|
id: 'claude-code',
|
|
@@ -11,6 +33,7 @@ exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
|
11
33
|
detectAliases: ['claude-code', 'claude code', 'claude'],
|
|
12
34
|
loginCommand: 'claude',
|
|
13
35
|
launchCommand: 'claude',
|
|
36
|
+
installPackage: '@anthropic-ai/claude-code',
|
|
14
37
|
},
|
|
15
38
|
{
|
|
16
39
|
id: 'codex',
|
|
@@ -18,6 +41,7 @@ exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
|
18
41
|
detectAliases: ['codex'],
|
|
19
42
|
loginCommand: 'codex login',
|
|
20
43
|
launchCommand: 'codex',
|
|
44
|
+
installPackage: '@openai/codex',
|
|
21
45
|
},
|
|
22
46
|
{
|
|
23
47
|
id: 'gemini-cli',
|
|
@@ -25,16 +49,49 @@ exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
|
25
49
|
detectAliases: ['gemini-cli', 'gemini cli', 'gemini'],
|
|
26
50
|
loginCommand: 'gemini',
|
|
27
51
|
launchCommand: 'gemini',
|
|
52
|
+
installPackage: '@google/gemini-cli',
|
|
28
53
|
},
|
|
29
54
|
];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The canonical row set, in display order. Each row starts in `pending`;
|
|
57
|
+
* detection updates statuses on session load.
|
|
58
|
+
*/
|
|
59
|
+
function createInitialRows() {
|
|
60
|
+
return [
|
|
61
|
+
{ id: 'node', label: 'Node.js', status: 'pending', verb: "we'll install" },
|
|
62
|
+
{ id: 'git', label: 'git', status: 'pending', verb: "we'll install" },
|
|
63
|
+
{ id: 'agent', label: 'AI agent', status: 'pending', verb: "we'll set up Claude Code (recommended)" },
|
|
64
|
+
{ id: 'agent-login', label: 'Sign in', status: 'pending', verb: "you'll sign in after install" },
|
|
65
|
+
{ id: 'project', label: 'Project folder', status: 'pending', verb: 'pick a folder where FRAIM should work' },
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* State-derived primary-button label per R2.0. NEVER audience-derived.
|
|
70
|
+
*
|
|
71
|
+
* - "Open Hub": every row is ok, OR the project row is ok and every other
|
|
72
|
+
* row is ok-or-manual-required (i.e. the user explicitly handed
|
|
73
|
+
* themselves any remaining steps via Skip-and-continue, and there is no
|
|
74
|
+
* forward action the wizard can take).
|
|
75
|
+
* - "Continue": at least one row is ok and there is still wizard work to do.
|
|
76
|
+
* - "Set up FRAIM": no rows ok yet.
|
|
77
|
+
*
|
|
78
|
+
* Manual-required is treated as "user-handled" for the Open-Hub gate so a
|
|
79
|
+
* Skip-and-continue path doesn't strand the user on a dead-end Continue
|
|
80
|
+
* button. It is treated as "in-progress / waiting on user" for the
|
|
81
|
+
* Continue gate so a fresh state with only the manual project row open
|
|
82
|
+
* still asks the user to continue picking a folder.
|
|
83
|
+
*/
|
|
84
|
+
function derivePrimaryButtonLabel(rows) {
|
|
85
|
+
const projectRow = rows.find((row) => row.id === 'project');
|
|
86
|
+
const projectOk = projectRow?.status === 'ok';
|
|
87
|
+
const allOk = rows.every((row) => row.status === 'ok');
|
|
88
|
+
if (allOk)
|
|
89
|
+
return 'Open Hub';
|
|
90
|
+
// User-skipped path: project ok and every other row ok-or-manual-required.
|
|
91
|
+
if (projectOk && rows.every((row) => row.status === 'ok' || row.status === 'manual-required')) {
|
|
92
|
+
return 'Open Hub';
|
|
93
|
+
}
|
|
94
|
+
if (rows.some((row) => row.status === 'ok'))
|
|
95
|
+
return 'Continue';
|
|
96
|
+
return 'Set up FRAIM';
|
|
40
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.128",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -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
|
+
})();
|
|
@@ -1,221 +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
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
backdrop-filter: blur(8px);
|
|
37
|
-
}
|
|
38
|
-
main {
|
|
39
|
-
padding: 32px;
|
|
40
|
-
}
|
|
41
|
-
h1 {
|
|
42
|
-
margin: 0 0 8px;
|
|
43
|
-
font-size: 28px;
|
|
44
|
-
}
|
|
45
|
-
.lede {
|
|
46
|
-
margin: 0 0 20px;
|
|
47
|
-
color: var(--muted);
|
|
48
|
-
line-height: 1.5;
|
|
49
|
-
}
|
|
50
|
-
.steps {
|
|
51
|
-
list-style: none;
|
|
52
|
-
padding: 0;
|
|
53
|
-
margin: 24px 0 0;
|
|
54
|
-
display: grid;
|
|
55
|
-
gap: 10px;
|
|
56
|
-
}
|
|
57
|
-
.step {
|
|
58
|
-
border: 1px solid var(--line);
|
|
59
|
-
border-radius: 16px;
|
|
60
|
-
padding: 12px 14px;
|
|
61
|
-
background: #fff;
|
|
62
|
-
}
|
|
63
|
-
.step strong {
|
|
64
|
-
display: block;
|
|
65
|
-
margin-bottom: 4px;
|
|
66
|
-
font-size: 14px;
|
|
67
|
-
}
|
|
68
|
-
.step span {
|
|
69
|
-
color: var(--muted);
|
|
70
|
-
font-size: 13px;
|
|
71
|
-
text-transform: capitalize;
|
|
72
|
-
}
|
|
73
|
-
.step.active {
|
|
74
|
-
border-color: var(--accent);
|
|
75
|
-
background: var(--accent-2);
|
|
76
|
-
}
|
|
77
|
-
.card {
|
|
78
|
-
max-width: 860px;
|
|
79
|
-
background: var(--panel);
|
|
80
|
-
border: 1px solid var(--line);
|
|
81
|
-
border-radius: 24px;
|
|
82
|
-
padding: 28px;
|
|
83
|
-
box-shadow: 0 24px 64px rgba(49, 45, 32, 0.08);
|
|
84
|
-
}
|
|
85
|
-
.grid {
|
|
86
|
-
display: grid;
|
|
87
|
-
gap: 14px;
|
|
88
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
89
|
-
margin: 20px 0;
|
|
90
|
-
}
|
|
91
|
-
.agent-card, .stat {
|
|
92
|
-
border: 1px solid var(--line);
|
|
93
|
-
border-radius: 18px;
|
|
94
|
-
padding: 16px;
|
|
95
|
-
background: #fff;
|
|
96
|
-
}
|
|
97
|
-
.agent-card h3, .stat h3 {
|
|
98
|
-
margin: 0 0 8px;
|
|
99
|
-
font-size: 18px;
|
|
100
|
-
}
|
|
101
|
-
.agent-card p, .stat p {
|
|
102
|
-
margin: 0;
|
|
103
|
-
color: var(--muted);
|
|
104
|
-
line-height: 1.45;
|
|
105
|
-
}
|
|
106
|
-
.actions {
|
|
107
|
-
display: flex;
|
|
108
|
-
gap: 12px;
|
|
109
|
-
flex-wrap: wrap;
|
|
110
|
-
margin-top: 20px;
|
|
111
|
-
}
|
|
112
|
-
button, input[type="text"] {
|
|
113
|
-
font: inherit;
|
|
114
|
-
}
|
|
115
|
-
button {
|
|
116
|
-
border: 0;
|
|
117
|
-
border-radius: 999px;
|
|
118
|
-
padding: 12px 18px;
|
|
119
|
-
background: var(--accent);
|
|
120
|
-
color: #fff;
|
|
121
|
-
cursor: pointer;
|
|
122
|
-
}
|
|
123
|
-
button.secondary {
|
|
124
|
-
background: #fff;
|
|
125
|
-
color: var(--ink);
|
|
126
|
-
border: 1px solid var(--line);
|
|
127
|
-
}
|
|
128
|
-
button:disabled {
|
|
129
|
-
opacity: 0.6;
|
|
130
|
-
cursor: default;
|
|
131
|
-
}
|
|
132
|
-
label {
|
|
133
|
-
display: block;
|
|
134
|
-
margin: 18px 0 8px;
|
|
135
|
-
font-weight: 600;
|
|
136
|
-
}
|
|
137
|
-
.path-row {
|
|
138
|
-
display: flex;
|
|
139
|
-
gap: 12px;
|
|
140
|
-
flex-wrap: wrap;
|
|
141
|
-
margin-top: 12px;
|
|
142
|
-
}
|
|
143
|
-
#project-path {
|
|
144
|
-
flex: 1 1 440px;
|
|
145
|
-
min-width: 260px;
|
|
146
|
-
border: 1px solid var(--line);
|
|
147
|
-
border-radius: 14px;
|
|
148
|
-
padding: 12px 14px;
|
|
149
|
-
background: #fff;
|
|
150
|
-
}
|
|
151
|
-
pre {
|
|
152
|
-
margin: 18px 0 0;
|
|
153
|
-
padding: 16px;
|
|
154
|
-
border-radius: 18px;
|
|
155
|
-
background: #12201a;
|
|
156
|
-
color: #d8fbe8;
|
|
157
|
-
overflow: auto;
|
|
158
|
-
white-space: pre-wrap;
|
|
159
|
-
min-height: 88px;
|
|
160
|
-
}
|
|
161
|
-
.status {
|
|
162
|
-
margin-top: 18px;
|
|
163
|
-
padding: 14px 16px;
|
|
164
|
-
border-radius: 16px;
|
|
165
|
-
background: #fff;
|
|
166
|
-
border: 1px solid var(--line);
|
|
167
|
-
color: var(--muted);
|
|
168
|
-
line-height: 1.5;
|
|
169
|
-
}
|
|
170
|
-
.error {
|
|
171
|
-
border-color: #e7b8b8;
|
|
172
|
-
color: var(--error);
|
|
173
|
-
background: #fff5f5;
|
|
174
|
-
}
|
|
175
|
-
.prompt {
|
|
176
|
-
border: 1px dashed var(--accent);
|
|
177
|
-
border-radius: 20px;
|
|
178
|
-
padding: 18px;
|
|
179
|
-
margin-top: 18px;
|
|
180
|
-
background: #fcfffd;
|
|
181
|
-
font-size: 22px;
|
|
182
|
-
font-weight: 700;
|
|
183
|
-
}
|
|
184
|
-
a {
|
|
185
|
-
color: var(--accent);
|
|
186
|
-
}
|
|
187
|
-
@media (max-width: 860px) {
|
|
188
|
-
.shell {
|
|
189
|
-
grid-template-columns: 1fr;
|
|
190
|
-
}
|
|
191
|
-
aside {
|
|
192
|
-
border-right: 0;
|
|
193
|
-
border-bottom: 1px solid var(--line);
|
|
194
|
-
}
|
|
195
|
-
main {
|
|
196
|
-
padding: 20px;
|
|
197
|
-
}
|
|
198
|
-
.card {
|
|
199
|
-
padding: 22px;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
</style>
|
|
203
|
-
</head>
|
|
204
|
-
<body>
|
|
205
|
-
<div class="shell">
|
|
206
|
-
<aside>
|
|
207
|
-
<h1>FRAIM First Run</h1>
|
|
208
|
-
<p class="lede">We’ll verify your machine, connect FRAIM to an AI agent, initialize your folder, and leave you with the exact next prompt.</p>
|
|
209
|
-
<ul id="steps" class="steps"></ul>
|
|
210
|
-
</aside>
|
|
211
|
-
<main>
|
|
212
|
-
<section class="card">
|
|
213
|
-
<div id="content"></div>
|
|
214
|
-
<div id="status" class="status">Loading first-run session…</div>
|
|
215
|
-
<pre id="details" hidden></pre>
|
|
216
|
-
</section>
|
|
217
|
-
</main>
|
|
218
|
-
</div>
|
|
219
|
-
<script src="/first-run/script.js"></script>
|
|
220
|
-
</body>
|
|
221
|
-
</html>
|
|
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>
|