@0xtiby/toby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3177 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import meow from "meow";
5
+ import { render, Text as Text12 } from "ink";
6
+
7
+ // src/commands/plan.tsx
8
+ import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2 } from "react";
9
+ import { Text as Text3, Box as Box3 } from "ink";
10
+
11
+ // src/lib/config.ts
12
+ import fs2 from "fs";
13
+ import path2 from "path";
14
+
15
+ // src/types.ts
16
+ import { z } from "zod";
17
+ var CLI_NAMES = ["claude", "codex", "opencode"];
18
+ var CommandConfigSchema = z.object({
19
+ cli: z.enum(CLI_NAMES).default("claude"),
20
+ model: z.string().default("default"),
21
+ iterations: z.number().int().positive()
22
+ });
23
+ var PlanConfigSchema = CommandConfigSchema.extend({
24
+ iterations: z.number().int().positive().default(2)
25
+ });
26
+ var BuildConfigSchema = CommandConfigSchema.extend({
27
+ iterations: z.number().int().positive().default(10)
28
+ });
29
+ var ConfigSchema = z.object({
30
+ plan: PlanConfigSchema.default({}),
31
+ build: BuildConfigSchema.default({}),
32
+ specsDir: z.string().default("specs"),
33
+ excludeSpecs: z.array(z.string()).default(["README.md"]),
34
+ verbose: z.boolean().default(false),
35
+ transcript: z.boolean().default(false),
36
+ templateVars: z.record(z.string(), z.string()).default({})
37
+ });
38
+ var IterationStateSchema = z.enum([
39
+ "in_progress",
40
+ "complete",
41
+ "failed"
42
+ ]);
43
+ var IterationSchema = z.object({
44
+ type: z.enum(["plan", "build"]),
45
+ iteration: z.number().int().positive(),
46
+ sessionId: z.string().nullable(),
47
+ state: IterationStateSchema.default("in_progress"),
48
+ cli: z.string(),
49
+ model: z.string(),
50
+ startedAt: z.string().datetime(),
51
+ completedAt: z.string().datetime().nullable(),
52
+ exitCode: z.number().int().nullable(),
53
+ taskCompleted: z.string().nullable(),
54
+ tokensUsed: z.number().int().nullable()
55
+ });
56
+ var StopReasonSchema = z.enum([
57
+ "sentinel",
58
+ "max_iterations",
59
+ "error",
60
+ "aborted"
61
+ ]);
62
+ var SpecStatusEntrySchema = z.object({
63
+ status: z.enum(["pending", "planned", "building", "done"]),
64
+ plannedAt: z.string().datetime().nullable(),
65
+ iterations: z.array(IterationSchema),
66
+ stopReason: StopReasonSchema.optional()
67
+ });
68
+ var StatusSchema = z.object({
69
+ specs: z.record(z.string(), SpecStatusEntrySchema),
70
+ sessionName: z.string().optional(),
71
+ lastCli: z.string().optional()
72
+ });
73
+
74
+ // src/lib/paths.ts
75
+ import path from "path";
76
+ import os from "os";
77
+ import fs from "fs";
78
+ var GLOBAL_TOBY_DIR = ".toby";
79
+ var LOCAL_TOBY_DIR = ".toby";
80
+ var DEFAULT_SPECS_DIR = "specs";
81
+ var STATUS_FILE = "status.json";
82
+ var CONFIG_FILE = "config.json";
83
+ function getGlobalDir() {
84
+ return path.join(os.homedir(), GLOBAL_TOBY_DIR);
85
+ }
86
+ function ensureGlobalDir() {
87
+ const dir = getGlobalDir();
88
+ const configPath = path.join(dir, CONFIG_FILE);
89
+ try {
90
+ fs.mkdirSync(dir, { recursive: true });
91
+ if (!fs.existsSync(configPath)) {
92
+ const defaults = ConfigSchema.parse({});
93
+ fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + "\n");
94
+ }
95
+ } catch (err) {
96
+ console.warn(
97
+ `Warning: could not initialize ${dir}: ${err.message}`
98
+ );
99
+ }
100
+ }
101
+ function getLocalDir(cwd) {
102
+ return path.join(cwd ?? process.cwd(), LOCAL_TOBY_DIR);
103
+ }
104
+ function ensureLocalDir(cwd) {
105
+ const dir = getLocalDir(cwd);
106
+ const statusPath = path.join(dir, STATUS_FILE);
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ if (!fs.existsSync(statusPath)) {
109
+ fs.writeFileSync(statusPath, JSON.stringify({ specs: {} }, null, 2) + "\n");
110
+ }
111
+ return dir;
112
+ }
113
+
114
+ // src/lib/config.ts
115
+ function readConfigFile(filePath) {
116
+ try {
117
+ const content = fs2.readFileSync(filePath, "utf-8");
118
+ return JSON.parse(content);
119
+ } catch {
120
+ if (fs2.existsSync(filePath)) {
121
+ console.warn(`Warning: corrupted config at ${filePath}, ignoring`);
122
+ }
123
+ return {};
124
+ }
125
+ }
126
+ function loadGlobalConfig() {
127
+ return readConfigFile(path2.join(getGlobalDir(), CONFIG_FILE));
128
+ }
129
+ function loadLocalConfig(cwd) {
130
+ return readConfigFile(path2.join(getLocalDir(cwd), CONFIG_FILE));
131
+ }
132
+ function mergeConfigs(global, local) {
133
+ const merged = { ...global };
134
+ for (const [key, value] of Object.entries(local)) {
135
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) {
136
+ merged[key] = { ...merged[key], ...value };
137
+ } else {
138
+ merged[key] = value;
139
+ }
140
+ }
141
+ return merged;
142
+ }
143
+ function loadConfig(cwd) {
144
+ const global = loadGlobalConfig();
145
+ const local = loadLocalConfig(cwd);
146
+ const merged = mergeConfigs(global, local);
147
+ return ConfigSchema.parse(merged);
148
+ }
149
+ function writeConfig(config, filePath) {
150
+ const dir = path2.dirname(filePath);
151
+ fs2.mkdirSync(dir, { recursive: true });
152
+ fs2.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
153
+ }
154
+ function validateCliName(cli2) {
155
+ if (cli2 && !CLI_NAMES.includes(cli2)) {
156
+ throw new Error(`Unknown CLI: ${cli2}. Must be one of: ${CLI_NAMES.join(", ")}`);
157
+ }
158
+ }
159
+ function resolveCommandConfig(config, command2, flags2 = {}) {
160
+ validateCliName(flags2.cli);
161
+ const base = config[command2];
162
+ return {
163
+ cli: flags2.cli ?? base.cli,
164
+ model: flags2.model || base.model || "default",
165
+ iterations: flags2.iterations ?? base.iterations
166
+ };
167
+ }
168
+
169
+ // src/lib/specs.ts
170
+ import fs3 from "fs";
171
+ import path3 from "path";
172
+ function parseSpecOrder(filename) {
173
+ const match = /^(\d+)([a-z])?-/.exec(filename);
174
+ if (!match) return null;
175
+ return { num: parseInt(match[1], 10), suffix: match[2] ?? null };
176
+ }
177
+ function sortSpecs(specs) {
178
+ return [...specs].sort((a, b) => {
179
+ if (a.order !== null && b.order !== null) {
180
+ if (a.order.num !== b.order.num) return a.order.num - b.order.num;
181
+ const sa = a.order.suffix ?? "";
182
+ const sb = b.order.suffix ?? "";
183
+ if (sa !== sb) return sa.localeCompare(sb);
184
+ return a.name.localeCompare(b.name);
185
+ }
186
+ if (a.order !== null) return -1;
187
+ if (b.order !== null) return 1;
188
+ return a.name.localeCompare(b.name);
189
+ });
190
+ }
191
+ function filterByStatus(specs, status) {
192
+ return specs.filter((s) => s.status === status);
193
+ }
194
+ function findSpec(specs, query) {
195
+ return specs.find((s) => {
196
+ if (s.name === query) return true;
197
+ if (`${s.name}.md` === query) return true;
198
+ const withoutPrefix = s.name.replace(/^\d+[a-z]?-/, "");
199
+ if (withoutPrefix === query) return true;
200
+ const prefixMatch = /^(\d+[a-z]?)-/.exec(s.name);
201
+ if (prefixMatch && prefixMatch[1] === query) return true;
202
+ return false;
203
+ });
204
+ }
205
+ function findSpecs(specs, query) {
206
+ const queries = query.split(",").map((q) => q.trim()).filter((q) => q.length > 0);
207
+ const seen = /* @__PURE__ */ new Set();
208
+ const results = [];
209
+ for (const q of queries) {
210
+ const found = findSpec(specs, q);
211
+ if (!found) {
212
+ throw new Error(`Spec not found: "${q}"`);
213
+ }
214
+ if (!seen.has(found.name)) {
215
+ seen.add(found.name);
216
+ results.push(found);
217
+ }
218
+ }
219
+ return sortSpecs(results);
220
+ }
221
+ function readStatusMap(cwd) {
222
+ const statusPath = path3.join(getLocalDir(cwd), STATUS_FILE);
223
+ try {
224
+ const raw = JSON.parse(fs3.readFileSync(statusPath, "utf-8"));
225
+ const parsed = StatusSchema.safeParse(raw);
226
+ if (!parsed.success) return {};
227
+ const result = {};
228
+ for (const [name, entry] of Object.entries(parsed.data.specs)) {
229
+ result[name] = entry.status;
230
+ }
231
+ return result;
232
+ } catch {
233
+ return {};
234
+ }
235
+ }
236
+ function discoverSpecs(cwd, config) {
237
+ const specsDir = path3.resolve(cwd, config.specsDir);
238
+ let entries;
239
+ try {
240
+ entries = fs3.readdirSync(specsDir);
241
+ } catch {
242
+ return [];
243
+ }
244
+ const mdFiles = entries.filter((f) => {
245
+ if (!f.endsWith(".md")) return false;
246
+ return !config.excludeSpecs.includes(f);
247
+ });
248
+ if (mdFiles.length === 0) return [];
249
+ const statusMap = readStatusMap(cwd);
250
+ const specs = mdFiles.map((filename) => {
251
+ const name = filename.replace(/\.md$/, "");
252
+ return {
253
+ name,
254
+ path: path3.join(specsDir, filename),
255
+ order: parseSpecOrder(filename),
256
+ status: statusMap[name] ?? "pending"
257
+ };
258
+ });
259
+ return sortSpecs(specs);
260
+ }
261
+
262
+ // src/lib/template.ts
263
+ import path4 from "path";
264
+ import fs4 from "fs";
265
+ import { fileURLToPath } from "url";
266
+ function getShippedPromptPath(name) {
267
+ const thisFile = fileURLToPath(import.meta.url);
268
+ let dir = path4.dirname(thisFile);
269
+ while (dir !== path4.dirname(dir)) {
270
+ const candidate = path4.join(dir, "prompts");
271
+ if (fs4.existsSync(candidate) && fs4.statSync(candidate).isDirectory()) {
272
+ return path4.join(candidate, `${name}.md`);
273
+ }
274
+ dir = path4.dirname(dir);
275
+ }
276
+ const fallback = path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts");
277
+ return path4.join(fallback, `${name}.md`);
278
+ }
279
+ function resolvePromptPath(name, cwd) {
280
+ const filename = `${name}.md`;
281
+ const candidates = [
282
+ path4.join(getLocalDir(cwd), filename),
283
+ path4.join(getGlobalDir(), filename),
284
+ getShippedPromptPath(name)
285
+ ];
286
+ for (const candidate of candidates) {
287
+ if (fs4.existsSync(candidate)) {
288
+ return candidate;
289
+ }
290
+ }
291
+ throw new Error(
292
+ `Prompt "${name}" not found. Checked:
293
+ ${candidates.map((p) => ` - ${p}`).join("\n")}`
294
+ );
295
+ }
296
+ function loadPrompt(name, vars, options = {}) {
297
+ const { cwd } = options;
298
+ const promptPath = resolvePromptPath(name, cwd);
299
+ const content = fs4.readFileSync(promptPath, "utf-8");
300
+ return substitute(content, vars);
301
+ }
302
+ function computeSpecSlug(specName) {
303
+ return specName.replace(/^\d+[a-z]?-/, "");
304
+ }
305
+ function computeCliVars(options) {
306
+ return {
307
+ SPEC_NAME: options.specName,
308
+ SPEC_SLUG: computeSpecSlug(options.specName),
309
+ ITERATION: String(options.iteration),
310
+ SPEC_INDEX: String(options.specIndex),
311
+ SPEC_COUNT: String(options.specCount),
312
+ SESSION: options.session,
313
+ SPECS: options.specs.join(", "),
314
+ SPECS_DIR: options.specsDir
315
+ };
316
+ }
317
+ function resolveConfigVars(configVars, cliVars, verbose = false) {
318
+ const resolved = {};
319
+ for (const [key, value] of Object.entries(configVars)) {
320
+ if (verbose && key in cliVars) {
321
+ console.warn(`Config var "${key}" is shadowed by CLI var`);
322
+ }
323
+ resolved[key] = substitute(value, cliVars);
324
+ }
325
+ return resolved;
326
+ }
327
+ function resolveTemplateVars(cliVars, configVars, verbose = false) {
328
+ const resolved = resolveConfigVars(configVars, cliVars, verbose);
329
+ return { ...resolved, ...cliVars };
330
+ }
331
+ var ADJECTIVES = [
332
+ "bold",
333
+ "calm",
334
+ "cool",
335
+ "dark",
336
+ "fast",
337
+ "free",
338
+ "glad",
339
+ "keen",
340
+ "kind",
341
+ "neat",
342
+ "pure",
343
+ "rare",
344
+ "safe",
345
+ "soft",
346
+ "tall",
347
+ "warm",
348
+ "wild",
349
+ "wise",
350
+ "blue",
351
+ "gold"
352
+ ];
353
+ var NOUNS = [
354
+ "bear",
355
+ "crow",
356
+ "deer",
357
+ "dove",
358
+ "fawn",
359
+ "hawk",
360
+ "lynx",
361
+ "mare",
362
+ "orca",
363
+ "puma",
364
+ "seal",
365
+ "swan",
366
+ "toad",
367
+ "vole",
368
+ "wolf",
369
+ "wren",
370
+ "colt",
371
+ "hare",
372
+ "moth",
373
+ "ibis"
374
+ ];
375
+ function generateSessionName() {
376
+ const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
377
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
378
+ const num = Math.floor(Math.random() * 90) + 10;
379
+ return `${adj}-${noun}-${num}`;
380
+ }
381
+ function substitute(template, vars) {
382
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
383
+ const value = vars[key];
384
+ return value !== void 0 ? value : match;
385
+ });
386
+ }
387
+
388
+ // src/lib/loop.ts
389
+ import { spawn } from "@0xtiby/spawner";
390
+ var SENTINEL = ":::TOBY_DONE:::";
391
+ function containsSentinel(text) {
392
+ return text.includes(SENTINEL);
393
+ }
394
+ async function runLoop(options) {
395
+ const {
396
+ maxIterations,
397
+ getPrompt,
398
+ cli: cli2,
399
+ model,
400
+ cwd,
401
+ autoApprove = true,
402
+ continueSession = false,
403
+ onEvent,
404
+ onIterationStart,
405
+ onIterationComplete,
406
+ abortSignal
407
+ } = options;
408
+ const results = [];
409
+ if (abortSignal?.aborted) {
410
+ return { iterations: results, stopReason: "aborted" };
411
+ }
412
+ if (maxIterations <= 0) {
413
+ return { iterations: results, stopReason: "max_iterations" };
414
+ }
415
+ let lastSessionId = options.sessionId ?? void 0;
416
+ let iteration = 1;
417
+ while (iteration <= maxIterations) {
418
+ const prompt = getPrompt(iteration);
419
+ const effectiveSessionId = continueSession ? lastSessionId : options.sessionId;
420
+ const spawnOpts = {
421
+ cli: cli2,
422
+ prompt,
423
+ cwd,
424
+ autoApprove,
425
+ ...model && model !== "default" ? { model } : {},
426
+ ...effectiveSessionId ? { sessionId: effectiveSessionId, continueSession: true } : {}
427
+ };
428
+ onIterationStart?.(iteration, effectiveSessionId ?? null);
429
+ const proc = spawn(spawnOpts);
430
+ let aborted = false;
431
+ const onAbort = () => {
432
+ aborted = true;
433
+ proc.interrupt();
434
+ };
435
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
436
+ let sentinelDetected = false;
437
+ for await (const event of proc.events) {
438
+ onEvent?.(event);
439
+ if (event.type === "text" && event.content && containsSentinel(event.content)) {
440
+ sentinelDetected = true;
441
+ }
442
+ }
443
+ const cliResult = await proc.done;
444
+ abortSignal?.removeEventListener("abort", onAbort);
445
+ const iterResult = {
446
+ iteration,
447
+ sessionId: cliResult.sessionId,
448
+ exitCode: cliResult.exitCode,
449
+ tokensUsed: cliResult.usage?.totalTokens ?? null,
450
+ model: cliResult.model,
451
+ durationMs: cliResult.durationMs,
452
+ sentinelDetected
453
+ };
454
+ results.push(iterResult);
455
+ onIterationComplete?.(iterResult);
456
+ if (aborted) {
457
+ return { iterations: results, stopReason: "aborted" };
458
+ }
459
+ if (sentinelDetected) {
460
+ return { iterations: results, stopReason: "sentinel" };
461
+ }
462
+ if (cliResult.exitCode !== 0) {
463
+ if (cliResult.error?.retryable) {
464
+ const delay = cliResult.error.retryAfterMs ?? 6e4;
465
+ await new Promise((r) => setTimeout(r, delay));
466
+ continue;
467
+ }
468
+ return { iterations: results, stopReason: "error" };
469
+ }
470
+ if (continueSession) {
471
+ lastSessionId = cliResult.sessionId ?? lastSessionId;
472
+ }
473
+ iteration++;
474
+ }
475
+ return { iterations: results, stopReason: "max_iterations" };
476
+ }
477
+
478
+ // src/lib/status.ts
479
+ import fs5 from "fs";
480
+ import path5 from "path";
481
+ function readStatus(cwd) {
482
+ const filePath = path5.join(getLocalDir(cwd), STATUS_FILE);
483
+ if (!fs5.existsSync(filePath)) {
484
+ return { specs: {} };
485
+ }
486
+ let raw;
487
+ try {
488
+ raw = fs5.readFileSync(filePath, "utf-8");
489
+ } catch (err) {
490
+ throw new Error(
491
+ `Failed to read status file at ${filePath}: ${err.message}`
492
+ );
493
+ }
494
+ let parsed;
495
+ try {
496
+ parsed = JSON.parse(raw);
497
+ } catch (err) {
498
+ throw new Error(
499
+ `Invalid JSON in status file at ${filePath}: ${err.message}`
500
+ );
501
+ }
502
+ const result = StatusSchema.safeParse(parsed);
503
+ if (!result.success) {
504
+ throw new Error(
505
+ `Invalid status data at ${filePath}: ${result.error.message}`
506
+ );
507
+ }
508
+ return result.data;
509
+ }
510
+ function writeStatus(status, cwd) {
511
+ const dir = getLocalDir(cwd);
512
+ fs5.mkdirSync(dir, { recursive: true });
513
+ const filePath = path5.join(dir, STATUS_FILE);
514
+ const validated = StatusSchema.parse(status);
515
+ fs5.writeFileSync(filePath, JSON.stringify(validated, null, 2) + "\n");
516
+ }
517
+ function getSpecStatus(status, specName) {
518
+ const existing = status.specs[specName];
519
+ if (existing) {
520
+ return existing;
521
+ }
522
+ return SpecStatusEntrySchema.parse({
523
+ status: "pending",
524
+ plannedAt: null,
525
+ iterations: []
526
+ });
527
+ }
528
+ function addIteration(status, specName, iteration) {
529
+ const entry = getSpecStatus(status, specName);
530
+ return {
531
+ ...status,
532
+ specs: {
533
+ ...status.specs,
534
+ [specName]: {
535
+ ...entry,
536
+ iterations: [...entry.iterations, iteration]
537
+ }
538
+ }
539
+ };
540
+ }
541
+ function updateSpecStatus(status, specName, newStatus) {
542
+ const entry = getSpecStatus(status, specName);
543
+ return {
544
+ ...status,
545
+ specs: {
546
+ ...status.specs,
547
+ [specName]: {
548
+ ...entry,
549
+ status: newStatus
550
+ }
551
+ }
552
+ };
553
+ }
554
+
555
+ // src/lib/errors.ts
556
+ var AbortError = class extends Error {
557
+ specName;
558
+ completedIterations;
559
+ constructor(specName, completedIterations) {
560
+ super(`Interrupted for ${specName} after ${completedIterations} iteration(s)`);
561
+ this.name = "AbortError";
562
+ this.specName = specName;
563
+ this.completedIterations = completedIterations;
564
+ }
565
+ };
566
+
567
+ // src/lib/transcript.ts
568
+ import fs6 from "fs";
569
+ import path6 from "path";
570
+ var TRANSCRIPTS_DIR = "transcripts";
571
+ function formatTimestamp() {
572
+ const now = /* @__PURE__ */ new Date();
573
+ const pad2 = (n, len = 2) => String(n).padStart(len, "0");
574
+ return [
575
+ now.getFullYear(),
576
+ pad2(now.getMonth() + 1),
577
+ pad2(now.getDate()),
578
+ "-",
579
+ pad2(now.getHours()),
580
+ pad2(now.getMinutes()),
581
+ pad2(now.getSeconds()),
582
+ "-",
583
+ pad2(now.getMilliseconds(), 3)
584
+ ].join("");
585
+ }
586
+ function formatEventPlaintext(event, verbose) {
587
+ if (event.type === "done") return null;
588
+ if (!verbose) {
589
+ if (event.type !== "text") return null;
590
+ return event.content ?? "";
591
+ }
592
+ switch (event.type) {
593
+ case "text":
594
+ return `[text] ${event.content ?? ""}`;
595
+ case "tool_use":
596
+ return `[tool_use] ${event.tool?.name ?? "tool"} ${JSON.stringify(event.tool?.input ?? {})}`;
597
+ case "tool_result":
598
+ return `[tool_result] ${(event.content ?? "").slice(0, 200)}`;
599
+ case "error":
600
+ return `[error] ${event.content ?? ""}`;
601
+ case "system":
602
+ return `[system] ${event.content ?? ""}`;
603
+ default:
604
+ return `[${event.type}] ${event.content ?? ""}`;
605
+ }
606
+ }
607
+ var TranscriptWriter = class {
608
+ filePath;
609
+ stream;
610
+ verbose;
611
+ constructor(filePath, stream, verbose) {
612
+ this.filePath = filePath;
613
+ this.stream = stream;
614
+ this.verbose = verbose;
615
+ }
616
+ writeEvent(event) {
617
+ const line = formatEventPlaintext(event, this.verbose);
618
+ if (line === null) return;
619
+ this.stream.write(line + "\n");
620
+ }
621
+ writeIterationHeader(meta) {
622
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
623
+ const header = `
624
+ ## Iteration ${meta.iteration}/${meta.total}
625
+
626
+ cli: ${meta.cli} | model: ${meta.model} | started: ${ts}
627
+
628
+ `;
629
+ this.stream.write(header);
630
+ }
631
+ writeSpecHeader(specIndex, totalSpecs, specName) {
632
+ const header = `
633
+ ## Spec ${specIndex}/${totalSpecs}: ${specName}
634
+
635
+ `;
636
+ this.stream.write(header);
637
+ }
638
+ close() {
639
+ try {
640
+ this.stream.end();
641
+ } catch (err) {
642
+ console.error(`Warning: transcript close error: ${err.message}`);
643
+ }
644
+ }
645
+ };
646
+ function openTranscript(options) {
647
+ const { command: command2, specName, session, verbose } = options;
648
+ const prefix = session ?? specName ?? "all";
649
+ const timestamp = formatTimestamp();
650
+ const filename = `${prefix}-${command2}-${timestamp}.md`;
651
+ const transcriptsDir = path6.join(getLocalDir(), TRANSCRIPTS_DIR);
652
+ fs6.mkdirSync(transcriptsDir, { recursive: true });
653
+ const filePath = path6.join(transcriptsDir, filename);
654
+ const header = [
655
+ "---",
656
+ `command: ${command2}`,
657
+ `session: ${session ?? ""}`,
658
+ `spec: ${specName ?? ""}`,
659
+ `verbose: ${verbose}`,
660
+ `created: ${(/* @__PURE__ */ new Date()).toISOString()}`,
661
+ "---",
662
+ "",
663
+ `# Transcript: ${prefix} ${command2}`,
664
+ ""
665
+ ].join("\n");
666
+ try {
667
+ fs6.writeFileSync(filePath, header);
668
+ } catch (err) {
669
+ console.error(`Warning: could not create transcript file: ${err.message}`);
670
+ }
671
+ const stream = fs6.createWriteStream(filePath, { flags: "a" });
672
+ stream.on("error", (err) => {
673
+ console.error(`Warning: transcript write error: ${err.message}`);
674
+ });
675
+ return new TranscriptWriter(filePath, stream, verbose);
676
+ }
677
+ async function withTranscript(options, externalWriter, fn) {
678
+ const owns = externalWriter === void 0;
679
+ const writer = externalWriter !== void 0 ? externalWriter : options.flags.transcript ?? options.config.transcript ? openTranscript({
680
+ command: options.command,
681
+ specName: options.specName,
682
+ session: options.flags.session,
683
+ verbose: options.flags.verbose || options.config.verbose
684
+ }) : null;
685
+ try {
686
+ return await fn(writer);
687
+ } finally {
688
+ if (owns) writer?.close();
689
+ }
690
+ }
691
+
692
+ // src/hooks/useCommandRunner.ts
693
+ import { useState, useEffect, useRef, useMemo } from "react";
694
+ import { useApp } from "ink";
695
+ var MAX_EVENTS = 100;
696
+ function useCommandRunner(options) {
697
+ const { flags: flags2, runPhase, filterSpecs, emptyMessage } = options;
698
+ const { exit } = useApp();
699
+ const [phase, setPhase] = useState(() => {
700
+ if (flags2.all) return "all";
701
+ if (flags2.spec && flags2.spec.includes(",")) return "multi";
702
+ if (flags2.spec) return "init";
703
+ return "selecting";
704
+ });
705
+ const [selectedSpecs, setSelectedSpecs] = useState([]);
706
+ const [currentIteration, setCurrentIteration] = useState(0);
707
+ const [maxIterations, setMaxIterations] = useState(0);
708
+ const [specName, setSpecName] = useState("");
709
+ const [events, setEvents] = useState([]);
710
+ const [errorMessage, setErrorMessage] = useState("");
711
+ const [specs, setSpecs] = useState([]);
712
+ const [activeFlags, setActiveFlags] = useState(flags2);
713
+ const [allProgress, setAllProgress] = useState({ current: 0, total: 0 });
714
+ const [interruptInfo, setInterruptInfo] = useState(null);
715
+ const abortControllerRef = useRef(new AbortController());
716
+ useEffect(() => {
717
+ const handler = () => {
718
+ abortControllerRef.current.abort();
719
+ };
720
+ process.on("SIGINT", handler);
721
+ return () => {
722
+ process.off("SIGINT", handler);
723
+ };
724
+ }, []);
725
+ const resolvedVerbose = useMemo(() => {
726
+ if (flags2.verbose) return true;
727
+ try {
728
+ return loadConfig().verbose;
729
+ } catch {
730
+ return false;
731
+ }
732
+ }, [flags2.verbose]);
733
+ useEffect(() => {
734
+ if (phase !== "selecting") return;
735
+ try {
736
+ const config = loadConfig();
737
+ const discovered = discoverSpecs(process.cwd(), config);
738
+ const filtered = filterSpecs ? filterSpecs(discovered) : discovered;
739
+ if (filtered.length === 0) {
740
+ setErrorMessage(emptyMessage ?? "No specs found.");
741
+ setPhase("error");
742
+ exit();
743
+ return;
744
+ }
745
+ setSpecs(filtered);
746
+ } catch (err) {
747
+ setErrorMessage(err.message);
748
+ setPhase("error");
749
+ exit(new Error(err.message));
750
+ }
751
+ }, [phase]);
752
+ useEffect(() => {
753
+ if (phase !== "multi" || selectedSpecs.length > 0) return;
754
+ if (!flags2.spec) return;
755
+ try {
756
+ const config = loadConfig();
757
+ const discovered = discoverSpecs(process.cwd(), config);
758
+ const resolved = findSpecs(discovered, flags2.spec);
759
+ setSelectedSpecs(resolved);
760
+ } catch (err) {
761
+ setErrorMessage(err.message);
762
+ setPhase("error");
763
+ exit(new Error(err.message));
764
+ }
765
+ }, [phase, selectedSpecs.length]);
766
+ function addEvent(event) {
767
+ setEvents(
768
+ (prev) => prev.length >= MAX_EVENTS ? [...prev.slice(-MAX_EVENTS + 1), event] : [...prev, event]
769
+ );
770
+ }
771
+ function handleSpecSelect(spec) {
772
+ setActiveFlags({ ...flags2, spec: spec.name });
773
+ setPhase("init");
774
+ }
775
+ function handleMultiSpecConfirm(specs2) {
776
+ if (specs2.length === 1) {
777
+ setActiveFlags({ ...flags2, spec: specs2[0].name });
778
+ setPhase("init");
779
+ } else {
780
+ setSelectedSpecs(specs2);
781
+ setPhase("multi");
782
+ }
783
+ }
784
+ function handleError(err) {
785
+ if (err instanceof AbortError) {
786
+ setInterruptInfo({ specName: err.specName, iterations: err.completedIterations });
787
+ setPhase("interrupted");
788
+ exit();
789
+ return;
790
+ }
791
+ setErrorMessage(err.message);
792
+ setPhase("error");
793
+ exit(new Error(err.message));
794
+ }
795
+ function handleDone() {
796
+ setPhase("done");
797
+ exit();
798
+ }
799
+ function onPhaseCallback(p) {
800
+ if (p === runPhase) setPhase("running");
801
+ }
802
+ function onIterationCallback(current, max) {
803
+ setCurrentIteration(current);
804
+ setMaxIterations(max);
805
+ }
806
+ function onSpecStartCallback(name, index, total) {
807
+ setSpecName(name);
808
+ setAllProgress({ current: index + 1, total });
809
+ }
810
+ return {
811
+ phase,
812
+ currentIteration,
813
+ maxIterations,
814
+ specName,
815
+ setSpecName,
816
+ events,
817
+ addEvent,
818
+ errorMessage,
819
+ specs,
820
+ activeFlags,
821
+ allProgress,
822
+ selectedSpecs,
823
+ interruptInfo,
824
+ abortSignal: abortControllerRef.current.signal,
825
+ resolvedVerbose,
826
+ handleSpecSelect,
827
+ handleMultiSpecConfirm,
828
+ handleError,
829
+ handleDone,
830
+ onPhaseCallback,
831
+ onIterationCallback,
832
+ onSpecStartCallback
833
+ };
834
+ }
835
+
836
+ // src/components/MultiSpecSelector.tsx
837
+ import { useState as useState2 } from "react";
838
+ import { Box, Text, useInput } from "ink";
839
+ import { jsx, jsxs } from "react/jsx-runtime";
840
+ function MultiSpecSelector({
841
+ specs,
842
+ onConfirm,
843
+ title = "Select specs to plan:"
844
+ }) {
845
+ const [cursor, setCursor] = useState2(0);
846
+ const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
847
+ const [warning, setWarning] = useState2("");
848
+ const allSelected = specs.length > 0 && specs.every((s) => selected.has(s.name));
849
+ useInput((input, key) => {
850
+ if (specs.length === 0) return;
851
+ if (key.upArrow) {
852
+ setCursor((c) => c <= 0 ? specs.length : c - 1);
853
+ setWarning("");
854
+ } else if (key.downArrow) {
855
+ setCursor((c) => c >= specs.length ? 0 : c + 1);
856
+ setWarning("");
857
+ } else if (input === " ") {
858
+ setWarning("");
859
+ if (cursor === 0) {
860
+ if (allSelected) {
861
+ setSelected(/* @__PURE__ */ new Set());
862
+ } else {
863
+ setSelected(new Set(specs.map((s) => s.name)));
864
+ }
865
+ } else {
866
+ const spec = specs[cursor - 1];
867
+ setSelected((prev) => {
868
+ const next = new Set(prev);
869
+ if (next.has(spec.name)) {
870
+ next.delete(spec.name);
871
+ } else {
872
+ next.add(spec.name);
873
+ }
874
+ return next;
875
+ });
876
+ }
877
+ } else if (key.return) {
878
+ const selectedSpecs = specs.filter((s) => selected.has(s.name));
879
+ if (selectedSpecs.length === 0) {
880
+ setWarning("Please select at least one spec");
881
+ return;
882
+ }
883
+ onConfirm(selectedSpecs);
884
+ }
885
+ });
886
+ if (specs.length === 0) {
887
+ return /* @__PURE__ */ jsx(Text, { color: "red", children: "No specs found." });
888
+ }
889
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
890
+ /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
891
+ /* @__PURE__ */ jsxs(Text, { children: [
892
+ cursor === 0 ? "\u276F " : " ",
893
+ allSelected ? "\u25C9" : "\u25CB",
894
+ " Select All"
895
+ ] }),
896
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
897
+ specs.map((spec, i) => {
898
+ const isHighlighted = cursor === i + 1;
899
+ const isSelected = selected.has(spec.name);
900
+ return /* @__PURE__ */ jsxs(Text, { children: [
901
+ isHighlighted ? "\u276F " : " ",
902
+ isSelected ? "\u25C9" : "\u25CB",
903
+ " ",
904
+ spec.name,
905
+ " [",
906
+ spec.status,
907
+ "]"
908
+ ] }, spec.name);
909
+ }),
910
+ warning && /* @__PURE__ */ jsx(Text, { color: "yellow", children: warning })
911
+ ] });
912
+ }
913
+
914
+ // src/components/StreamOutput.tsx
915
+ import { Text as Text2, Box as Box2 } from "ink";
916
+ import { jsx as jsx2 } from "react/jsx-runtime";
917
+ function filterEvents(events, verbose) {
918
+ if (verbose) return events;
919
+ return events.filter((e) => e.type === "text");
920
+ }
921
+ function formatEvent(event) {
922
+ switch (event.type) {
923
+ case "text":
924
+ return event.content ?? "";
925
+ case "tool_use":
926
+ return `\u2699 ${event.tool?.name ?? "tool"}`;
927
+ case "tool_result":
928
+ return ` \u21B3 ${(event.content ?? "").slice(0, 120)}`;
929
+ case "error":
930
+ return `\u2717 ${event.content ?? "error"}`;
931
+ case "system":
932
+ return `[system] ${event.content ?? ""}`;
933
+ default:
934
+ return event.content ?? "";
935
+ }
936
+ }
937
+ function colorForType(type) {
938
+ switch (type) {
939
+ case "tool_use":
940
+ return "cyan";
941
+ case "tool_result":
942
+ return "gray";
943
+ case "error":
944
+ return "red";
945
+ case "system":
946
+ return "yellow";
947
+ default:
948
+ return void 0;
949
+ }
950
+ }
951
+ function StreamOutput({ events, verbose = false, maxLines = 20 }) {
952
+ const filtered = filterEvents(events, verbose);
953
+ const visible = filtered.slice(-maxLines);
954
+ if (visible.length === 0) return null;
955
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: visible.map((event, i) => /* @__PURE__ */ jsx2(Text2, { color: colorForType(event.type), children: formatEvent(event) }, i)) });
956
+ }
957
+
958
+ // src/commands/plan.tsx
959
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
960
+ async function executePlan(flags2, callbacks = {}, cwd = process.cwd(), abortSignal, externalWriter) {
961
+ ensureLocalDir(cwd);
962
+ const config = loadConfig(cwd);
963
+ const commandConfig = resolveCommandConfig(config, "plan", {
964
+ cli: flags2.cli,
965
+ iterations: flags2.iterations
966
+ });
967
+ if (!flags2.spec) {
968
+ throw new Error("No --spec flag provided. Usage: toby plan --spec=<name>");
969
+ }
970
+ const specs = discoverSpecs(cwd, config);
971
+ if (specs.length === 0) {
972
+ throw new Error("No specs found in specs/");
973
+ }
974
+ const found = findSpec(specs, flags2.spec);
975
+ if (!found) {
976
+ throw new Error(`Spec '${flags2.spec}' not found`);
977
+ }
978
+ let status = readStatus(cwd);
979
+ const specEntry = status.specs[found.name];
980
+ const existingIterations = specEntry?.iterations.length ?? 0;
981
+ const isRefinement = specEntry?.status === "planned";
982
+ if (isRefinement) {
983
+ callbacks.onRefinement?.(found.name);
984
+ }
985
+ const session = flags2.session || computeSpecSlug(found.name);
986
+ return withTranscript(
987
+ { flags: flags2, config, command: "plan", specName: found.name },
988
+ externalWriter,
989
+ async (writer) => {
990
+ let iterationStartTime = (/* @__PURE__ */ new Date()).toISOString();
991
+ callbacks.onPhase?.("planning");
992
+ callbacks.onIteration?.(1, commandConfig.iterations);
993
+ const loopResult = await runLoop({
994
+ maxIterations: commandConfig.iterations,
995
+ getPrompt: (iteration) => {
996
+ const cliVars = computeCliVars({
997
+ specName: found.name,
998
+ iteration: iteration + existingIterations,
999
+ specIndex: 1,
1000
+ specCount: 1,
1001
+ session,
1002
+ specs: [found.name],
1003
+ specsDir: config.specsDir
1004
+ });
1005
+ const vars = resolveTemplateVars(cliVars, config.templateVars);
1006
+ return loadPrompt("PROMPT_PLAN", vars, { cwd });
1007
+ },
1008
+ cli: commandConfig.cli,
1009
+ model: commandConfig.model,
1010
+ cwd,
1011
+ continueSession: true,
1012
+ abortSignal,
1013
+ onEvent: (event) => {
1014
+ writer?.writeEvent(event);
1015
+ callbacks.onEvent?.(event);
1016
+ },
1017
+ onIterationComplete: (iterResult) => {
1018
+ writer?.writeIterationHeader({
1019
+ iteration: iterResult.iteration,
1020
+ total: commandConfig.iterations,
1021
+ cli: commandConfig.cli,
1022
+ model: iterResult.model ?? commandConfig.model
1023
+ });
1024
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1025
+ const iteration = {
1026
+ type: "plan",
1027
+ iteration: iterResult.iteration,
1028
+ sessionId: iterResult.sessionId,
1029
+ cli: commandConfig.cli,
1030
+ model: iterResult.model ?? commandConfig.model,
1031
+ startedAt: iterationStartTime,
1032
+ completedAt,
1033
+ exitCode: iterResult.exitCode,
1034
+ taskCompleted: null,
1035
+ tokensUsed: iterResult.tokensUsed
1036
+ };
1037
+ iterationStartTime = (/* @__PURE__ */ new Date()).toISOString();
1038
+ status = addIteration(status, found.name, iteration);
1039
+ writeStatus(status, cwd);
1040
+ callbacks.onIteration?.(iterResult.iteration + 1, commandConfig.iterations);
1041
+ }
1042
+ });
1043
+ if (loopResult.stopReason === "aborted") {
1044
+ status = updateSpecStatus(status, found.name, "planned");
1045
+ writeStatus(status, cwd);
1046
+ throw new AbortError(found.name, loopResult.iterations.length);
1047
+ }
1048
+ status = updateSpecStatus(status, found.name, "planned");
1049
+ writeStatus(status, cwd);
1050
+ return { specName: found.name };
1051
+ }
1052
+ );
1053
+ }
1054
+ async function executePlanAll(flags2, callbacks = {}, cwd = process.cwd(), abortSignal, specs) {
1055
+ ensureLocalDir(cwd);
1056
+ const config = loadConfig(cwd);
1057
+ let pending;
1058
+ if (specs) {
1059
+ pending = specs;
1060
+ } else {
1061
+ const discovered = discoverSpecs(cwd, config);
1062
+ if (discovered.length === 0) {
1063
+ throw new Error("No specs found in specs/");
1064
+ }
1065
+ pending = filterByStatus(discovered, "pending");
1066
+ }
1067
+ const planned = [];
1068
+ const session = flags2.session || generateSessionName();
1069
+ return withTranscript(
1070
+ { flags: flags2, config, command: "plan" },
1071
+ void 0,
1072
+ async (writer) => {
1073
+ for (let i = 0; i < pending.length; i++) {
1074
+ const spec = pending[i];
1075
+ writer?.writeSpecHeader(i + 1, pending.length, spec.name);
1076
+ callbacks.onSpecStart?.(spec.name, i, pending.length);
1077
+ const result = await executePlan(
1078
+ { ...flags2, spec: spec.name, all: false, session },
1079
+ {
1080
+ onPhase: callbacks.onPhase,
1081
+ onIteration: callbacks.onIteration,
1082
+ onEvent: callbacks.onEvent,
1083
+ onRefinement: callbacks.onRefinement
1084
+ },
1085
+ cwd,
1086
+ abortSignal,
1087
+ writer
1088
+ );
1089
+ planned.push(result);
1090
+ callbacks.onSpecComplete?.(result);
1091
+ }
1092
+ return { planned };
1093
+ }
1094
+ );
1095
+ }
1096
+ function Plan(flags2) {
1097
+ const runner = useCommandRunner({
1098
+ flags: flags2,
1099
+ runPhase: "planning",
1100
+ filterSpecs: (specs) => filterByStatus(specs, "pending"),
1101
+ emptyMessage: "No pending specs to plan. All specs have been planned."
1102
+ });
1103
+ const [result, setResult] = useState3(null);
1104
+ const [allResult, setAllResult] = useState3(null);
1105
+ const [refinementInfo, setRefinementInfo] = useState3(null);
1106
+ const allCallbacks = useMemo2(() => ({
1107
+ onSpecStart: runner.onSpecStartCallback,
1108
+ onSpecComplete: () => {
1109
+ },
1110
+ onPhase: runner.onPhaseCallback,
1111
+ onRefinement: (name) => {
1112
+ setRefinementInfo({ specName: name });
1113
+ },
1114
+ onIteration: runner.onIterationCallback,
1115
+ onEvent: runner.addEvent
1116
+ }), [runner.onSpecStartCallback, runner.onPhaseCallback, runner.onIterationCallback, runner.addEvent]);
1117
+ useEffect2(() => {
1118
+ if (runner.phase !== "multi" || runner.selectedSpecs.length === 0) return;
1119
+ executePlanAll(flags2, allCallbacks, void 0, runner.abortSignal, runner.selectedSpecs).then((r) => {
1120
+ setAllResult(r);
1121
+ runner.handleDone();
1122
+ }).catch(runner.handleError);
1123
+ }, [runner.phase, runner.selectedSpecs]);
1124
+ useEffect2(() => {
1125
+ if (runner.phase !== "all") return;
1126
+ executePlanAll(flags2, allCallbacks, void 0, runner.abortSignal).then((r) => {
1127
+ setAllResult(r);
1128
+ runner.handleDone();
1129
+ }).catch(runner.handleError);
1130
+ }, [runner.phase]);
1131
+ useEffect2(() => {
1132
+ if (runner.phase !== "init") return;
1133
+ executePlan(runner.activeFlags, {
1134
+ onPhase: runner.onPhaseCallback,
1135
+ onRefinement: (name) => {
1136
+ setRefinementInfo({ specName: name });
1137
+ },
1138
+ onIteration: runner.onIterationCallback,
1139
+ onEvent: runner.addEvent
1140
+ }, void 0, runner.abortSignal).then((r) => {
1141
+ runner.setSpecName(r.specName);
1142
+ setResult(r);
1143
+ runner.handleDone();
1144
+ }).catch(runner.handleError);
1145
+ }, [runner.activeFlags, runner.phase]);
1146
+ if (runner.phase === "interrupted" && runner.interruptInfo) {
1147
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
1148
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: `\u26A0 Planning interrupted for ${runner.interruptInfo.specName}` }),
1149
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${runner.interruptInfo.iterations} iteration(s) completed, partial status saved` })
1150
+ ] });
1151
+ }
1152
+ if (runner.phase === "error") {
1153
+ return /* @__PURE__ */ jsx3(Text3, { color: "red", children: runner.errorMessage });
1154
+ }
1155
+ if (runner.phase === "selecting") {
1156
+ return /* @__PURE__ */ jsx3(MultiSpecSelector, { specs: runner.specs, onConfirm: runner.handleMultiSpecConfirm });
1157
+ }
1158
+ if (runner.phase === "done" && allResult) {
1159
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
1160
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: `\u2713 All specs planned (${allResult.planned.length} planned)` }),
1161
+ allResult.planned.map((r) => /* @__PURE__ */ jsx3(Text3, { children: ` ${r.specName}` }, r.specName))
1162
+ ] });
1163
+ }
1164
+ if (runner.phase === "done" && result) {
1165
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { color: "green", children: `\u2713 Plan complete for ${result.specName}` }) });
1166
+ }
1167
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
1168
+ refinementInfo && /* @__PURE__ */ jsxs2(Fragment, { children: [
1169
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: `Existing plan found for ${refinementInfo.specName}` }),
1170
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "Running in refinement mode..." })
1171
+ ] }),
1172
+ runner.allProgress.total > 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `[${runner.allProgress.current}/${runner.allProgress.total}]` }),
1173
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `Planning: ${runner.specName || runner.activeFlags.spec} (iteration ${Math.min(runner.currentIteration, runner.maxIterations)}/${runner.maxIterations})` }),
1174
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(40) }),
1175
+ /* @__PURE__ */ jsx3(StreamOutput, { events: runner.events, verbose: runner.resolvedVerbose })
1176
+ ] });
1177
+ }
1178
+
1179
+ // src/commands/build.tsx
1180
+ import { useState as useState4, useEffect as useEffect3, useMemo as useMemo3 } from "react";
1181
+ import { Text as Text4, Box as Box4 } from "ink";
1182
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1183
+ function detectResume(specEntry, currentCli, lastCli) {
1184
+ const lastIteration = specEntry?.iterations.at(-1);
1185
+ const isCrashResume = !!(specEntry?.status !== "done" && lastIteration?.state === "in_progress");
1186
+ const isExhaustedResume = !!(specEntry?.status !== "done" && specEntry?.stopReason === "max_iterations");
1187
+ const needsResume = isCrashResume || isExhaustedResume;
1188
+ const isSameCli = currentCli === lastCli;
1189
+ const sessionId = isSameCli && isCrashResume ? lastIteration?.sessionId ?? void 0 : void 0;
1190
+ return { isCrashResume, isExhaustedResume, needsResume, sessionId };
1191
+ }
1192
+ async function runSpecBuild(options) {
1193
+ const { spec, iterations, cli: cli2, model, cwd, callbacks } = options;
1194
+ let status = readStatus(cwd);
1195
+ let iterationStartTime = (/* @__PURE__ */ new Date()).toISOString();
1196
+ callbacks.onPhase?.("building");
1197
+ callbacks.onIteration?.(1, iterations);
1198
+ const loopResult = await runLoop({
1199
+ maxIterations: iterations,
1200
+ getPrompt: (iteration) => {
1201
+ const cliVars = computeCliVars({
1202
+ specName: spec.name,
1203
+ iteration: iteration + options.existingIterations,
1204
+ specIndex: options.specIndex,
1205
+ specCount: options.specCount,
1206
+ session: options.session,
1207
+ specs: options.specs,
1208
+ specsDir: options.specsDir
1209
+ });
1210
+ const vars = resolveTemplateVars(cliVars, options.templateVars);
1211
+ return loadPrompt(options.promptName, vars, { cwd });
1212
+ },
1213
+ cli: cli2,
1214
+ model,
1215
+ cwd,
1216
+ sessionId: options.sessionId,
1217
+ continueSession: true,
1218
+ abortSignal: options.abortSignal,
1219
+ onEvent: (event) => {
1220
+ options.writer?.writeEvent(event);
1221
+ callbacks.onEvent?.(event);
1222
+ },
1223
+ onIterationStart: (iteration, sessionId) => {
1224
+ iterationStartTime = (/* @__PURE__ */ new Date()).toISOString();
1225
+ const iterationRecord = {
1226
+ type: "build",
1227
+ iteration: iteration + options.existingIterations,
1228
+ sessionId,
1229
+ state: "in_progress",
1230
+ cli: cli2,
1231
+ model: model ?? "default",
1232
+ startedAt: iterationStartTime,
1233
+ completedAt: null,
1234
+ exitCode: null,
1235
+ taskCompleted: null,
1236
+ tokensUsed: null
1237
+ };
1238
+ status = addIteration(status, spec.name, iterationRecord);
1239
+ status = { ...status, sessionName: options.session, lastCli: cli2 };
1240
+ writeStatus(status, cwd);
1241
+ },
1242
+ onIterationComplete: (iterResult) => {
1243
+ options.writer?.writeIterationHeader({
1244
+ iteration: iterResult.iteration,
1245
+ total: iterations,
1246
+ cli: cli2,
1247
+ model: iterResult.model ?? model ?? "default"
1248
+ });
1249
+ const state = iterResult.sentinelDetected ? "complete" : "failed";
1250
+ const specEntry = status.specs[spec.name];
1251
+ const iters = [...specEntry.iterations];
1252
+ iters[iters.length - 1] = {
1253
+ ...iters[iters.length - 1],
1254
+ state,
1255
+ sessionId: iterResult.sessionId,
1256
+ model: iterResult.model ?? iters[iters.length - 1].model,
1257
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1258
+ exitCode: iterResult.exitCode,
1259
+ tokensUsed: iterResult.tokensUsed
1260
+ };
1261
+ status = {
1262
+ ...status,
1263
+ specs: {
1264
+ ...status.specs,
1265
+ [spec.name]: { ...specEntry, iterations: iters }
1266
+ }
1267
+ };
1268
+ writeStatus(status, cwd);
1269
+ callbacks.onIteration?.(iterResult.iteration + 1, iterations);
1270
+ }
1271
+ });
1272
+ const specEntryAfterLoop = status.specs[spec.name] ?? { status: "building", plannedAt: null, iterations: [] };
1273
+ status = {
1274
+ ...status,
1275
+ specs: {
1276
+ ...status.specs,
1277
+ [spec.name]: { ...specEntryAfterLoop, stopReason: loopResult.stopReason }
1278
+ }
1279
+ };
1280
+ if (loopResult.stopReason === "aborted") {
1281
+ status = updateSpecStatus(status, spec.name, "building");
1282
+ writeStatus(status, cwd);
1283
+ throw new AbortError(spec.name, loopResult.iterations.length);
1284
+ }
1285
+ const totalIterations = loopResult.iterations.length;
1286
+ const totalTokens = loopResult.iterations.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
1287
+ if (loopResult.stopReason === "error") {
1288
+ status = updateSpecStatus(status, spec.name, "building");
1289
+ writeStatus(status, cwd);
1290
+ const lastIter = loopResult.iterations[loopResult.iterations.length - 1];
1291
+ const errorMsg = `Build failed after ${totalIterations} iteration(s). Last exit code: ${lastIter?.exitCode ?? "unknown"}`;
1292
+ return { result: { specName: spec.name, totalIterations, totalTokens, specDone: false, error: errorMsg }, status };
1293
+ }
1294
+ const specDone = loopResult.stopReason === "sentinel";
1295
+ status = updateSpecStatus(status, spec.name, specDone ? "done" : "building");
1296
+ writeStatus(status, cwd);
1297
+ return { result: { specName: spec.name, totalIterations, totalTokens, specDone }, status };
1298
+ }
1299
+ async function executeBuild(flags2, callbacks = {}, cwd = process.cwd(), abortSignal, externalWriter) {
1300
+ ensureLocalDir(cwd);
1301
+ const config = loadConfig(cwd);
1302
+ const commandConfig = resolveCommandConfig(config, "build", {
1303
+ cli: flags2.cli,
1304
+ iterations: flags2.iterations
1305
+ });
1306
+ if (!flags2.spec) {
1307
+ throw new Error("No --spec flag provided. Usage: toby build --spec=<name>");
1308
+ }
1309
+ const specs = discoverSpecs(cwd, config);
1310
+ if (specs.length === 0) {
1311
+ throw new Error("No specs found in specs/");
1312
+ }
1313
+ const found = findSpec(specs, flags2.spec);
1314
+ if (!found) {
1315
+ throw new Error(`Spec '${flags2.spec}' not found`);
1316
+ }
1317
+ const status = readStatus(cwd);
1318
+ const specEntry = status.specs[found.name];
1319
+ if (!specEntry || specEntry.status !== "planned" && specEntry.status !== "building") {
1320
+ throw new Error(`No plan found for ${found.name}. Run 'toby plan --spec=${flags2.spec}' first.`);
1321
+ }
1322
+ const existingIterations = specEntry.iterations.length;
1323
+ const resume = detectResume(specEntry, commandConfig.cli, status.lastCli);
1324
+ const session = flags2.session || (resume.needsResume ? status.sessionName : null) || computeSpecSlug(found.name);
1325
+ if (resume.isCrashResume) {
1326
+ const isSameCli = commandConfig.cli === status.lastCli;
1327
+ const resumeType = isSameCli ? "continuing session" : `switching from ${status.lastCli} to ${commandConfig.cli}`;
1328
+ callbacks.onOutput?.(`Resuming session "${session}" (${resumeType})`);
1329
+ } else if (resume.isExhaustedResume) {
1330
+ callbacks.onOutput?.(`\u26A0 Previous build exhausted iterations without completing. Resuming in worktree "${session}"...`);
1331
+ }
1332
+ return withTranscript(
1333
+ { flags: flags2, config, command: "build", specName: found.name },
1334
+ externalWriter,
1335
+ async (writer) => {
1336
+ const { result } = await runSpecBuild({
1337
+ spec: found,
1338
+ promptName: "PROMPT_BUILD",
1339
+ existingIterations,
1340
+ iterations: commandConfig.iterations,
1341
+ cli: commandConfig.cli,
1342
+ model: commandConfig.model,
1343
+ templateVars: config.templateVars,
1344
+ specsDir: config.specsDir,
1345
+ session,
1346
+ sessionId: resume.sessionId,
1347
+ specIndex: 1,
1348
+ specCount: 1,
1349
+ specs: [found.name],
1350
+ cwd,
1351
+ abortSignal,
1352
+ callbacks,
1353
+ writer
1354
+ });
1355
+ return { ...result, needsResume: resume.needsResume };
1356
+ }
1357
+ );
1358
+ }
1359
+ async function executeBuildAll(flags2, callbacks = {}, cwd = process.cwd(), abortSignal, specs) {
1360
+ ensureLocalDir(cwd);
1361
+ const config = loadConfig(cwd);
1362
+ let planned;
1363
+ if (specs) {
1364
+ planned = specs;
1365
+ } else {
1366
+ const discovered = discoverSpecs(cwd, config);
1367
+ if (discovered.length === 0) {
1368
+ throw new Error("No specs found in specs/");
1369
+ }
1370
+ planned = sortSpecs([...filterByStatus(discovered, "planned"), ...filterByStatus(discovered, "building")]);
1371
+ if (planned.length === 0) {
1372
+ throw new Error("No planned specs found. Run 'toby plan' first.");
1373
+ }
1374
+ }
1375
+ const built = [];
1376
+ const specNames = planned.map((s) => s.name);
1377
+ const status = readStatus(cwd);
1378
+ const commandConfig = resolveCommandConfig(config, "build", {
1379
+ cli: flags2.cli,
1380
+ iterations: flags2.iterations
1381
+ });
1382
+ const anyNeedsResume = planned.some((spec) => {
1383
+ return detectResume(status.specs[spec.name], commandConfig.cli, status.lastCli).needsResume;
1384
+ });
1385
+ const session = flags2.session || (anyNeedsResume ? status.sessionName : null) || generateSessionName();
1386
+ return withTranscript(
1387
+ { flags: { ...flags2, session: flags2.session ?? session }, config, command: "build" },
1388
+ void 0,
1389
+ async (writer) => {
1390
+ for (let i = 0; i < planned.length; i++) {
1391
+ const spec = planned[i];
1392
+ writer?.writeSpecHeader(i + 1, planned.length, spec.name);
1393
+ callbacks.onSpecStart?.(spec.name, i, planned.length);
1394
+ const specEntry = status.specs[spec.name];
1395
+ const existingIterations = specEntry?.iterations.length ?? 0;
1396
+ const resume = detectResume(specEntry, commandConfig.cli, status.lastCli);
1397
+ if (resume.isCrashResume) {
1398
+ const lastIteration = specEntry?.iterations.at(-1);
1399
+ callbacks.onOutput?.(
1400
+ `\u26A0 [${spec.name}] Previous build interrupted (iteration ${lastIteration?.iteration} was in progress). Resuming...`
1401
+ );
1402
+ } else if (resume.isExhaustedResume) {
1403
+ callbacks.onOutput?.(
1404
+ `\u26A0 [${spec.name}] Previous build exhausted iterations without completing. Resuming in same worktree...`
1405
+ );
1406
+ }
1407
+ const { result } = await runSpecBuild({
1408
+ spec,
1409
+ promptName: "PROMPT_BUILD",
1410
+ existingIterations,
1411
+ iterations: commandConfig.iterations,
1412
+ cli: commandConfig.cli,
1413
+ model: commandConfig.model,
1414
+ templateVars: config.templateVars,
1415
+ specsDir: config.specsDir,
1416
+ session,
1417
+ sessionId: resume.sessionId,
1418
+ specIndex: i + 1,
1419
+ specCount: planned.length,
1420
+ specs: specNames,
1421
+ cwd,
1422
+ abortSignal,
1423
+ callbacks: {
1424
+ onPhase: callbacks.onPhase,
1425
+ onIteration: callbacks.onIteration,
1426
+ onEvent: callbacks.onEvent,
1427
+ onOutput: callbacks.onOutput
1428
+ },
1429
+ writer
1430
+ });
1431
+ built.push({ ...result, needsResume: resume.needsResume });
1432
+ callbacks.onSpecComplete?.({ ...result, needsResume: resume.needsResume });
1433
+ }
1434
+ return { built };
1435
+ }
1436
+ );
1437
+ }
1438
+ function Build(flags2) {
1439
+ const runner = useCommandRunner({
1440
+ flags: flags2,
1441
+ runPhase: "building",
1442
+ filterSpecs: (specs) => {
1443
+ const buildable = [...filterByStatus(specs, "planned"), ...filterByStatus(specs, "building")];
1444
+ return buildable;
1445
+ },
1446
+ emptyMessage: "No planned specs found. Run 'toby plan' first."
1447
+ });
1448
+ const [result, setResult] = useState4(null);
1449
+ const [allResult, setAllResult] = useState4(null);
1450
+ const allCallbacks = useMemo3(() => ({
1451
+ onSpecStart: runner.onSpecStartCallback,
1452
+ onSpecComplete: () => {
1453
+ },
1454
+ onPhase: runner.onPhaseCallback,
1455
+ onIteration: runner.onIterationCallback,
1456
+ onEvent: runner.addEvent
1457
+ }), [runner.onSpecStartCallback, runner.onPhaseCallback, runner.onIterationCallback, runner.addEvent]);
1458
+ useEffect3(() => {
1459
+ if (runner.phase !== "multi" || runner.selectedSpecs.length === 0) return;
1460
+ executeBuildAll(flags2, allCallbacks, void 0, runner.abortSignal, runner.selectedSpecs).then((r) => {
1461
+ setAllResult(r);
1462
+ runner.handleDone();
1463
+ }).catch(runner.handleError);
1464
+ }, [runner.phase, runner.selectedSpecs]);
1465
+ useEffect3(() => {
1466
+ if (runner.phase !== "all") return;
1467
+ executeBuildAll(flags2, allCallbacks, void 0, runner.abortSignal).then((r) => {
1468
+ setAllResult(r);
1469
+ runner.handleDone();
1470
+ }).catch(runner.handleError);
1471
+ }, [runner.phase]);
1472
+ useEffect3(() => {
1473
+ if (runner.phase !== "init") return;
1474
+ executeBuild(runner.activeFlags, {
1475
+ onPhase: runner.onPhaseCallback,
1476
+ onIteration: runner.onIterationCallback,
1477
+ onEvent: runner.addEvent
1478
+ }, void 0, runner.abortSignal).then((r) => {
1479
+ runner.setSpecName(r.specName);
1480
+ setResult(r);
1481
+ runner.handleDone();
1482
+ }).catch(runner.handleError);
1483
+ }, [runner.activeFlags, runner.phase]);
1484
+ if (runner.phase === "interrupted" && runner.interruptInfo) {
1485
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1486
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: `\u26A0 Building interrupted for ${runner.interruptInfo.specName}` }),
1487
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${runner.interruptInfo.iterations} iteration(s) completed, partial status saved` })
1488
+ ] });
1489
+ }
1490
+ if (runner.phase === "error") {
1491
+ return /* @__PURE__ */ jsx4(Text4, { color: "red", children: runner.errorMessage });
1492
+ }
1493
+ if (runner.phase === "selecting") {
1494
+ if (runner.specs.length === 0) {
1495
+ return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading specs..." });
1496
+ }
1497
+ return /* @__PURE__ */ jsx4(MultiSpecSelector, { specs: runner.specs, onConfirm: runner.handleMultiSpecConfirm, title: "Select specs to build:" });
1498
+ }
1499
+ if (runner.phase === "done" && allResult) {
1500
+ const totalIter = allResult.built.reduce((s, r) => s + r.totalIterations, 0);
1501
+ const totalTok = allResult.built.reduce((s, r) => s + r.totalTokens, 0);
1502
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1503
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: `\u2713 All specs built (${allResult.built.length} built)` }),
1504
+ allResult.built.map((r) => /* @__PURE__ */ jsx4(Text4, { children: ` ${r.specName}: ${r.totalIterations} iterations, ${r.totalTokens} tokens${r.specDone ? " [done]" : ""}` }, r.specName)),
1505
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` Total: ${totalIter} iterations, ${totalTok} tokens` })
1506
+ ] });
1507
+ }
1508
+ if (runner.phase === "done" && result) {
1509
+ if (result.error) {
1510
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: /* @__PURE__ */ jsx4(Text4, { color: "red", children: `\u2717 ${result.error}` }) });
1511
+ }
1512
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1513
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: `\u2713 Build ${result.specDone ? "complete" : "paused"} for ${result.specName}` }),
1514
+ /* @__PURE__ */ jsx4(Text4, { children: ` Iterations: ${result.totalIterations}, Tokens: ${result.totalTokens}` })
1515
+ ] });
1516
+ }
1517
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1518
+ runner.allProgress.total > 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `[${runner.allProgress.current}/${runner.allProgress.total}]` }),
1519
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `Building: ${runner.specName || runner.activeFlags.spec} (iteration ${Math.min(runner.currentIteration, runner.maxIterations)}/${runner.maxIterations})` }),
1520
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(40) }),
1521
+ /* @__PURE__ */ jsx4(StreamOutput, { events: runner.events, verbose: runner.resolvedVerbose })
1522
+ ] });
1523
+ }
1524
+
1525
+ // src/commands/init.tsx
1526
+ import { useState as useState5, useEffect as useEffect4 } from "react";
1527
+ import { Text as Text5, Box as Box5, useApp as useApp2 } from "ink";
1528
+ import SelectInput from "ink-select-input";
1529
+ import TextInput from "ink-text-input";
1530
+ import fs7 from "fs";
1531
+ import path7 from "path";
1532
+ import { detectAll, getKnownModels } from "@0xtiby/spawner";
1533
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1534
+ function createProject(selections, cwd = process.cwd()) {
1535
+ const localDir = getLocalDir(cwd);
1536
+ const configPath = path7.join(localDir, CONFIG_FILE);
1537
+ const statusPath = path7.join(localDir, STATUS_FILE);
1538
+ const specsPath = path7.join(cwd, selections.specsDir);
1539
+ try {
1540
+ fs7.mkdirSync(localDir, { recursive: true });
1541
+ } catch (err) {
1542
+ const msg = err.code === "EACCES" ? `Permission denied creating ${localDir}` : `Failed to create project directory: ${err.message}`;
1543
+ throw new Error(msg);
1544
+ }
1545
+ try {
1546
+ const config = {
1547
+ plan: {
1548
+ cli: selections.planCli,
1549
+ model: selections.planModel,
1550
+ iterations: 2
1551
+ },
1552
+ build: {
1553
+ cli: selections.buildCli,
1554
+ model: selections.buildModel,
1555
+ iterations: 10
1556
+ },
1557
+ specsDir: selections.specsDir,
1558
+ verbose: selections.verbose,
1559
+ templateVars: {
1560
+ PRD_PATH: ".toby/{{SPEC_NAME}}.prd.json"
1561
+ }
1562
+ };
1563
+ writeConfig(config, configPath);
1564
+ } catch (err) {
1565
+ throw new Error(
1566
+ `Failed to write config: ${err.message}`
1567
+ );
1568
+ }
1569
+ const statusCreated = !fs7.existsSync(statusPath);
1570
+ if (statusCreated) {
1571
+ try {
1572
+ fs7.writeFileSync(
1573
+ statusPath,
1574
+ JSON.stringify({ specs: {} }, null, 2) + "\n"
1575
+ );
1576
+ } catch (err) {
1577
+ throw new Error(
1578
+ `Failed to write status file: ${err.message}`
1579
+ );
1580
+ }
1581
+ }
1582
+ const specsDirCreated = !fs7.existsSync(specsPath);
1583
+ if (specsDirCreated) {
1584
+ fs7.mkdirSync(specsPath, { recursive: true });
1585
+ }
1586
+ return { configPath, statusCreated, specsDirCreated };
1587
+ }
1588
+ function getInstalledClis(result) {
1589
+ return Object.entries(result).filter(([, info]) => info.installed).map(([name]) => name);
1590
+ }
1591
+ function CliTable({ clis }) {
1592
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
1593
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Detected CLIs:" }),
1594
+ Object.entries(clis).map(
1595
+ ([name, info]) => /* @__PURE__ */ jsxs4(Text5, { children: [
1596
+ " ",
1597
+ info.installed ? /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u2713" }) : /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2717" }),
1598
+ " ",
1599
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: name }),
1600
+ info.installed && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1601
+ " ",
1602
+ info.version,
1603
+ info.authenticated ? " (authenticated)" : " (not authenticated)"
1604
+ ] })
1605
+ ] }, name)
1606
+ )
1607
+ ] });
1608
+ }
1609
+ function modelItems(cli2) {
1610
+ const models = getKnownModels(cli2);
1611
+ return [
1612
+ { label: "default", value: "default" },
1613
+ ...models.map((m) => ({ label: `${m.name} (${m.id})`, value: m.id }))
1614
+ ];
1615
+ }
1616
+ function hasAllInitFlags(flags2) {
1617
+ return flags2.planCli !== void 0 && flags2.planModel !== void 0 && flags2.buildCli !== void 0 && flags2.buildModel !== void 0 && flags2.specsDir !== void 0;
1618
+ }
1619
+ function NonInteractiveInit({ flags: flags2 }) {
1620
+ const { exit } = useApp2();
1621
+ const planCli = flags2.planCli;
1622
+ const buildCli = flags2.buildCli;
1623
+ const invalidCli = [planCli, buildCli].find(
1624
+ (cli2) => !CLI_NAMES.includes(cli2)
1625
+ );
1626
+ const [status, setStatus] = useState5(invalidCli ? { type: "invalid_cli", cli: invalidCli } : { type: "detecting" });
1627
+ useEffect4(() => {
1628
+ if (invalidCli) {
1629
+ process.exitCode = 1;
1630
+ exit();
1631
+ return;
1632
+ }
1633
+ detectAll().then((detectResult) => {
1634
+ for (const cli2 of [planCli, buildCli]) {
1635
+ if (!detectResult[cli2]?.installed) {
1636
+ setStatus({
1637
+ type: "error",
1638
+ message: `CLI not installed: ${cli2}`
1639
+ });
1640
+ process.exitCode = 1;
1641
+ exit();
1642
+ return;
1643
+ }
1644
+ }
1645
+ const selections2 = {
1646
+ planCli,
1647
+ planModel: flags2.planModel,
1648
+ buildCli,
1649
+ buildModel: flags2.buildModel,
1650
+ specsDir: flags2.specsDir,
1651
+ verbose: flags2.verbose ?? false
1652
+ };
1653
+ try {
1654
+ const result2 = createProject(selections2);
1655
+ setStatus({ type: "success", result: result2, selections: selections2 });
1656
+ } catch (err) {
1657
+ setStatus({ type: "error", message: err.message });
1658
+ process.exitCode = 1;
1659
+ }
1660
+ exit();
1661
+ });
1662
+ }, []);
1663
+ if (status.type === "invalid_cli") {
1664
+ return /* @__PURE__ */ jsx5(Text5, { color: "red", children: `\u2717 Unknown CLI: ${status.cli}. Must be one of: ${CLI_NAMES.join(", ")}` });
1665
+ }
1666
+ if (status.type === "detecting") {
1667
+ return /* @__PURE__ */ jsx5(Text5, { children: "Detecting installed CLIs..." });
1668
+ }
1669
+ if (status.type === "error") {
1670
+ return /* @__PURE__ */ jsx5(Text5, { color: "red", children: `\u2717 ${status.message}` });
1671
+ }
1672
+ const { result, selections } = status;
1673
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1674
+ /* @__PURE__ */ jsx5(Text5, { color: "green", bold: true, children: "\u2713 Project initialized!" }),
1675
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1676
+ " created ",
1677
+ path7.relative(process.cwd(), result.configPath)
1678
+ ] }),
1679
+ result.statusCreated && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " created .toby/status.json" }),
1680
+ result.specsDirCreated && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1681
+ " created ",
1682
+ selections.specsDir,
1683
+ "/"
1684
+ ] })
1685
+ ] });
1686
+ }
1687
+ function Init(flags2) {
1688
+ if (hasAllInitFlags(flags2)) {
1689
+ return /* @__PURE__ */ jsx5(NonInteractiveInit, { flags: flags2 });
1690
+ }
1691
+ return /* @__PURE__ */ jsx5(InteractiveInit, { version: flags2.version });
1692
+ }
1693
+ function InteractiveInit({ version: version2 }) {
1694
+ const { exit } = useApp2();
1695
+ const [phase, setPhase] = useState5("detecting");
1696
+ const [clis, setClis] = useState5(null);
1697
+ const [installedClis, setInstalledClis] = useState5([]);
1698
+ const [selections, setSelections] = useState5({
1699
+ planCli: "claude",
1700
+ planModel: "default",
1701
+ buildCli: "claude",
1702
+ buildModel: "default",
1703
+ specsDir: DEFAULT_SPECS_DIR,
1704
+ verbose: false
1705
+ });
1706
+ const [specsDirInput, setSpecsDirInput] = useState5(DEFAULT_SPECS_DIR);
1707
+ const [result, setResult] = useState5(null);
1708
+ const [error, setError] = useState5(null);
1709
+ useEffect4(() => {
1710
+ if (phase !== "detecting") return;
1711
+ detectAll().then((detectResult) => {
1712
+ setClis(detectResult);
1713
+ const installed = getInstalledClis(detectResult);
1714
+ setInstalledClis(installed);
1715
+ if (installed.length === 0) {
1716
+ setPhase("no_cli");
1717
+ exit();
1718
+ } else {
1719
+ setSelections((s) => ({
1720
+ ...s,
1721
+ planCli: installed[0],
1722
+ buildCli: installed[0]
1723
+ }));
1724
+ setPhase("plan_cli");
1725
+ }
1726
+ });
1727
+ }, [phase]);
1728
+ function handlePlanCliSelect(item) {
1729
+ const cli2 = item.value;
1730
+ setSelections((s) => ({ ...s, planCli: cli2 }));
1731
+ setPhase("plan_model");
1732
+ }
1733
+ function handlePlanModelSelect(item) {
1734
+ setSelections((s) => ({ ...s, planModel: item.value }));
1735
+ setPhase("build_cli");
1736
+ }
1737
+ function handleBuildCliSelect(item) {
1738
+ const cli2 = item.value;
1739
+ setSelections((s) => ({ ...s, buildCli: cli2 }));
1740
+ setPhase("build_model");
1741
+ }
1742
+ function handleBuildModelSelect(item) {
1743
+ setSelections((s) => ({ ...s, buildModel: item.value }));
1744
+ setPhase("specs_dir");
1745
+ }
1746
+ function handleSpecsDirSubmit(value) {
1747
+ const dir = value.trim() || DEFAULT_SPECS_DIR;
1748
+ setSelections((s) => ({ ...s, specsDir: dir }));
1749
+ setPhase("verbose");
1750
+ }
1751
+ function handleVerboseSelect(item) {
1752
+ const verbose = item.value === "true";
1753
+ setSelections((s) => {
1754
+ const final = { ...s, verbose };
1755
+ try {
1756
+ const res = createProject(final);
1757
+ setResult(res);
1758
+ setPhase("done");
1759
+ } catch (err) {
1760
+ setError(err.message);
1761
+ setPhase("done");
1762
+ }
1763
+ exit();
1764
+ return final;
1765
+ });
1766
+ }
1767
+ if (phase === "detecting") {
1768
+ return /* @__PURE__ */ jsx5(Text5, { children: "Detecting installed CLIs..." });
1769
+ }
1770
+ if (phase === "no_cli") {
1771
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1772
+ clis && /* @__PURE__ */ jsx5(CliTable, { clis }),
1773
+ /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, children: "No AI CLIs found. Install one of the following:" }),
1774
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2022 claude \u2014 npm install -g @anthropic-ai/claude-code" }),
1775
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2022 codex \u2014 npm install -g @openai/codex" }),
1776
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2022 opencode \u2014 go install github.com/opencode-ai/opencode@latest" })
1777
+ ] });
1778
+ }
1779
+ const cliItems = installedClis.map((name) => ({
1780
+ label: `${name} \u2014 ${clis?.[name]?.version ?? "unknown"}`,
1781
+ value: name
1782
+ }));
1783
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1784
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: `toby v${version2} \u2014 project setup
1785
+ ` }),
1786
+ clis && /* @__PURE__ */ jsx5(CliTable, { clis }),
1787
+ phase === "plan_cli" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1788
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Select CLI for planning:" }),
1789
+ /* @__PURE__ */ jsx5(SelectInput, { items: cliItems, onSelect: handlePlanCliSelect })
1790
+ ] }),
1791
+ phase === "plan_model" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1792
+ /* @__PURE__ */ jsxs4(Text5, { bold: true, children: [
1793
+ "Select model for planning (",
1794
+ selections.planCli,
1795
+ "):"
1796
+ ] }),
1797
+ /* @__PURE__ */ jsx5(
1798
+ SelectInput,
1799
+ {
1800
+ items: modelItems(selections.planCli),
1801
+ onSelect: handlePlanModelSelect
1802
+ }
1803
+ )
1804
+ ] }),
1805
+ phase === "build_cli" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1806
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Select CLI for building:" }),
1807
+ /* @__PURE__ */ jsx5(SelectInput, { items: cliItems, onSelect: handleBuildCliSelect })
1808
+ ] }),
1809
+ phase === "build_model" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1810
+ /* @__PURE__ */ jsxs4(Text5, { bold: true, children: [
1811
+ "Select model for building (",
1812
+ selections.buildCli,
1813
+ "):"
1814
+ ] }),
1815
+ /* @__PURE__ */ jsx5(
1816
+ SelectInput,
1817
+ {
1818
+ items: modelItems(selections.buildCli),
1819
+ onSelect: handleBuildModelSelect
1820
+ }
1821
+ )
1822
+ ] }),
1823
+ phase === "specs_dir" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1824
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Specs directory:" }),
1825
+ /* @__PURE__ */ jsxs4(Box5, { children: [
1826
+ /* @__PURE__ */ jsx5(Text5, { children: " > " }),
1827
+ /* @__PURE__ */ jsx5(
1828
+ TextInput,
1829
+ {
1830
+ value: specsDirInput,
1831
+ onChange: setSpecsDirInput,
1832
+ onSubmit: handleSpecsDirSubmit
1833
+ }
1834
+ )
1835
+ ] })
1836
+ ] }),
1837
+ phase === "verbose" && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1838
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Verbose output:" }),
1839
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " Show full CLI output including tool use and system events" }),
1840
+ /* @__PURE__ */ jsx5(
1841
+ SelectInput,
1842
+ {
1843
+ items: [
1844
+ { label: "false", value: "false" },
1845
+ { label: "true", value: "true" }
1846
+ ],
1847
+ onSelect: handleVerboseSelect
1848
+ }
1849
+ )
1850
+ ] }),
1851
+ phase === "done" && error && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1852
+ /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, children: "\u2717 Initialization failed" }),
1853
+ /* @__PURE__ */ jsx5(Text5, { color: "red", children: ` ${error}` })
1854
+ ] }),
1855
+ phase === "done" && result && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1856
+ /* @__PURE__ */ jsx5(Text5, { color: "green", bold: true, children: "\u2713 Project initialized!" }),
1857
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
1858
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1859
+ " created ",
1860
+ path7.relative(process.cwd(), result.configPath)
1861
+ ] }),
1862
+ result.statusCreated && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " created .toby/status.json" }),
1863
+ result.specsDirCreated && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1864
+ " created ",
1865
+ selections.specsDir,
1866
+ "/"
1867
+ ] }),
1868
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
1869
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Next steps:" }),
1870
+ /* @__PURE__ */ jsxs4(Text5, { children: [
1871
+ " 1. Add spec files to ",
1872
+ selections.specsDir,
1873
+ "/"
1874
+ ] }),
1875
+ /* @__PURE__ */ jsxs4(Text5, { children: [
1876
+ " 2. Run ",
1877
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "toby plan" }),
1878
+ " to plan a spec"
1879
+ ] }),
1880
+ /* @__PURE__ */ jsxs4(Text5, { children: [
1881
+ " 3. Run ",
1882
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "toby build" }),
1883
+ " to build tasks"
1884
+ ] })
1885
+ ] })
1886
+ ] });
1887
+ }
1888
+
1889
+ // src/commands/status.tsx
1890
+ import { Text as Text6, Box as Box6 } from "ink";
1891
+ import fs8 from "fs";
1892
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1893
+ function formatDuration(startedAt, completedAt) {
1894
+ if (!completedAt) return "\u2014";
1895
+ const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime();
1896
+ const seconds = Math.floor(ms / 1e3);
1897
+ const minutes = Math.floor(seconds / 60);
1898
+ const remainingSeconds = seconds % 60;
1899
+ return `${minutes}m ${remainingSeconds}s`;
1900
+ }
1901
+ function buildRows(cwd) {
1902
+ const config = loadConfig(cwd);
1903
+ const specs = discoverSpecs(cwd, config);
1904
+ const warnings = [];
1905
+ let statusData;
1906
+ try {
1907
+ statusData = readStatus(cwd);
1908
+ } catch {
1909
+ warnings.push("Corrupt status.json \u2014 showing defaults. Run toby init to re-create.");
1910
+ statusData = { specs: {} };
1911
+ }
1912
+ const rows = specs.map((spec) => {
1913
+ const entry = getSpecStatus(statusData, spec.name);
1914
+ const tokens = entry.iterations.reduce(
1915
+ (sum, iter) => sum + (iter.tokensUsed ?? 0),
1916
+ 0
1917
+ );
1918
+ return {
1919
+ name: spec.name,
1920
+ status: entry.status,
1921
+ tokens,
1922
+ iterations: entry.iterations.length
1923
+ };
1924
+ });
1925
+ return { rows, warnings };
1926
+ }
1927
+ function pad(str, len) {
1928
+ return str + " ".repeat(Math.max(0, len - str.length));
1929
+ }
1930
+ function StatusTable({ rows }) {
1931
+ const headers = { name: "Spec", status: "Status", iterations: "Iter", tokens: "Tokens" };
1932
+ const colWidths = {
1933
+ name: Math.max(headers.name.length, ...rows.map((r) => r.name.length)),
1934
+ status: Math.max(headers.status.length, ...rows.map((r) => r.status.length)),
1935
+ iterations: Math.max(headers.iterations.length, ...rows.map((r) => String(r.iterations).length)),
1936
+ tokens: Math.max(headers.tokens.length, ...rows.map((r) => String(r.tokens).length))
1937
+ };
1938
+ const separator = `${"\u2500".repeat(colWidths.name + 2)}\u253C${"\u2500".repeat(colWidths.status + 2)}\u253C${"\u2500".repeat(colWidths.iterations + 2)}\u253C${"\u2500".repeat(colWidths.tokens + 2)}`;
1939
+ const headerLine = ` ${pad(headers.name, colWidths.name)} \u2502 ${pad(headers.status, colWidths.status)} \u2502 ${pad(headers.iterations, colWidths.iterations)} \u2502 ${pad(headers.tokens, colWidths.tokens)} `;
1940
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1941
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: headerLine }),
1942
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: separator }),
1943
+ rows.map((row) => /* @__PURE__ */ jsx6(Text6, { children: ` ${pad(row.name, colWidths.name)} \u2502 ${pad(row.status, colWidths.status)} \u2502 ${pad(String(row.iterations), colWidths.iterations)} \u2502 ${pad(String(row.tokens), colWidths.tokens)} ` }, row.name))
1944
+ ] });
1945
+ }
1946
+ function IterationTable({ rows }) {
1947
+ if (rows.length === 0) {
1948
+ return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No iterations yet" });
1949
+ }
1950
+ const headers = { index: "#", type: "Type", cli: "CLI", tokens: "Tokens", duration: "Duration", exitCode: "Exit" };
1951
+ const w = {
1952
+ index: Math.max(headers.index.length, ...rows.map((r) => r.index.length)),
1953
+ type: Math.max(headers.type.length, ...rows.map((r) => r.type.length)),
1954
+ cli: Math.max(headers.cli.length, ...rows.map((r) => r.cli.length)),
1955
+ tokens: Math.max(headers.tokens.length, ...rows.map((r) => r.tokens.length)),
1956
+ duration: Math.max(headers.duration.length, ...rows.map((r) => r.duration.length)),
1957
+ exitCode: Math.max(headers.exitCode.length, ...rows.map((r) => r.exitCode.length))
1958
+ };
1959
+ const separator = `${"\u2500".repeat(w.index + 2)}\u253C${"\u2500".repeat(w.type + 2)}\u253C${"\u2500".repeat(w.cli + 2)}\u253C${"\u2500".repeat(w.tokens + 2)}\u253C${"\u2500".repeat(w.duration + 2)}\u253C${"\u2500".repeat(w.exitCode + 2)}`;
1960
+ const headerLine = ` ${pad(headers.index, w.index)} \u2502 ${pad(headers.type, w.type)} \u2502 ${pad(headers.cli, w.cli)} \u2502 ${pad(headers.tokens, w.tokens)} \u2502 ${pad(headers.duration, w.duration)} \u2502 ${pad(headers.exitCode, w.exitCode)} `;
1961
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1962
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: headerLine }),
1963
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: separator }),
1964
+ rows.map((row) => /* @__PURE__ */ jsx6(Text6, { children: ` ${pad(row.index, w.index)} \u2502 ${pad(row.type, w.type)} \u2502 ${pad(row.cli, w.cli)} \u2502 ${pad(row.tokens, w.tokens)} \u2502 ${pad(row.duration, w.duration)} \u2502 ${pad(row.exitCode, w.exitCode)} ` }, row.index))
1965
+ ] });
1966
+ }
1967
+ function DetailedView({ specName, cwd }) {
1968
+ const config = loadConfig(cwd);
1969
+ const specs = discoverSpecs(cwd, config);
1970
+ const spec = findSpec(specs, specName);
1971
+ if (!spec) {
1972
+ return /* @__PURE__ */ jsxs5(Text6, { color: "red", children: [
1973
+ "Spec not found: ",
1974
+ specName
1975
+ ] });
1976
+ }
1977
+ let statusData;
1978
+ let statusWarning = null;
1979
+ try {
1980
+ statusData = readStatus(cwd);
1981
+ } catch {
1982
+ statusWarning = "Corrupt status.json \u2014 showing defaults.";
1983
+ statusData = { specs: {} };
1984
+ }
1985
+ const entry = getSpecStatus(statusData, spec.name);
1986
+ const iterationRows = entry.iterations.map((iter, i) => ({
1987
+ index: String(i + 1),
1988
+ type: iter.type,
1989
+ cli: iter.cli,
1990
+ tokens: iter.tokensUsed != null ? String(iter.tokensUsed) : "\u2014",
1991
+ duration: formatDuration(iter.startedAt, iter.completedAt),
1992
+ exitCode: iter.exitCode != null ? String(iter.exitCode) : "\u2014"
1993
+ }));
1994
+ const totalTokens = entry.iterations.reduce(
1995
+ (sum, iter) => sum + (iter.tokensUsed ?? 0),
1996
+ 0
1997
+ );
1998
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1999
+ statusWarning && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: statusWarning }),
2000
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: spec.name }),
2001
+ /* @__PURE__ */ jsxs5(Text6, { children: [
2002
+ "Status: ",
2003
+ entry.status
2004
+ ] }),
2005
+ /* @__PURE__ */ jsx6(Text6, { children: "" }),
2006
+ /* @__PURE__ */ jsx6(IterationTable, { rows: iterationRows }),
2007
+ /* @__PURE__ */ jsx6(Text6, { children: "" }),
2008
+ /* @__PURE__ */ jsxs5(Text6, { children: [
2009
+ "Iterations: ",
2010
+ entry.iterations.length
2011
+ ] }),
2012
+ /* @__PURE__ */ jsxs5(Text6, { children: [
2013
+ "Tokens used: ",
2014
+ totalTokens
2015
+ ] })
2016
+ ] });
2017
+ }
2018
+ function Status({ spec, version: version2 }) {
2019
+ const cwd = process.cwd();
2020
+ const localDir = getLocalDir(cwd);
2021
+ if (!fs8.existsSync(localDir)) {
2022
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
2023
+ /* @__PURE__ */ jsx6(Text6, { color: "red", bold: true, children: "Toby not initialized" }),
2024
+ /* @__PURE__ */ jsxs5(Text6, { children: [
2025
+ "Run ",
2026
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "toby init" }),
2027
+ " to set up your project."
2028
+ ] })
2029
+ ] });
2030
+ }
2031
+ if (spec) {
2032
+ return /* @__PURE__ */ jsx6(DetailedView, { specName: spec, cwd });
2033
+ }
2034
+ const { rows, warnings } = buildRows(cwd);
2035
+ if (rows.length === 0) {
2036
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
2037
+ /* @__PURE__ */ jsx6(Text6, { children: `toby v${version2}` }),
2038
+ warnings.map((w) => /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: w }, w)),
2039
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No specs found. Add .md files to your specs directory." })
2040
+ ] });
2041
+ }
2042
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
2043
+ /* @__PURE__ */ jsx6(Text6, { children: `toby v${version2}` }),
2044
+ warnings.map((w) => /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: w }, w)),
2045
+ /* @__PURE__ */ jsx6(Text6, { children: "" }),
2046
+ /* @__PURE__ */ jsx6(StatusTable, { rows })
2047
+ ] });
2048
+ }
2049
+
2050
+ // src/commands/config.tsx
2051
+ import { useState as useState6, useEffect as useEffect5 } from "react";
2052
+ import { Text as Text7, Box as Box7, useApp as useApp3 } from "ink";
2053
+ import SelectInput2 from "ink-select-input";
2054
+ import TextInput2 from "ink-text-input";
2055
+ import fs9 from "fs";
2056
+ import path8 from "path";
2057
+ import { detectAll as detectAll2, getKnownModels as getKnownModels2 } from "@0xtiby/spawner";
2058
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2059
+ var VALID_KEYS = {
2060
+ "plan.cli": "string",
2061
+ "plan.model": "string",
2062
+ "plan.iterations": "number",
2063
+ "build.cli": "string",
2064
+ "build.model": "string",
2065
+ "build.iterations": "number",
2066
+ specsDir: "string",
2067
+ verbose: "boolean",
2068
+ transcript: "boolean"
2069
+ };
2070
+ function getNestedValue(obj, key) {
2071
+ const parts = key.split(".");
2072
+ let current = obj;
2073
+ for (const part of parts) {
2074
+ if (current === null || current === void 0 || typeof current !== "object") {
2075
+ return void 0;
2076
+ }
2077
+ current = current[part];
2078
+ }
2079
+ return current;
2080
+ }
2081
+ function setNestedValue(obj, key, value) {
2082
+ const parts = key.split(".");
2083
+ let current = obj;
2084
+ for (let i = 0; i < parts.length - 1; i++) {
2085
+ const part = parts[i];
2086
+ if (!(part in current) || typeof current[part] !== "object" || current[part] === null) {
2087
+ current[part] = {};
2088
+ }
2089
+ current = current[part];
2090
+ }
2091
+ current[parts[parts.length - 1]] = value;
2092
+ }
2093
+ function parseValue(raw, type) {
2094
+ switch (type) {
2095
+ case "number": {
2096
+ const n = Number(raw);
2097
+ if (Number.isNaN(n)) throw new Error(`Expected a number, got "${raw}"`);
2098
+ return n;
2099
+ }
2100
+ case "boolean": {
2101
+ if (raw === "true") return true;
2102
+ if (raw === "false") return false;
2103
+ throw new Error(`Expected true or false, got "${raw}"`);
2104
+ }
2105
+ default:
2106
+ return raw;
2107
+ }
2108
+ }
2109
+ function readMergeWriteConfig(mutations) {
2110
+ const cwd = process.cwd();
2111
+ const configPath = path8.join(getLocalDir(cwd), CONFIG_FILE);
2112
+ let existing = {};
2113
+ try {
2114
+ const content = fs9.readFileSync(configPath, "utf-8");
2115
+ existing = JSON.parse(content);
2116
+ } catch {
2117
+ }
2118
+ for (const { key, value } of mutations) {
2119
+ setNestedValue(existing, key, value);
2120
+ }
2121
+ writeConfig(existing, configPath);
2122
+ }
2123
+ function ConfigGet({ configKey }) {
2124
+ if (!(configKey in VALID_KEYS)) {
2125
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Unknown config key: ${configKey}
2126
+ Valid keys: ${Object.keys(VALID_KEYS).join(", ")}` });
2127
+ }
2128
+ const cwd = process.cwd();
2129
+ const localDir = getLocalDir(cwd);
2130
+ if (!fs9.existsSync(localDir)) {
2131
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2132
+ /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "No config found" }),
2133
+ /* @__PURE__ */ jsxs6(Text7, { children: [
2134
+ "Run ",
2135
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "toby init" }),
2136
+ " to set up your project."
2137
+ ] })
2138
+ ] });
2139
+ }
2140
+ const config = loadConfig();
2141
+ const value = getNestedValue(config, configKey);
2142
+ return /* @__PURE__ */ jsx7(Text7, { children: String(value) });
2143
+ }
2144
+ function ConfigSet({ configKey, value }) {
2145
+ if (!(configKey in VALID_KEYS)) {
2146
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Unknown config key: ${configKey}
2147
+ Valid keys: ${Object.keys(VALID_KEYS).join(", ")}` });
2148
+ }
2149
+ const type = VALID_KEYS[configKey];
2150
+ let parsed;
2151
+ try {
2152
+ parsed = parseValue(value, type);
2153
+ } catch (err) {
2154
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Invalid value for ${configKey}: ${err.message}` });
2155
+ }
2156
+ const partial = {};
2157
+ setNestedValue(partial, configKey, parsed);
2158
+ try {
2159
+ ConfigSchema.parse({ ...partial });
2160
+ } catch (err) {
2161
+ const msg = err instanceof Error ? err.message : String(err);
2162
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Validation error for ${configKey}: ${msg}` });
2163
+ }
2164
+ try {
2165
+ readMergeWriteConfig([{ key: configKey, value: parsed }]);
2166
+ } catch (err) {
2167
+ const code = err.code;
2168
+ const msg = code === "EACCES" ? `Permission denied writing to ${path8.join(getLocalDir(process.cwd()), CONFIG_FILE)}` : `Failed to write config: ${err.message}`;
2169
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: msg });
2170
+ }
2171
+ return /* @__PURE__ */ jsx7(Text7, { color: "green", children: `Set ${configKey} = ${String(parsed)}` });
2172
+ }
2173
+ function modelItems2(cli2) {
2174
+ const models = getKnownModels2(cli2);
2175
+ return [
2176
+ { label: "default", value: "default" },
2177
+ ...models.map((m) => ({ label: `${m.name} (${m.id})`, value: m.id }))
2178
+ ];
2179
+ }
2180
+ function configToEditorValues(config) {
2181
+ return {
2182
+ planCli: config.plan.cli,
2183
+ planModel: config.plan.model,
2184
+ planIterations: config.plan.iterations,
2185
+ buildCli: config.build.cli,
2186
+ buildModel: config.build.model,
2187
+ buildIterations: config.build.iterations,
2188
+ specsDir: config.specsDir,
2189
+ verbose: config.verbose
2190
+ };
2191
+ }
2192
+ function editorValuesToConfig(values) {
2193
+ return {
2194
+ plan: {
2195
+ cli: values.planCli,
2196
+ model: values.planModel,
2197
+ iterations: values.planIterations
2198
+ },
2199
+ build: {
2200
+ cli: values.buildCli,
2201
+ model: values.buildModel,
2202
+ iterations: values.buildIterations
2203
+ },
2204
+ specsDir: values.specsDir,
2205
+ verbose: values.verbose
2206
+ };
2207
+ }
2208
+ var PHASE_ORDER = {
2209
+ loading: 0,
2210
+ plan_cli: 1,
2211
+ plan_model: 2,
2212
+ plan_iterations: 3,
2213
+ build_cli: 4,
2214
+ build_model: 5,
2215
+ build_iterations: 6,
2216
+ specs_dir: 7,
2217
+ verbose: 8,
2218
+ saving: 9,
2219
+ done: 10
2220
+ };
2221
+ function pastPhase(current, target) {
2222
+ return PHASE_ORDER[current] > PHASE_ORDER[target];
2223
+ }
2224
+ function CompletedField({ label, value }) {
2225
+ return /* @__PURE__ */ jsxs6(Text7, { children: [
2226
+ " ",
2227
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
2228
+ label,
2229
+ ":"
2230
+ ] }),
2231
+ " ",
2232
+ value
2233
+ ] });
2234
+ }
2235
+ function ConfigEditor({ version: version2 }) {
2236
+ const { exit } = useApp3();
2237
+ const [phase, setPhase] = useState6("loading");
2238
+ const [installedClis, setInstalledClis] = useState6([]);
2239
+ const [values, setValues] = useState6({
2240
+ planCli: "claude",
2241
+ planModel: "default",
2242
+ planIterations: 2,
2243
+ buildCli: "claude",
2244
+ buildModel: "default",
2245
+ buildIterations: 10,
2246
+ specsDir: "specs",
2247
+ verbose: false
2248
+ });
2249
+ const [iterInput, setIterInput] = useState6("");
2250
+ const [specsDirInput, setSpecsDirInput] = useState6("");
2251
+ const [saveError, setSaveError] = useState6(null);
2252
+ useEffect5(() => {
2253
+ if (phase !== "loading") return;
2254
+ const config = loadConfig();
2255
+ const initial = configToEditorValues(config);
2256
+ setValues(initial);
2257
+ detectAll2().then((result) => {
2258
+ const installed = Object.entries(result).filter(([, info]) => info.installed).map(([name]) => name);
2259
+ const cliSet = new Set(installed);
2260
+ if (!cliSet.has(initial.planCli)) cliSet.add(initial.planCli);
2261
+ if (!cliSet.has(initial.buildCli)) cliSet.add(initial.buildCli);
2262
+ setInstalledClis(Array.from(cliSet));
2263
+ setPhase("plan_cli");
2264
+ });
2265
+ }, [phase]);
2266
+ function cliItems() {
2267
+ return installedClis.map((name) => ({
2268
+ label: name,
2269
+ value: name
2270
+ }));
2271
+ }
2272
+ function initialIndex(items, currentValue) {
2273
+ const idx = items.findIndex((i) => i.value === currentValue);
2274
+ return idx >= 0 ? idx : 0;
2275
+ }
2276
+ function handleSave() {
2277
+ const cwd = process.cwd();
2278
+ const configPath = path8.join(getLocalDir(cwd), CONFIG_FILE);
2279
+ const partial = editorValuesToConfig(values);
2280
+ try {
2281
+ writeConfig(partial, configPath);
2282
+ } catch (err) {
2283
+ const code = err.code;
2284
+ const msg = code === "EACCES" ? `Permission denied writing to ${configPath}` : `Failed to save config: ${err.message}`;
2285
+ setSaveError(msg);
2286
+ }
2287
+ setPhase("done");
2288
+ exit();
2289
+ }
2290
+ if (phase === "loading") {
2291
+ return /* @__PURE__ */ jsx7(Text7, { children: "Loading configuration..." });
2292
+ }
2293
+ const planCliItems = cliItems();
2294
+ const buildCliItems = cliItems();
2295
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2296
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: `toby v${version2} \u2014 config editor
2297
+ ` }),
2298
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Plan" }),
2299
+ phase === "plan_cli" && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2300
+ /* @__PURE__ */ jsx7(Text7, { children: " cli:" }),
2301
+ /* @__PURE__ */ jsx7(
2302
+ SelectInput2,
2303
+ {
2304
+ items: planCliItems,
2305
+ initialIndex: initialIndex(planCliItems, values.planCli),
2306
+ onSelect: (item) => {
2307
+ setValues((v) => ({ ...v, planCli: item.value }));
2308
+ setPhase("plan_model");
2309
+ }
2310
+ }
2311
+ )
2312
+ ] }),
2313
+ phase !== "plan_cli" && /* @__PURE__ */ jsx7(CompletedField, { label: "cli", value: values.planCli }),
2314
+ phase === "plan_model" && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2315
+ /* @__PURE__ */ jsx7(Text7, { children: " model:" }),
2316
+ /* @__PURE__ */ jsx7(
2317
+ SelectInput2,
2318
+ {
2319
+ items: modelItems2(values.planCli),
2320
+ initialIndex: initialIndex(modelItems2(values.planCli), values.planModel),
2321
+ onSelect: (item) => {
2322
+ setValues((v) => ({ ...v, planModel: item.value }));
2323
+ setIterInput(String(values.planIterations));
2324
+ setPhase("plan_iterations");
2325
+ }
2326
+ }
2327
+ )
2328
+ ] }),
2329
+ pastPhase(phase, "plan_model") && /* @__PURE__ */ jsx7(CompletedField, { label: "model", value: values.planModel }),
2330
+ phase === "plan_iterations" && /* @__PURE__ */ jsxs6(Box7, { children: [
2331
+ /* @__PURE__ */ jsx7(Text7, { children: " iterations: " }),
2332
+ /* @__PURE__ */ jsx7(
2333
+ TextInput2,
2334
+ {
2335
+ value: iterInput,
2336
+ onChange: setIterInput,
2337
+ onSubmit: (val) => {
2338
+ const n = Number(val);
2339
+ if (!Number.isNaN(n) && n > 0 && Number.isInteger(n)) {
2340
+ setValues((v) => ({ ...v, planIterations: n }));
2341
+ }
2342
+ setPhase("build_cli");
2343
+ }
2344
+ }
2345
+ )
2346
+ ] }),
2347
+ pastPhase(phase, "plan_iterations") && /* @__PURE__ */ jsx7(CompletedField, { label: "iterations", value: String(values.planIterations) }),
2348
+ pastPhase(phase, "plan_iterations") && /* @__PURE__ */ jsxs6(Fragment2, { children: [
2349
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2350
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Build" })
2351
+ ] }),
2352
+ phase === "build_cli" && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2353
+ /* @__PURE__ */ jsx7(Text7, { children: " cli:" }),
2354
+ /* @__PURE__ */ jsx7(
2355
+ SelectInput2,
2356
+ {
2357
+ items: buildCliItems,
2358
+ initialIndex: initialIndex(buildCliItems, values.buildCli),
2359
+ onSelect: (item) => {
2360
+ setValues((v) => ({ ...v, buildCli: item.value }));
2361
+ setPhase("build_model");
2362
+ }
2363
+ }
2364
+ )
2365
+ ] }),
2366
+ pastPhase(phase, "build_cli") && /* @__PURE__ */ jsx7(CompletedField, { label: "cli", value: values.buildCli }),
2367
+ phase === "build_model" && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2368
+ /* @__PURE__ */ jsx7(Text7, { children: " model:" }),
2369
+ /* @__PURE__ */ jsx7(
2370
+ SelectInput2,
2371
+ {
2372
+ items: modelItems2(values.buildCli),
2373
+ initialIndex: initialIndex(modelItems2(values.buildCli), values.buildModel),
2374
+ onSelect: (item) => {
2375
+ setValues((v) => ({ ...v, buildModel: item.value }));
2376
+ setIterInput(String(values.buildIterations));
2377
+ setPhase("build_iterations");
2378
+ }
2379
+ }
2380
+ )
2381
+ ] }),
2382
+ pastPhase(phase, "build_model") && /* @__PURE__ */ jsx7(CompletedField, { label: "model", value: values.buildModel }),
2383
+ phase === "build_iterations" && /* @__PURE__ */ jsxs6(Box7, { children: [
2384
+ /* @__PURE__ */ jsx7(Text7, { children: " iterations: " }),
2385
+ /* @__PURE__ */ jsx7(
2386
+ TextInput2,
2387
+ {
2388
+ value: iterInput,
2389
+ onChange: setIterInput,
2390
+ onSubmit: (val) => {
2391
+ const n = Number(val);
2392
+ if (!Number.isNaN(n) && n > 0 && Number.isInteger(n)) {
2393
+ setValues((v) => ({ ...v, buildIterations: n }));
2394
+ }
2395
+ setSpecsDirInput(values.specsDir);
2396
+ setPhase("specs_dir");
2397
+ }
2398
+ }
2399
+ )
2400
+ ] }),
2401
+ pastPhase(phase, "build_iterations") && /* @__PURE__ */ jsx7(CompletedField, { label: "iterations", value: String(values.buildIterations) }),
2402
+ pastPhase(phase, "build_iterations") && /* @__PURE__ */ jsxs6(Fragment2, { children: [
2403
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2404
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "General" })
2405
+ ] }),
2406
+ phase === "specs_dir" && /* @__PURE__ */ jsxs6(Box7, { children: [
2407
+ /* @__PURE__ */ jsx7(Text7, { children: " specsDir: " }),
2408
+ /* @__PURE__ */ jsx7(
2409
+ TextInput2,
2410
+ {
2411
+ value: specsDirInput,
2412
+ onChange: setSpecsDirInput,
2413
+ onSubmit: (val) => {
2414
+ const dir = val.trim() || values.specsDir;
2415
+ setValues((v) => ({ ...v, specsDir: dir }));
2416
+ setPhase("verbose");
2417
+ }
2418
+ }
2419
+ )
2420
+ ] }),
2421
+ pastPhase(phase, "specs_dir") && /* @__PURE__ */ jsx7(CompletedField, { label: "specsDir", value: values.specsDir }),
2422
+ phase === "verbose" && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2423
+ /* @__PURE__ */ jsx7(Text7, { children: " verbose:" }),
2424
+ /* @__PURE__ */ jsx7(
2425
+ SelectInput2,
2426
+ {
2427
+ items: [
2428
+ { label: "false", value: "false" },
2429
+ { label: "true", value: "true" }
2430
+ ],
2431
+ initialIndex: values.verbose ? 1 : 0,
2432
+ onSelect: (item) => {
2433
+ setValues((v) => ({ ...v, verbose: item.value === "true" }));
2434
+ handleSave();
2435
+ }
2436
+ }
2437
+ )
2438
+ ] }),
2439
+ phase === "done" && /* @__PURE__ */ jsxs6(Fragment2, { children: [
2440
+ /* @__PURE__ */ jsx7(CompletedField, { label: "verbose", value: String(values.verbose) }),
2441
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2442
+ saveError ? /* @__PURE__ */ jsxs6(Text7, { color: "red", bold: true, children: [
2443
+ "\u2717 ",
2444
+ saveError
2445
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "\u2713 Config saved" })
2446
+ ] })
2447
+ ] });
2448
+ }
2449
+ function ConfigSetBatch({ pairs }) {
2450
+ const parsed = [];
2451
+ const errors = [];
2452
+ for (const pair of pairs) {
2453
+ const eqIndex = pair.indexOf("=");
2454
+ if (eqIndex === -1) {
2455
+ errors.push(`Invalid format: "${pair}" (expected key=value)`);
2456
+ continue;
2457
+ }
2458
+ const key = pair.slice(0, eqIndex);
2459
+ const raw = pair.slice(eqIndex + 1);
2460
+ if (!(key in VALID_KEYS)) {
2461
+ errors.push(`Unknown config key: ${key}`);
2462
+ continue;
2463
+ }
2464
+ try {
2465
+ const value = parseValue(raw, VALID_KEYS[key]);
2466
+ parsed.push({ key, value, raw });
2467
+ } catch (err) {
2468
+ errors.push(`Invalid value for ${key}: ${err.message}`);
2469
+ }
2470
+ }
2471
+ if (errors.length > 0) {
2472
+ process.exitCode = 1;
2473
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: errors.map((e, i) => /* @__PURE__ */ jsx7(Text7, { color: "red", children: e }, i)) });
2474
+ }
2475
+ const partial = {};
2476
+ for (const { key, value } of parsed) {
2477
+ setNestedValue(partial, key, value);
2478
+ }
2479
+ try {
2480
+ ConfigSchema.parse({ ...partial });
2481
+ } catch (err) {
2482
+ process.exitCode = 1;
2483
+ const msg = err instanceof Error ? err.message : String(err);
2484
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Validation error: ${msg}` });
2485
+ }
2486
+ try {
2487
+ readMergeWriteConfig(parsed);
2488
+ } catch (err) {
2489
+ process.exitCode = 1;
2490
+ const code = err.code;
2491
+ const msg = code === "EACCES" ? `Permission denied writing to ${path8.join(getLocalDir(process.cwd()), CONFIG_FILE)}` : `Failed to write config: ${err.message}`;
2492
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: msg });
2493
+ }
2494
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: parsed.map(({ key, value }) => /* @__PURE__ */ jsx7(Text7, { color: "green", children: `Set ${key} = ${String(value)}` }, key)) });
2495
+ }
2496
+ function UnknownSubcommand({ subcommand }) {
2497
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Unknown config subcommand: ${subcommand}
2498
+ Usage: toby config [get <key> | set <key> <value>]` });
2499
+ }
2500
+ function Config({
2501
+ subcommand,
2502
+ configKey,
2503
+ value,
2504
+ version: version2
2505
+ }) {
2506
+ if (subcommand && subcommand !== "get" && subcommand !== "set") {
2507
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2508
+ /* @__PURE__ */ jsx7(Text7, { children: `toby v${version2}` }),
2509
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2510
+ /* @__PURE__ */ jsx7(UnknownSubcommand, { subcommand })
2511
+ ] });
2512
+ }
2513
+ if (subcommand === "get" && configKey) {
2514
+ return /* @__PURE__ */ jsx7(ConfigGet, { configKey });
2515
+ }
2516
+ if (subcommand === "set" && configKey && value) {
2517
+ return /* @__PURE__ */ jsx7(ConfigSet, { configKey, value });
2518
+ }
2519
+ if (subcommand === "set" && configKey && !value) {
2520
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: `Missing value for config set.
2521
+ Usage: toby config set <key> <value>` });
2522
+ }
2523
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2524
+ /* @__PURE__ */ jsx7(Text7, { children: `toby v${version2}` }),
2525
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2526
+ /* @__PURE__ */ jsx7(Text7, { children: "Usage: toby config [get <key> | set <key> <value>]" }),
2527
+ /* @__PURE__ */ jsx7(Text7, { children: "" }),
2528
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Available keys:" }),
2529
+ Object.entries(VALID_KEYS).map(([key, type]) => /* @__PURE__ */ jsx7(Text7, { children: ` ${key} (${type})` }, key))
2530
+ ] });
2531
+ }
2532
+
2533
+ // src/components/Welcome.tsx
2534
+ import { useState as useState8, useEffect as useEffect7, useMemo as useMemo5 } from "react";
2535
+ import { Box as Box11, Text as Text11, useApp as useApp4, useStdout as useStdout2 } from "ink";
2536
+
2537
+ // src/components/hamster/HamsterWheel.tsx
2538
+ import { useState as useState7, useEffect as useEffect6, useMemo as useMemo4 } from "react";
2539
+ import { Box as Box8, Text as Text8, useStdout } from "ink";
2540
+
2541
+ // src/components/hamster/palette.ts
2542
+ var PALETTE = {
2543
+ // Hamster colors
2544
+ body: "#d4883c",
2545
+ bodyLight: "#e8a85c",
2546
+ bodyDark: "#b06828",
2547
+ belly: "#f0d8b0",
2548
+ ear: "#ff8899",
2549
+ earInner: "#ff6680",
2550
+ eye: "#1a1a2e",
2551
+ cheek: "#ff9977",
2552
+ feet: "#c47830",
2553
+ tail: "#b06828",
2554
+ // Wheel colors
2555
+ wheelBright: "#8888aa",
2556
+ wheelDim: "#555577",
2557
+ wheelInner: "#444466",
2558
+ wheelSpoke: "#3a3a55",
2559
+ wheelHub: "#7777aa"
2560
+ };
2561
+
2562
+ // src/components/hamster/sprites.ts
2563
+ var HAMSTER_BODY = [
2564
+ // Head
2565
+ [3, -5, "ear"],
2566
+ [2, -4, "ear"],
2567
+ [3, -4, "earInner"],
2568
+ [4, -4, "ear"],
2569
+ [1, -3, "body"],
2570
+ [2, -3, "body"],
2571
+ [3, -3, "body"],
2572
+ [4, -3, "body"],
2573
+ [5, -3, "body"],
2574
+ [0, -2, "body"],
2575
+ [1, -2, "bodyLight"],
2576
+ [2, -2, "bodyLight"],
2577
+ [3, -2, "bodyLight"],
2578
+ [4, -2, "bodyLight"],
2579
+ [5, -2, "body"],
2580
+ [6, -2, "bodyDark"],
2581
+ [-1, -1, "body"],
2582
+ [0, -1, "bodyLight"],
2583
+ [1, -1, "eye"],
2584
+ [2, -1, "bodyLight"],
2585
+ [3, -1, "bodyLight"],
2586
+ [4, -1, "body"],
2587
+ [5, -1, "body"],
2588
+ [6, -1, "bodyDark"],
2589
+ // Face
2590
+ [-2, 0, "earInner"],
2591
+ [-1, 0, "bodyLight"],
2592
+ [0, 0, "cheek"],
2593
+ [1, 0, "bodyLight"],
2594
+ [2, 0, "belly"],
2595
+ [3, 0, "belly"],
2596
+ [4, 0, "body"],
2597
+ [5, 0, "body"],
2598
+ [6, 0, "bodyDark"],
2599
+ // Body
2600
+ [0, 1, "body"],
2601
+ [1, 1, "belly"],
2602
+ [2, 1, "belly"],
2603
+ [3, 1, "belly"],
2604
+ [4, 1, "body"],
2605
+ [5, 1, "body"],
2606
+ [6, 1, "bodyDark"],
2607
+ [7, 1, "bodyDark"],
2608
+ [0, 2, "body"],
2609
+ [1, 2, "belly"],
2610
+ [2, 2, "belly"],
2611
+ [3, 2, "belly"],
2612
+ [4, 2, "body"],
2613
+ [5, 2, "body"],
2614
+ [6, 2, "bodyDark"],
2615
+ [7, 2, "bodyDark"],
2616
+ [8, 2, "tail"],
2617
+ [1, 3, "body"],
2618
+ [2, 3, "belly"],
2619
+ [3, 3, "belly"],
2620
+ [4, 3, "body"],
2621
+ [5, 3, "body"],
2622
+ [6, 3, "bodyDark"],
2623
+ [7, 3, "bodyDark"],
2624
+ // Tail
2625
+ [8, 3, "tail"],
2626
+ [9, 3, "tail"],
2627
+ [9, 2, "tail"]
2628
+ ];
2629
+ var FRAME_A_LEGS = [
2630
+ [0, 4, "feet"],
2631
+ [-1, 4, "feet"],
2632
+ [5, 4, "feet"],
2633
+ [6, 4, "feet"],
2634
+ [6, 5, "feet"],
2635
+ [7, 4, "bodyDark"]
2636
+ ];
2637
+ var FRAME_B_LEGS = [
2638
+ [1, 4, "feet"],
2639
+ [2, 4, "feet"],
2640
+ [5, 4, "feet"],
2641
+ [6, 4, "feet"],
2642
+ [7, 4, "feet"],
2643
+ [7, 5, "feet"],
2644
+ [-1, 5, "feet"],
2645
+ [8, 4, "bodyDark"]
2646
+ ];
2647
+ var FRAME_A = [...HAMSTER_BODY, ...FRAME_A_LEGS];
2648
+ var FRAME_B = [...HAMSTER_BODY, ...FRAME_B_LEGS];
2649
+ var HAMSTER_FRAMES = [FRAME_A, FRAME_B];
2650
+
2651
+ // src/components/hamster/wheel.ts
2652
+ function computeWheelGeometry(width, height) {
2653
+ const cx = Math.floor(width / 2);
2654
+ const cy = Math.floor(height / 2);
2655
+ const outerRadius = Math.floor(height / 2) - 1;
2656
+ const innerRadius = Math.floor(outerRadius * 0.85);
2657
+ return { cx, cy, outerRadius, innerRadius };
2658
+ }
2659
+ function generateWheelPixels(cx, cy, outerRadius, innerRadius, spokeAngle, aspectRatio = 1) {
2660
+ const pixels = [];
2661
+ const outerSteps = Math.max(16, outerRadius * 8);
2662
+ for (let i = 0; i < outerSteps; i++) {
2663
+ const angle = i / outerSteps * 2 * Math.PI;
2664
+ const x = Math.round(cx + Math.cos(angle) * outerRadius * aspectRatio);
2665
+ const y = Math.round(cy + Math.sin(angle) * outerRadius);
2666
+ const color = i % 3 === 0 ? PALETTE.wheelBright : PALETTE.wheelDim;
2667
+ pixels.push({ x, y, color });
2668
+ }
2669
+ const innerSteps = Math.max(12, innerRadius * 6);
2670
+ for (let i = 0; i < innerSteps; i += 3) {
2671
+ const angle = i / innerSteps * 2 * Math.PI;
2672
+ const x = Math.round(
2673
+ cx + Math.cos(angle) * innerRadius * aspectRatio
2674
+ );
2675
+ const y = Math.round(cy + Math.sin(angle) * innerRadius);
2676
+ pixels.push({ x, y, color: PALETTE.wheelInner });
2677
+ }
2678
+ const spokeStep = Math.max(1, outerRadius / 8);
2679
+ for (let s = 0; s < 8; s++) {
2680
+ const angle = spokeAngle + s / 8 * 2 * Math.PI;
2681
+ for (let r = outerRadius * 0.25; r <= outerRadius; r += spokeStep) {
2682
+ const x = Math.round(cx + Math.cos(angle) * r * aspectRatio);
2683
+ const y = Math.round(cy + Math.sin(angle) * r);
2684
+ pixels.push({ x, y, color: PALETTE.wheelSpoke });
2685
+ }
2686
+ }
2687
+ for (let dx = -1; dx <= 1; dx++) {
2688
+ for (let dy = -1; dy <= 1; dy++) {
2689
+ if (Math.abs(dx) + Math.abs(dy) <= 1) {
2690
+ pixels.push({ x: cx + dx, y: cy + dy, color: PALETTE.wheelHub });
2691
+ }
2692
+ }
2693
+ }
2694
+ return pixels;
2695
+ }
2696
+
2697
+ // src/components/hamster/HamsterWheel.tsx
2698
+ import { jsx as jsx8 } from "react/jsx-runtime";
2699
+ function buildGrid(width, height) {
2700
+ return Array.from(
2701
+ { length: height },
2702
+ () => Array.from({ length: width }).fill(null)
2703
+ );
2704
+ }
2705
+ function resolveHalfBlock(top, bottom) {
2706
+ if (top && bottom) {
2707
+ return { char: "\u2580", fg: top, bg: bottom };
2708
+ }
2709
+ if (top) {
2710
+ return { char: "\u2580", fg: top, bg: void 0 };
2711
+ }
2712
+ if (bottom) {
2713
+ return { char: "\u2584", fg: bottom, bg: void 0 };
2714
+ }
2715
+ return { char: " ", fg: void 0, bg: void 0 };
2716
+ }
2717
+ function buildColorRuns(grid, width, height) {
2718
+ const charHeight = Math.ceil(height / 2);
2719
+ const rows = [];
2720
+ for (let cy = 0; cy < charHeight; cy++) {
2721
+ const topRow = cy * 2;
2722
+ const bottomRow = cy * 2 + 1;
2723
+ const runs = [];
2724
+ let current = null;
2725
+ for (let x = 0; x < width; x++) {
2726
+ const top = grid[topRow]?.[x] ?? null;
2727
+ const bottom = bottomRow < height ? grid[bottomRow]?.[x] ?? null : null;
2728
+ const { char, fg, bg } = resolveHalfBlock(top, bottom);
2729
+ if (current && current.fg === fg && current.bg === bg && current.char === char) {
2730
+ current.length++;
2731
+ } else {
2732
+ if (current) runs.push(current);
2733
+ current = { fg, bg, char, length: 1 };
2734
+ }
2735
+ }
2736
+ if (current) runs.push(current);
2737
+ rows.push(runs);
2738
+ }
2739
+ return rows;
2740
+ }
2741
+ var SIZE_TIERS = {
2742
+ compact: { width: 25, height: 13 },
2743
+ full: { width: 35, height: 18 }
2744
+ };
2745
+ var MIN_VIABLE_WIDTH = 15;
2746
+ function getSizeTier(columns) {
2747
+ if (columns < 60) return "static";
2748
+ if (columns < 100) return "compact";
2749
+ return "full";
2750
+ }
2751
+ var HAMSTER_BASE_INTERVAL = 140;
2752
+ var WHEEL_BASE_INTERVAL = 100;
2753
+ var MIN_INTERVAL = 16;
2754
+ function computeInterval(baseInterval, speed) {
2755
+ return Math.max(MIN_INTERVAL, Math.round(baseInterval / speed));
2756
+ }
2757
+ function resolveDimensions(widthProp, heightProp, columns) {
2758
+ if (widthProp !== void 0 && heightProp !== void 0) {
2759
+ return { width: widthProp, height: heightProp, isStatic: widthProp < MIN_VIABLE_WIDTH };
2760
+ }
2761
+ const tier = getSizeTier(columns);
2762
+ if (tier === "static") return { width: 0, height: 0, isStatic: true };
2763
+ return {
2764
+ width: widthProp ?? SIZE_TIERS[tier].width,
2765
+ height: heightProp ?? SIZE_TIERS[tier].height,
2766
+ isStatic: false
2767
+ };
2768
+ }
2769
+ function HamsterWheel({
2770
+ width: widthProp,
2771
+ height: heightProp,
2772
+ speed = 1
2773
+ }) {
2774
+ const { stdout } = useStdout();
2775
+ const columns = stdout?.columns ?? 80;
2776
+ const { width: resolvedWidth, height: resolvedHeight, isStatic } = resolveDimensions(
2777
+ widthProp,
2778
+ heightProp,
2779
+ columns
2780
+ );
2781
+ const [frame, setFrame] = useState7(0);
2782
+ const [spokeAngle, setSpokeAngle] = useState7(0);
2783
+ useEffect6(() => {
2784
+ if (speed === 0 || isStatic) return;
2785
+ const interval = computeInterval(HAMSTER_BASE_INTERVAL, speed);
2786
+ const id = setInterval(() => {
2787
+ setFrame((f) => (f + 1) % 2);
2788
+ }, interval);
2789
+ return () => clearInterval(id);
2790
+ }, [speed, isStatic]);
2791
+ useEffect6(() => {
2792
+ if (speed === 0 || isStatic) return;
2793
+ const interval = computeInterval(WHEEL_BASE_INTERVAL, speed);
2794
+ const id = setInterval(() => {
2795
+ setSpokeAngle((a) => (a + 0.15) % (2 * Math.PI));
2796
+ }, interval);
2797
+ return () => clearInterval(id);
2798
+ }, [speed, isStatic]);
2799
+ const renderedRows = useMemo4(() => {
2800
+ if (isStatic) return [];
2801
+ const grid = buildGrid(resolvedWidth, resolvedHeight);
2802
+ const { cx, cy, outerRadius, innerRadius } = computeWheelGeometry(
2803
+ resolvedWidth,
2804
+ resolvedHeight
2805
+ );
2806
+ const wheelPixels = generateWheelPixels(
2807
+ cx,
2808
+ cy,
2809
+ outerRadius,
2810
+ innerRadius,
2811
+ spokeAngle
2812
+ );
2813
+ for (const pixel of wheelPixels) {
2814
+ if (pixel.x >= 0 && pixel.x < resolvedWidth && pixel.y >= 0 && pixel.y < resolvedHeight) {
2815
+ grid[pixel.y][pixel.x] = pixel.color;
2816
+ }
2817
+ }
2818
+ const hamsterOriginX = cx - 2;
2819
+ const hamsterOriginY = cy + innerRadius - 5;
2820
+ const spriteFrame = HAMSTER_FRAMES[frame];
2821
+ for (const [col, row, colorToken] of spriteFrame) {
2822
+ const px = hamsterOriginX + col;
2823
+ const py = hamsterOriginY + row;
2824
+ if (px >= 0 && px < resolvedWidth && py >= 0 && py < resolvedHeight) {
2825
+ grid[py][px] = PALETTE[colorToken];
2826
+ }
2827
+ }
2828
+ return buildColorRuns(grid, resolvedWidth, resolvedHeight);
2829
+ }, [resolvedWidth, resolvedHeight, frame, spokeAngle, isStatic]);
2830
+ if (isStatic) {
2831
+ return /* @__PURE__ */ jsx8(Text8, { children: " \u{1F439} toby" });
2832
+ }
2833
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: renderedRows.map((runs, y) => /* @__PURE__ */ jsx8(Text8, { children: runs.map((run, i) => /* @__PURE__ */ jsx8(Text8, { color: run.fg, backgroundColor: run.bg, children: run.char.repeat(run.length) }, i)) }, y)) });
2834
+ }
2835
+
2836
+ // src/components/InfoPanel.tsx
2837
+ import { Box as Box9, Text as Text9 } from "ink";
2838
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2839
+ var formatTokens = (n) => new Intl.NumberFormat().format(n);
2840
+ function StatRow({ label, value }) {
2841
+ return /* @__PURE__ */ jsxs7(Box9, { children: [
2842
+ /* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
2843
+ String(label).padStart(9),
2844
+ " "
2845
+ ] }),
2846
+ /* @__PURE__ */ jsx9(Text9, { children: value })
2847
+ ] });
2848
+ }
2849
+ function InfoPanel({ version: version2, stats }) {
2850
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", children: [
2851
+ /* @__PURE__ */ jsxs7(Text9, { bold: true, color: "#f0a030", children: [
2852
+ "toby v",
2853
+ version2
2854
+ ] }),
2855
+ stats !== null && /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", marginTop: 1, children: [
2856
+ /* @__PURE__ */ jsx9(StatRow, { label: "Specs", value: stats.totalSpecs }),
2857
+ /* @__PURE__ */ jsx9(StatRow, { label: "Planned", value: stats.planned }),
2858
+ /* @__PURE__ */ jsx9(StatRow, { label: "Done", value: stats.done }),
2859
+ /* @__PURE__ */ jsx9(StatRow, { label: "Tokens", value: formatTokens(stats.totalTokens) })
2860
+ ] })
2861
+ ] });
2862
+ }
2863
+
2864
+ // src/components/MainMenu.tsx
2865
+ import { Text as Text10, Box as Box10 } from "ink";
2866
+ import SelectInput3 from "ink-select-input";
2867
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2868
+ var MENU_ITEMS = [
2869
+ { label: "plan", value: "plan", description: "Plan specs with AI loop engine" },
2870
+ { label: "build", value: "build", description: "Build tasks one-per-spawn with AI" },
2871
+ { label: "status", value: "status", description: "Show project status" },
2872
+ { label: "config", value: "config", description: "Manage configuration" }
2873
+ ];
2874
+ function MenuItem({ isSelected = false, label, description }) {
2875
+ return /* @__PURE__ */ jsxs8(Box10, { children: [
2876
+ /* @__PURE__ */ jsx10(Text10, { color: isSelected ? "blue" : void 0, children: label.padEnd(10) }),
2877
+ description && /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
2878
+ "\u2014 ",
2879
+ description
2880
+ ] })
2881
+ ] });
2882
+ }
2883
+ function MainMenu({ onSelect }) {
2884
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(
2885
+ SelectInput3,
2886
+ {
2887
+ items: MENU_ITEMS,
2888
+ itemComponent: MenuItem,
2889
+ onSelect: (item) => onSelect(item.value)
2890
+ }
2891
+ ) });
2892
+ }
2893
+
2894
+ // src/lib/stats.ts
2895
+ import fs10 from "fs";
2896
+ function computeProjectStats(cwd) {
2897
+ if (!fs10.existsSync(getLocalDir(cwd))) {
2898
+ return null;
2899
+ }
2900
+ let statusData;
2901
+ try {
2902
+ statusData = readStatus(cwd);
2903
+ } catch (err) {
2904
+ console.warn(`Warning: could not read status file: ${err.message}`);
2905
+ return null;
2906
+ }
2907
+ const config = loadConfig(cwd);
2908
+ const resolvedCwd = cwd ?? process.cwd();
2909
+ const specs = discoverSpecs(resolvedCwd, config);
2910
+ const counts = {
2911
+ pending: 0,
2912
+ planned: 0,
2913
+ building: 0,
2914
+ done: 0
2915
+ };
2916
+ for (const spec of specs) {
2917
+ counts[spec.status]++;
2918
+ }
2919
+ let totalIterations = 0;
2920
+ let totalTokens = 0;
2921
+ for (const entry of Object.values(statusData.specs)) {
2922
+ totalIterations += entry.iterations.length;
2923
+ for (const iter of entry.iterations) {
2924
+ totalTokens += iter.tokensUsed ?? 0;
2925
+ }
2926
+ }
2927
+ return {
2928
+ totalSpecs: specs.length,
2929
+ pending: counts.pending,
2930
+ planned: counts.planned,
2931
+ building: counts.building,
2932
+ done: counts.done,
2933
+ totalIterations,
2934
+ totalTokens
2935
+ };
2936
+ }
2937
+
2938
+ // src/components/Welcome.tsx
2939
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2940
+ var NARROW_THRESHOLD = 60;
2941
+ function Welcome({ version: version2 }) {
2942
+ const { exit } = useApp4();
2943
+ const { stdout } = useStdout2();
2944
+ const [selectedCommand, setSelectedCommand] = useState8(null);
2945
+ const stats = useMemo5(() => computeProjectStats(), []);
2946
+ const isNarrow = (stdout.columns ?? 80) < NARROW_THRESHOLD;
2947
+ useEffect7(() => {
2948
+ if (selectedCommand === "status") {
2949
+ const timer = setTimeout(() => exit(), 0);
2950
+ return () => clearTimeout(timer);
2951
+ }
2952
+ }, [selectedCommand, exit]);
2953
+ if (selectedCommand === "plan") {
2954
+ return /* @__PURE__ */ jsx11(Plan, {});
2955
+ }
2956
+ if (selectedCommand === "build") {
2957
+ return /* @__PURE__ */ jsx11(Build, {});
2958
+ }
2959
+ if (selectedCommand === "status") {
2960
+ return /* @__PURE__ */ jsx11(Status, { version: version2 });
2961
+ }
2962
+ if (selectedCommand === "config") {
2963
+ return /* @__PURE__ */ jsx11(ConfigEditor, { version: version2 });
2964
+ }
2965
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", gap: 1, children: [
2966
+ isNarrow ? /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", children: [
2967
+ /* @__PURE__ */ jsxs9(Text11, { bold: true, color: "#f0a030", children: [
2968
+ "\u{1F439} toby v",
2969
+ version2
2970
+ ] }),
2971
+ stats !== null && /* @__PURE__ */ jsxs9(Text11, { children: [
2972
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Specs: " }),
2973
+ /* @__PURE__ */ jsx11(Text11, { children: stats.totalSpecs }),
2974
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \xB7 Planned: " }),
2975
+ /* @__PURE__ */ jsx11(Text11, { children: stats.planned }),
2976
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \xB7 Done: " }),
2977
+ /* @__PURE__ */ jsx11(Text11, { children: stats.done }),
2978
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \xB7 Tokens: " }),
2979
+ /* @__PURE__ */ jsx11(Text11, { children: formatTokens(stats.totalTokens) })
2980
+ ] })
2981
+ ] }) : /* @__PURE__ */ jsxs9(Box11, { flexDirection: "row", gap: 2, children: [
2982
+ /* @__PURE__ */ jsx11(HamsterWheel, {}),
2983
+ /* @__PURE__ */ jsx11(InfoPanel, { version: version2, stats })
2984
+ ] }),
2985
+ /* @__PURE__ */ jsx11(MainMenu, { onSelect: setSelectedCommand })
2986
+ ] });
2987
+ }
2988
+
2989
+ // src/cli.tsx
2990
+ import { jsx as jsx12 } from "react/jsx-runtime";
2991
+ function Help({ version: version2 }) {
2992
+ return /* @__PURE__ */ jsx12(Text12, { children: `toby v${version2} \u2014 AI-assisted development loop engine
2993
+
2994
+ Usage
2995
+ $ toby <command> [options]
2996
+
2997
+ Commands
2998
+ plan Plan specs with AI loop engine
2999
+ build Build tasks one-per-spawn with AI
3000
+ init Initialize toby in current project
3001
+ status Show project status
3002
+ config Manage configuration
3003
+
3004
+ Options
3005
+ --help Show this help
3006
+ --version Show version
3007
+
3008
+ Spec Selection
3009
+ --spec=<name> Single spec or comma-separated (e.g. --spec=auth,payments)
3010
+ --specs=<names> Alias for --spec` });
3011
+ }
3012
+ function UnknownCommand({ command: command2 }) {
3013
+ return /* @__PURE__ */ jsx12(Text12, { color: "red", children: `Unknown command: ${command2}
3014
+ Run "toby --help" for available commands.` });
3015
+ }
3016
+ var cli = meow(
3017
+ `
3018
+ Usage
3019
+ $ toby <command> [options]
3020
+
3021
+ Commands
3022
+ plan Plan specs with AI loop engine
3023
+ build Build tasks one-per-spawn with AI
3024
+ init Initialize toby in current project
3025
+ status Show project status
3026
+ config Manage configuration
3027
+
3028
+ Plan Options
3029
+ --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3030
+ --specs=<names> Alias for --spec with comma-separated specs
3031
+ --all Plan all pending specs
3032
+ --iterations=<n> Override iteration count
3033
+ --verbose Show full CLI output
3034
+ --transcript Save session transcript to file
3035
+ --cli=<name> Override AI CLI (claude, codex, opencode)
3036
+ --session=<name> Name the session for branch/PR naming
3037
+
3038
+ Build Options
3039
+ --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3040
+ --specs=<names> Alias for --spec with comma-separated specs
3041
+ --all Build all planned specs in order
3042
+ --iterations=<n> Override max iteration count
3043
+ --verbose Show full CLI output
3044
+ --transcript Save session transcript to file
3045
+ --cli=<name> Override AI CLI (claude, codex, opencode)
3046
+ --session=<name> Name the session for branch/PR naming
3047
+
3048
+ Status Options
3049
+ --spec=<query> Show status for a spec by name, slug, or number
3050
+
3051
+ Init Options
3052
+ --plan-cli=<name> Set plan CLI (claude, codex, opencode)
3053
+ --plan-model=<id> Set plan model
3054
+ --build-cli=<name> Set build CLI (claude, codex, opencode)
3055
+ --build-model=<id> Set build model
3056
+ --specs-dir=<path> Set specs directory
3057
+ --verbose Enable verbose output in config
3058
+
3059
+ Config Subcommands
3060
+ config Interactive config editor
3061
+ config get <key> Show a config value (dot-notation)
3062
+ config set <key> <value> Set a config value
3063
+ config set <k>=<v> [<k>=<v>...] Batch set config values
3064
+ `,
3065
+ {
3066
+ importMeta: import.meta,
3067
+ flags: {
3068
+ spec: { type: "string" },
3069
+ specs: { type: "string" },
3070
+ all: { type: "boolean", default: false },
3071
+ iterations: { type: "number" },
3072
+ verbose: { type: "boolean", default: false },
3073
+ transcript: { type: "boolean" },
3074
+ cli: { type: "string" },
3075
+ planCli: { type: "string" },
3076
+ planModel: { type: "string" },
3077
+ buildCli: { type: "string" },
3078
+ buildModel: { type: "string" },
3079
+ specsDir: { type: "string" },
3080
+ session: { type: "string" }
3081
+ }
3082
+ }
3083
+ );
3084
+ ensureGlobalDir();
3085
+ var resolvedSpec = cli.flags.specs ?? cli.flags.spec;
3086
+ var flags = { ...cli.flags, spec: resolvedSpec };
3087
+ var commands = {
3088
+ plan: {
3089
+ render: (flags2) => /* @__PURE__ */ jsx12(
3090
+ Plan,
3091
+ {
3092
+ spec: flags2.spec,
3093
+ all: flags2.all,
3094
+ iterations: flags2.iterations,
3095
+ verbose: flags2.verbose,
3096
+ transcript: flags2.transcript,
3097
+ cli: flags2.cli,
3098
+ session: flags2.session
3099
+ }
3100
+ ),
3101
+ waitForExit: true
3102
+ },
3103
+ build: {
3104
+ render: (flags2) => /* @__PURE__ */ jsx12(
3105
+ Build,
3106
+ {
3107
+ spec: flags2.spec,
3108
+ all: flags2.all,
3109
+ iterations: flags2.iterations,
3110
+ verbose: flags2.verbose,
3111
+ transcript: flags2.transcript,
3112
+ cli: flags2.cli,
3113
+ session: flags2.session
3114
+ }
3115
+ ),
3116
+ waitForExit: true
3117
+ },
3118
+ init: {
3119
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx12(
3120
+ Init,
3121
+ {
3122
+ version: version2,
3123
+ planCli: flags2.planCli,
3124
+ planModel: flags2.planModel,
3125
+ buildCli: flags2.buildCli,
3126
+ buildModel: flags2.buildModel,
3127
+ specsDir: flags2.specsDir,
3128
+ verbose: flags2.verbose
3129
+ }
3130
+ ),
3131
+ waitForExit: true
3132
+ },
3133
+ status: {
3134
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx12(Status, { spec: flags2.spec, version: version2 })
3135
+ },
3136
+ config: {
3137
+ render: (_flags, input, version2) => {
3138
+ const [, subcommand, ...rest] = input;
3139
+ if (!subcommand) return /* @__PURE__ */ jsx12(ConfigEditor, { version: version2 });
3140
+ if (subcommand === "set" && rest.some((arg) => arg.includes("="))) {
3141
+ return /* @__PURE__ */ jsx12(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
3142
+ }
3143
+ const [configKey, value] = rest;
3144
+ return /* @__PURE__ */ jsx12(
3145
+ Config,
3146
+ {
3147
+ subcommand,
3148
+ configKey,
3149
+ value,
3150
+ version: version2
3151
+ }
3152
+ );
3153
+ },
3154
+ waitForExit: true
3155
+ }
3156
+ };
3157
+ var version = cli.pkg.version ?? "0.0.0";
3158
+ var [command] = cli.input;
3159
+ if (!command) {
3160
+ if (process.stdin.isTTY) {
3161
+ const app = render(/* @__PURE__ */ jsx12(Welcome, { version }));
3162
+ await app.waitUntilExit();
3163
+ } else {
3164
+ render(/* @__PURE__ */ jsx12(Help, { version })).unmount();
3165
+ }
3166
+ } else if (command in commands) {
3167
+ const entry = commands[command];
3168
+ const app = render(entry.render(flags, cli.input, version));
3169
+ if (entry.waitForExit) {
3170
+ await app.waitUntilExit();
3171
+ } else {
3172
+ app.unmount();
3173
+ }
3174
+ } else {
3175
+ render(/* @__PURE__ */ jsx12(UnknownCommand, { command })).unmount();
3176
+ process.exitCode = 1;
3177
+ }