@madebywild/agent-harness-framework 1.4.0 → 1.6.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.
Files changed (52) hide show
  1. package/README.md +12 -1
  2. package/dist/cli/adapters/autocomplete-select.d.ts +16 -0
  3. package/dist/cli/adapters/autocomplete-select.d.ts.map +1 -1
  4. package/dist/cli/adapters/autocomplete-select.js +83 -0
  5. package/dist/cli/adapters/autocomplete-select.js.map +1 -1
  6. package/dist/cli/adapters/commander.d.ts.map +1 -1
  7. package/dist/cli/adapters/commander.js +38 -0
  8. package/dist/cli/adapters/commander.js.map +1 -1
  9. package/dist/cli/adapters/interactive.d.ts +23 -1
  10. package/dist/cli/adapters/interactive.d.ts.map +1 -1
  11. package/dist/cli/adapters/interactive.js +906 -48
  12. package/dist/cli/adapters/interactive.js.map +1 -1
  13. package/dist/cli/command-registry.d.ts.map +1 -1
  14. package/dist/cli/command-registry.js +59 -0
  15. package/dist/cli/command-registry.js.map +1 -1
  16. package/dist/cli/contracts.d.ts +14 -3
  17. package/dist/cli/contracts.d.ts.map +1 -1
  18. package/dist/cli/handlers/skills.d.ts +13 -0
  19. package/dist/cli/handlers/skills.d.ts.map +1 -0
  20. package/dist/cli/handlers/skills.js +43 -0
  21. package/dist/cli/handlers/skills.js.map +1 -0
  22. package/dist/cli/main.d.ts.map +1 -1
  23. package/dist/cli/main.js +2 -6
  24. package/dist/cli/main.js.map +1 -1
  25. package/dist/cli/renderers/text.d.ts.map +1 -1
  26. package/dist/cli/renderers/text.js +41 -0
  27. package/dist/cli/renderers/text.js.map +1 -1
  28. package/dist/engine/entities.d.ts +1 -0
  29. package/dist/engine/entities.d.ts.map +1 -1
  30. package/dist/engine/entities.js +9 -2
  31. package/dist/engine/entities.js.map +1 -1
  32. package/dist/engine/utils.d.ts.map +1 -1
  33. package/dist/engine/utils.js +3 -0
  34. package/dist/engine/utils.js.map +1 -1
  35. package/dist/engine.d.ts +15 -1
  36. package/dist/engine.d.ts.map +1 -1
  37. package/dist/engine.js +126 -2
  38. package/dist/engine.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/paths.d.ts +3 -0
  43. package/dist/paths.d.ts.map +1 -1
  44. package/dist/paths.js +6 -0
  45. package/dist/paths.js.map +1 -1
  46. package/dist/skills-integration.d.ts +55 -0
  47. package/dist/skills-integration.d.ts.map +1 -0
  48. package/dist/skills-integration.js +524 -0
  49. package/dist/skills-integration.js.map +1 -0
  50. package/dist/types.d.ts +44 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +9 -3
@@ -3,12 +3,42 @@ import { Spinner, TextInput } from "@inkjs/ui";
3
3
  import { providerIdSchema } from "@madebywild/agent-harness-manifest";
4
4
  import { Box, render, Static, Text, useApp, useInput } from "ink";
5
5
  import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { resolveHarnessPaths } from "../../paths.js";
6
7
  import { listBuiltinPresets, summarizePreset } from "../../presets.js";
7
8
  import { CLI_ENTITY_TYPES } from "../../types.js";
9
+ import { exists } from "../../utils.js";
10
+ import { runDoctor } from "../../versioning/doctor.js";
8
11
  import { getCommandDefinition } from "../command-registry.js";
9
12
  import { renderTextOutput } from "../renderers/text.js";
10
- import { AutocompleteSelect } from "./autocomplete-select.js";
13
+ import { AutocompleteMultiSelect, AutocompleteSelect } from "./autocomplete-select.js";
11
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
+ }
12
42
  // ---------------------------------------------------------------------------
13
43
  // Command list shown in the main selector
14
44
  // ---------------------------------------------------------------------------
