@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.
@@ -1,9 +1,47 @@
1
- import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
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 ora from "ora";
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
- function toErrorMessage(error) {
35
- return error instanceof Error ? error.message : String(error);
36
- }
37
- function getSelectedValue(value) {
38
- if (isCancel(value)) {
39
- return null;
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
- async function promptOptionalText(message) {
44
- const value = await text({
45
- message,
46
- placeholder: "optional",
47
- });
48
- const resolved = getSelectedValue(value);
49
- if (resolved === null) {
50
- return null;
51
- }
52
- if (typeof resolved !== "string") {
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 trimmed = resolved.trim();
56
- return trimmed.length > 0 ? trimmed : undefined;
57
- }
58
- async function promptRequiredText(message) {
59
- const value = await text({
60
- message,
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: Boolean(resolvedForce),
114
- preset: String(resolvedPreset) || undefined,
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
- const provider = await select({
122
- message: "Select provider",
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
- const name = await promptRequiredText("Registry name");
176
- if (name === null) {
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
- name,
183
- },
357
+ command: commandId,
358
+ args: { entityType: str("entityType"), id: str("id") },
359
+ options: { registry: str("registry"), force: bool("force") },
184
360
  };
185
- }
186
- case "registry.pull": {
187
- const entityType = await select({
188
- message: "Entity type filter",
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
- entityType: String(resolvedEntityType) || undefined,
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
- case "preset.list": {
227
- const registry = await promptOptionalText("Registry id");
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
- options: {
234
- registry,
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
- presetId,
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
- options: {
266
- registry,
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
- skillId,
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
- configId,
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
- subagentId,
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
- hookId,
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
- args: {
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
- case "add.command": {
373
- const commandId = await promptRequiredText("Command id");
374
- if (commandId === null) {
375
- return null;
376
- }
377
- const registry = await promptOptionalText("Registry id");
378
- if (registry === null) {
379
- return null;
380
- }
381
- return {
382
- command,
383
- args: {
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
- options: {
387
- registry,
388
- },
514
+ collector: {
515
+ prompts: newPrompts,
516
+ values: newValues,
517
+ index: collector.index + 1,
518
+ },
519
+ });
389
520
  };
390
- }
391
- case "remove": {
392
- const entityType = await select({
393
- message: "Entity type",
394
- options: CLI_ENTITY_TYPES.map((entry) => ({
395
- value: entry,
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
- const id = await promptRequiredText("Entity id");
404
- if (id === null) {
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
- const deleteSource = await confirm({
408
- message: "Delete source files too?",
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
- case "migrate": {
427
- const dryRun = await confirm({
428
- message: "Run as dry-run only?",
429
- initialValue: false,
430
- });
431
- const resolvedDryRun = getSelectedValue(dryRun);
432
- if (resolvedDryRun === null) {
433
- return null;
434
- }
435
- return {
436
- command,
437
- options: {
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
- default:
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 requiresConfirmation(command) {
448
- return getCommandDefinition(command).mutatesWorkspace;
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
- export async function runInteractiveAdapter(context, api) {
451
- intro("Harness interactive mode");
452
- let exitCode = 0;
453
- while (true) {
454
- const commandOptions = [
455
- ...INTERACTIVE_COMMAND_IDS.map((id) => ({
456
- value: id,
457
- label: getCommandDefinition(id).interactiveLabel ?? getCommandDefinition(id).description,
458
- })),
459
- {
460
- value: "exit",
461
- label: "Exit",
462
- },
463
- ];
464
- // @clack/prompts distributes the generic over the union, requiring a type assertion here
465
- const command = await select({
466
- message: "Select a command",
467
- options: commandOptions,
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
- const resolvedCommand = getSelectedValue(command);
470
- if (resolvedCommand === null || resolvedCommand === "exit") {
471
- break;
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 parsedCommand = String(resolvedCommand);
474
- const input = await promptCommandInput(parsedCommand);
475
- if (input === null) {
476
- cancel("Cancelled command input.");
477
- continue;
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
- if (requiresConfirmation(parsedCommand)) {
480
- const shouldRun = await confirm({
481
- message: `Run '${getCommandDefinition(parsedCommand).interactiveLabel ?? parsedCommand}' now?`,
482
- initialValue: true,
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
- const startedAt = context.now();
494
- const spinner = ora({
495
- text: `Running ${getCommandDefinition(parsedCommand).interactiveLabel ?? parsedCommand}...`,
496
- }).start();
497
- try {
498
- const output = await api.execute(input);
499
- const durationMs = context.now() - startedAt;
500
- spinner.succeed("Done.");
501
- api.renderOutput(output, durationMs, false);
502
- if (output.exitCode !== 0) {
503
- exitCode = output.exitCode;
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
- catch (error) {
507
- spinner.fail("Command failed.");
508
- context.stderr(`Error: ${toErrorMessage(error)}`);
509
- exitCode = 1;
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
- outro("Interactive session ended.");
513
- return { exitCode };
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