ralphctl 0.5.0 → 0.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 (58) hide show
  1. package/README.md +29 -16
  2. package/dist/absolute-path-WUTZQ37D.mjs +8 -0
  3. package/dist/chunk-6RDMCLWU.mjs +108 -0
  4. package/dist/chunk-HIU74KTO.mjs +1046 -0
  5. package/dist/chunk-S3PTDH57.mjs +78 -0
  6. package/dist/chunk-WV4D2CPG.mjs +26 -0
  7. package/dist/cli.mjs +22413 -717
  8. package/dist/manifest.json +24 -0
  9. package/dist/prompt-adapter-JQICGVX7.mjs +7 -0
  10. package/dist/prompts/ideate.md +3 -1
  11. package/dist/prompts/plan-auto.md +23 -8
  12. package/dist/prompts/plan-common-examples.md +3 -3
  13. package/dist/prompts/plan-common.md +6 -5
  14. package/dist/prompts/plan-interactive.md +30 -7
  15. package/dist/prompts/repo-onboard.md +154 -64
  16. package/dist/prompts/signals-task.md +3 -0
  17. package/dist/prompts/sprint-feedback.md +3 -0
  18. package/dist/prompts/task-evaluation.md +74 -53
  19. package/dist/prompts/task-execution.md +65 -21
  20. package/dist/prompts/ticket-refine.md +11 -8
  21. package/dist/prompts/validation-checklist.md +3 -2
  22. package/dist/skills/default/abstraction-first/SKILL.md +45 -0
  23. package/dist/skills/default/alignment/SKILL.md +46 -0
  24. package/dist/skills/default/iterative-review/SKILL.md +48 -0
  25. package/dist/skills/exec/.gitkeep +0 -0
  26. package/dist/skills/plan/.gitkeep +0 -0
  27. package/dist/skills/refine/.gitkeep +0 -0
  28. package/dist/storage-paths-IPNZZM5D.mjs +15 -0
  29. package/dist/validation-error-QT6Q7FYU.mjs +7 -0
  30. package/package.json +9 -4
  31. package/dist/add-67UFUI54.mjs +0 -17
  32. package/dist/add-DVPVHENV.mjs +0 -18
  33. package/dist/bootstrap-FMHG6DRY.mjs +0 -11
  34. package/dist/chunk-62HYDA7L.mjs +0 -1128
  35. package/dist/chunk-747KW2RW.mjs +0 -24
  36. package/dist/chunk-BSB4EDGR.mjs +0 -260
  37. package/dist/chunk-BT5FKIZX.mjs +0 -787
  38. package/dist/chunk-CBMFRQ4Y.mjs +0 -441
  39. package/dist/chunk-CFUVE2BP.mjs +0 -16
  40. package/dist/chunk-D6QZNEYN.mjs +0 -5520
  41. package/dist/chunk-FNAAA32W.mjs +0 -103
  42. package/dist/chunk-GQ2WFKBN.mjs +0 -269
  43. package/dist/chunk-IWXBJD2D.mjs +0 -27
  44. package/dist/chunk-OGEXYSFS.mjs +0 -228
  45. package/dist/chunk-VAZ3LJBI.mjs +0 -179
  46. package/dist/chunk-WDMLPXOD.mjs +0 -363
  47. package/dist/chunk-XN2UIHBY.mjs +0 -589
  48. package/dist/chunk-ZE2BRQA2.mjs +0 -5542
  49. package/dist/create-Z635FQKO.mjs +0 -15
  50. package/dist/handle-23EFF3BE.mjs +0 -22
  51. package/dist/mount-NCYR22SN.mjs +0 -7434
  52. package/dist/project-DQHF4ISP.mjs +0 -34
  53. package/dist/prompts/check-script-discover.md +0 -69
  54. package/dist/prompts/ideate-auto.md +0 -195
  55. package/dist/prompts/task-evaluation-resume.md +0 -41
  56. package/dist/resolver-OVPYVW6Q.mjs +0 -163
  57. package/dist/sprint-4E26AB5F.mjs +0 -38
  58. package/dist/start-T34NI3LF.mjs +0 -19