@@ -25,6 +55,7 @@ const INTERACTIVE_COMMAND_IDS = [
25
55
  "preset.list",
26
56
  "preset.describe",
27
57
  "preset.apply",
58
+ "skill.import",
28
59
  "add.prompt",
29
60
  "add.skill",
30
61
  "add.mcp",
@@ -50,33 +81,66 @@ const COMMAND_OPTIONS = [
50
81
  // Build the prompt list for each command
51
82
  // ---------------------------------------------------------------------------
52
83
  function buildPromptsForCommand(commandId, presets) {
53
- const providers = providerIdSchema.options.map((p) => ({ label: p, value: p }));
84
+ const providers = providerIdSchema.options.map((p) => ({
85
+ label: p,
86
+ value: p,
87
+ }));
54
88
  const entityTypes = CLI_ENTITY_TYPES.map((t) => ({ label: t, value: t }));
55
89
  switch (commandId) {
56
90
  case "init":
57
91
  return [
58
- { id: "force", type: "confirm", message: "Overwrite existing .harness workspace if present?", initial: false },
92
+ {
93
+ id: "force",
94
+ type: "confirm",
95
+ message: "Overwrite existing .harness workspace if present?",
96
+ initial: false,
97
+ },
59
98
  {
60
99
  id: "preset",
61
100
  type: "select",
62
101
  message: "Select a preset to apply during init",
63
102
  options: [
64
103
  { value: "", label: "Skip preset" },
65
- ...presets.map((p) => ({ value: p.id, label: `${p.name} (${p.id})` })),
104
+ ...presets.map((p) => ({
105
+ value: p.id,
106
+ label: `${p.name} (${p.id})`,
107
+ })),
66
108
  ],
67
109
  },
68
110
  // delegate prompt inserted dynamically when preset === "delegate"
69
111
  ];
70
112
  case "provider.enable":
71
113
  case "provider.disable":
72
- return [{ id: "provider", type: "select", message: "Select provider", options: providers }];
114
+ return [
115
+ {
116
+ id: "provider",
117
+ type: "select",
118
+ message: "Select provider",
119
+ options: providers,
120
+ },
121
+ ];
73
122
  case "registry.add":
74
123
  return [
75
124
  { id: "name", type: "text", message: "Registry name", required: true },
76
125
  { id: "gitUrl", type: "text", message: "Git URL", required: true },
77
- { id: "ref", type: "text", message: "Git ref (default: main)", required: false },
78
- { id: "root", type: "text", message: "Registry root path", required: false },
79
- { id: "tokenEnv", type: "text", message: "Token env var", required: false },
126
+ {
127
+ id: "ref",
128
+ type: "text",
129
+ message: "Git ref (default: main)",
130
+ required: false,
131
+ },
132
+ {
133
+ id: "root",
134
+ type: "text",
135
+ message: "Registry root path",
136
+ required: false,
137
+ },
138
+ {
139
+ id: "tokenEnv",
140
+ type: "text",
141
+ message: "Token env var",
142
+ required: false,
143
+ },
80
144
  ];
81
145
  case "registry.remove":
82
146
  case "registry.default.set":
@@ -89,58 +153,200 @@ function buildPromptsForCommand(commandId, presets) {
89
153
  message: "Entity type filter",
90
154
  options: [{ value: "", label: "All entity types" }, ...entityTypes],
91
155
  },
92
- { id: "id", type: "text", message: "Entity id filter", required: false },
93
- { id: "registry", type: "text", message: "Registry filter", required: false },
94
- { id: "force", type: "confirm", message: "Overwrite locally modified imported sources?", initial: false },
156
+ {
157
+ id: "id",
158
+ type: "text",
159
+ message: "Entity id filter",
160
+ required: false,
161
+ },
162
+ {
163
+ id: "registry",
164
+ type: "text",
165
+ message: "Registry filter",
166
+ required: false,
167
+ },
168
+ {
169
+ id: "force",
170
+ type: "confirm",
171
+ message: "Overwrite locally modified imported sources?",
172
+ initial: false,
173
+ },
95
174
  ];
96
175
  case "preset.list":
97
- return [{ id: "registry", type: "text", message: "Registry id", required: false }];
176
+ return [
177
+ {
178
+ id: "registry",
179
+ type: "text",
180
+ message: "Registry id",
181
+ required: false,
182
+ },
183
+ ];
98
184
  case "preset.describe":
99
185
  case "preset.apply":
100
186
  return [
101
187
  { id: "presetId", type: "text", message: "Preset id", required: true },
102
- { id: "registry", type: "text", message: "Registry id", required: false },
188
+ {
189
+ id: "registry",
190
+ type: "text",
191
+ message: "Registry id",
192
+ required: false,
193
+ },
194
+ ];
195
+ case "skill.find":
196
+ return [{ id: "query", type: "text", message: "Search query", required: true }];
197
+ case "skill.import":
198
+ return [
199
+ {
200
+ id: "source",
201
+ type: "text",
202
+ message: "Source (owner/repo, URL, or local path)",
203
+ required: true,
204
+ },
205
+ {
206
+ id: "upstreamSkill",
207
+ type: "text",
208
+ message: "Upstream skill id",
209
+ required: true,
210
+ },
211
+ {
212
+ id: "as",
213
+ type: "text",
214
+ message: "Target harness skill id",
215
+ required: false,
216
+ },
217
+ {
218
+ id: "replace",
219
+ type: "confirm",
220
+ message: "Replace existing skill if it already exists?",
221
+ initial: false,
222
+ },
223
+ {
224
+ id: "allowUnsafe",
225
+ type: "confirm",
226
+ message: "Allow non-pass audited skills?",
227
+ initial: false,
228
+ },
229
+ {
230
+ id: "allowUnaudited",
231
+ type: "confirm",
232
+ message: "Allow unaudited sources?",
233
+ initial: false,
234
+ },
103
235
  ];
104
236
  case "add.prompt":
105
- return [{ id: "registry", type: "text", message: "Registry id", required: false }];
237
+ return [
238
+ {
239
+ id: "registry",
240
+ type: "text",
241
+ message: "Registry id",
242
+ required: false,
243
+ },
244
+ ];
106
245
  case "add.skill":
107
246
  return [
108
247
  { id: "skillId", type: "text", message: "Skill id", required: true },
109
- { id: "registry", type: "text", message: "Registry id", required: false },
248
+ {
249
+ id: "registry",
250
+ type: "text",
251
+ message: "Registry id",
252
+ required: false,
253
+ },
110
254
  ];
111
255
  case "add.mcp":
112
256
  return [
113
- { id: "configId", type: "text", message: "MCP config id", required: true },
114
- { id: "registry", type: "text", message: "Registry id", required: false },
257
+ {
258
+ id: "configId",
259
+ type: "text",
260
+ message: "MCP config id",
261
+ required: true,
262
+ },
263
+ {
264
+ id: "registry",
265
+ type: "text",
266
+ message: "Registry id",
267
+ required: false,
268
+ },
115
269
  ];
116
270
  case "add.subagent":
117
271
  return [
118
- { id: "subagentId", type: "text", message: "Subagent id", required: true },
119
- { id: "registry", type: "text", message: "Registry id", required: false },
272
+ {
273
+ id: "subagentId",
274
+ type: "text",
275
+ message: "Subagent id",
276
+ required: true,
277
+ },
278
+ {
279
+ id: "registry",
280
+ type: "text",
281
+ message: "Registry id",
282
+ required: false,
283
+ },
120
284
  ];
121
285
  case "add.hook":
122
286
  return [
123
287
  { id: "hookId", type: "text", message: "Hook id", required: true },
124
- { id: "registry", type: "text", message: "Registry id", required: false },
288
+ {
289
+ id: "registry",
290
+ type: "text",
291
+ message: "Registry id",
292
+ required: false,
293
+ },
125
294
  ];
126
295
  case "add.settings":
127
296
  return [
128
- { id: "provider", type: "select", message: "Provider", options: providers },
129
- { id: "registry", type: "text", message: "Registry id", required: false },
297
+ {
298
+ id: "provider",
299
+ type: "select",
300
+ message: "Provider",
301
+ options: providers,
302
+ },
303
+ {
304
+ id: "registry",
305
+ type: "text",
306
+ message: "Registry id",
307
+ required: false,
308
+ },
130
309
  ];
131
310
  case "add.command":
132
311
  return [
133
- { id: "commandId", type: "text", message: "Command id", required: true },
134
- { id: "registry", type: "text", message: "Registry id", required: false },
312
+ {
313
+ id: "commandId",
314
+ type: "text",
315
+ message: "Command id",
316
+ required: true,
317
+ },
318
+ {
319
+ id: "registry",
320
+ type: "text",
321
+ message: "Registry id",
322
+ required: false,
323
+ },
135
324
  ];
136
325
  case "remove":
137
326
  return [
138
- { id: "entityType", type: "select", message: "Entity type", options: entityTypes },
327
+ {
328
+ id: "entityType",
329
+ type: "select",
330
+ message: "Entity type",
331
+ options: entityTypes,
332
+ },
139
333
  { id: "id", type: "text", message: "Entity id", required: true },
140
- { id: "deleteSource", type: "confirm", message: "Delete source files too?", initial: true },
334
+ {
335
+ id: "deleteSource",
336
+ type: "confirm",
337
+ message: "Delete source files too?",
338
+ initial: true,
339
+ },
141
340
  ];
142
341
  case "migrate":
143
- return [{ id: "dryRun", type: "confirm", message: "Run as dry-run only?", initial: false }];
342
+ return [
343
+ {
344
+ id: "dryRun",
345
+ type: "confirm",
346
+ message: "Run as dry-run only?",
347
+ initial: false,
348
+ },
349
+ ];
144
350
  default:
145
351
  return [];
146
352
  }
@@ -165,7 +371,11 @@ function buildCommandInput(commandId, values) {
165
371
  case "init":
166
372
  return {
167
373
  command: commandId,
168
- options: { force: bool("force"), preset: str("preset"), delegate: str("delegate") },
374
+ options: {
375
+ force: bool("force"),
376
+ preset: str("preset"),
377
+ delegate: str("delegate"),
378
+ },
169
379
  };
170
380
  case "provider.enable":
171
381
  case "provider.disable":
@@ -174,7 +384,12 @@ function buildCommandInput(commandId, values) {
174
384
  return {
175
385
  command: commandId,
176
386
  args: { name: str("name") },
177
- options: { gitUrl: str("gitUrl"), ref: str("ref"), root: str("root"), tokenEnv: str("tokenEnv") },
387
+ options: {
388
+ gitUrl: str("gitUrl"),
389
+ ref: str("ref"),
390
+ root: str("root"),
391
+ tokenEnv: str("tokenEnv"),
392
+ },
178
393
  };
179
394
  case "registry.remove":
180
395
  case "registry.default.set":
@@ -189,21 +404,66 @@ function buildCommandInput(commandId, values) {
189
404
  return { command: commandId, options: { registry: str("registry") } };
190
405
  case "preset.describe":
191
406
  case "preset.apply":
192
- return { command: commandId, args: { presetId: str("presetId") }, options: { registry: str("registry") } };
407
+ return {
408
+ command: commandId,
409
+ args: { presetId: str("presetId") },
410
+ options: { registry: str("registry") },
411
+ };
412
+ case "skill.find":
413
+ return {
414
+ command: commandId,
415
+ args: { query: str("query") },
416
+ };
417
+ case "skill.import":
418
+ return {
419
+ command: commandId,
420
+ args: { source: str("source") },
421
+ options: {
422
+ skill: str("upstreamSkill"),
423
+ as: str("as"),
424
+ replace: bool("replace"),
425
+ allowUnsafe: bool("allowUnsafe"),
426
+ allowUnaudited: bool("allowUnaudited"),
427
+ },
428
+ };
193
429
  case "add.prompt":
194
430
  return { command: commandId, options: { registry: str("registry") } };
195
431
  case "add.skill":
196
- return { command: commandId, args: { skillId: str("skillId") }, options: { registry: str("registry") } };
432
+ return {
433
+ command: commandId,
434
+ args: { skillId: str("skillId") },
435
+ options: { registry: str("registry") },
436
+ };
197
437
  case "add.mcp":
198
- return { command: commandId, args: { configId: str("configId") }, options: { registry: str("registry") } };
438
+ return {
439
+ command: commandId,
440
+ args: { configId: str("configId") },
441
+ options: { registry: str("registry") },
442
+ };
199
443
  case "add.subagent":
200
- return { command: commandId, args: { subagentId: str("subagentId") }, options: { registry: str("registry") } };
444
+ return {
445
+ command: commandId,
446
+ args: { subagentId: str("subagentId") },
447
+ options: { registry: str("registry") },
448
+ };
201
449
  case "add.hook":
202
- return { command: commandId, args: { hookId: str("hookId") }, options: { registry: str("registry") } };
450
+ return {
451
+ command: commandId,
452
+ args: { hookId: str("hookId") },
453
+ options: { registry: str("registry") },
454
+ };
203
455
  case "add.settings":
204
- return { command: commandId, args: { provider: str("provider") }, options: { registry: str("registry") } };
456
+ return {
457
+ command: commandId,
458
+ args: { provider: str("provider") },
459
+ options: { registry: str("registry") },
460
+ };
205
461
  case "add.command":
206
- return { command: commandId, args: { commandId: str("commandId") }, options: { registry: str("registry") } };
462
+ return {
463
+ command: commandId,
464
+ args: { commandId: str("commandId") },
465
+ options: { registry: str("registry") },
466
+ };
207
467
  case "remove":
208
468
  return {
209
469
  command: commandId,
@@ -211,14 +471,24 @@ function buildCommandInput(commandId, values) {
211
471
  options: { deleteSource: bool("deleteSource", true) },
212
472
  };
213
473
  case "migrate":
214
- return { command: commandId, options: { to: "latest", dryRun: bool("dryRun") } };
474
+ return {
475
+ command: commandId,
476
+ options: { to: "latest", dryRun: bool("dryRun") },
477
+ };
215
478
  default:
216
479
  return { command: commandId };
217
480
  }
218
481
  }
219
- function App({ api, presets, onExit }) {
482
+ function initialStepFromStatus(status) {
483
+ if (!status || status.state === "healthy")
484
+ return { type: "select-command" };
485
+ if (status.state === "missing")
486
+ return { type: "onboarding" };
487
+ return { type: "workspace-warning", diagnostics: status.diagnostics };
488
+ }
489
+ export function App({ api, presets, workspaceStatus, onExit }) {
220
490
  const { exit } = useApp();
221
- const [step, setStep] = useState({ type: "select-command" });
491
+ const [step, setStep] = useState(() => initialStepFromStatus(workspaceStatus));
222
492
  const [pastLines, setPastLines] = useState([{ id: 0, text: "Harness interactive mode" }]);
223
493
  const nextLineId = useRef(1);
224
494
  const [exitCode, setExitCode] = useState(0);
@@ -246,6 +516,15 @@ function App({ api, presets, onExit }) {
246
516
  }
247
517
  }, [step, exitCode, exit, onExit]);
248
518
  const renderCurrentStep = () => {
519
+ if (step.type === "onboarding") {
520
+ return (_jsx(OnboardingWizard, { api: api, presets: presets, onComplete: () => {
521
+ addPastLine("Onboarding complete.");
522
+ setStep({ type: "select-command" });
523
+ } }));
524
+ }
525
+ if (step.type === "workspace-warning") {
526
+ return (_jsx(WorkspaceWarningStep, { diagnostics: step.diagnostics, api: api, onDismiss: () => setStep({ type: "select-command" }) }));
527
+ }
249
528
  if (step.type === "select-command") {
250
529
  return (_jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: "Command", options: COMMAND_OPTIONS, onChange: (value) => {
251
530
  if (value === "exit") {
@@ -254,9 +533,30 @@ function App({ api, presets, onExit }) {
254
533
  return;
255
534
  }
256
535
  const commandId = value;
536
+ if (commandId === "skill.import") {
537
+ setStep({ type: "skills-import-workflow" });
538
+ return;
539
+ }
257
540
  const prompts = buildPromptsForCommand(commandId, presets);
258
- setStep({ type: "prompt-input", commandId, collector: { prompts, values: {}, index: 0 } });
259
- } }) }));
541
+ setStep({
542
+ type: "prompt-input",
543
+ commandId,
544
+ collector: { prompts, values: {}, index: 0 },
545
+ });
546
+ } }, "select-command") }));
547
+ }
548
+ if (step.type === "skills-import-workflow") {
549
+ return (_jsx(SkillsImportWorkflow, { api: api, onCancel: () => {
550
+ addPastLine("Cancelled command input.");
551
+ setStep({ type: "select-command" });
552
+ }, onComplete: (lines, isError) => {
553
+ setStep({
554
+ type: "show-output",
555
+ label: "Search and import third-party skills",
556
+ lines,
557
+ isError,
558
+ });
559
+ }, onDismiss: () => setStep({ type: "select-command" }) }));
260
560
  }
