@madebywild/agent-harness-framework 1.3.1 → 1.5.0
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/README.md +2 -0
- package/dist/cli/adapters/autocomplete-select.d.ts +13 -0
- package/dist/cli/adapters/autocomplete-select.d.ts.map +1 -0
- package/dist/cli/adapters/autocomplete-select.js +74 -0
- package/dist/cli/adapters/autocomplete-select.js.map +1 -0
- package/dist/cli/adapters/interactive.d.ts +24 -3
- package/dist/cli/adapters/interactive.d.ts.map +1 -1
- package/dist/cli/adapters/interactive.js +849 -431
- package/dist/cli/adapters/interactive.js.map +1 -1
- package/dist/cli/adapters/toggle-confirm.d.ts +8 -0
- package/dist/cli/adapters/toggle-confirm.d.ts.map +1 -0
- package/dist/cli/adapters/toggle-confirm.js +22 -0
- package/dist/cli/adapters/toggle-confirm.js.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2 -20
- package/dist/cli/main.js.map +1 -1
- package/package.json +10 -6
|
@@ -1,9 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Spinner, TextInput } from "@inkjs/ui";
|
|
2
3
|
import { providerIdSchema } from "@madebywild/agent-harness-manifest";
|
|
3
|
-
import
|
|
4
|
+
import { Box, render, Static, Text, useApp, useInput } from "ink";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { resolveHarnessPaths } from "../../paths.js";
|
|
4
7
|
import { listBuiltinPresets, summarizePreset } from "../../presets.js";
|
|
5
8
|
import { CLI_ENTITY_TYPES } from "../../types.js";
|
|
9
|
+
import { exists } from "../../utils.js";
|
|
10
|
+
import { runDoctor } from "../../versioning/doctor.js";
|
|
6
11
|
import { getCommandDefinition } from "../command-registry.js";
|
|
12
|
+
import { renderTextOutput } from "../renderers/text.js";
|
|
13
|
+
import { AutocompleteSelect } from "./autocomplete-select.js";
|
|
14
|
+
import { ToggleConfirm } from "./toggle-confirm.js";
|
|
15
|
+
export async function detectWorkspaceStatus(cwd) {
|
|
16
|
+
const paths = resolveHarnessPaths(cwd);
|
|
17
|
+
const [dirExists, manifestExists] = await Promise.all([exists(paths.agentsDir), exists(paths.manifestFile)]);
|
|
18
|
+
if (!dirExists || !manifestExists) {
|
|
19
|
+
return { state: "missing" };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const doctor = await runDoctor(paths);
|
|
23
|
+
if (!doctor.healthy) {
|
|
24
|
+
return { state: "unhealthy", diagnostics: doctor.diagnostics };
|
|
25
|
+
}
|
|
26
|
+
return { state: "healthy" };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return {
|
|
31
|
+
state: "unhealthy",
|
|
32
|
+
diagnostics: [
|
|
33
|
+
{
|
|
34
|
+
code: "INTERACTIVE_WORKSPACE_STATUS_CHECK_FAILED",
|
|
35
|
+
severity: "error",
|
|
36
|
+
message: `Failed to determine workspace health: ${message}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Command list shown in the main selector
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
7
45
|
const INTERACTIVE_COMMAND_IDS = [
|
|
8
46
|
"init",
|
|
9
47
|
"provider.enable",
|
|
@@ -31,485 +69,865 @@ const INTERACTIVE_COMMAND_IDS = [
|
|
|
31
69
|
"plan",
|
|
32
70
|
"apply",
|
|
33
71
|
];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
const COMMAND_OPTIONS = [
|
|
73
|
+
...INTERACTIVE_COMMAND_IDS.map((id) => ({
|
|
74
|
+
label: getCommandDefinition(id).interactiveLabel ?? getCommandDefinition(id).description,
|
|
75
|
+
value: id,
|
|
76
|
+
})),
|
|
77
|
+
{ label: "Exit", value: "exit" },
|
|
78
|
+
];
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Build the prompt list for each command
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function buildPromptsForCommand(commandId, presets) {
|
|
83
|
+
const providers = providerIdSchema.options.map((p) => ({
|
|
84
|
+
label: p,
|
|
85
|
+
value: p,
|
|
86
|
+
}));
|
|
87
|
+
const entityTypes = CLI_ENTITY_TYPES.map((t) => ({ label: t, value: t }));
|
|
88
|
+
switch (commandId) {
|
|
89
|
+
case "init":
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
id: "force",
|
|
93
|
+
type: "confirm",
|
|
94
|
+
message: "Overwrite existing .harness workspace if present?",
|
|
95
|
+
initial: false,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "preset",
|
|
99
|
+
type: "select",
|
|
100
|
+
message: "Select a preset to apply during init",
|
|
101
|
+
options: [
|
|
102
|
+
{ value: "", label: "Skip preset" },
|
|
103
|
+
...presets.map((p) => ({
|
|
104
|
+
value: p.id,
|
|
105
|
+
label: `${p.name} (${p.id})`,
|
|
106
|
+
})),
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
// delegate prompt inserted dynamically when preset === "delegate"
|
|
110
|
+
];
|
|
111
|
+
case "provider.enable":
|
|
112
|
+
case "provider.disable":
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
id: "provider",
|
|
116
|
+
type: "select",
|
|
117
|
+
message: "Select provider",
|
|
118
|
+
options: providers,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
case "registry.add":
|
|
122
|
+
return [
|
|
123
|
+
{ id: "name", type: "text", message: "Registry name", required: true },
|
|
124
|
+
{ id: "gitUrl", type: "text", message: "Git URL", required: true },
|
|
125
|
+
{
|
|
126
|
+
id: "ref",
|
|
127
|
+
type: "text",
|
|
128
|
+
message: "Git ref (default: main)",
|
|
129
|
+
required: false,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "root",
|
|
133
|
+
type: "text",
|
|
134
|
+
message: "Registry root path",
|
|
135
|
+
required: false,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "tokenEnv",
|
|
139
|
+
type: "text",
|
|
140
|
+
message: "Token env var",
|
|
141
|
+
required: false,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
case "registry.remove":
|
|
145
|
+
case "registry.default.set":
|
|
146
|
+
return [{ id: "name", type: "text", message: "Registry name", required: true }];
|
|
147
|
+
case "registry.pull":
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
id: "entityType",
|
|
151
|
+
type: "select",
|
|
152
|
+
message: "Entity type filter",
|
|
153
|
+
options: [{ value: "", label: "All entity types" }, ...entityTypes],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "id",
|
|
157
|
+
type: "text",
|
|
158
|
+
message: "Entity id filter",
|
|
159
|
+
required: false,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "registry",
|
|
163
|
+
type: "text",
|
|
164
|
+
message: "Registry filter",
|
|
165
|
+
required: false,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "force",
|
|
169
|
+
type: "confirm",
|
|
170
|
+
message: "Overwrite locally modified imported sources?",
|
|
171
|
+
initial: false,
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
case "preset.list":
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
id: "registry",
|
|
178
|
+
type: "text",
|
|
179
|
+
message: "Registry id",
|
|
180
|
+
required: false,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
case "preset.describe":
|
|
184
|
+
case "preset.apply":
|
|
185
|
+
return [
|
|
186
|
+
{ id: "presetId", type: "text", message: "Preset id", required: true },
|
|
187
|
+
{
|
|
188
|
+
id: "registry",
|
|
189
|
+
type: "text",
|
|
190
|
+
message: "Registry id",
|
|
191
|
+
required: false,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
case "add.prompt":
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
id: "registry",
|
|
198
|
+
type: "text",
|
|
199
|
+
message: "Registry id",
|
|
200
|
+
required: false,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
case "add.skill":
|
|
204
|
+
return [
|
|
205
|
+
{ id: "skillId", type: "text", message: "Skill id", required: true },
|
|
206
|
+
{
|
|
207
|
+
id: "registry",
|
|
208
|
+
type: "text",
|
|
209
|
+
message: "Registry id",
|
|
210
|
+
required: false,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
case "add.mcp":
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
id: "configId",
|
|
217
|
+
type: "text",
|
|
218
|
+
message: "MCP config id",
|
|
219
|
+
required: true,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "registry",
|
|
223
|
+
type: "text",
|
|
224
|
+
message: "Registry id",
|
|
225
|
+
required: false,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
case "add.subagent":
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
id: "subagentId",
|
|
232
|
+
type: "text",
|
|
233
|
+
message: "Subagent id",
|
|
234
|
+
required: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "registry",
|
|
238
|
+
type: "text",
|
|
239
|
+
message: "Registry id",
|
|
240
|
+
required: false,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
case "add.hook":
|
|
244
|
+
return [
|
|
245
|
+
{ id: "hookId", type: "text", message: "Hook id", required: true },
|
|
246
|
+
{
|
|
247
|
+
id: "registry",
|
|
248
|
+
type: "text",
|
|
249
|
+
message: "Registry id",
|
|
250
|
+
required: false,
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
case "add.settings":
|
|
254
|
+
return [
|
|
255
|
+
{
|
|
256
|
+
id: "provider",
|
|
257
|
+
type: "select",
|
|
258
|
+
message: "Provider",
|
|
259
|
+
options: providers,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "registry",
|
|
263
|
+
type: "text",
|
|
264
|
+
message: "Registry id",
|
|
265
|
+
required: false,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
case "add.command":
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
id: "commandId",
|
|
272
|
+
type: "text",
|
|
273
|
+
message: "Command id",
|
|
274
|
+
required: true,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: "registry",
|
|
278
|
+
type: "text",
|
|
279
|
+
message: "Registry id",
|
|
280
|
+
required: false,
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
case "remove":
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
id: "entityType",
|
|
287
|
+
type: "select",
|
|
288
|
+
message: "Entity type",
|
|
289
|
+
options: entityTypes,
|
|
290
|
+
},
|
|
291
|
+
{ id: "id", type: "text", message: "Entity id", required: true },
|
|
292
|
+
{
|
|
293
|
+
id: "deleteSource",
|
|
294
|
+
type: "confirm",
|
|
295
|
+
message: "Delete source files too?",
|
|
296
|
+
initial: true,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
case "migrate":
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
id: "dryRun",
|
|
303
|
+
type: "confirm",
|
|
304
|
+
message: "Run as dry-run only?",
|
|
305
|
+
initial: false,
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
default:
|
|
309
|
+
return [];
|
|
40
310
|
}
|
|
41
|
-
return value;
|
|
42
311
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Build CommandInput from collected values
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
function buildCommandInput(commandId, values) {
|
|
316
|
+
const str = (k) => {
|
|
317
|
+
const v = values[k];
|
|
318
|
+
if (typeof v === "string") {
|
|
319
|
+
const t = v.trim();
|
|
320
|
+
return t.length > 0 ? t : undefined;
|
|
321
|
+
}
|
|
53
322
|
return undefined;
|
|
54
|
-
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
validate: (entry) => (!entry || entry.trim().length === 0 ? "This value is required" : undefined),
|
|
62
|
-
});
|
|
63
|
-
const resolved = getSelectedValue(value);
|
|
64
|
-
if (resolved === null) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return String(resolved);
|
|
68
|
-
}
|
|
69
|
-
async function promptCommandInput(command) {
|
|
70
|
-
switch (command) {
|
|
71
|
-
case "init": {
|
|
72
|
-
const force = await confirm({
|
|
73
|
-
message: "Overwrite existing .harness workspace if present?",
|
|
74
|
-
initialValue: false,
|
|
75
|
-
});
|
|
76
|
-
const resolvedForce = getSelectedValue(force);
|
|
77
|
-
if (resolvedForce === null) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const presets = (await listBuiltinPresets()).map((preset) => summarizePreset(preset));
|
|
81
|
-
const preset = await select({
|
|
82
|
-
message: "Select a preset to apply during init",
|
|
83
|
-
options: [
|
|
84
|
-
{ value: "", label: "Skip preset" },
|
|
85
|
-
...presets.map((entry) => ({
|
|
86
|
-
value: entry.id,
|
|
87
|
-
label: `${entry.name} (${entry.id})`,
|
|
88
|
-
})),
|
|
89
|
-
],
|
|
90
|
-
});
|
|
91
|
-
const resolvedPreset = getSelectedValue(preset);
|
|
92
|
-
if (resolvedPreset === null) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
let delegate;
|
|
96
|
-
if (String(resolvedPreset) === "delegate") {
|
|
97
|
-
const delegateProvider = await select({
|
|
98
|
-
message: "Select the provider CLI to delegate prompt authoring to",
|
|
99
|
-
options: providerIdSchema.options.map((entry) => ({
|
|
100
|
-
value: entry,
|
|
101
|
-
label: entry,
|
|
102
|
-
})),
|
|
103
|
-
});
|
|
104
|
-
const resolvedDelegateProvider = getSelectedValue(delegateProvider);
|
|
105
|
-
if (resolvedDelegateProvider === null) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
delegate = String(resolvedDelegateProvider);
|
|
109
|
-
}
|
|
323
|
+
};
|
|
324
|
+
const bool = (k, fallback = false) => {
|
|
325
|
+
const v = values[k];
|
|
326
|
+
return typeof v === "boolean" ? v : fallback;
|
|
327
|
+
};
|
|
328
|
+
switch (commandId) {
|
|
329
|
+
case "init":
|
|
110
330
|
return {
|
|
111
|
-
command,
|
|
331
|
+
command: commandId,
|
|
112
332
|
options: {
|
|
113
|
-
force:
|
|
114
|
-
preset:
|
|
115
|
-
delegate,
|
|
333
|
+
force: bool("force"),
|
|
334
|
+
preset: str("preset"),
|
|
335
|
+
delegate: str("delegate"),
|
|
116
336
|
},
|
|
117
337
|
};
|
|
118
|
-
}
|
|
119
338
|
case "provider.enable":
|
|
120
|
-
case "provider.disable":
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
options: providerIdSchema.options.map((entry) => ({
|
|
124
|
-
value: entry,
|
|
125
|
-
label: entry,
|
|
126
|
-
})),
|
|
127
|
-
});
|
|
128
|
-
const resolvedProvider = getSelectedValue(provider);
|
|
129
|
-
if (resolvedProvider === null) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
command,
|
|
134
|
-
args: {
|
|
135
|
-
provider: String(resolvedProvider),
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
case "registry.add": {
|
|
140
|
-
const name = await promptRequiredText("Registry name");
|
|
141
|
-
if (name === null) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const gitUrl = await promptRequiredText("Git URL");
|
|
145
|
-
if (gitUrl === null) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const ref = await promptOptionalText("Git ref (default: main)");
|
|
149
|
-
if (ref === null) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
const root = await promptOptionalText("Registry root path");
|
|
153
|
-
if (root === null) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
const tokenEnv = await promptOptionalText("Token env var");
|
|
157
|
-
if (tokenEnv === null) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
339
|
+
case "provider.disable":
|
|
340
|
+
return { command: commandId, args: { provider: str("provider") } };
|
|
341
|
+
case "registry.add":
|
|
160
342
|
return {
|
|
161
|
-
command,
|
|
162
|
-
args: {
|
|
163
|
-
name,
|
|
164
|
-
},
|
|
343
|
+
command: commandId,
|
|
344
|
+
args: { name: str("name") },
|
|
165
345
|
options: {
|
|
166
|
-
gitUrl,
|
|
167
|
-
ref,
|
|
168
|
-
root,
|
|
169
|
-
tokenEnv,
|
|
346
|
+
gitUrl: str("gitUrl"),
|
|
347
|
+
ref: str("ref"),
|
|
348
|
+
root: str("root"),
|
|
349
|
+
tokenEnv: str("tokenEnv"),
|
|
170
350
|
},
|
|
171
351
|
};
|
|
172
|
-
}
|
|
173
352
|
case "registry.remove":
|
|
174
|
-
case "registry.default.set":
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
353
|
+
case "registry.default.set":
|
|
354
|
+
return { command: commandId, args: { name: str("name") } };
|
|
355
|
+
case "registry.pull":
|
|
179
356
|
return {
|
|
180
|
-
command,
|
|
181
|
-
args: {
|
|
182
|
-
|
|
183
|
-
},
|
|
357
|
+
command: commandId,
|
|
358
|
+
args: { entityType: str("entityType"), id: str("id") },
|
|
359
|
+
options: { registry: str("registry"), force: bool("force") },
|
|
184
360
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
options: [
|
|
190
|
-
{ value: "", label: "All entity types" },
|
|
191
|
-
...CLI_ENTITY_TYPES.map((entry) => ({ value: entry, label: entry })),
|
|
192
|
-
],
|
|
193
|
-
});
|
|
194
|
-
const resolvedEntityType = getSelectedValue(entityType);
|
|
195
|
-
if (resolvedEntityType === null) {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
const id = await promptOptionalText("Entity id filter");
|
|
199
|
-
if (id === null) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
const registry = await promptOptionalText("Registry filter");
|
|
203
|
-
if (registry === null) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
const force = await confirm({
|
|
207
|
-
message: "Overwrite locally modified imported sources?",
|
|
208
|
-
initialValue: false,
|
|
209
|
-
});
|
|
210
|
-
const resolvedForce = getSelectedValue(force);
|
|
211
|
-
if (resolvedForce === null) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
361
|
+
case "preset.list":
|
|
362
|
+
return { command: commandId, options: { registry: str("registry") } };
|
|
363
|
+
case "preset.describe":
|
|
364
|
+
case "preset.apply":
|
|
214
365
|
return {
|
|
215
|
-
command,
|
|
216
|
-
args: {
|
|
217
|
-
|
|
218
|
-
id,
|
|
219
|
-
},
|
|
220
|
-
options: {
|
|
221
|
-
registry,
|
|
222
|
-
force: Boolean(resolvedForce),
|
|
223
|
-
},
|
|
366
|
+
command: commandId,
|
|
367
|
+
args: { presetId: str("presetId") },
|
|
368
|
+
options: { registry: str("registry") },
|
|
224
369
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (registry === null) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
370
|
+
case "add.prompt":
|
|
371
|
+
return { command: commandId, options: { registry: str("registry") } };
|
|
372
|
+
case "add.skill":
|
|
231
373
|
return {
|
|
232
|
-
command,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
},
|
|
374
|
+
command: commandId,
|
|
375
|
+
args: { skillId: str("skillId") },
|
|
376
|
+
options: { registry: str("registry") },
|
|
236
377
|
};
|
|
237
|
-
|
|
238
|
-
case "preset.describe":
|
|
239
|
-
case "preset.apply": {
|
|
240
|
-
const presetId = await promptRequiredText("Preset id");
|
|
241
|
-
if (presetId === null) {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
const registry = await promptOptionalText("Registry id");
|
|
245
|
-
if (registry === null) {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
378
|
+
case "add.mcp":
|
|
248
379
|
return {
|
|
249
|
-
command,
|
|
250
|
-
args: {
|
|
251
|
-
|
|
252
|
-
},
|
|
253
|
-
options: {
|
|
254
|
-
registry,
|
|
255
|
-
},
|
|
380
|
+
command: commandId,
|
|
381
|
+
args: { configId: str("configId") },
|
|
382
|
+
options: { registry: str("registry") },
|
|
256
383
|
};
|
|
257
|
-
|
|
258
|
-
case "add.prompt": {
|
|
259
|
-
const registry = await promptOptionalText("Registry id");
|
|
260
|
-
if (registry === null) {
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
384
|
+
case "add.subagent":
|
|
263
385
|
return {
|
|
264
|
-
command,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
386
|
+
command: commandId,
|
|
387
|
+
args: { subagentId: str("subagentId") },
|
|
388
|
+
options: { registry: str("registry") },
|
|
268
389
|
};
|
|
269
|
-
|
|
270
|
-
case "add.skill": {
|
|
271
|
-
const skillId = await promptRequiredText("Skill id");
|
|
272
|
-
if (skillId === null) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
const registry = await promptOptionalText("Registry id");
|
|
276
|
-
if (registry === null) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
390
|
+
case "add.hook":
|
|
279
391
|
return {
|
|
280
|
-
command,
|
|
281
|
-
args: {
|
|
282
|
-
|
|
283
|
-
},
|
|
284
|
-
options: {
|
|
285
|
-
registry,
|
|
286
|
-
},
|
|
392
|
+
command: commandId,
|
|
393
|
+
args: { hookId: str("hookId") },
|
|
394
|
+
options: { registry: str("registry") },
|
|
287
395
|
};
|
|
288
|
-
|
|
289
|
-
case "add.mcp": {
|
|
290
|
-
const configId = await promptRequiredText("MCP config id");
|
|
291
|
-
if (configId === null) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
const registry = await promptOptionalText("Registry id");
|
|
295
|
-
if (registry === null) {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
396
|
+
case "add.settings":
|
|
298
397
|
return {
|
|
299
|
-
command,
|
|
300
|
-
args: {
|
|
301
|
-
|
|
302
|
-
},
|
|
303
|
-
options: {
|
|
304
|
-
registry,
|
|
305
|
-
},
|
|
398
|
+
command: commandId,
|
|
399
|
+
args: { provider: str("provider") },
|
|
400
|
+
options: { registry: str("registry") },
|
|
306
401
|
};
|
|
307
|
-
|
|
308
|
-
case "add.subagent": {
|
|
309
|
-
const subagentId = await promptRequiredText("Subagent id");
|
|
310
|
-
if (subagentId === null) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
const registry = await promptOptionalText("Registry id");
|
|
314
|
-
if (registry === null) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
402
|
+
case "add.command":
|
|
317
403
|
return {
|
|
318
|
-
command,
|
|
319
|
-
args: {
|
|
320
|
-
|
|
321
|
-
},
|
|
322
|
-
options: {
|
|
323
|
-
registry,
|
|
324
|
-
},
|
|
404
|
+
command: commandId,
|
|
405
|
+
args: { commandId: str("commandId") },
|
|
406
|
+
options: { registry: str("registry") },
|
|
325
407
|
};
|
|
326
|
-
|
|
327
|
-
case "add.hook": {
|
|
328
|
-
const hookId = await promptRequiredText("Hook id");
|
|
329
|
-
if (hookId === null) {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
const registry = await promptOptionalText("Registry id");
|
|
333
|
-
if (registry === null) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
408
|
+
case "remove":
|
|
336
409
|
return {
|
|
337
|
-
command,
|
|
338
|
-
args: {
|
|
339
|
-
|
|
340
|
-
},
|
|
341
|
-
options: {
|
|
342
|
-
registry,
|
|
343
|
-
},
|
|
410
|
+
command: commandId,
|
|
411
|
+
args: { entityType: str("entityType"), id: str("id") },
|
|
412
|
+
options: { deleteSource: bool("deleteSource", true) },
|
|
344
413
|
};
|
|
345
|
-
|
|
346
|
-
case "add.settings": {
|
|
347
|
-
const provider = await select({
|
|
348
|
-
message: "Provider",
|
|
349
|
-
options: providerIdSchema.options.map((entry) => ({
|
|
350
|
-
value: entry,
|
|
351
|
-
label: entry,
|
|
352
|
-
})),
|
|
353
|
-
});
|
|
354
|
-
const resolvedProvider = getSelectedValue(provider);
|
|
355
|
-
if (resolvedProvider === null) {
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
const registry = await promptOptionalText("Registry id");
|
|
359
|
-
if (registry === null) {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
414
|
+
case "migrate":
|
|
362
415
|
return {
|
|
363
|
-
command,
|
|
364
|
-
|
|
365
|
-
provider: String(resolvedProvider),
|
|
366
|
-
},
|
|
367
|
-
options: {
|
|
368
|
-
registry,
|
|
369
|
-
},
|
|
416
|
+
command: commandId,
|
|
417
|
+
options: { to: "latest", dryRun: bool("dryRun") },
|
|
370
418
|
};
|
|
419
|
+
default:
|
|
420
|
+
return { command: commandId };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function initialStepFromStatus(status) {
|
|
424
|
+
if (!status || status.state === "healthy")
|
|
425
|
+
return { type: "select-command" };
|
|
426
|
+
if (status.state === "missing")
|
|
427
|
+
return { type: "onboarding" };
|
|
428
|
+
return { type: "workspace-warning", diagnostics: status.diagnostics };
|
|
429
|
+
}
|
|
430
|
+
export function App({ api, presets, workspaceStatus, onExit }) {
|
|
431
|
+
const { exit } = useApp();
|
|
432
|
+
const [step, setStep] = useState(() => initialStepFromStatus(workspaceStatus));
|
|
433
|
+
const [pastLines, setPastLines] = useState([{ id: 0, text: "Harness interactive mode" }]);
|
|
434
|
+
const nextLineId = useRef(1);
|
|
435
|
+
const [exitCode, setExitCode] = useState(0);
|
|
436
|
+
const addPastLine = useCallback((text) => {
|
|
437
|
+
const id = nextLineId.current++;
|
|
438
|
+
setPastLines((prev) => [...prev, { id, text }]);
|
|
439
|
+
}, []);
|
|
440
|
+
// FIX 2: Transition out of prompt-input when all prompts are answered via useEffect,
|
|
441
|
+
// not during render. Renders must be pure — no state updates allowed.
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
if (step.type !== "prompt-input")
|
|
444
|
+
return;
|
|
445
|
+
const { commandId, collector } = step;
|
|
446
|
+
if (collector.prompts[collector.index])
|
|
447
|
+
return;
|
|
448
|
+
const input = buildCommandInput(commandId, collector.values);
|
|
449
|
+
setStep(getCommandDefinition(commandId).mutatesWorkspace
|
|
450
|
+
? { type: "confirm-run", commandId, input }
|
|
451
|
+
: { type: "running", commandId, input });
|
|
452
|
+
}, [step]);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (step.type === "done") {
|
|
455
|
+
onExit(exitCode);
|
|
456
|
+
exit();
|
|
371
457
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
|
|
383
|
-
|
|
458
|
+
}, [step, exitCode, exit, onExit]);
|
|
459
|
+
const renderCurrentStep = () => {
|
|
460
|
+
if (step.type === "onboarding") {
|
|
461
|
+
return (_jsx(OnboardingWizard, { api: api, presets: presets, onComplete: () => {
|
|
462
|
+
addPastLine("Onboarding complete.");
|
|
463
|
+
setStep({ type: "select-command" });
|
|
464
|
+
} }));
|
|
465
|
+
}
|
|
466
|
+
if (step.type === "workspace-warning") {
|
|
467
|
+
return (_jsx(WorkspaceWarningStep, { diagnostics: step.diagnostics, api: api, onDismiss: () => setStep({ type: "select-command" }) }));
|
|
468
|
+
}
|
|
469
|
+
if (step.type === "select-command") {
|
|
470
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Command", options: COMMAND_OPTIONS, onChange: (value) => {
|
|
471
|
+
if (value === "exit") {
|
|
472
|
+
addPastLine("Interactive session ended.");
|
|
473
|
+
setStep({ type: "done" });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const commandId = value;
|
|
477
|
+
const prompts = buildPromptsForCommand(commandId, presets);
|
|
478
|
+
setStep({
|
|
479
|
+
type: "prompt-input",
|
|
480
|
+
commandId,
|
|
481
|
+
collector: { prompts, values: {}, index: 0 },
|
|
482
|
+
});
|
|
483
|
+
} }, "select-command") }));
|
|
484
|
+
}
|
|
485
|
+
if (step.type === "prompt-input") {
|
|
486
|
+
const { commandId, collector } = step;
|
|
487
|
+
const prompt = collector.prompts[collector.index];
|
|
488
|
+
// Effect above handles the transition when prompts are exhausted
|
|
489
|
+
if (!prompt)
|
|
490
|
+
return null;
|
|
491
|
+
const advanceWith = (value) => {
|
|
492
|
+
const newValues = { ...collector.values, [prompt.id]: value };
|
|
493
|
+
let newPrompts = collector.prompts;
|
|
494
|
+
// Dynamically inject delegate-provider prompt when "delegate" preset is selected
|
|
495
|
+
if (commandId === "init" && prompt.id === "preset" && value === "delegate") {
|
|
496
|
+
const delegatePrompt = {
|
|
497
|
+
id: "delegate",
|
|
498
|
+
type: "select",
|
|
499
|
+
message: "Select the provider CLI to delegate prompt authoring to",
|
|
500
|
+
options: providerIdSchema.options.map((p) => ({
|
|
501
|
+
label: p,
|
|
502
|
+
value: p,
|
|
503
|
+
})),
|
|
504
|
+
};
|
|
505
|
+
newPrompts = [
|
|
506
|
+
...collector.prompts.slice(0, collector.index + 1),
|
|
507
|
+
delegatePrompt,
|
|
508
|
+
...collector.prompts.slice(collector.index + 1),
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
setStep({
|
|
512
|
+
type: "prompt-input",
|
|
384
513
|
commandId,
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
514
|
+
collector: {
|
|
515
|
+
prompts: newPrompts,
|
|
516
|
+
values: newValues,
|
|
517
|
+
index: collector.index + 1,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
389
520
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
label: entry,
|
|
397
|
-
})),
|
|
398
|
-
});
|
|
399
|
-
const resolvedEntityType = getSelectedValue(entityType);
|
|
400
|
-
if (resolvedEntityType === null) {
|
|
401
|
-
return null;
|
|
521
|
+
const cancelPrompt = () => {
|
|
522
|
+
addPastLine("Cancelled command input.");
|
|
523
|
+
setStep({ type: "select-command" });
|
|
524
|
+
};
|
|
525
|
+
if (prompt.type === "text") {
|
|
526
|
+
return (_jsx(TextPrompt, { message: prompt.message, required: prompt.required, onSubmit: advanceWith, onCancel: cancelPrompt }, `${commandId}-${collector.index}`));
|
|
402
527
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return null;
|
|
528
|
+
if (prompt.type === "confirm") {
|
|
529
|
+
return (_jsx(ToggleConfirm, { message: prompt.message, defaultValue: prompt.initial, onSubmit: advanceWith, onEscape: cancelPrompt }));
|
|
406
530
|
}
|
|
407
|
-
|
|
408
|
-
message:
|
|
409
|
-
initialValue: true,
|
|
410
|
-
});
|
|
411
|
-
const resolvedDeleteSource = getSelectedValue(deleteSource);
|
|
412
|
-
if (resolvedDeleteSource === null) {
|
|
413
|
-
return null;
|
|
531
|
+
if (prompt.type === "select") {
|
|
532
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: prompt.message, options: prompt.options, onChange: (value) => advanceWith(value), onCancel: cancelPrompt }, `${commandId}-${collector.index}`) }));
|
|
414
533
|
}
|
|
415
|
-
return {
|
|
416
|
-
command,
|
|
417
|
-
args: {
|
|
418
|
-
entityType: String(resolvedEntityType),
|
|
419
|
-
id,
|
|
420
|
-
},
|
|
421
|
-
options: {
|
|
422
|
-
deleteSource: Boolean(resolvedDeleteSource),
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
534
|
}
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
to: "latest",
|
|
439
|
-
dryRun: Boolean(resolvedDryRun),
|
|
440
|
-
},
|
|
441
|
-
};
|
|
535
|
+
if (step.type === "confirm-run") {
|
|
536
|
+
const { commandId, input } = step;
|
|
537
|
+
const label = getCommandDefinition(commandId).interactiveLabel ?? commandId;
|
|
538
|
+
return (_jsx(ToggleConfirm, { message: `Run '${label}' now?`, defaultValue: true, onSubmit: (confirmed) => {
|
|
539
|
+
if (confirmed) {
|
|
540
|
+
setStep({ type: "running", commandId, input });
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
addPastLine("Cancelled command execution.");
|
|
544
|
+
setStep({ type: "select-command" });
|
|
545
|
+
}
|
|
546
|
+
} }));
|
|
442
547
|
}
|
|
443
|
-
|
|
444
|
-
return { command };
|
|
445
|
-
|
|
548
|
+
if (step.type === "show-output") {
|
|
549
|
+
return (_jsx(OutputStep, { label: step.label, lines: step.lines, isError: step.isError, onDismiss: () => setStep({ type: "select-command" }) }));
|
|
550
|
+
}
|
|
551
|
+
if (step.type === "running") {
|
|
552
|
+
const { commandId, input } = step;
|
|
553
|
+
const label = getCommandDefinition(commandId).interactiveLabel ?? commandId;
|
|
554
|
+
return (_jsx(RunningStep, { label: label, input: input, api: api, onDone: (output, code) => {
|
|
555
|
+
if (code !== 0)
|
|
556
|
+
setExitCode(code);
|
|
557
|
+
const lines = [];
|
|
558
|
+
renderTextOutput(output, (line) => lines.push(line));
|
|
559
|
+
setStep({ type: "show-output", label, lines, isError: code !== 0 });
|
|
560
|
+
}, onError: (message) => {
|
|
561
|
+
setExitCode(1);
|
|
562
|
+
setStep({
|
|
563
|
+
type: "show-output",
|
|
564
|
+
label,
|
|
565
|
+
lines: [`Error: ${message}`],
|
|
566
|
+
isError: true,
|
|
567
|
+
});
|
|
568
|
+
} }));
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
};
|
|
572
|
+
// FIX 1: A single Static at the root — never unmounts, never re-renders old items.
|
|
573
|
+
// Step-specific content renders below it.
|
|
574
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: pastLines, children: (line) => _jsx(Text, { children: line.text }, line.id) }), renderCurrentStep()] }));
|
|
446
575
|
}
|
|
447
|
-
function
|
|
448
|
-
|
|
576
|
+
function TextPrompt({ message, required, onSubmit, onCancel }) {
|
|
577
|
+
const [error, setError] = useState(false);
|
|
578
|
+
useInput((_input, key) => {
|
|
579
|
+
if (key.escape)
|
|
580
|
+
onCancel();
|
|
581
|
+
else if (error && !key.return)
|
|
582
|
+
setError(false);
|
|
583
|
+
});
|
|
584
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [message, ": "] }), _jsx(TextInput, { placeholder: required ? "" : "optional", onSubmit: (value) => {
|
|
585
|
+
if (required && value.trim().length === 0) {
|
|
586
|
+
setError(true);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
onSubmit(value);
|
|
590
|
+
} })] }), error && _jsx(Text, { color: "red", children: " This value is required" })] }));
|
|
449
591
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
592
|
+
function OutputStep({ label, lines, isError, onDismiss }) {
|
|
593
|
+
useInput((_input, key) => {
|
|
594
|
+
if (key.return)
|
|
595
|
+
onDismiss();
|
|
596
|
+
});
|
|
597
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "green", children: isError ? `✗ ${label}` : `✓ ${label}` }), _jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: _jsx(Text, { children: lines.join("\n") }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue..." }) })] }));
|
|
598
|
+
}
|
|
599
|
+
function RunningStep({ label, input, api, onDone, onError }) {
|
|
600
|
+
const callbacks = useRef({ onDone, onError });
|
|
601
|
+
callbacks.current = { onDone, onError };
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
api
|
|
604
|
+
.execute(input)
|
|
605
|
+
.then((output) => {
|
|
606
|
+
callbacks.current.onDone(output, output.exitCode);
|
|
607
|
+
})
|
|
608
|
+
.catch((err) => {
|
|
609
|
+
callbacks.current.onError(err instanceof Error ? err.message : String(err));
|
|
468
610
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
611
|
+
}, [api, input]);
|
|
612
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Running ${label}...` }) }));
|
|
613
|
+
}
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// OnboardingWizard — full guided setup for new workspaces
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
const HARNESS_LOGO = ` __ __
|
|
618
|
+
/ // /__ ________ ___ ___ ___
|
|
619
|
+
/ _ / _ \`/ __/ _ \\/ -_|_-<(_-<
|
|
620
|
+
/_//_/\\_,_/_/ /_//_/\\__/___/___/`;
|
|
621
|
+
const HARNESS_TAGLINE = "Configure your AI coding agents from a single source of truth.";
|
|
622
|
+
function OnboardingWizard({ api, presets, onComplete }) {
|
|
623
|
+
const [subStep, setSubStep] = useState({
|
|
624
|
+
type: "welcome",
|
|
625
|
+
});
|
|
626
|
+
const [revealIndex, setRevealIndex] = useState(0);
|
|
627
|
+
const [animationDone, setAnimationDone] = useState(false);
|
|
628
|
+
const summaryRef = useRef([]);
|
|
629
|
+
const runningRef = useRef(false);
|
|
630
|
+
const fullText = `${HARNESS_LOGO}\n\n ${HARNESS_TAGLINE}`;
|
|
631
|
+
// Animated typing effect for welcome screen
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
if (subStep.type !== "welcome")
|
|
634
|
+
return;
|
|
635
|
+
if (revealIndex >= fullText.length) {
|
|
636
|
+
setAnimationDone(true);
|
|
637
|
+
return;
|
|
472
638
|
}
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
639
|
+
const timer = setTimeout(() => setRevealIndex((i) => i + 1), revealIndex === 0 ? 100 : 8);
|
|
640
|
+
return () => clearTimeout(timer);
|
|
641
|
+
}, [subStep.type, revealIndex, fullText.length]);
|
|
642
|
+
useInput((_input, key) => {
|
|
643
|
+
if (subStep.type === "welcome" && key.return) {
|
|
644
|
+
if (!animationDone) {
|
|
645
|
+
setRevealIndex(fullText.length);
|
|
646
|
+
setAnimationDone(true);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
setSubStep({ type: "preset" });
|
|
650
|
+
}
|
|
478
651
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
652
|
+
});
|
|
653
|
+
// Single effect for all async onboarding actions, guarded by ref.
|
|
654
|
+
// Uses else-if so only one branch can fire per render cycle.
|
|
655
|
+
useEffect(() => {
|
|
656
|
+
if (runningRef.current)
|
|
657
|
+
return;
|
|
658
|
+
if (subStep.type === "running-init") {
|
|
659
|
+
runningRef.current = true;
|
|
660
|
+
api
|
|
661
|
+
.execute({
|
|
662
|
+
command: "init",
|
|
663
|
+
options: {
|
|
664
|
+
force: false,
|
|
665
|
+
preset: subStep.preset,
|
|
666
|
+
delegate: subStep.delegate,
|
|
667
|
+
},
|
|
668
|
+
})
|
|
669
|
+
.then((output) => {
|
|
670
|
+
if (output.exitCode !== 0) {
|
|
671
|
+
const lines = [];
|
|
672
|
+
renderTextOutput(output, (line) => lines.push(line));
|
|
673
|
+
setSubStep({
|
|
674
|
+
type: "init-error",
|
|
675
|
+
message: lines.join("\n"),
|
|
676
|
+
preset: subStep.preset,
|
|
677
|
+
delegate: subStep.delegate,
|
|
678
|
+
});
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
summaryRef.current.push("Initialized .harness/ workspace");
|
|
682
|
+
if (subStep.preset)
|
|
683
|
+
summaryRef.current.push(`Applied preset: ${subStep.preset}`);
|
|
684
|
+
setSubStep({ type: "providers", selected: [] });
|
|
685
|
+
})
|
|
686
|
+
.catch((err) => {
|
|
687
|
+
setSubStep({
|
|
688
|
+
type: "init-error",
|
|
689
|
+
message: err instanceof Error ? err.message : String(err),
|
|
690
|
+
preset: subStep.preset,
|
|
691
|
+
delegate: subStep.delegate,
|
|
692
|
+
});
|
|
693
|
+
})
|
|
694
|
+
.finally(() => {
|
|
695
|
+
runningRef.current = false;
|
|
483
696
|
});
|
|
484
|
-
const resolvedShouldRun = getSelectedValue(shouldRun);
|
|
485
|
-
if (resolvedShouldRun === null) {
|
|
486
|
-
cancel("Cancelled command execution.");
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (!resolvedShouldRun) {
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
697
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
698
|
+
else if (subStep.type === "running-providers") {
|
|
699
|
+
runningRef.current = true;
|
|
700
|
+
const { selected } = subStep;
|
|
701
|
+
(async () => {
|
|
702
|
+
for (const provider of selected) {
|
|
703
|
+
await api.execute({ command: "provider.enable", args: { provider } });
|
|
704
|
+
}
|
|
705
|
+
summaryRef.current.push(`Enabled provider(s): ${selected.join(", ")}`);
|
|
706
|
+
setSubStep({ type: "add-prompt" });
|
|
707
|
+
})()
|
|
708
|
+
.catch((err) => {
|
|
709
|
+
summaryRef.current.push(`Warning: provider enablement failed (${err instanceof Error ? err.message : String(err)})`);
|
|
710
|
+
setSubStep({ type: "add-prompt" });
|
|
711
|
+
})
|
|
712
|
+
.finally(() => {
|
|
713
|
+
runningRef.current = false;
|
|
714
|
+
});
|
|
505
715
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
716
|
+
else if (subStep.type === "running-add-prompt") {
|
|
717
|
+
runningRef.current = true;
|
|
718
|
+
api
|
|
719
|
+
.execute({ command: "add.prompt" })
|
|
720
|
+
.then(() => {
|
|
721
|
+
summaryRef.current.push("Added system prompt entity");
|
|
722
|
+
setSubStep({ type: "running-apply" });
|
|
723
|
+
})
|
|
724
|
+
.catch((err) => {
|
|
725
|
+
summaryRef.current.push(`Warning: failed to add prompt (${err instanceof Error ? err.message : String(err)})`);
|
|
726
|
+
setSubStep({ type: "running-apply" });
|
|
727
|
+
})
|
|
728
|
+
.finally(() => {
|
|
729
|
+
runningRef.current = false;
|
|
730
|
+
});
|
|
510
731
|
}
|
|
732
|
+
else if (subStep.type === "running-apply") {
|
|
733
|
+
runningRef.current = true;
|
|
734
|
+
api
|
|
735
|
+
.execute({ command: "apply" })
|
|
736
|
+
.then(() => {
|
|
737
|
+
summaryRef.current.push("Applied workspace (generated provider artifacts)");
|
|
738
|
+
setSubStep({ type: "complete", summary: summaryRef.current });
|
|
739
|
+
})
|
|
740
|
+
.catch((err) => {
|
|
741
|
+
summaryRef.current.push(`Warning: apply failed (${err instanceof Error ? err.message : String(err)})`);
|
|
742
|
+
setSubStep({ type: "complete", summary: summaryRef.current });
|
|
743
|
+
})
|
|
744
|
+
.finally(() => {
|
|
745
|
+
runningRef.current = false;
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}, [subStep, api]);
|
|
749
|
+
if (subStep.type === "welcome") {
|
|
750
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: fullText.slice(0, revealIndex) }), animationDone && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to get started..." }) }))] }));
|
|
751
|
+
}
|
|
752
|
+
if (subStep.type === "preset") {
|
|
753
|
+
const presetOptions = [
|
|
754
|
+
{ value: "", label: "Skip preset" },
|
|
755
|
+
...presets.map((p) => ({ value: p.id, label: `${p.name} (${p.id})` })),
|
|
756
|
+
];
|
|
757
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Step 1/4 \u2014 Choose a preset" }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Preset", options: presetOptions, onChange: (value) => {
|
|
758
|
+
if (value === "delegate") {
|
|
759
|
+
setSubStep({ type: "delegate-provider" });
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
setSubStep({
|
|
763
|
+
type: "running-init",
|
|
764
|
+
preset: value || undefined,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
} }, "onboarding-preset") })] }));
|
|
768
|
+
}
|
|
769
|
+
if (subStep.type === "delegate-provider") {
|
|
770
|
+
const providers = providerIdSchema.options.map((p) => ({
|
|
771
|
+
label: p,
|
|
772
|
+
value: p,
|
|
773
|
+
}));
|
|
774
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Step 1/4 \u2014 Select delegate provider" }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Provider", options: providers, onChange: (value) => {
|
|
775
|
+
setSubStep({
|
|
776
|
+
type: "running-init",
|
|
777
|
+
preset: "delegate",
|
|
778
|
+
delegate: value,
|
|
779
|
+
});
|
|
780
|
+
} }, "onboarding-delegate") })] }));
|
|
781
|
+
}
|
|
782
|
+
if (subStep.type === "running-init") {
|
|
783
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Initializing workspace..." }) }));
|
|
784
|
+
}
|
|
785
|
+
if (subStep.type === "init-error") {
|
|
786
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Initialization failed" }), _jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsx(Text, { children: subStep.message }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Please resolve the issue and try again." }) }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Action", options: [
|
|
787
|
+
{ label: "Retry initialization", value: "retry" },
|
|
788
|
+
{ label: "Back", value: "back" },
|
|
789
|
+
{ label: "Continue to main menu", value: "continue" },
|
|
790
|
+
], onChange: (value) => {
|
|
791
|
+
if (value === "retry") {
|
|
792
|
+
setSubStep({
|
|
793
|
+
type: "running-init",
|
|
794
|
+
preset: subStep.preset,
|
|
795
|
+
delegate: subStep.delegate,
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (value === "back") {
|
|
800
|
+
if (subStep.preset === "delegate") {
|
|
801
|
+
setSubStep({ type: "delegate-provider" });
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
setSubStep({ type: "preset" });
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
onComplete();
|
|
809
|
+
} }, "onboarding-init-error-action") })] }));
|
|
810
|
+
}
|
|
811
|
+
if (subStep.type === "providers") {
|
|
812
|
+
const remaining = providerIdSchema.options
|
|
813
|
+
.filter((p) => !subStep.selected.includes(p))
|
|
814
|
+
.map((p) => ({ label: p, value: p }));
|
|
815
|
+
const doneLabel = subStep.selected.length === 0 ? "Skip (enable later)" : `Done (${subStep.selected.join(", ")})`;
|
|
816
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Step 2/4 \u2014 Enable providers" }), subStep.selected.length > 0 && _jsxs(Text, { dimColor: true, children: ["Selected: ", subStep.selected.join(", ")] }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Provider", options: [{ label: doneLabel, value: "" }, ...remaining], onChange: (value) => {
|
|
817
|
+
if (!value) {
|
|
818
|
+
if (subStep.selected.length === 0) {
|
|
819
|
+
setSubStep({ type: "add-prompt" });
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
setSubStep({
|
|
823
|
+
type: "running-providers",
|
|
824
|
+
selected: subStep.selected,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
setSubStep({
|
|
830
|
+
type: "providers",
|
|
831
|
+
selected: [...subStep.selected, value],
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
} }, `onboarding-providers-${subStep.selected.length}`) })] }));
|
|
511
835
|
}
|
|
512
|
-
|
|
513
|
-
|
|
836
|
+
if (subStep.type === "running-providers") {
|
|
837
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Enabling provider(s): ${subStep.selected.join(", ")}...` }) }));
|
|
838
|
+
}
|
|
839
|
+
if (subStep.type === "add-prompt") {
|
|
840
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Step 3/4 \u2014 System prompt" }), _jsx(ToggleConfirm, { message: "Add a system prompt entity?", defaultValue: true, onSubmit: (yes) => {
|
|
841
|
+
if (yes) {
|
|
842
|
+
setSubStep({ type: "running-add-prompt" });
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
setSubStep({ type: "running-apply" });
|
|
846
|
+
}
|
|
847
|
+
} })] }));
|
|
848
|
+
}
|
|
849
|
+
if (subStep.type === "running-add-prompt") {
|
|
850
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Adding system prompt..." }) }));
|
|
851
|
+
}
|
|
852
|
+
if (subStep.type === "running-apply") {
|
|
853
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Step 4/4 \u2014 Applying workspace..." }) }));
|
|
854
|
+
}
|
|
855
|
+
if (subStep.type === "complete") {
|
|
856
|
+
return _jsx(OnboardingComplete, { summary: subStep.summary, onDismiss: onComplete });
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
function OnboardingComplete({ summary, onDismiss }) {
|
|
861
|
+
useInput((_input, key) => {
|
|
862
|
+
if (key.return)
|
|
863
|
+
onDismiss();
|
|
864
|
+
});
|
|
865
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Setup complete!" }), summary.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: summary.map((line, i) => (
|
|
866
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static list, never reordered
|
|
867
|
+
_jsxs(Text, { dimColor: true, children: ["- ", line] }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue to the main menu..." }) })] }));
|
|
868
|
+
}
|
|
869
|
+
function WorkspaceWarningStep({ diagnostics, api, onDismiss }) {
|
|
870
|
+
const [running, setRunning] = useState(false);
|
|
871
|
+
const [output, setOutput] = useState(null);
|
|
872
|
+
const runningRef = useRef(false);
|
|
873
|
+
useEffect(() => {
|
|
874
|
+
if (!running || runningRef.current)
|
|
875
|
+
return;
|
|
876
|
+
runningRef.current = true;
|
|
877
|
+
api
|
|
878
|
+
.execute({ command: "doctor" })
|
|
879
|
+
.then((result) => {
|
|
880
|
+
const lines = [];
|
|
881
|
+
renderTextOutput(result, (line) => lines.push(line));
|
|
882
|
+
setOutput({ lines, isError: result.exitCode !== 0 });
|
|
883
|
+
})
|
|
884
|
+
.catch((err) => {
|
|
885
|
+
setOutput({
|
|
886
|
+
lines: [err instanceof Error ? err.message : String(err)],
|
|
887
|
+
isError: true,
|
|
888
|
+
});
|
|
889
|
+
})
|
|
890
|
+
.finally(() => {
|
|
891
|
+
runningRef.current = false;
|
|
892
|
+
setRunning(false);
|
|
893
|
+
});
|
|
894
|
+
}, [running, api]);
|
|
895
|
+
useInput((_input, key) => {
|
|
896
|
+
if (output && key.return)
|
|
897
|
+
onDismiss();
|
|
898
|
+
});
|
|
899
|
+
if (running) {
|
|
900
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Running doctor..." }) }));
|
|
901
|
+
}
|
|
902
|
+
if (output) {
|
|
903
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: output.isError ? "red" : "green", children: output.isError ? "✗ Doctor" : "✓ Doctor" }), _jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: _jsx(Text, { children: output.lines.join("\n") }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue..." }) })] }));
|
|
904
|
+
}
|
|
905
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Workspace issues detected" }), _jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: diagnostics.map((d) => (_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", children: ["[", d.severity, "]"] }), " ", d.code, ": ", d.message] }, d.code))) }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Action", options: [
|
|
906
|
+
{ label: "Run doctor", value: "doctor" },
|
|
907
|
+
{ label: "Continue to menu", value: "continue" },
|
|
908
|
+
], onChange: (value) => {
|
|
909
|
+
if (value === "doctor") {
|
|
910
|
+
setRunning(true);
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
onDismiss();
|
|
914
|
+
}
|
|
915
|
+
} }, "workspace-warning") })] }));
|
|
916
|
+
}
|
|
917
|
+
// ---------------------------------------------------------------------------
|
|
918
|
+
// Exported entry point
|
|
919
|
+
// ---------------------------------------------------------------------------
|
|
920
|
+
export async function runInteractiveAdapter(api, options) {
|
|
921
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
922
|
+
const [presets, workspaceStatus] = await Promise.all([
|
|
923
|
+
listBuiltinPresets().then((ps) => ps.map(summarizePreset)),
|
|
924
|
+
detectWorkspaceStatus(cwd),
|
|
925
|
+
]);
|
|
926
|
+
let resolvedExitCode = 0;
|
|
927
|
+
const { waitUntilExit } = render(_jsx(App, { api: api, presets: presets, workspaceStatus: workspaceStatus, onExit: (code) => {
|
|
928
|
+
resolvedExitCode = code;
|
|
929
|
+
} }), { exitOnCtrlC: true });
|
|
930
|
+
await waitUntilExit();
|
|
931
|
+
return { exitCode: resolvedExitCode };
|
|
514
932
|
}
|
|
515
933
|
//# sourceMappingURL=interactive.js.map
|