@@ -0,0 +1,1046 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/integration/ui/prompts/auto-mount.tsx
4
+ import "react";
5
+ import { render } from "ink";
6
+
7
+ // src/business/ports/prompt-port.ts
8
+ var PromptCancelledError = class extends Error {
9
+ constructor(message = "Prompt cancelled by user") {
10
+ super(message);
11
+ this.name = "PromptCancelledError";
12
+ }
13
+ };
14
+
15
+ // src/integration/ui/prompts/prompt-host.tsx
16
+ import "react";
17
+ import { Box as Box8 } from "ink";
18
+
19
+ // src/integration/ui/prompts/hooks.ts
20
+ import { useEffect, useState } from "react";
21
+
22
+ // src/integration/ui/prompts/prompt-queue.ts
23
+ var SEQUENCE_IDLE_MS = 250;
24
+ var PromptQueue = class {
25
+ queue = [];
26
+ history = [];
27
+ lastResolveAt = 0;
28
+ idleClearTimer = null;
29
+ listeners = /* @__PURE__ */ new Set();
30
+ enqueue(prompt) {
31
+ if (this.queue.length === 0 && Date.now() - this.lastResolveAt > SEQUENCE_IDLE_MS) {
32
+ this.history = [];
33
+ }
34
+ if (this.idleClearTimer !== null) {
35
+ clearTimeout(this.idleClearTimer);
36
+ this.idleClearTimer = null;
37
+ }
38
+ this.queue.push(prompt);
39
+ this.notify();
40
+ }
41
+ current() {
42
+ return this.queue[0] ?? null;
43
+ }
44
+ resolveCurrent(value) {
45
+ const head = this.queue.shift();
46
+ if (!head) return;
47
+ this.history.push(buildResolved(head, value));
48
+ this.lastResolveAt = Date.now();
49
+ head.resolve(value);
50
+ this.scheduleIdleClear();
51
+ this.notify();
52
+ }
53
+ cancelCurrent(err) {
54
+ const head = this.queue.shift();
55
+ if (!head) return;
56
+ this.lastResolveAt = Date.now();
57
+ head.reject(err);
58
+ this.scheduleIdleClear();
59
+ this.notify();
60
+ }
61
+ /**
62
+ * After the queue empties, schedule a clear of the visible transcript.
63
+ * If a new prompt arrives within the idle window the timer is cancelled
64
+ * (see `enqueue`) and the transcript continues to accumulate. Without
65
+ * this, completed workflows leave their transcript on screen indefinitely
66
+ * (e.g. project-add answers persisting on the home view after pop).
67
+ */
68
+ scheduleIdleClear() {
69
+ if (this.queue.length > 0) return;
70
+ if (this.idleClearTimer !== null) clearTimeout(this.idleClearTimer);
71
+ this.idleClearTimer = setTimeout(() => {
72
+ this.idleClearTimer = null;
73
+ if (this.queue.length === 0) {
74
+ this.history = [];
75
+ this.notify();
76
+ }
77
+ }, SEQUENCE_IDLE_MS);
78
+ }
79
+ /** Drop all pending prompts without resolving. Used on app shutdown. */
80
+ clear(reason) {
81
+ while (this.queue.length > 0) {
82
+ const p = this.queue.shift();
83
+ p?.reject(reason);
84
+ }
85
+ this.history = [];
86
+ this.notify();
87
+ }
88
+ /** Discard the visible transcript without affecting pending prompts. */
89
+ clearHistory() {
90
+ this.history = [];
91
+ this.notify();
92
+ }
93
+ subscribe(listener) {
94
+ this.listeners.add(listener);
95
+ try {
96
+ listener(this.snapshot());
97
+ } catch {
98
+ }
99
+ return () => {
100
+ this.listeners.delete(listener);
101
+ };
102
+ }
103
+ size() {
104
+ return this.queue.length;
105
+ }
106
+ historySnapshot() {
107
+ return this.history;
108
+ }
109
+ snapshot() {
110
+ return { current: this.current(), history: this.history };
111
+ }
112
+ notify() {
113
+ const snap = this.snapshot();
114
+ for (const l of this.listeners) {
115
+ try {
116
+ l(snap);
117
+ } catch {
118
+ }
119
+ }
120
+ }
121
+ };
122
+ function buildResolved(prompt, value) {
123
+ switch (prompt.kind) {
124
+ case "confirm":
125
+ return { kind: "confirm", options: prompt.options, value };
126
+ case "input":
127
+ return { kind: "input", options: prompt.options, value };
128
+ case "select":
129
+ return { kind: "select", options: prompt.options, value };
130
+ case "checkbox":
131
+ return { kind: "checkbox", options: prompt.options, value };
132
+ case "editor":
133
+ return { kind: "editor", options: prompt.options, value };
134
+ case "fileBrowser":
135
+ return { kind: "fileBrowser", options: prompt.options, value };
136
+ }
137
+ }
138
+ var promptQueue = new PromptQueue();
139
+
140
+ // src/integration/ui/prompts/hooks.ts
141
+ function useCurrentPrompt() {
142
+ const [current, setCurrent] = useState(null);
143
+ useEffect(() => {
144
+ const unsubscribe = promptQueue.subscribe((state) => {
145
+ setCurrent(state.current);
146
+ });
147
+ return unsubscribe;
148
+ }, []);
149
+ return current;
150
+ }
151
+ function usePromptState() {
152
+ const [state, setState] = useState({ current: null, history: [] });
153
+ useEffect(() => {
154
+ const unsubscribe = promptQueue.subscribe(setState);
155
+ return unsubscribe;
156
+ }, []);
157
+ return state;
158
+ }
159
+
160
+ // src/integration/ui/prompts/prompt-transcript.tsx
161
+ import "react";
162
+ import { Box, Text } from "ink";
163
+
164
+ // src/integration/ui/theme/tokens.ts
165
+ var inkColors = {
166
+ // Semantic state
167
+ success: "#7FB069",
168
+ // sage — not neon green, easier on the eyes
169
+ error: "#E76F51",
170
+ // warm coral — not a pure red klaxon
171
+ warning: "#E8A13B",
172
+ // amber
173
+ info: "#6CA6B0",
174
+ // dusty cyan
175
+ // UI state
176
+ muted: "#8B8680",
177
+ // warm gray (hint of yellow, matches the mustard brand)
178
+ highlight: "#E8C547",
179
+ // brand mustard — focus / active
180
+ // Brand
181
+ primary: "#E8C547",
182
+ // mustard — section stamps, accents
183
+ secondary: "#D98880"
184
+ // muted rose — Ralph personality pull-quotes
185
+ };
186
+ var glyphs = {
187
+ // Phase / status
188
+ phaseDone: "\u25A0",
189
+ phaseActive: "\u25C6",
190
+ phasePending: "\u25C7",
191
+ phaseDisabled: "\u25CC",
192
+ // Action cursors / bullets
193
+ actionCursor: "\u25B8",
194
+ selectMarker: "\u203A",
195
+ bulletListItem: "\xB7",
196
+ arrowRight: "\u2192",
197
+ activityArrow: "\u21B3",
198
+ // Section markers
199
+ badge: "\u25A3",
200
+ sectionRule: "\u2501",
201
+ // State confirmation
202
+ check: "\u2713",
203
+ cross: "\u2717",
204
+ warningGlyph: "\u26A0",
205
+ infoGlyph: "i",
206
+ // Loading (braille spinner frames)
207
+ spinner: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
208
+ // Personality rail
209
+ quoteRail: "\u2503",
210
+ // Separators
211
+ inlineDot: "\xB7",
212
+ emDash: "\u2014",
213
+ separatorVertical: "\u2502"
214
+ };
215
+ var spacing = {
216
+ /** Between top-level sections (blank line). */
217
+ section: 1,
218
+ /** Before a final CTA row — a beat of breath before a decision. */
219
+ actionBreak: 2,
220
+ /** Card internal x-padding. */
221
+ cardPadX: 1,
222
+ /** Left-indent for nested content (steps, bullets, children). */
223
+ indent: 2,
224
+ /** Internal gutter inside card-like boxes. */
225
+ gutter: 1
226
+ };
227
+ var FIELD_LABEL_WIDTH = 12;
228
+ var DONUT_EMOJI = "\u{1F369}";
229
+
230
+ // src/integration/ui/prompts/prompt-transcript.tsx
231
+ import { jsx, jsxs } from "react/jsx-runtime";
232
+ function PromptTranscript({ history }) {
233
+ if (history.length === 0) return null;
234
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: history.map((entry, i) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
235
+ DONUT_EMOJI,
236
+ " ",
237
+ entry.options.message,
238
+ ": ",
239
+ renderValue(entry)
240
+ ] }) }, i)) });
241
+ }
242
+ function renderValue(entry) {
243
+ switch (entry.kind) {
244
+ case "confirm":
245
+ return entry.value ? "yes" : "no";
246
+ case "input":
247
+ return entry.value === "" ? "(empty)" : entry.value;
248
+ case "select": {
249
+ const choice = entry.options.choices.find((c) => c.value === entry.value);
250
+ return choice?.label ?? String(entry.value);
251
+ }
252
+ case "checkbox": {
253
+ if (entry.value.length === 0) return "(none)";
254
+ const labels = entry.value.map((v) => {
255
+ const choice = entry.options.choices.find((c) => c.value === v);
256
+ return choice?.label ?? String(v);
257
+ });
258
+ return labels.join(", ");
259
+ }
260
+ case "editor":
261
+ if (entry.value === null) return "(cancelled)";
262
+ if (entry.value === "") return "(empty)";
263
+ return summariseMultiline(entry.value);
264
+ case "fileBrowser":
265
+ return entry.value ?? "(cancelled)";
266
+ }
267
+ }
268
+ function summariseMultiline(text) {
269
+ const lines = text.split("\n");
270
+ if (lines.length === 1) {
271
+ const first = lines[0] ?? "";
272
+ return first.length > 60 ? `${first.slice(0, 57)}\u2026` : first;
273
+ }
274
+ return `(${String(lines.length)} lines)`;
275
+ }
276
+
277
+ // src/integration/ui/prompts/confirm-prompt.tsx
278
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
279
+ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
280
+ import { ConfirmInput } from "@inkjs/ui";
281
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
282
+ var RESERVED_ROWS = 10;
283
+ var MIN_VIEWPORT = 6;
284
+ var MAX_VIEWPORT = 40;
285
+ function useTerminalRows() {
286
+ const { stdout } = useStdout();
287
+ const [rows, setRows] = useState2(stdout.rows);
288
+ useEffect2(() => {
289
+ const onResize = () => {
290
+ setRows(stdout.rows);
291
+ };
292
+ stdout.on("resize", onResize);
293
+ return () => {
294
+ stdout.off("resize", onResize);
295
+ };
296
+ }, [stdout]);
297
+ return rows;
298
+ }
299
+ function ConfirmPrompt({ options, onSubmit }) {
300
+ const details = options.details?.trim();
301
+ const lines = useMemo(() => details ? details.split("\n") : [], [details]);
302
+ const terminalRows = useTerminalRows();
303
+ const viewport = Math.max(MIN_VIEWPORT, Math.min(MAX_VIEWPORT, terminalRows - RESERVED_ROWS));
304
+ const total = lines.length;
305
+ const maxOffset = Math.max(0, total - viewport);
306
+ const scrollable = total > viewport;
307
+ const [offset, setOffset] = useState2(0);
308
+ useInput((_input, key) => {
309
+ if (!scrollable) return;
310
+ if (key.upArrow) setOffset((o) => Math.max(0, o - 1));
311
+ else if (key.downArrow) setOffset((o) => Math.min(maxOffset, o + 1));
312
+ else if (key.pageUp) setOffset((o) => Math.max(0, o - viewport));
313
+ else if (key.pageDown) setOffset((o) => Math.min(maxOffset, o + viewport));
314
+ });
315
+ const visibleLines = scrollable ? lines.slice(offset, offset + viewport) : lines;
316
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
317
+ details ? /* @__PURE__ */ jsxs2(
318
+ Box2,
319
+ {
320
+ flexDirection: "column",
321
+ borderStyle: "round",
322
+ borderColor: inkColors.muted,
323
+ paddingX: spacing.gutter,
324
+ marginBottom: spacing.section,
325
+ children: [
326
+ visibleLines.map((line, idx) => /* @__PURE__ */ jsx2(Text2, { children: line.length > 0 ? /* @__PURE__ */ jsxs2(Fragment, { children: [
327
+ /* @__PURE__ */ jsxs2(Text2, { color: inkColors.muted, children: [
328
+ glyphs.quoteRail,
329
+ " "
330
+ ] }),
331
+ line
332
+ ] }) : " " }, idx)),
333
+ scrollable ? /* @__PURE__ */ jsxs2(Text2, { color: inkColors.muted, children: [
334
+ glyphs.inlineDot,
335
+ " lines ",
336
+ String(offset + 1),
337
+ "\u2013",
338
+ String(Math.min(offset + viewport, total)),
339
+ " of",
340
+ " ",
341
+ String(total),
342
+ " ",
343
+ glyphs.inlineDot,
344
+ " \u2191/\u2193 scroll ",
345
+ glyphs.inlineDot,
346
+ " PgUp/PgDn page"
347
+ ] }) : null
348
+ ]
349
+ }
350
+ ) : null,
351
+ /* @__PURE__ */ jsxs2(Box2, { children: [
352
+ /* @__PURE__ */ jsxs2(Text2, { children: [
353
+ DONUT_EMOJI,
354
+ " ",
355
+ options.message,
356
+ " "
357
+ ] }),
358
+ /* @__PURE__ */ jsx2(
359
+ ConfirmInput,
360
+ {
361
+ defaultChoice: options.default === false ? "cancel" : "confirm",
362
+ onConfirm: () => {
363
+ onSubmit(true);
364
+ },
365
+ onCancel: () => {
366
+ onSubmit(false);
367
+ }
368
+ }
369
+ )
370
+ ] })
371
+ ] });
372
+ }
373
+
374
+ // src/integration/ui/prompts/input-prompt.tsx
375
+ import { useState as useState3 } from "react";
376
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
377
+ import { TextInput } from "@inkjs/ui";
378
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
379
+ function InputPrompt({ options, onSubmit, onCancel }) {
380
+ const [error, setError] = useState3(null);
381
+ useInput2((_input, key) => {
382
+ if (key.escape) onCancel();
383
+ });
384
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
385
+ /* @__PURE__ */ jsxs3(Box3, { children: [
386
+ /* @__PURE__ */ jsxs3(Text3, { children: [
387
+ DONUT_EMOJI,
388
+ " ",
389
+ options.message,
390
+ ":",
391
+ " "
392
+ ] }),
393
+ /* @__PURE__ */ jsx3(
394
+ TextInput,
395
+ {
396
+ defaultValue: options.default,
397
+ placeholder: options.default,
398
+ onSubmit: (value) => {
399
+ const validation = options.validate?.(value);
400
+ if (validation !== void 0 && validation !== true) {
401
+ if (typeof validation === "string") {
402
+ setError(validation);
403
+ return;
404
+ }
405
+ }
406
+ setError(null);
407
+ onSubmit(value);
408
+ }
409
+ }
410
+ )
411
+ ] }),
412
+ error !== null && /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: inkColors.error, children: [
413
+ " \u2717 ",
414
+ error
415
+ ] }) })
416
+ ] });
417
+ }
418
+
419
+ // src/integration/ui/prompts/select-prompt.tsx
420
+ import { useState as useState4 } from "react";
421
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
422
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
423
+ function SelectPrompt({ options, onSubmit, onCancel }) {
424
+ const initialIdx = findInitialIdx(options);
425
+ const [focusedIdx, setFocusedIdx] = useState4(initialIdx);
426
+ useInput3((_input, key) => {
427
+ if (key.escape) {
428
+ onCancel();
429
+ return;
430
+ }
431
+ if (key.upArrow) {
432
+ setFocusedIdx((i) => stepFocus(options.choices, i, -1));
433
+ return;
434
+ }
435
+ if (key.downArrow) {
436
+ setFocusedIdx((i) => stepFocus(options.choices, i, 1));
437
+ return;
438
+ }
439
+ if (key.return) {
440
+ const picked = options.choices[focusedIdx];
441
+ if (picked && !isDisabled(picked)) {
442
+ onSubmit(picked.value);
443
+ }
444
+ }
445
+ });
446
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
447
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
448
+ DONUT_EMOJI,
449
+ " ",
450
+ options.message,
451
+ ":"
452
+ ] }) }),
453
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, flexDirection: "column", children: options.choices.map((choice, i) => {
454
+ const isFocused = i === focusedIdx;
455
+ const disabled = isDisabled(choice);
456
+ const color = disabled ? inkColors.muted : isFocused ? inkColors.highlight : void 0;
457
+ const prefix = isFocused ? glyphs.actionCursor : " ";
458
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
459
+ /* @__PURE__ */ jsx4(Text4, { color, bold: isFocused, children: `${prefix} ${choice.label}` }),
460
+ typeof choice.disabled === "string" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` (${choice.disabled})` }) : null
461
+ ] }, `${String(i)}-${choice.label}`);
462
+ }) })
463
+ ] });
464
+ }
465
+ function isDisabled(choice) {
466
+ return choice.disabled === true || typeof choice.disabled === "string";
467
+ }
468
+ function findInitialIdx(options) {
469
+ if (options.default !== void 0) {
470
+ const idx = options.choices.findIndex((c) => c.value === options.default);
471
+ const chosen = options.choices[idx];
472
+ if (idx >= 0 && chosen && !isDisabled(chosen)) return idx;
473
+ }
474
+ const firstEnabled = options.choices.findIndex((c) => !isDisabled(c));
475
+ return firstEnabled >= 0 ? firstEnabled : 0;
476
+ }
477
+ function stepFocus(choices, from, delta) {
478
+ const len = choices.length;
479
+ if (len === 0) return from;
480
+ let next = from;
481
+ for (let i = 0; i < len; i++) {
482
+ next = (next + delta + len) % len;
483
+ const candidate = choices[next];
484
+ if (candidate && !isDisabled(candidate)) return next;
485
+ }
486
+ return from;
487
+ }
488
+
489
+ // src/integration/ui/prompts/checkbox-prompt.tsx
490
+ import { useState as useState5 } from "react";
491
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
492
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
493
+ function CheckboxPrompt({ options, onSubmit, onCancel }) {
494
+ const initialFocus = options.choices.findIndex((c) => !isDisabled2(c));
495
+ const [focusedIdx, setFocusedIdx] = useState5(initialFocus >= 0 ? initialFocus : 0);
496
+ const [checked, setChecked] = useState5(() => seedCheckedSet(options));
497
+ useInput4((input, key) => {
498
+ if (key.escape) {
499
+ onCancel();
500
+ return;
501
+ }
502
+ if (key.upArrow) {
503
+ setFocusedIdx((i) => stepFocus2(options.choices, i, -1));
504
+ return;
505
+ }
506
+ if (key.downArrow) {
507
+ setFocusedIdx((i) => stepFocus2(options.choices, i, 1));
508
+ return;
509
+ }
510
+ if (input === " ") {
511
+ const choice = options.choices[focusedIdx];
512
+ if (choice && !isDisabled2(choice)) {
513
+ setChecked((prev) => {
514
+ const next = new Set(prev);
515
+ if (next.has(focusedIdx)) next.delete(focusedIdx);
516
+ else next.add(focusedIdx);
517
+ return next;
518
+ });
519
+ }
520
+ return;
521
+ }
522
+ if (key.return) {
523
+ const picked = [...checked].sort((a, b) => a - b).map((i) => options.choices[i]?.value).filter((v) => v !== void 0);
524
+ onSubmit(picked);
525
+ }
526
+ });
527
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
528
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { children: [
529
+ DONUT_EMOJI,
530
+ " ",
531
+ options.message,
532
+ ": ",
533
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(space toggles, enter submits)" })
534
+ ] }) }),
535
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 2, flexDirection: "column", children: options.choices.map((choice, i) => {
536
+ const isFocused = i === focusedIdx;
537
+ const disabled = isDisabled2(choice);
538
+ const color = disabled ? inkColors.muted : isFocused ? inkColors.highlight : void 0;
539
+ const cursor = isFocused ? glyphs.actionCursor : " ";
540
+ const mark = checked.has(i) ? glyphs.check : glyphs.phasePending;
541
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
542
+ /* @__PURE__ */ jsx5(Text5, { color, bold: isFocused, children: `${cursor} ${mark} ${choice.label}` }),
543
+ typeof choice.disabled === "string" ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` (${choice.disabled})` }) : null
544
+ ] }, `${String(i)}-${choice.label}`);
545
+ }) })
546
+ ] });
547
+ }
548
+ function isDisabled2(choice) {
549
+ return choice.disabled === true || typeof choice.disabled === "string";
550
+ }
551
+ function seedCheckedSet(options) {
552
+ const defaults = options.defaults ?? [];
553
+ const set = /* @__PURE__ */ new Set();
554
+ for (const v of defaults) {
555
+ const idx = options.choices.findIndex((c) => c.value === v);
556
+ if (idx >= 0) set.add(idx);
557
+ }
558
+ return set;
559
+ }
560
+ function stepFocus2(choices, from, delta) {
561
+ const len = choices.length;
562
+ if (len === 0) return from;
563
+ let next = from;
564
+ for (let i = 0; i < len; i++) {
565
+ next = (next + delta + len) % len;
566
+ const candidate = choices[next];
567
+ if (candidate && !isDisabled2(candidate)) return next;
568
+ }
569
+ return from;
570
+ }
571
+
572
+ // src/integration/ui/prompts/editor-prompt.tsx
573
+ import { useState as useState6, useMemo as useMemo2 } from "react";
574
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
575
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
576
+ var MIN_EDIT_ROWS = 8;
577
+ function splitLines(text) {
578
+ return text.split("\n");
579
+ }
580
+ function joinLines(lines) {
581
+ return lines.join("\n");
582
+ }
583
+ function clampCursor(lines, cursor) {
584
+ const row = Math.max(0, Math.min(cursor.row, lines.length - 1));
585
+ const line = lines[row] ?? "";
586
+ const col = Math.max(0, Math.min(cursor.col, line.length));
587
+ return { row, col };
588
+ }
589
+ function EditorPrompt({ options, onSubmit, onCancel }) {
590
+ const [lines, setLines] = useState6(() => splitLines(options.default ?? ""));
591
+ const [cursor, setCursor] = useState6(() => {
592
+ const init = splitLines(options.default ?? "");
593
+ const lastRow = init.length - 1;
594
+ return { row: lastRow, col: (init[lastRow] ?? "").length };
595
+ });
596
+ useInput5((input, key) => {
597
+ if (key.escape || key.ctrl && input === "c") {
598
+ onCancel();
599
+ return;
600
+ }
601
+ if (key.ctrl && input === "d") {
602
+ onSubmit(joinLines(lines));
603
+ return;
604
+ }
605
+ if (key.leftArrow) {
606
+ setCursor((prev) => {
607
+ if (prev.col > 0) return { row: prev.row, col: prev.col - 1 };
608
+ if (prev.row > 0) {
609
+ const up = lines[prev.row - 1] ?? "";
610
+ return { row: prev.row - 1, col: up.length };
611
+ }
612
+ return prev;
613
+ });
614
+ return;
615
+ }
616
+ if (key.rightArrow) {
617
+ setCursor((prev) => {
618
+ const curLine = lines[prev.row] ?? "";
619
+ if (prev.col < curLine.length) return { row: prev.row, col: prev.col + 1 };
620
+ if (prev.row < lines.length - 1) return { row: prev.row + 1, col: 0 };
621
+ return prev;
622
+ });
623
+ return;
624
+ }
625
+ if (key.upArrow) {
626
+ setCursor((prev) => clampCursor(lines, { row: prev.row - 1, col: prev.col }));
627
+ return;
628
+ }
629
+ if (key.downArrow) {
630
+ setCursor((prev) => clampCursor(lines, { row: prev.row + 1, col: prev.col }));
631
+ return;
632
+ }
633
+ if (key.ctrl && input === "a") {
634
+ setCursor((prev) => ({ row: prev.row, col: 0 }));
635
+ return;
636
+ }
637
+ if (key.ctrl && input === "e") {
638
+ setCursor((prev) => {
639
+ const curLine = lines[prev.row] ?? "";
640
+ return { row: prev.row, col: curLine.length };
641
+ });
642
+ return;
643
+ }
644
+ if (key.backspace || key.delete) {
645
+ setLines((prev) => {
646
+ const next = [...prev];
647
+ const line = next[cursor.row] ?? "";
648
+ if (cursor.col > 0) {
649
+ next[cursor.row] = line.slice(0, cursor.col - 1) + line.slice(cursor.col);
650
+ setCursor({ row: cursor.row, col: cursor.col - 1 });
651
+ } else if (cursor.row > 0) {
652
+ const prevLine = next[cursor.row - 1] ?? "";
653
+ const mergedCol = prevLine.length;
654
+ next[cursor.row - 1] = prevLine + line;
655
+ next.splice(cursor.row, 1);
656
+ setCursor({ row: cursor.row - 1, col: mergedCol });
657
+ }
658
+ return next;
659
+ });
660
+ return;
661
+ }
662
+ if (key.return) {
663
+ setLines((prev) => {
664
+ const next = [...prev];
665
+ const line = next[cursor.row] ?? "";
666
+ const before = line.slice(0, cursor.col);
667
+ const after = line.slice(cursor.col);
668
+ next[cursor.row] = before;
669
+ next.splice(cursor.row + 1, 0, after);
670
+ return next;
671
+ });
672
+ setCursor({ row: cursor.row + 1, col: 0 });
673
+ return;
674
+ }
675
+ if (input && !key.ctrl && !key.meta) {
676
+ const chunk = input;
677
+ setLines((prev) => {
678
+ const next = [...prev];
679
+ const line = next[cursor.row] ?? "";
680
+ const before = line.slice(0, cursor.col);
681
+ const after = line.slice(cursor.col);
682
+ const parts = (before + chunk + after).split("\n");
683
+ next.splice(cursor.row, 1, ...parts);
684
+ const insertedLines = chunk.split("\n");
685
+ const insertedRow = cursor.row + insertedLines.length - 1;
686
+ const insertedCol = insertedLines.length === 1 ? before.length + chunk.length : (insertedLines[insertedLines.length - 1] ?? "").length;
687
+ setCursor({ row: insertedRow, col: insertedCol });
688
+ return next;
689
+ });
690
+ }
691
+ });
692
+ const renderedLines = useMemo2(() => {
693
+ const padCount = Math.max(0, MIN_EDIT_ROWS - lines.length);
694
+ const padded = lines.map((line, i) => {
695
+ if (i !== cursor.row) return line.length > 0 ? line : " ";
696
+ const before = line.slice(0, cursor.col);
697
+ const at = line[cursor.col] ?? " ";
698
+ const after = line.slice(cursor.col + 1);
699
+ return { before, at, after };
700
+ });
701
+ for (let i = 0; i < padCount; i++) padded.push(" ");
702
+ return padded;
703
+ }, [lines, cursor]);
704
+ const charCount = lines.reduce((sum, l) => sum + l.length, 0);
705
+ const lineCount = lines.length;
706
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: inkColors.muted, paddingX: 1, width: "100%", children: [
707
+ /* @__PURE__ */ jsxs6(Box6, { children: [
708
+ /* @__PURE__ */ jsx6(Text6, { color: inkColors.primary, bold: true, children: glyphs.badge }),
709
+ /* @__PURE__ */ jsx6(Text6, { color: inkColors.primary, bold: true, children: ` ${options.message.toUpperCase()}` })
710
+ ] }),
711
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, marginLeft: 1, flexDirection: "column", children: renderedLines.map((item, i) => {
712
+ if (typeof item === "string") {
713
+ return /* @__PURE__ */ jsx6(Text6, { dimColor: i >= lines.length, children: item }, i);
714
+ }
715
+ return /* @__PURE__ */ jsxs6(Text6, { children: [
716
+ item.before,
717
+ /* @__PURE__ */ jsx6(Text6, { inverse: true, children: item.at }),
718
+ item.after
719
+ ] }, i);
720
+ }) }),
721
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, justifyContent: "space-between", children: [
722
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
723
+ "Ctrl+D submit ",
724
+ glyphs.inlineDot,
725
+ " Esc cancel ",
726
+ glyphs.inlineDot,
727
+ " Enter newline"
728
+ ] }),
729
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
730
+ String(lineCount),
731
+ " ",
732
+ lineCount === 1 ? "line" : "lines",
733
+ " ",
734
+ glyphs.inlineDot,
735
+ " ",
736
+ String(charCount),
737
+ " ",
738
+ charCount === 1 ? "char" : "chars"
739
+ ] })
740
+ ] })
741
+ ] });
742
+ }
743
+
744
+ // src/integration/ui/prompts/file-browser-prompt.tsx
745
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState7 } from "react";
746
+ import { readdirSync, statSync } from "fs";
747
+ import { homedir } from "os";
748
+ import { dirname, join, resolve } from "path";
749
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
750
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
751
+ function listDirectories(dirPath) {
752
+ try {
753
+ return readdirSync(dirPath, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
754
+ } catch {
755
+ return [];
756
+ }
757
+ }
758
+ function isGitRepo(dirPath) {
759
+ try {
760
+ return statSync(join(dirPath, ".git")).isDirectory();
761
+ } catch {
762
+ return false;
763
+ }
764
+ }
765
+ var PAGE_SIZE = 12;
766
+ function FileBrowserPrompt({ options, onSubmit, onCancel }) {
767
+ const [currentPath, setCurrentPath] = useState7(
768
+ () => options.startPath ? resolve(options.startPath) : homedir()
769
+ );
770
+ const [dirs, setDirs] = useState7([]);
771
+ const [cursor, setCursor] = useState7(0);
772
+ const [offset, setOffset] = useState7(0);
773
+ useEffect3(() => {
774
+ setDirs(listDirectories(currentPath));
775
+ setCursor(0);
776
+ setOffset(0);
777
+ }, [currentPath]);
778
+ const message = options.message ?? "Browse to directory:";
779
+ const parent = dirname(currentPath);
780
+ useInput6((input, key) => {
781
+ if (key.escape) {
782
+ onCancel();
783
+ return;
784
+ }
785
+ if (key.upArrow) {
786
+ setCursor((c) => Math.max(0, c - 1));
787
+ return;
788
+ }
789
+ if (key.downArrow) {
790
+ setCursor((c) => Math.min(dirs.length - 1, c + 1));
791
+ return;
792
+ }
793
+ if (key.return) {
794
+ const name = dirs[cursor];
795
+ if (name) setCurrentPath(join(currentPath, name));
796
+ return;
797
+ }
798
+ if (input === ".") {
799
+ if (options.mustBeGitRepo && !isGitRepo(currentPath)) return;
800
+ onSubmit(currentPath);
801
+ return;
802
+ }
803
+ if (key.backspace || input === "u") {
804
+ if (parent !== currentPath) setCurrentPath(parent);
805
+ return;
806
+ }
807
+ if (input === "h") {
808
+ setCurrentPath(homedir());
809
+ return;
810
+ }
811
+ });
812
+ useEffect3(() => {
813
+ if (cursor < offset) setOffset(cursor);
814
+ else if (cursor >= offset + PAGE_SIZE) setOffset(cursor - PAGE_SIZE + 1);
815
+ }, [cursor, offset]);
816
+ const visible = useMemo3(() => dirs.slice(offset, offset + PAGE_SIZE), [dirs, offset]);
817
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: inkColors.muted, paddingX: 1, children: [
818
+ /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { children: [
819
+ DONUT_EMOJI,
820
+ " ",
821
+ message
822
+ ] }) }),
823
+ /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: currentPath }) }),
824
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
825
+ visible.length === 0 && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(no subdirectories)" }),
826
+ visible.map((name, i) => {
827
+ const absoluteIdx = offset + i;
828
+ const isSelected = absoluteIdx === cursor;
829
+ const full = join(currentPath, name);
830
+ const repo = isGitRepo(full);
831
+ const icon = repo ? "\u2699 " : "\u25B8 ";
832
+ return /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? inkColors.highlight : void 0, children: [
833
+ isSelected ? "\u203A " : " ",
834
+ icon,
835
+ name,
836
+ repo && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (git repo)" })
837
+ ] }, name);
838
+ })
839
+ ] }),
840
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191/\u2193 move \xB7 Enter descend \xB7 Backspace up \xB7 h home \xB7 . select \xB7 Esc cancel" }) })
841
+ ] });
842
+ }
843
+
844
+ // src/integration/ui/prompts/prompt-host.tsx
845
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
846
+ function cancel() {
847
+ promptQueue.cancelCurrent(new PromptCancelledError());
848
+ }
849
+ function PromptHost() {
850
+ const { current, history } = usePromptState();
851
+ if (!current && history.length === 0) return null;
852
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
853
+ /* @__PURE__ */ jsx8(PromptTranscript, { history }),
854
+ current ? /* @__PURE__ */ jsx8(ActivePrompt, { prompt: current }) : null
855
+ ] });
856
+ }
857
+ function ActivePrompt({
858
+ prompt: current
859
+ }) {
860
+ switch (current.kind) {
861
+ case "confirm":
862
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
863
+ ConfirmPrompt,
864
+ {
865
+ options: current.options,
866
+ onSubmit: (v) => {
867
+ promptQueue.resolveCurrent(v);
868
+ },
869
+ onCancel: cancel
870
+ }
871
+ ) });
872
+ case "input":
873
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
874
+ InputPrompt,
875
+ {
876
+ options: current.options,
877
+ onSubmit: (v) => {
878
+ promptQueue.resolveCurrent(v);
879
+ },
880
+ onCancel: cancel
881
+ }
882
+ ) });
883
+ case "select":
884
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
885
+ SelectPrompt,
886
+ {
887
+ options: current.options,
888
+ onSubmit: (v) => {
889
+ promptQueue.resolveCurrent(v);
890
+ },
891
+ onCancel: cancel
892
+ }
893
+ ) });
894
+ case "checkbox":
895
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
896
+ CheckboxPrompt,
897
+ {
898
+ options: current.options,
899
+ onSubmit: (v) => {
900
+ promptQueue.resolveCurrent(v);
901
+ },
902
+ onCancel: cancel
903
+ }
904
+ ) });
905
+ case "editor":
906
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
907
+ EditorPrompt,
908
+ {
909
+ options: current.options,
910
+ onSubmit: (v) => {
911
+ promptQueue.resolveCurrent(v);
912
+ },
913
+ onCancel: () => {
914
+ promptQueue.resolveCurrent(null);
915
+ }
916
+ }
917
+ ) });
918
+ case "fileBrowser":
919
+ return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(
920
+ FileBrowserPrompt,
921
+ {
922
+ options: current.options,
923
+ onSubmit: (v) => {
924
+ promptQueue.resolveCurrent(v);
925
+ },
926
+ onCancel: () => {
927
+ promptQueue.resolveCurrent(null);
928
+ }
929
+ }
930
+ ) });
931
+ default: {
932
+ const _exhaustive = current;
933
+ void _exhaustive;
934
+ return null;
935
+ }
936
+ }
937
+ }
938
+
939
+ // src/integration/ui/prompts/auto-mount.tsx
940
+ import { jsx as jsx9 } from "react/jsx-runtime";
941
+ var hostState = "none";
942
+ var autoInstance = null;
943
+ var drainUnsubscribe = null;
944
+ function registerExternalHost() {
945
+ hostState = "external";
946
+ return () => {
947
+ hostState = "none";
948
+ };
949
+ }
950
+ function canInteract() {
951
+ if (process.env["RALPHCTL_NO_TUI"]) return false;
952
+ if (process.env["CI"]) return false;
953
+ if (process.env["RALPHCTL_JSON"]) return false;
954
+ if (!process.stdout.isTTY) return false;
955
+ if (!process.stdin.isTTY) return false;
956
+ return true;
957
+ }
958
+ function ensurePromptHost() {
959
+ if (hostState === "external" || hostState === "auto") return;
960
+ if (!canInteract()) {
961
+ throw new PromptCancelledError(
962
+ "Interactive prompt requested in non-interactive environment. Pass the value as a CLI flag."
963
+ );
964
+ }
965
+ hostState = "auto";
966
+ autoInstance = render(/* @__PURE__ */ jsx9(PromptHost, {}), { exitOnCtrlC: false });
967
+ drainUnsubscribe = promptQueue.subscribe((state) => {
968
+ if (state.current === null) {
969
+ setImmediate(() => {
970
+ if (promptQueue.size() === 0) teardownAutoHost();
971
+ });
972
+ }
973
+ });
974
+ }
975
+ function teardownAutoHost() {
976
+ if (hostState !== "auto") return;
977
+ drainUnsubscribe?.();
978
+ drainUnsubscribe = null;
979
+ autoInstance?.unmount();
980
+ autoInstance = null;
981
+ hostState = "none";
982
+ }
983
+
984
+ // src/integration/ui/prompts/prompt-adapter.ts
985
+ var InkPromptAdapter = class {
986
+ select(options) {
987
+ ensurePromptHost();
988
+ return new Promise((resolve2, reject) => {
989
+ promptQueue.enqueue({
990
+ kind: "select",
991
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
992
+ options,
993
+ resolve: resolve2,
994
+ reject
995
+ });
996
+ });
997
+ }
998
+ confirm(options) {
999
+ ensurePromptHost();
1000
+ return new Promise((resolve2, reject) => {
1001
+ promptQueue.enqueue({ kind: "confirm", options, resolve: resolve2, reject });
1002
+ });
1003
+ }
1004
+ input(options) {
1005
+ ensurePromptHost();
1006
+ return new Promise((resolve2, reject) => {
1007
+ promptQueue.enqueue({ kind: "input", options, resolve: resolve2, reject });
1008
+ });
1009
+ }
1010
+ checkbox(options) {
1011
+ ensurePromptHost();
1012
+ return new Promise((resolve2, reject) => {
1013
+ promptQueue.enqueue({
1014
+ kind: "checkbox",
1015
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
1016
+ options,
1017
+ resolve: resolve2,
1018
+ reject
1019
+ });
1020
+ });
1021
+ }
1022
+ editor(options) {
1023
+ ensurePromptHost();
1024
+ return new Promise((resolve2, reject) => {
1025
+ promptQueue.enqueue({ kind: "editor", options, resolve: resolve2, reject });
1026
+ });
1027
+ }
1028
+ fileBrowser(options) {
1029
+ ensurePromptHost();
1030
+ return new Promise((resolve2, reject) => {
1031
+ promptQueue.enqueue({ kind: "fileBrowser", options, resolve: resolve2, reject });
1032
+ });
1033
+ }
1034
+ };
1035
+
1036
+ export {
1037
+ PromptCancelledError,
1038
+ useCurrentPrompt,
1039
+ inkColors,
1040
+ glyphs,
1041
+ spacing,
1042
+ FIELD_LABEL_WIDTH,
1043
+ PromptHost,
1044
+ registerExternalHost,
1045
+ InkPromptAdapter
1046
+ };