261
561
  if (step.type === "prompt-input") {
262
562
  const { commandId, collector } = step;
@@ -273,7 +573,10 @@ function App({ api, presets, onExit }) {
273
573
  id: "delegate",
274
574
  type: "select",
275
575
  message: "Select the provider CLI to delegate prompt authoring to",
276
- options: providerIdSchema.options.map((p) => ({ label: p, value: p })),
576
+ options: providerIdSchema.options.map((p) => ({
577
+ label: p,
578
+ value: p,
579
+ })),
277
580
  };
278
581
  newPrompts = [
279
582
  ...collector.prompts.slice(0, collector.index + 1),
@@ -284,7 +587,11 @@ function App({ api, presets, onExit }) {
284
587
  setStep({
285
588
  type: "prompt-input",
286
589
  commandId,
287
- collector: { prompts: newPrompts, values: newValues, index: collector.index + 1 },
590
+ collector: {
591
+ prompts: newPrompts,
592
+ values: newValues,
593
+ index: collector.index + 1,
594
+ },
288
595
  });
289
596
  };
290
597
  const cancelPrompt = () => {
@@ -298,7 +605,7 @@ function App({ api, presets, onExit }) {
298
605
  return (_jsx(ToggleConfirm, { message: prompt.message, defaultValue: prompt.initial, onSubmit: advanceWith, onEscape: cancelPrompt }));
299
606
  }
300
607
  if (prompt.type === "select") {
301
- return (_jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: prompt.message, options: prompt.options, onChange: (value) => advanceWith(value), onCancel: cancelPrompt }) }));
608
+ return (_jsx(Box, { marginTop: 1, children: _jsx(AutocompleteSelect, { label: prompt.message, options: prompt.options, onChange: (value) => advanceWith(value), onCancel: cancelPrompt }, `${commandId}-${collector.index}`) }));
302
609
  }
303
610
  }
304
611
  if (step.type === "confirm-run") {
@@ -328,7 +635,12 @@ function App({ api, presets, onExit }) {
328
635
  setStep({ type: "show-output", label, lines, isError: code !== 0 });
329
636
  }, onError: (message) => {
330
637
  setExitCode(1);
331
- setStep({ type: "show-output", label, lines: [`Error: ${message}`], isError: true });
638
+ setStep({
639
+ type: "show-output",
640
+ label,
641
+ lines: [`Error: ${message}`],
642
+ isError: true,
643
+ });
332
644
  } }));
333
645
  }
334
646
  return null;
@@ -375,13 +687,559 @@ function RunningStep({ label, input, api, onDone, onError }) {
375
687
  }, [api, input]);
376
688
  return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Running ${label}...` }) }));
377
689
  }
690
+ const DEFAULT_SKILL_IMPORT_OPTIONS = {
691
+ replace: false,
692
+ allowUnsafe: false,
693
+ allowUnaudited: false,
694
+ };
695
+ const SKILL_IMPORT_OPTION_PROMPTS = [
696
+ { key: "replace", message: "Replace existing local skills if they already exist?" },
697
+ { key: "allowUnsafe", message: "Allow non-pass audited skills?" },
698
+ { key: "allowUnaudited", message: "Allow unaudited sources?" },
699
+ ];
700
+ function formatDiagnosticLine(diagnostic) {
701
+ const p = diagnostic.path ? ` (${diagnostic.path})` : "";
702
+ return `[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}${p}`;
703
+ }
704
+ function summarizeAudit(output) {
705
+ if (output.family !== "skills" || output.data.operation !== "import")
706
+ return "";
707
+ const audit = output.data.result.audit;
708
+ if (!audit.audited)
709
+ return "";
710
+ return audit.providers.map((p) => `${p.provider}: ${p.raw}`).join(" · ");
711
+ }
712
+ function extractUserError(output) {
713
+ const errors = output.diagnostics.filter((d) => d.severity === "error");
714
+ if (errors.length === 0)
715
+ return undefined;
716
+ for (const e of errors) {
717
+ if (e.code === "SKILL_IMPORT_SUBPROCESS_FAILED")
718
+ return "Skill source not found or unavailable.";
719
+ if (e.code === "SKILL_IMPORT_AUDIT_BLOCKED")
720
+ return "Blocked by security audit. Use --allow-unsafe to override.";
721
+ if (e.code === "SKILL_IMPORT_AUDIT_UNAUDITED")
722
+ return "No audit report available. Use --allow-unaudited to override.";
723
+ if (e.code === "SKILL_IMPORT_COLLISION")
724
+ return "Skill already exists. Enable replace to overwrite.";
725
+ if (e.code?.startsWith("SKILL_IMPORT_PAYLOAD_"))
726
+ return "Skill payload validation failed.";
727
+ }
728
+ return errors[0]?.message;
729
+ }
730
+ function formatSkillResultLabel(result) {
731
+ return `${result.source}@${result.upstreamSkill}`;
732
+ }
733
+ function SkillItemLabel({ result, isFocused, isSelected, }) {
734
+ const nameColor = isSelected ? "green" : isFocused ? "cyan" : undefined;
735
+ const meta = [result.source, result.installs].filter(Boolean).join(" · ");
736
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: nameColor, children: result.upstreamSkill }), meta && _jsx(Text, { dimColor: true, children: ` ${meta}` })] }));
737
+ }
738
+ function readSkillFindState(output) {
739
+ if (output.family !== "skills")
740
+ return null;
741
+ if (output.data.operation !== "find")
742
+ return null;
743
+ return {
744
+ query: output.data.query,
745
+ results: output.data.results,
746
+ diagnostics: output.diagnostics,
747
+ rawText: output.data.rawText,
748
+ };
749
+ }
750
+ function buildSkillImportInput(skill, options) {
751
+ return {
752
+ command: "skill.import",
753
+ args: { source: skill.source },
754
+ options: {
755
+ skill: skill.upstreamSkill,
756
+ replace: options.replace,
757
+ allowUnsafe: options.allowUnsafe,
758
+ allowUnaudited: options.allowUnaudited,
759
+ },
760
+ };
761
+ }
762
+ function SkillsImportWorkflow({ api, onCancel, onComplete, onDismiss }) {
763
+ const [step, setStep] = useState({ type: "query" });
764
+ const runningRef = useRef(false);
765
+ const onCompleteRef = useRef(onComplete);
766
+ onCompleteRef.current = onComplete;
767
+ useEffect(() => {
768
+ if (runningRef.current)
769
+ return;
770
+ if (step.type === "searching") {
771
+ runningRef.current = true;
772
+ api
773
+ .execute({ command: "skill.find", args: { query: step.query } })
774
+ .then((output) => {
775
+ const search = readSkillFindState(output);
776
+ if (!search) {
777
+ onCompleteRef.current(["Error: Unexpected output while searching third-party skills."], true);
778
+ return;
779
+ }
780
+ if (search.results.length === 0) {
781
+ const lines = [`No skills found for query '${search.query}'.`];
782
+ if (search.rawText.trim().length > 0) {
783
+ lines.push("", search.rawText.trim());
784
+ }
785
+ if (search.diagnostics.length > 0) {
786
+ lines.push("", ...search.diagnostics.map(formatDiagnosticLine));
787
+ }
788
+ onCompleteRef.current(lines, output.exitCode !== 0);
789
+ return;
790
+ }
791
+ setStep({ type: "select", search });
792
+ })
793
+ .catch((error) => {
794
+ const message = error instanceof Error ? error.message : String(error);
795
+ onCompleteRef.current([`Error: ${message}`], true);
796
+ })
797
+ .finally(() => {
798
+ runningRef.current = false;
799
+ });
800
+ return;
801
+ }
802
+ if (step.type === "running") {
803
+ runningRef.current = true;
804
+ (async () => {
805
+ const entries = [];
806
+ for (const skill of step.selected) {
807
+ const output = await api.execute(buildSkillImportInput(skill, step.options));
808
+ const fileCount = output.family === "skills" && output.data.operation === "import" ? output.data.result.fileCount : 0;
809
+ entries.push({
810
+ skill,
811
+ ok: output.ok,
812
+ fileCount,
813
+ auditSummary: summarizeAudit(output),
814
+ errorMessage: output.ok ? undefined : extractUserError(output),
815
+ });
816
+ }
817
+ setStep({ type: "results", entries });
818
+ })()
819
+ .catch((error) => {
820
+ const message = error instanceof Error ? error.message : String(error);
821
+ onCompleteRef.current([`Error: ${message}`], true);
822
+ })
823
+ .finally(() => {
824
+ runningRef.current = false;
825
+ });
826
+ }
827
+ }, [api, step]);
828
+ if (step.type === "query") {
829
+ return (_jsx(TextPrompt, { message: "Search third-party skills", required: true, onSubmit: (value) => {
830
+ const query = value.trim();
831
+ if (query.length === 0)
832
+ return;
833
+ setStep({ type: "searching", query });
834
+ }, onCancel: onCancel }, "skill-import-query"));
835
+ }
836
+ if (step.type === "searching") {
837
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Searching skills for '${step.query}'...` }) }));
838
+ }
839
+ if (step.type === "select") {
840
+ const results = step.search.results;
841
+ const options = results.map((result, index) => ({
842
+ value: String(index),
843
+ label: formatSkillResultLabel(result),
844
+ }));
845
+ const renderSkillLabel = ({ option, isFocused, isSelected }) => {
846
+ const result = results[Number(option.value)];
847
+ if (!result)
848
+ return null;
849
+ return _jsx(SkillItemLabel, { result: result, isFocused: isFocused, isSelected: isSelected });
850
+ };
851
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: `Found ${results.length} skill(s) for '${step.search.query}'.` }), _jsx(Box, { marginTop: 1, children: _jsx(AutocompleteMultiSelect, { label: "Filter skills", options: options, renderLabel: renderSkillLabel, onCancel: onCancel, onSubmit: (selectedValues) => {
852
+ const selected = selectedValues
853
+ .map((value) => Number.parseInt(value, 10))
854
+ .filter((index) => Number.isInteger(index))
855
+ .map((index) => step.search.results[index])
856
+ .filter((result) => result !== undefined);
857
+ if (selected.length === 0) {
858
+ onComplete(["No skills selected. Nothing imported."], false);
859
+ return;
860
+ }
861
+ setStep({
862
+ type: "options",
863
+ search: step.search,
864
+ selected,
865
+ options: { ...DEFAULT_SKILL_IMPORT_OPTIONS },
866
+ optionIndex: 0,
867
+ });
868
+ } }, `skill-import-select-${step.search.query}`) })] }));
869
+ }
870
+ if (step.type === "options") {
871
+ const prompt = SKILL_IMPORT_OPTION_PROMPTS[step.optionIndex];
872
+ if (!prompt) {
873
+ return null;
874
+ }
875
+ return (_jsx(ToggleConfirm, { message: prompt.message, defaultValue: step.options[prompt.key], onEscape: onCancel, onSubmit: (value) => {
876
+ const nextOptions = {
877
+ ...step.options,
878
+ [prompt.key]: value,
879
+ };
880
+ const nextIndex = step.optionIndex + 1;
881
+ if (nextIndex < SKILL_IMPORT_OPTION_PROMPTS.length) {
882
+ setStep({
883
+ ...step,
884
+ options: nextOptions,
885
+ optionIndex: nextIndex,
886
+ });
887
+ return;
888
+ }
889
+ setStep({
890
+ type: "confirm",
891
+ search: step.search,
892
+ selected: step.selected,
893
+ options: nextOptions,
894
+ });
895
+ } }, `skill-import-option-${step.optionIndex}`));
896
+ }
897
+ if (step.type === "confirm") {
898
+ return (_jsx(ToggleConfirm, { message: `Import ${step.selected.length} selected skill(s) now?`, defaultValue: true, onEscape: onCancel, onSubmit: (confirmed) => {
899
+ if (!confirmed) {
900
+ onCancel();
901
+ return;
902
+ }
903
+ setStep({
904
+ type: "running",
905
+ search: step.search,
906
+ selected: step.selected,
907
+ options: step.options,
908
+ });
909
+ } }));
910
+ }
911
+ if (step.type === "running") {
912
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Importing ${step.selected.length} skill(s)...` }) }));
913
+ }
914
+ if (step.type === "results") {
915
+ return _jsx(SkillImportResults, { entries: step.entries, onDismiss: onDismiss });
916
+ }
917
+ return null;
918
+ }
919
+ function SkillImportResults({ entries, onDismiss }) {
920
+ useInput((_input, key) => {
921
+ if (key.return)
922
+ onDismiss();
923
+ });
924
+ const imported = entries.filter((e) => e.ok);
925
+ const failed = entries.filter((e) => !e.ok);
926
+ const allOk = failed.length === 0;
927
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: allOk ? "green" : "yellow", children: allOk ? `✓ Imported ${imported.length} skill(s)` : `${imported.length} imported, ${failed.length} failed` }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: entries.map((entry) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: entry.ok ? "green" : "red", children: entry.ok ? "✓" : "✗" }), _jsx(Text, { bold: true, children: entry.skill.upstreamSkill }), _jsx(Text, { dimColor: true, children: entry.skill.source })] }), entry.ok && entry.fileCount > 0 && _jsx(Text, { dimColor: true, children: ` ${entry.fileCount} file(s) added` }), entry.ok && entry.auditSummary && _jsx(Text, { dimColor: true, children: ` Audit: ${entry.auditSummary}` }), !entry.ok && entry.errorMessage && _jsx(Text, { color: "red", children: ` ${entry.errorMessage}` })] }, `${entry.skill.source}/${entry.skill.upstreamSkill}`))) }), _jsx(Text, { dimColor: true, children: "Press Enter to continue..." })] }));
928
+ }
929
+ // ---------------------------------------------------------------------------
930
+ // OnboardingWizard — full guided setup for new workspaces
931
+ // ---------------------------------------------------------------------------
932
+ const HARNESS_LOGO = ` __ __
933
+ / // /__ ________ ___ ___ ___
934
+ / _ / _ \`/ __/ _ \\/ -_|_-<(_-<
935
+ /_//_/\\_,_/_/ /_//_/\\__/___/___/`;
936
+ const HARNESS_TAGLINE = "Configure your AI coding agents from a single source of truth.";
937
+ function OnboardingWizard({ api, presets, onComplete }) {
938
+ const [subStep, setSubStep] = useState({
939
+ type: "welcome",
940
+ });
941
+ const [revealIndex, setRevealIndex] = useState(0);
942
+ const [animationDone, setAnimationDone] = useState(false);
943
+ const summaryRef = useRef([]);
944
+ const runningRef = useRef(false);
945
+ const fullText = `${HARNESS_LOGO}\n\n ${HARNESS_TAGLINE}`;
946
+ // Animated typing effect for welcome screen
947
+ useEffect(() => {
948
+ if (subStep.type !== "welcome")
949
+ return;
950
+ if (revealIndex >= fullText.length) {
951
+ setAnimationDone(true);
952
+ return;
953
+ }
954
+ const timer = setTimeout(() => setRevealIndex((i) => i + 1), revealIndex === 0 ? 100 : 8);
955
+ return () => clearTimeout(timer);
956
+ }, [subStep.type, revealIndex, fullText.length]);
957
+ useInput((_input, key) => {
958
+ if (subStep.type === "welcome" && key.return) {
959
+ if (!animationDone) {
960
+ setRevealIndex(fullText.length);
961
+ setAnimationDone(true);
962
+ }
963
+ else {
964
+ setSubStep({ type: "preset" });
965
+ }
966
+ }
967
+ });
968
+ // Single effect for all async onboarding actions, guarded by ref.
969
+ // Uses else-if so only one branch can fire per render cycle.
970
+ useEffect(() => {
971
+ if (runningRef.current)
972
+ return;
973
+ if (subStep.type === "running-init") {
974
+ runningRef.current = true;
975
+ api
976
+ .execute({
977
+ command: "init",
978
+ options: {
979
+ force: false,
980
+ preset: subStep.preset,
981
+ delegate: subStep.delegate,
982
+ },
983
+ })
984
+ .then((output) => {
985
+ if (output.exitCode !== 0) {
986
+ const lines = [];
987
+ renderTextOutput(output, (line) => lines.push(line));
988
+ setSubStep({
989
+ type: "init-error",
990
+ message: lines.join("\n"),
991
+ preset: subStep.preset,
992
+ delegate: subStep.delegate,
993
+ });
994
+ return;
995
+ }
996
+ summaryRef.current.push("Initialized .harness/ workspace");
997
+ if (subStep.preset)
998
+ summaryRef.current.push(`Applied preset: ${subStep.preset}`);
999
+ setSubStep({ type: "providers", selected: [] });
1000
+ })
1001
+ .catch((err) => {
1002
+ setSubStep({
1003
+ type: "init-error",
1004
+ message: err instanceof Error ? err.message : String(err),
1005
+ preset: subStep.preset,
1006
+ delegate: subStep.delegate,
1007
+ });
1008
+ })
1009
+ .finally(() => {
1010
+ runningRef.current = false;
1011
+ });
1012
+ }
1013
+ else if (subStep.type === "running-providers") {
1014
+ runningRef.current = true;
1015
+ const { selected } = subStep;
1016
+ (async () => {
1017
+ for (const provider of selected) {
1018
+ await api.execute({ command: "provider.enable", args: { provider } });
1019
+ }
1020
+ summaryRef.current.push(`Enabled provider(s): ${selected.join(", ")}`);
1021
+ setSubStep({ type: "add-prompt" });
1022
+ })()
1023
+ .catch((err) => {
1024
+ summaryRef.current.push(`Warning: provider enablement failed (${err instanceof Error ? err.message : String(err)})`);
1025
+ setSubStep({ type: "add-prompt" });
1026
+ })
1027
+ .finally(() => {
1028
+ runningRef.current = false;
1029
+ });
1030
+ }
1031
+ else if (subStep.type === "running-add-prompt") {
1032
+ runningRef.current = true;
1033
+ api
1034
+ .execute({ command: "add.prompt" })
1035
+ .then(() => {
1036
+ summaryRef.current.push("Added system prompt entity");
1037
+ setSubStep({ type: "running-apply" });
1038
+ })
1039
+ .catch((err) => {
1040
+ summaryRef.current.push(`Warning: failed to add prompt (${err instanceof Error ? err.message : String(err)})`);
1041
+ setSubStep({ type: "running-apply" });
1042
+ })
1043
+ .finally(() => {
1044
+ runningRef.current = false;
1045
+ });
1046
+ }
1047
+ else if (subStep.type === "running-apply") {
1048
+ runningRef.current = true;
1049
+ api
1050
+ .execute({ command: "apply" })
1051
+ .then(() => {
1052
+ summaryRef.current.push("Applied workspace (generated provider artifacts)");
1053
+ setSubStep({ type: "complete", summary: summaryRef.current });
1054
+ })
1055
+ .catch((err) => {
1056
+ summaryRef.current.push(`Warning: apply failed (${err instanceof Error ? err.message : String(err)})`);
1057
+ setSubStep({ type: "complete", summary: summaryRef.current });
1058
+ })
1059
+ .finally(() => {
1060
+ runningRef.current = false;
1061
+ });
1062
+ }
1063
+ }, [subStep, api]);
1064
+ if (subStep.type === "welcome") {
1065
+ 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..." }) }))] }));
1066
+ }
1067
+ if (subStep.type === "preset") {
1068
+ const presetOptions = [
1069
+ { value: "", label: "Skip preset" },
1070
+ ...presets.map((p) => ({ value: p.id, label: `${p.name} (${p.id})` })),
1071
+ ];
1072
+ 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) => {
1073
+ if (value === "delegate") {
1074
+ setSubStep({ type: "delegate-provider" });
1075
+ }
1076
+ else {
1077
+ setSubStep({
1078
+ type: "running-init",
1079
+ preset: value || undefined,
1080
+ });
1081
+ }
1082
+ } }, "onboarding-preset") })] }));
1083
+ }
1084
+ if (subStep.type === "delegate-provider") {
1085
+ const providers = providerIdSchema.options.map((p) => ({
1086
+ label: p,
1087
+ value: p,
1088
+ }));
1089
+ 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) => {
1090
+ setSubStep({
1091
+ type: "running-init",
1092
+ preset: "delegate",
1093
+ delegate: value,
1094
+ });
1095
+ } }, "onboarding-delegate") })] }));
1096
+ }
1097
+ if (subStep.type === "running-init") {
1098
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Initializing workspace..." }) }));
1099
+ }
1100
+ if (subStep.type === "init-error") {
1101
+ 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: [
1102
+ { label: "Retry initialization", value: "retry" },
1103
+ { label: "Back", value: "back" },
1104
+ { label: "Continue to main menu", value: "continue" },
1105
+ ], onChange: (value) => {
1106
+ if (value === "retry") {
1107
+ setSubStep({
1108
+ type: "running-init",
1109
+ preset: subStep.preset,
1110
+ delegate: subStep.delegate,
1111
+ });
1112
+ return;
1113
+ }
1114
+ if (value === "back") {
1115
+ if (subStep.preset === "delegate") {
1116
+ setSubStep({ type: "delegate-provider" });
1117
+ }
1118
+ else {
1119
+ setSubStep({ type: "preset" });
1120
+ }
1121
+ return;
1122
+ }
1123
+ onComplete();
1124
+ } }, "onboarding-init-error-action") })] }));
1125
+ }
1126
+ if (subStep.type === "providers") {
1127
+ const remaining = providerIdSchema.options
1128
+ .filter((p) => !subStep.selected.includes(p))
1129
+ .map((p) => ({ label: p, value: p }));
1130
+ const doneLabel = subStep.selected.length === 0 ? "Skip (enable later)" : `Done (${subStep.selected.join(", ")})`;
1131
+ 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) => {
1132
+ if (!value) {
1133
+ if (subStep.selected.length === 0) {
1134
+ setSubStep({ type: "add-prompt" });
1135
+ }
1136
+ else {
1137
+ setSubStep({
1138
+ type: "running-providers",
1139
+ selected: subStep.selected,
1140
+ });
1141
+ }
1142
+ }
1143
+ else {
1144
+ setSubStep({
1145
+ type: "providers",
1146
+ selected: [...subStep.selected, value],
1147
+ });
1148
+ }
1149
+ } }, `onboarding-providers-${subStep.selected.length}`) })] }));
1150
+ }
1151
+ if (subStep.type === "running-providers") {
1152
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: `Enabling provider(s): ${subStep.selected.join(", ")}...` }) }));
1153
+ }
1154
+ if (subStep.type === "add-prompt") {
1155
+ 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) => {
1156
+ if (yes) {
1157
+ setSubStep({ type: "running-add-prompt" });
1158
+ }
1159
+ else {
1160
+ setSubStep({ type: "running-apply" });
1161
+ }
1162
+ } })] }));
1163
+ }
1164
+ if (subStep.type === "running-add-prompt") {
1165
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Adding system prompt..." }) }));
1166
+ }
1167
+ if (subStep.type === "running-apply") {
1168
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Step 4/4 \u2014 Applying workspace..." }) }));
1169
+ }
1170
+ if (subStep.type === "complete") {
1171
+ return _jsx(OnboardingComplete, { summary: subStep.summary, onDismiss: onComplete });
1172
+ }
1173
+ return null;
1174
+ }
1175
+ function OnboardingComplete({ summary, onDismiss }) {
1176
+ useInput((_input, key) => {
1177
+ if (key.return)
1178
+ onDismiss();
1179
+ });
1180
+ 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) => (
1181
+ // biome-ignore lint/suspicious/noArrayIndexKey: static list, never reordered
1182
+ _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..." }) })] }));
1183
+ }
1184
+ function WorkspaceWarningStep({ diagnostics, api, onDismiss }) {
1185
+ const [running, setRunning] = useState(false);
1186
+ const [output, setOutput] = useState(null);
1187
+ const runningRef = useRef(false);
1188
+ useEffect(() => {
1189
+ if (!running || runningRef.current)
1190
+ return;
1191
+ runningRef.current = true;
1192
+ api
1193
+ .execute({ command: "doctor" })
1194
+ .then((result) => {
1195
+ const lines = [];
1196
+ renderTextOutput(result, (line) => lines.push(line));
1197
+ setOutput({ lines, isError: result.exitCode !== 0 });
1198
+ })
1199
+ .catch((err) => {
1200
+ setOutput({
1201
+ lines: [err instanceof Error ? err.message : String(err)],
1202
+ isError: true,
1203
+ });
1204
+ })
1205
+ .finally(() => {
1206
+ runningRef.current = false;
1207
+ setRunning(false);
1208
+ });
1209
+ }, [running, api]);
1210
+ useInput((_input, key) => {
1211
+ if (output && key.return)
1212
+ onDismiss();
1213
+ });
1214
+ if (running) {
1215
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Running doctor..." }) }));
1216
+ }
1217
+ if (output) {
1218
+ 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..." }) })] }));
1219
+ }
1220
+ 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: [
1221
+ { label: "Run doctor", value: "doctor" },
1222
+ { label: "Continue to menu", value: "continue" },
1223
+ ], onChange: (value) => {
1224
+ if (value === "doctor") {
1225
+ setRunning(true);
1226
+ }
1227
+ else {
1228
+ onDismiss();
1229
+ }
1230
+ } }, "workspace-warning") })] }));
1231
+ }
378
1232
  // ---------------------------------------------------------------------------
379
1233
  // Exported entry point
380
1234
  // ---------------------------------------------------------------------------
381
- export async function runInteractiveAdapter(api) {
382
- const presets = (await listBuiltinPresets()).map((p) => summarizePreset(p));
1235
+ export async function runInteractiveAdapter(api, options) {
1236
+ const cwd = options?.cwd ?? process.cwd();
1237
+ const [presets, workspaceStatus] = await Promise.all([
1238
+ listBuiltinPresets().then((ps) => ps.map(summarizePreset)),
1239
+ detectWorkspaceStatus(cwd),
1240
+ ]);
383
1241
  let resolvedExitCode = 0;
384
- const { waitUntilExit } = render(_jsx(App, { api: api, presets: presets, onExit: (code) => {
1242
+ const { waitUntilExit } = render(_jsx(App, { api: api, presets: presets, workspaceStatus: workspaceStatus, onExit: (code) => {
385
1243
  resolvedExitCode = code;
386
1244
  } }), { exitOnCtrlC: true });
387
1245
  await waitUntilExit();