@0xtiby/toby 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +627 -238
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.tsx
4
4
  import meow from "meow";
5
- import { render, Text as Text14 } from "ink";
5
+ import { render } from "ink";
6
6
 
7
7
  // src/commands/plan.tsx
8
8
  import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2 } from "react";
@@ -65,11 +65,18 @@ var SpecStatusEntrySchema = z.object({
65
65
  iterations: z.array(IterationSchema),
66
66
  stopReason: StopReasonSchema.optional()
67
67
  });
68
+ var SessionStateSchema = z.enum(["active", "interrupted"]);
69
+ var SessionSchema = z.object({
70
+ name: z.string(),
71
+ cli: z.string(),
72
+ specs: z.array(z.string()),
73
+ state: SessionStateSchema,
74
+ startedAt: z.string().datetime()
75
+ });
68
76
  var StatusSchema = z.object({
69
77
  specs: z.record(z.string(), SpecStatusEntrySchema),
70
- sessionName: z.string().optional(),
71
- lastCli: z.string().optional()
72
- });
78
+ session: SessionSchema.optional()
79
+ }).strip();
73
80
 
74
81
  // src/lib/paths.ts
75
82
  import path from "path";
@@ -112,6 +119,243 @@ function ensureLocalDir(cwd) {
112
119
  return dir;
113
120
  }
114
121
 
122
+ // src/lib/help.ts
123
+ var SPEC_FLAGS = [
124
+ { name: "--spec=<query>", description: "Target spec(s) by name, slug, number, or list" },
125
+ { name: "--specs=<names>", description: "Alias for --spec" },
126
+ { name: "--all", description: "Process all matching specs" },
127
+ { name: "--iterations=<n>", description: "Override iteration count" },
128
+ { name: "--verbose", description: "Show full CLI output" },
129
+ { name: "--transcript", description: "Save session transcript to file" },
130
+ { name: "--cli=<name>", description: "Override AI CLI (claude, codex, opencode)" },
131
+ { name: "--session=<name>", description: "Name the session for branch/PR naming" }
132
+ ];
133
+ var commandHelp = {
134
+ plan: {
135
+ summary: "Plan specs with AI loop engine",
136
+ usage: ["$ toby plan [options]"],
137
+ flags: SPEC_FLAGS,
138
+ examples: [
139
+ {
140
+ command: "toby plan --spec=auth --cli=claude --session=auth-feature",
141
+ description: 'Plan the auth spec using Claude, naming the session "auth-feature"'
142
+ },
143
+ {
144
+ command: "toby plan --spec=auth,payments --iterations=3 --verbose",
145
+ description: "Plan auth and payments specs with 3 iterations, showing full output"
146
+ },
147
+ {
148
+ command: "toby plan --all --transcript",
149
+ description: "Plan all pending specs and save a transcript of the session"
150
+ }
151
+ ]
152
+ },
153
+ build: {
154
+ summary: "Build tasks one-per-spawn with AI",
155
+ usage: ["$ toby build [options]"],
156
+ flags: SPEC_FLAGS,
157
+ examples: [
158
+ {
159
+ command: "toby build --spec=auth --cli=claude --session=auth-feature",
160
+ description: 'Build the auth spec using Claude, resuming "auth-feature"'
161
+ },
162
+ {
163
+ command: "toby build --all --iterations=5 --transcript",
164
+ description: "Build all planned specs with up to 5 iterations, saving transcripts"
165
+ },
166
+ {
167
+ command: "toby build --spec=2 --verbose",
168
+ description: "Build spec #2 with full CLI output visible"
169
+ }
170
+ ]
171
+ },
172
+ resume: {
173
+ summary: "Resume an interrupted build session",
174
+ usage: ["$ toby resume [options]"],
175
+ flags: [
176
+ { name: "--iterations=<n>", description: "Override iteration count" },
177
+ { name: "--verbose", description: "Show full CLI output" },
178
+ { name: "--transcript", description: "Save session transcript to file" }
179
+ ],
180
+ examples: [
181
+ {
182
+ command: "toby resume",
183
+ description: "Resume the most recent interrupted session from where it left off"
184
+ },
185
+ {
186
+ command: "toby resume --iterations=10 --verbose",
187
+ description: "Resume with 10 iterations per spec and full CLI output"
188
+ },
189
+ {
190
+ command: "toby resume --transcript",
191
+ description: "Resume and save a transcript of the resumed session"
192
+ }
193
+ ]
194
+ },
195
+ init: {
196
+ summary: "Initialize toby in current project",
197
+ usage: ["$ toby init [options]"],
198
+ flags: [
199
+ {
200
+ name: "--plan-cli=<name>",
201
+ description: "Set plan CLI (claude, codex, opencode)"
202
+ },
203
+ { name: "--plan-model=<id>", description: "Set plan model" },
204
+ {
205
+ name: "--build-cli=<name>",
206
+ description: "Set build CLI (claude, codex, opencode)"
207
+ },
208
+ { name: "--build-model=<id>", description: "Set build model" },
209
+ { name: "--specs-dir=<path>", description: "Set specs directory" },
210
+ {
211
+ name: "--verbose",
212
+ description: "Enable verbose output in config"
213
+ }
214
+ ],
215
+ examples: [
216
+ {
217
+ command: "toby init",
218
+ description: "Launch the interactive setup wizard"
219
+ },
220
+ {
221
+ command: "toby init --plan-cli=claude --build-cli=claude --specs-dir=specs",
222
+ description: "Non-interactive init with required flags (for CI/agents)"
223
+ },
224
+ {
225
+ command: "toby init --plan-cli=codex --build-cli=codex --specs-dir=specs --verbose",
226
+ description: "Initialize with Codex for both phases, verbose enabled"
227
+ }
228
+ ]
229
+ },
230
+ status: {
231
+ summary: "Show project status",
232
+ usage: ["$ toby status [options]"],
233
+ flags: [
234
+ {
235
+ name: "--spec=<query>",
236
+ description: "Show status for a specific spec by name, slug, or number"
237
+ }
238
+ ],
239
+ examples: [
240
+ {
241
+ command: "toby status",
242
+ description: "Show status overview for all specs in the project"
243
+ },
244
+ {
245
+ command: "toby status --spec=auth",
246
+ description: "Show detailed status for the auth spec"
247
+ }
248
+ ]
249
+ },
250
+ config: {
251
+ summary: "Manage configuration",
252
+ usage: [
253
+ "$ toby config Interactive config editor",
254
+ "$ toby config get <key> Show a config value (dot-notation)",
255
+ "$ toby config set <key> <value> Set a config value",
256
+ "$ toby config set <k>=<v> [<k>=<v>...] Batch set values"
257
+ ],
258
+ flags: [],
259
+ examples: [
260
+ {
261
+ command: "toby config",
262
+ description: "Open the interactive config editor"
263
+ },
264
+ {
265
+ command: "toby config get plan.cli",
266
+ description: "Show the configured plan CLI"
267
+ },
268
+ {
269
+ command: "toby config set plan.cli=claude build.iterations=5",
270
+ description: "Batch set plan CLI to claude and build iterations to 5"
271
+ }
272
+ ]
273
+ },
274
+ clean: {
275
+ summary: "Delete session transcripts",
276
+ usage: ["$ toby clean [options]"],
277
+ flags: [
278
+ {
279
+ name: "--force",
280
+ description: "Skip confirmation prompt (required in non-TTY)"
281
+ }
282
+ ],
283
+ examples: [
284
+ {
285
+ command: "toby clean",
286
+ description: "Delete all transcripts with confirmation prompt"
287
+ },
288
+ {
289
+ command: "toby clean --force",
290
+ description: "Delete all transcripts without confirmation (for CI/agents)"
291
+ }
292
+ ]
293
+ }
294
+ };
295
+ function formatCommandHelp(command2, help) {
296
+ const lines = [];
297
+ lines.push(`toby ${command2} \u2014 ${help.summary}`);
298
+ lines.push("");
299
+ lines.push("Usage");
300
+ for (const usage of help.usage) {
301
+ lines.push(` ${usage}`);
302
+ }
303
+ if (help.flags.length > 0) {
304
+ lines.push("");
305
+ lines.push("Options");
306
+ const maxName = Math.max(...help.flags.map((f) => f.name.length));
307
+ for (const flag of help.flags) {
308
+ lines.push(` ${flag.name.padEnd(maxName)} ${flag.description}`);
309
+ }
310
+ }
311
+ lines.push("");
312
+ lines.push("Examples");
313
+ const exampleBlocks = help.examples.map(
314
+ (ex) => ` $ ${ex.command}
315
+ ${ex.description}`
316
+ );
317
+ lines.push(exampleBlocks.join("\n\n"));
318
+ lines.push("");
319
+ return lines.join("\n");
320
+ }
321
+ function formatErrorWithHint(message, validValues, example) {
322
+ const lines = [];
323
+ if (validValues) {
324
+ lines.push(`\u2717 ${message}. Valid options: ${validValues.join(", ")}`);
325
+ } else {
326
+ lines.push(`\u2717 ${message}`);
327
+ }
328
+ if (example) {
329
+ lines.push("");
330
+ lines.push("Example:");
331
+ lines.push(` $ ${example}`);
332
+ }
333
+ lines.push("");
334
+ return lines.join("\n");
335
+ }
336
+ function formatGlobalHelp(version2) {
337
+ const maxCmd = Math.max(
338
+ ...Object.keys(commandHelp).map((c) => c.length)
339
+ );
340
+ const cmdLines = Object.entries(commandHelp).map(([name, h]) => ` ${name.padEnd(maxCmd)} ${h.summary}`).join("\n");
341
+ return [
342
+ `toby v${version2} \u2014 AI-assisted development loop engine`,
343
+ "",
344
+ "Usage",
345
+ " $ toby <command> [options]",
346
+ "",
347
+ "Commands",
348
+ cmdLines,
349
+ "",
350
+ "Options",
351
+ " --help Show help (use with a command for details)",
352
+ " --version Show version",
353
+ "",
354
+ "Run toby <command> --help for command-specific options and examples.",
355
+ ""
356
+ ].join("\n");
357
+ }
358
+
115
359
  // src/lib/config.ts
116
360
  function readConfigFile(filePath) {
117
361
  try {
@@ -154,7 +398,13 @@ function writeConfig(config, filePath) {
154
398
  }
155
399
  function validateCliName(cli2) {
156
400
  if (cli2 && !CLI_NAMES.includes(cli2)) {
157
- throw new Error(`Unknown CLI: ${cli2}. Must be one of: ${CLI_NAMES.join(", ")}`);
401
+ throw new Error(
402
+ formatErrorWithHint(
403
+ `Unknown CLI: ${cli2}`,
404
+ [...CLI_NAMES],
405
+ "toby plan --cli=claude --spec=auth"
406
+ )
407
+ );
158
408
  }
159
409
  }
160
410
  function resolveCommandConfig(config, command2, flags2 = {}) {
@@ -539,6 +789,28 @@ function addIteration(status, specName, iteration) {
539
789
  }
540
790
  };
541
791
  }
792
+ function createSession(name, cli2, specs) {
793
+ return {
794
+ name,
795
+ cli: cli2,
796
+ specs,
797
+ state: "active",
798
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
799
+ };
800
+ }
801
+ function updateSessionState(status, state) {
802
+ if (!status.session) return status;
803
+ return {
804
+ ...status,
805
+ session: { ...status.session, state }
806
+ };
807
+ }
808
+ function clearSession(status) {
809
+ return { specs: status.specs };
810
+ }
811
+ function hasResumableSession(status) {
812
+ return status.session?.state === "active" || status.session?.state === "interrupted";
813
+ }
542
814
  function updateSpecStatus(status, specName, newStatus) {
543
815
  const entry = getSpecStatus(status, specName);
544
816
  return {
@@ -739,16 +1011,19 @@ function useCommandRunner(options) {
739
1011
  if (filtered.length === 0) {
740
1012
  setErrorMessage(emptyMessage ?? "No specs found.");
741
1013
  setPhase("error");
742
- exit();
743
1014
  return;
744
1015
  }
745
1016
  setSpecs(filtered);
746
1017
  } catch (err) {
747
1018
  setErrorMessage(err.message);
748
1019
  setPhase("error");
749
- exit(new Error(err.message));
750
1020
  }
751
1021
  }, [phase]);
1022
+ useEffect(() => {
1023
+ if (phase === "error" && errorMessage) {
1024
+ exit(new Error(errorMessage));
1025
+ }
1026
+ }, [phase, errorMessage]);
752
1027
  useEffect(() => {
753
1028
  if (phase !== "multi" || selectedSpecs.length > 0) return;
754
1029
  if (!flags2.spec) return;
@@ -1180,14 +1455,10 @@ function Plan(flags2) {
1180
1455
  import { useState as useState4, useEffect as useEffect3, useMemo as useMemo3 } from "react";
1181
1456
  import { Text as Text4, Box as Box4 } from "ink";
1182
1457
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1183
- function detectResume(specEntry, currentCli, lastCli) {
1458
+ function resolveResumeSessionId(specEntry, currentCli, sessionCli) {
1459
+ if (currentCli !== sessionCli) return void 0;
1184
1460
  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 };
1461
+ return lastIteration?.sessionId ?? void 0;
1191
1462
  }
1192
1463
  async function runSpecBuild(options) {
1193
1464
  const { spec, iterations, cli: cli2, model, cwd, callbacks } = options;
@@ -1236,7 +1507,6 @@ async function runSpecBuild(options) {
1236
1507
  tokensUsed: null
1237
1508
  };
1238
1509
  status = addIteration(status, spec.name, iterationRecord);
1239
- status = { ...status, sessionName: options.session, lastCli: cli2 };
1240
1510
  writeStatus(status, cwd);
1241
1511
  },
1242
1512
  onIterationComplete: (iterResult) => {
@@ -1314,20 +1584,22 @@ async function executeBuild(flags2, callbacks = {}, cwd = process.cwd(), abortSi
1314
1584
  if (!found) {
1315
1585
  throw new Error(`Spec '${flags2.spec}' not found`);
1316
1586
  }
1317
- const status = readStatus(cwd);
1587
+ let status = readStatus(cwd);
1318
1588
  const specEntry = status.specs[found.name];
1589
+ if (specEntry?.status === "done") {
1590
+ throw new Error(`Spec '${found.name}' is already done. Reset its status in .toby/status.json to rebuild.`);
1591
+ }
1319
1592
  if (!specEntry || specEntry.status !== "planned" && specEntry.status !== "building") {
1320
1593
  throw new Error(`No plan found for ${found.name}. Run 'toby plan --spec=${flags2.spec}' first.`);
1321
1594
  }
1322
1595
  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}"...`);
1596
+ const session = flags2.session || status.session?.name || computeSpecSlug(found.name);
1597
+ const sessionCli = status.session?.cli ?? commandConfig.cli;
1598
+ const resumeSessionId = resolveResumeSessionId(specEntry, commandConfig.cli, sessionCli);
1599
+ if (!status.session) {
1600
+ const sessionObj = createSession(session, commandConfig.cli, [found.name]);
1601
+ status = { ...status, session: sessionObj };
1602
+ writeStatus(status, cwd);
1331
1603
  }
1332
1604
  return withTranscript(
1333
1605
  { flags: flags2, config, command: "build", specName: found.name },
@@ -1343,7 +1615,7 @@ async function executeBuild(flags2, callbacks = {}, cwd = process.cwd(), abortSi
1343
1615
  templateVars: config.templateVars,
1344
1616
  specsDir: config.specsDir,
1345
1617
  session,
1346
- sessionId: resume.sessionId,
1618
+ sessionId: resumeSessionId,
1347
1619
  specIndex: 1,
1348
1620
  specCount: 1,
1349
1621
  specs: [found.name],
@@ -1352,7 +1624,14 @@ async function executeBuild(flags2, callbacks = {}, cwd = process.cwd(), abortSi
1352
1624
  callbacks,
1353
1625
  writer
1354
1626
  });
1355
- return { ...result, needsResume: resume.needsResume };
1627
+ let finalStatus = readStatus(cwd);
1628
+ if (result.specDone) {
1629
+ finalStatus = clearSession(finalStatus);
1630
+ } else {
1631
+ finalStatus = updateSessionState(finalStatus, "interrupted");
1632
+ }
1633
+ writeStatus(finalStatus, cwd);
1634
+ return result;
1356
1635
  }
1357
1636
  );
1358
1637
  }
@@ -1374,62 +1653,99 @@ async function executeBuildAll(flags2, callbacks = {}, cwd = process.cwd(), abor
1374
1653
  }
1375
1654
  const built = [];
1376
1655
  const specNames = planned.map((s) => s.name);
1377
- const status = readStatus(cwd);
1656
+ let status = readStatus(cwd);
1657
+ const buildable = planned.filter((spec) => {
1658
+ const entry = status.specs[spec.name];
1659
+ return entry?.status !== "done";
1660
+ });
1378
1661
  const commandConfig = resolveCommandConfig(config, "build", {
1379
1662
  cli: flags2.cli,
1380
1663
  iterations: flags2.iterations
1381
1664
  });
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();
1665
+ const session = flags2.session || status.session?.name || generateSessionName();
1666
+ const sessionCli = status.session?.cli ?? commandConfig.cli;
1667
+ const existingSession = status.session;
1668
+ if (!existingSession) {
1669
+ const sessionObj2 = createSession(session, commandConfig.cli, specNames);
1670
+ status = { ...status, session: sessionObj2 };
1671
+ writeStatus(status, cwd);
1672
+ } else {
1673
+ status = updateSessionState(status, "active");
1674
+ writeStatus(status, cwd);
1675
+ }
1676
+ const sessionObj = status.session;
1386
1677
  return withTranscript(
1387
1678
  { flags: { ...flags2, session: flags2.session ?? session }, config, command: "build" },
1388
1679
  void 0,
1389
1680
  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
- );
1681
+ try {
1682
+ for (let i = 0; i < buildable.length; i++) {
1683
+ const spec = buildable[i];
1684
+ const specIndex = planned.indexOf(spec) + 1;
1685
+ writer?.writeSpecHeader(specIndex, planned.length, spec.name);
1686
+ callbacks.onSpecStart?.(spec.name, specIndex - 1, planned.length);
1687
+ const specEntry = status.specs[spec.name];
1688
+ const existingIterations = specEntry?.iterations.length ?? 0;
1689
+ const resumeSessionId = resolveResumeSessionId(specEntry, commandConfig.cli, sessionCli);
1690
+ const { result } = await runSpecBuild({
1691
+ spec,
1692
+ promptName: "PROMPT_BUILD",
1693
+ existingIterations,
1694
+ iterations: commandConfig.iterations,
1695
+ cli: commandConfig.cli,
1696
+ model: commandConfig.model,
1697
+ templateVars: config.templateVars,
1698
+ specsDir: config.specsDir,
1699
+ session,
1700
+ sessionId: resumeSessionId,
1701
+ specIndex,
1702
+ specCount: planned.length,
1703
+ specs: specNames,
1704
+ cwd,
1705
+ abortSignal,
1706
+ callbacks: {
1707
+ onPhase: callbacks.onPhase,
1708
+ onIteration: callbacks.onIteration,
1709
+ onEvent: callbacks.onEvent,
1710
+ onOutput: callbacks.onOutput
1711
+ },
1712
+ writer
1713
+ });
1714
+ built.push(result);
1715
+ callbacks.onSpecComplete?.(result);
1716
+ if (!result.specDone) {
1717
+ let currentStatus = readStatus(cwd);
1718
+ currentStatus = updateSessionState(currentStatus, "interrupted");
1719
+ writeStatus(currentStatus, cwd);
1720
+ const allSpecNames = sessionObj.specs;
1721
+ const doneSpecs = allSpecNames.filter((name) => {
1722
+ return currentStatus.specs[name]?.status === "done";
1723
+ });
1724
+ const remainingSpecs = allSpecNames.filter((name) => !doneSpecs.includes(name));
1725
+ callbacks.onOutput?.(
1726
+ `Session "${sessionObj.name}" interrupted at ${spec.name} (${result.error ? "error" : "incomplete"}).`
1727
+ );
1728
+ callbacks.onOutput?.(
1729
+ `Completed: ${doneSpecs.join(", ") || "none"} (${doneSpecs.length}/${allSpecNames.length})`
1730
+ );
1731
+ callbacks.onOutput?.(
1732
+ `Remaining: ${remainingSpecs.join(", ")} (${remainingSpecs.length}/${allSpecNames.length})`
1733
+ );
1734
+ callbacks.onOutput?.("Run 'toby resume' to continue.");
1735
+ break;
1736
+ }
1406
1737
  }
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 });
1738
+ } catch (err) {
1739
+ if (err instanceof AbortError) {
1740
+ const currentStatus = readStatus(cwd);
1741
+ writeStatus(updateSessionState(currentStatus, "interrupted"), cwd);
1742
+ }
1743
+ throw err;
1744
+ }
1745
+ const finalStatus = readStatus(cwd);
1746
+ const allDone = sessionObj.specs.every((name) => finalStatus.specs[name]?.status === "done");
1747
+ if (allDone) {
1748
+ writeStatus(clearSession(finalStatus), cwd);
1433
1749
  }
1434
1750
  return { built };
1435
1751
  }
@@ -2696,13 +3012,123 @@ function Clean({ force }) {
2696
3012
  return null;
2697
3013
  }
2698
3014
 
3015
+ // src/commands/resume.tsx
3016
+ import { useState as useState9, useEffect as useEffect8, useRef as useRef2 } from "react";
3017
+ import { Box as Box9, Text as Text10, useApp as useApp5 } from "ink";
3018
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3019
+ async function executeResume(flags2, callbacks = {}, cwd = process.cwd(), abortSignal) {
3020
+ const status = readStatus(cwd);
3021
+ if (!hasResumableSession(status)) {
3022
+ throw new Error(
3023
+ "No active session to resume. Use 'toby build --spec=<name>' to start a new build."
3024
+ );
3025
+ }
3026
+ const session = status.session;
3027
+ const config = loadConfig(cwd);
3028
+ const commandConfig = resolveCommandConfig(config, "build", {
3029
+ iterations: flags2.iterations
3030
+ });
3031
+ const allSpecs = discoverSpecs(cwd, config);
3032
+ const incompleteNames = [];
3033
+ const missingNames = [];
3034
+ for (const specName of session.specs) {
3035
+ const entry = status.specs[specName];
3036
+ if (entry?.status === "done") {
3037
+ callbacks.onOutput?.(` \u2713 ${specName} (done, skipping)`);
3038
+ continue;
3039
+ }
3040
+ const found = findSpec(allSpecs, specName);
3041
+ if (!found) {
3042
+ missingNames.push(specName);
3043
+ callbacks.onOutput?.(` \u26A0 ${specName} (not found in specs/, skipping)`);
3044
+ continue;
3045
+ }
3046
+ incompleteNames.push(specName);
3047
+ }
3048
+ if (missingNames.length === session.specs.length) {
3049
+ throw new Error(
3050
+ "All session specs are missing from specs/ directory. Cannot resume."
3051
+ );
3052
+ }
3053
+ if (incompleteNames.length === 0) {
3054
+ throw new Error(
3055
+ missingNames.length > 0 ? "All remaining session specs are missing from specs/. Nothing to resume." : "All specs in this session are already done. Nothing to resume."
3056
+ );
3057
+ }
3058
+ const specsToResume = incompleteNames.map((name) => findSpec(allSpecs, name));
3059
+ callbacks.onOutput?.(`Resuming session "${session.name}" with ${specsToResume.length} spec(s):`);
3060
+ for (const spec of specsToResume) {
3061
+ callbacks.onOutput?.(` \u2192 ${spec.name}`);
3062
+ }
3063
+ const updatedStatus = updateSessionState(status, "active");
3064
+ writeStatus(updatedStatus, cwd);
3065
+ const buildFlags = {
3066
+ spec: void 0,
3067
+ all: true,
3068
+ iterations: flags2.iterations ?? commandConfig.iterations,
3069
+ verbose: flags2.verbose ?? false,
3070
+ transcript: flags2.transcript,
3071
+ cli: commandConfig.cli,
3072
+ session: session.name
3073
+ };
3074
+ return executeBuildAll(buildFlags, callbacks, cwd, abortSignal, specsToResume);
3075
+ }
3076
+ function Resume(props) {
3077
+ const { exit } = useApp5();
3078
+ const [phase, setPhase] = useState9("loading");
3079
+ const [messages, setMessages] = useState9([]);
3080
+ const [result, setResult] = useState9(null);
3081
+ const [errorMessage, setErrorMessage] = useState9("");
3082
+ const abortController = useRef2(new AbortController());
3083
+ useEffect8(() => {
3084
+ const callbacks = {
3085
+ onOutput: (msg) => setMessages((prev) => [...prev, msg])
3086
+ };
3087
+ setPhase("building");
3088
+ executeResume(props, callbacks, void 0, abortController.current.signal).then((r) => {
3089
+ setResult(r);
3090
+ setPhase("done");
3091
+ }).catch((err) => {
3092
+ if (abortController.current.signal.aborted) return;
3093
+ setErrorMessage(err.message);
3094
+ setPhase("error");
3095
+ });
3096
+ return () => {
3097
+ abortController.current.abort();
3098
+ };
3099
+ }, []);
3100
+ useEffect8(() => {
3101
+ if (phase === "done" || phase === "error") {
3102
+ const timer = setTimeout(() => exit(), 100);
3103
+ return () => clearTimeout(timer);
3104
+ }
3105
+ }, [phase, exit]);
3106
+ if (phase === "error") {
3107
+ return /* @__PURE__ */ jsx10(Text10, { color: "red", children: errorMessage });
3108
+ }
3109
+ if (phase === "done" && result) {
3110
+ const totalIter = result.built.reduce((s, r) => s + r.totalIterations, 0);
3111
+ const totalTok = result.built.reduce((s, r) => s + r.totalTokens, 0);
3112
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3113
+ messages.map((msg, i) => /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: msg }, i)),
3114
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: `\u2713 Resume complete (${result.built.length} spec(s) built)` }),
3115
+ result.built.map((r) => /* @__PURE__ */ jsx10(Text10, { children: ` ${r.specName}: ${r.totalIterations} iterations, ${r.totalTokens} tokens${r.specDone ? " [done]" : ""}` }, r.specName)),
3116
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` Total: ${totalIter} iterations, ${totalTok} tokens` })
3117
+ ] });
3118
+ }
3119
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3120
+ messages.map((msg, i) => /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: msg }, i)),
3121
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Resuming build..." })
3122
+ ] });
3123
+ }
3124
+
2699
3125
  // src/components/Welcome.tsx
2700
- import { useState as useState10, useEffect as useEffect9, useMemo as useMemo6 } from "react";
2701
- import { Box as Box12, Text as Text13, useApp as useApp5, useStdout as useStdout2 } from "ink";
3126
+ import { useState as useState11, useEffect as useEffect10, useMemo as useMemo6 } from "react";
3127
+ import { Box as Box13, Text as Text14, useApp as useApp6, useStdout as useStdout2 } from "ink";
2702
3128
 
2703
3129
  // src/components/hamster/HamsterWheel.tsx
2704
- import { useState as useState9, useEffect as useEffect8, useMemo as useMemo5 } from "react";
2705
- import { Box as Box9, Text as Text10, useStdout } from "ink";
3130
+ import { useState as useState10, useEffect as useEffect9, useMemo as useMemo5 } from "react";
3131
+ import { Box as Box10, Text as Text11, useStdout } from "ink";
2706
3132
 
2707
3133
  // src/components/hamster/palette.ts
2708
3134
  var PALETTE = {
@@ -2861,7 +3287,7 @@ function generateWheelPixels(cx, cy, outerRadius, innerRadius, spokeAngle, aspec
2861
3287
  }
2862
3288
 
2863
3289
  // src/components/hamster/HamsterWheel.tsx
2864
- import { jsx as jsx10 } from "react/jsx-runtime";
3290
+ import { jsx as jsx11 } from "react/jsx-runtime";
2865
3291
  function buildGrid(width, height) {
2866
3292
  return Array.from(
2867
3293
  { length: height },
@@ -2944,9 +3370,9 @@ function HamsterWheel({
2944
3370
  heightProp,
2945
3371
  columns
2946
3372
  );
2947
- const [frame, setFrame] = useState9(0);
2948
- const [spokeAngle, setSpokeAngle] = useState9(0);
2949
- useEffect8(() => {
3373
+ const [frame, setFrame] = useState10(0);
3374
+ const [spokeAngle, setSpokeAngle] = useState10(0);
3375
+ useEffect9(() => {
2950
3376
  if (speed === 0 || isStatic) return;
2951
3377
  const interval = computeInterval(HAMSTER_BASE_INTERVAL, speed);
2952
3378
  const id = setInterval(() => {
@@ -2954,7 +3380,7 @@ function HamsterWheel({
2954
3380
  }, interval);
2955
3381
  return () => clearInterval(id);
2956
3382
  }, [speed, isStatic]);
2957
- useEffect8(() => {
3383
+ useEffect9(() => {
2958
3384
  if (speed === 0 || isStatic) return;
2959
3385
  const interval = computeInterval(WHEEL_BASE_INTERVAL, speed);
2960
3386
  const id = setInterval(() => {
@@ -2994,60 +3420,61 @@ function HamsterWheel({
2994
3420
  return buildColorRuns(grid, resolvedWidth, resolvedHeight);
2995
3421
  }, [resolvedWidth, resolvedHeight, frame, spokeAngle, isStatic]);
2996
3422
  if (isStatic) {
2997
- return /* @__PURE__ */ jsx10(Text10, { children: " \u{1F439} toby" });
3423
+ return /* @__PURE__ */ jsx11(Text11, { children: " \u{1F439} toby" });
2998
3424
  }
2999
- return /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", children: renderedRows.map((runs, y) => /* @__PURE__ */ jsx10(Text10, { children: runs.map((run, i) => /* @__PURE__ */ jsx10(Text10, { color: run.fg, backgroundColor: run.bg, children: run.char.repeat(run.length) }, i)) }, y)) });
3425
+ return /* @__PURE__ */ jsx11(Box10, { flexDirection: "column", children: renderedRows.map((runs, y) => /* @__PURE__ */ jsx11(Text11, { children: runs.map((run, i) => /* @__PURE__ */ jsx11(Text11, { color: run.fg, backgroundColor: run.bg, children: run.char.repeat(run.length) }, i)) }, y)) });
3000
3426
  }
3001
3427
 
3002
3428
  // src/components/InfoPanel.tsx
3003
- import { Box as Box10, Text as Text11 } from "ink";
3004
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3429
+ import { Box as Box11, Text as Text12 } from "ink";
3430
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3005
3431
  var formatTokens = (n) => new Intl.NumberFormat().format(n);
3006
3432
  function StatRow({ label, value }) {
3007
- return /* @__PURE__ */ jsxs9(Box10, { children: [
3008
- /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
3433
+ return /* @__PURE__ */ jsxs10(Box11, { children: [
3434
+ /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
3009
3435
  String(label).padStart(9),
3010
3436
  " "
3011
3437
  ] }),
3012
- /* @__PURE__ */ jsx11(Text11, { children: value })
3438
+ /* @__PURE__ */ jsx12(Text12, { children: value })
3013
3439
  ] });
3014
3440
  }
3015
3441
  function InfoPanel({ version: version2, stats }) {
3016
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
3017
- /* @__PURE__ */ jsxs9(Text11, { bold: true, color: "#f0a030", children: [
3442
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
3443
+ /* @__PURE__ */ jsxs10(Text12, { bold: true, color: "#f0a030", children: [
3018
3444
  "toby v",
3019
3445
  version2
3020
3446
  ] }),
3021
- stats !== null && /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
3022
- /* @__PURE__ */ jsx11(StatRow, { label: "Specs", value: stats.totalSpecs }),
3023
- /* @__PURE__ */ jsx11(StatRow, { label: "Planned", value: stats.planned }),
3024
- /* @__PURE__ */ jsx11(StatRow, { label: "Done", value: stats.done }),
3025
- /* @__PURE__ */ jsx11(StatRow, { label: "Tokens", value: formatTokens(stats.totalTokens) })
3447
+ stats !== null && /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
3448
+ /* @__PURE__ */ jsx12(StatRow, { label: "Specs", value: stats.totalSpecs }),
3449
+ /* @__PURE__ */ jsx12(StatRow, { label: "Planned", value: stats.planned }),
3450
+ /* @__PURE__ */ jsx12(StatRow, { label: "Done", value: stats.done }),
3451
+ /* @__PURE__ */ jsx12(StatRow, { label: "Tokens", value: formatTokens(stats.totalTokens) })
3026
3452
  ] })
3027
3453
  ] });
3028
3454
  }
3029
3455
 
3030
3456
  // src/components/MainMenu.tsx
3031
- import { Text as Text12, Box as Box11 } from "ink";
3457
+ import { Text as Text13, Box as Box12 } from "ink";
3032
3458
  import SelectInput3 from "ink-select-input";
3033
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3459
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3034
3460
  var MENU_ITEMS = [
3035
3461
  { label: "plan", value: "plan", description: "Plan specs with AI loop engine" },
3036
3462
  { label: "build", value: "build", description: "Build tasks one-per-spawn with AI" },
3463
+ { label: "resume", value: "resume", description: "Resume an interrupted build session" },
3037
3464
  { label: "status", value: "status", description: "Show project status" },
3038
3465
  { label: "config", value: "config", description: "Manage configuration" }
3039
3466
  ];
3040
3467
  function MenuItem({ isSelected = false, label, description }) {
3041
- return /* @__PURE__ */ jsxs10(Box11, { children: [
3042
- /* @__PURE__ */ jsx12(Text12, { color: isSelected ? "blue" : void 0, children: label.padEnd(10) }),
3043
- description && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
3468
+ return /* @__PURE__ */ jsxs11(Box12, { children: [
3469
+ /* @__PURE__ */ jsx13(Text13, { color: isSelected ? "blue" : void 0, children: label.padEnd(10) }),
3470
+ description && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
3044
3471
  "\u2014 ",
3045
3472
  description
3046
3473
  ] })
3047
3474
  ] });
3048
3475
  }
3049
3476
  function MainMenu({ onSelect }) {
3050
- return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx12(
3477
+ return /* @__PURE__ */ jsx13(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsx13(
3051
3478
  SelectInput3,
3052
3479
  {
3053
3480
  items: MENU_ITEMS,
@@ -3102,163 +3529,102 @@ function computeProjectStats(cwd) {
3102
3529
  }
3103
3530
 
3104
3531
  // src/components/Welcome.tsx
3105
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3532
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3106
3533
  var NARROW_THRESHOLD = 60;
3107
3534
  function Welcome({ version: version2 }) {
3108
- const { exit } = useApp5();
3535
+ const { exit } = useApp6();
3109
3536
  const { stdout } = useStdout2();
3110
- const [selectedCommand, setSelectedCommand] = useState10(null);
3537
+ const [selectedCommand, setSelectedCommand] = useState11(null);
3111
3538
  const stats = useMemo6(() => computeProjectStats(), []);
3112
3539
  const isNarrow = (stdout.columns ?? 80) < NARROW_THRESHOLD;
3113
- useEffect9(() => {
3540
+ useEffect10(() => {
3114
3541
  if (selectedCommand === "status") {
3115
3542
  const timer = setTimeout(() => exit(), 0);
3116
3543
  return () => clearTimeout(timer);
3117
3544
  }
3118
3545
  }, [selectedCommand, exit]);
3119
3546
  if (selectedCommand === "plan") {
3120
- return /* @__PURE__ */ jsx13(Plan, {});
3547
+ return /* @__PURE__ */ jsx14(Plan, {});
3121
3548
  }
3122
3549
  if (selectedCommand === "build") {
3123
- return /* @__PURE__ */ jsx13(Build, {});
3550
+ return /* @__PURE__ */ jsx14(Build, {});
3551
+ }
3552
+ if (selectedCommand === "resume") {
3553
+ return /* @__PURE__ */ jsx14(Resume, {});
3124
3554
  }
3125
3555
  if (selectedCommand === "status") {
3126
- return /* @__PURE__ */ jsx13(Status, { version: version2 });
3556
+ return /* @__PURE__ */ jsx14(Status, { version: version2 });
3127
3557
  }
3128
3558
  if (selectedCommand === "config") {
3129
- return /* @__PURE__ */ jsx13(ConfigEditor, { version: version2 });
3559
+ return /* @__PURE__ */ jsx14(ConfigEditor, { version: version2 });
3130
3560
  }
3131
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
3132
- isNarrow ? /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", children: [
3133
- /* @__PURE__ */ jsxs11(Text13, { bold: true, color: "#f0a030", children: [
3561
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
3562
+ isNarrow ? /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
3563
+ /* @__PURE__ */ jsxs12(Text14, { bold: true, color: "#f0a030", children: [
3134
3564
  "\u{1F439} toby v",
3135
3565
  version2
3136
3566
  ] }),
3137
- stats !== null && /* @__PURE__ */ jsxs11(Text13, { children: [
3138
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Specs: " }),
3139
- /* @__PURE__ */ jsx13(Text13, { children: stats.totalSpecs }),
3140
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Planned: " }),
3141
- /* @__PURE__ */ jsx13(Text13, { children: stats.planned }),
3142
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Done: " }),
3143
- /* @__PURE__ */ jsx13(Text13, { children: stats.done }),
3144
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Tokens: " }),
3145
- /* @__PURE__ */ jsx13(Text13, { children: formatTokens(stats.totalTokens) })
3567
+ stats !== null && /* @__PURE__ */ jsxs12(Text14, { children: [
3568
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "Specs: " }),
3569
+ /* @__PURE__ */ jsx14(Text14, { children: stats.totalSpecs }),
3570
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: " \xB7 Planned: " }),
3571
+ /* @__PURE__ */ jsx14(Text14, { children: stats.planned }),
3572
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: " \xB7 Done: " }),
3573
+ /* @__PURE__ */ jsx14(Text14, { children: stats.done }),
3574
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: " \xB7 Tokens: " }),
3575
+ /* @__PURE__ */ jsx14(Text14, { children: formatTokens(stats.totalTokens) })
3146
3576
  ] })
3147
- ] }) : /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 2, children: [
3148
- /* @__PURE__ */ jsx13(HamsterWheel, {}),
3149
- /* @__PURE__ */ jsx13(InfoPanel, { version: version2, stats })
3577
+ ] }) : /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 2, children: [
3578
+ /* @__PURE__ */ jsx14(HamsterWheel, {}),
3579
+ /* @__PURE__ */ jsx14(InfoPanel, { version: version2, stats })
3150
3580
  ] }),
3151
- /* @__PURE__ */ jsx13(MainMenu, { onSelect: setSelectedCommand })
3581
+ /* @__PURE__ */ jsx14(MainMenu, { onSelect: setSelectedCommand })
3152
3582
  ] });
3153
3583
  }
3154
3584
 
3155
- // src/cli.tsx
3156
- import { jsx as jsx14 } from "react/jsx-runtime";
3157
- function Help({ version: version2 }) {
3158
- return /* @__PURE__ */ jsx14(Text14, { children: `toby v${version2} \u2014 AI-assisted development loop engine
3159
-
3160
- Usage
3161
- $ toby <command> [options]
3162
-
3163
- Commands
3164
- plan Plan specs with AI loop engine
3165
- build Build tasks one-per-spawn with AI
3166
- init Initialize toby in current project
3167
- status Show project status
3168
- config Manage configuration
3169
- clean Delete all transcript files
3170
-
3171
- Options
3172
- --help Show this help
3173
- --version Show version
3174
-
3175
- Spec Selection
3176
- --spec=<name> Single spec or comma-separated (e.g. --spec=auth,payments)
3177
- --specs=<names> Alias for --spec` });
3178
- }
3179
- function UnknownCommand({ command: command2 }) {
3180
- return /* @__PURE__ */ jsx14(Text14, { color: "red", children: `Unknown command: ${command2}
3181
- Run "toby --help" for available commands.` });
3182
- }
3183
- var cli = meow(
3184
- `
3185
- Usage
3186
- $ toby <command> [options]
3187
-
3188
- Commands
3189
- plan Plan specs with AI loop engine
3190
- build Build tasks one-per-spawn with AI
3191
- init Initialize toby in current project
3192
- status Show project status
3193
- config Manage configuration
3194
- clean Delete all transcript files
3195
-
3196
- Plan Options
3197
- --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3198
- --specs=<names> Alias for --spec with comma-separated specs
3199
- --all Plan all pending specs
3200
- --iterations=<n> Override iteration count
3201
- --verbose Show full CLI output
3202
- --transcript Save session transcript to file
3203
- --cli=<name> Override AI CLI (claude, codex, opencode)
3204
- --session=<name> Name the session for branch/PR naming
3205
-
3206
- Build Options
3207
- --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3208
- --specs=<names> Alias for --spec with comma-separated specs
3209
- --all Build all planned specs in order
3210
- --iterations=<n> Override max iteration count
3211
- --verbose Show full CLI output
3212
- --transcript Save session transcript to file
3213
- --cli=<name> Override AI CLI (claude, codex, opencode)
3214
- --session=<name> Name the session for branch/PR naming
3215
-
3216
- Status Options
3217
- --spec=<query> Show status for a spec by name, slug, or number
3218
-
3219
- Init Options
3220
- --plan-cli=<name> Set plan CLI (claude, codex, opencode)
3221
- --plan-model=<id> Set plan model
3222
- --build-cli=<name> Set build CLI (claude, codex, opencode)
3223
- --build-model=<id> Set build model
3224
- --specs-dir=<path> Set specs directory
3225
- --verbose Enable verbose output in config
3226
-
3227
- Config Subcommands
3228
- config Interactive config editor
3229
- config get <key> Show a config value (dot-notation)
3230
- config set <key> <value> Set a config value
3231
- config set <k>=<v> [<k>=<v>...] Batch set config values
3585
+ // src/lib/cli-meta.ts
3586
+ var COMMAND_NAMES = Object.keys(commandHelp);
3587
+ var MEOW_FLAGS = {
3588
+ help: { type: "boolean", default: false },
3589
+ spec: { type: "string" },
3590
+ specs: { type: "string" },
3591
+ all: { type: "boolean", default: false },
3592
+ iterations: { type: "number" },
3593
+ verbose: { type: "boolean", default: false },
3594
+ transcript: { type: "boolean" },
3595
+ cli: { type: "string" },
3596
+ planCli: { type: "string" },
3597
+ planModel: { type: "string" },
3598
+ buildCli: { type: "string" },
3599
+ buildModel: { type: "string" },
3600
+ specsDir: { type: "string" },
3601
+ session: { type: "string" },
3602
+ force: { type: "boolean", default: false }
3603
+ };
3604
+ var MEOW_FLAG_NAMES = Object.keys(MEOW_FLAGS);
3232
3605
 
3233
- Clean Options
3234
- --force Skip confirmation prompt
3235
- `,
3236
- {
3237
- importMeta: import.meta,
3238
- flags: {
3239
- spec: { type: "string" },
3240
- specs: { type: "string" },
3241
- all: { type: "boolean", default: false },
3242
- iterations: { type: "number" },
3243
- verbose: { type: "boolean", default: false },
3244
- transcript: { type: "boolean" },
3245
- cli: { type: "string" },
3246
- planCli: { type: "string" },
3247
- planModel: { type: "string" },
3248
- buildCli: { type: "string" },
3249
- buildModel: { type: "string" },
3250
- specsDir: { type: "string" },
3251
- session: { type: "string" },
3252
- force: { type: "boolean", default: false }
3253
- }
3254
- }
3255
- );
3606
+ // src/cli.tsx
3607
+ import { jsx as jsx15 } from "react/jsx-runtime";
3608
+ function writeUnknownCommandError(command2) {
3609
+ process.stderr.write(
3610
+ formatErrorWithHint(
3611
+ `Unknown command: ${command2}`,
3612
+ COMMAND_NAMES,
3613
+ "toby --help"
3614
+ )
3615
+ );
3616
+ }
3617
+ var cli = meow("", {
3618
+ importMeta: import.meta,
3619
+ autoHelp: false,
3620
+ flags: MEOW_FLAGS
3621
+ });
3256
3622
  ensureGlobalDir();
3257
3623
  var resolvedSpec = cli.flags.specs ?? cli.flags.spec;
3258
3624
  var flags = { ...cli.flags, spec: resolvedSpec };
3259
3625
  var commands = {
3260
3626
  plan: {
3261
- render: (flags2) => /* @__PURE__ */ jsx14(
3627
+ render: (flags2) => /* @__PURE__ */ jsx15(
3262
3628
  Plan,
3263
3629
  {
3264
3630
  spec: flags2.spec,
@@ -3273,7 +3639,7 @@ var commands = {
3273
3639
  waitForExit: true
3274
3640
  },
3275
3641
  build: {
3276
- render: (flags2) => /* @__PURE__ */ jsx14(
3642
+ render: (flags2) => /* @__PURE__ */ jsx15(
3277
3643
  Build,
3278
3644
  {
3279
3645
  spec: flags2.spec,
@@ -3288,7 +3654,7 @@ var commands = {
3288
3654
  waitForExit: true
3289
3655
  },
3290
3656
  init: {
3291
- render: (flags2, _input, version2) => /* @__PURE__ */ jsx14(
3657
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx15(
3292
3658
  Init,
3293
3659
  {
3294
3660
  version: version2,
@@ -3303,21 +3669,32 @@ var commands = {
3303
3669
  waitForExit: true
3304
3670
  },
3305
3671
  status: {
3306
- render: (flags2, _input, version2) => /* @__PURE__ */ jsx14(Status, { spec: flags2.spec, version: version2 })
3672
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx15(Status, { spec: flags2.spec, version: version2 })
3673
+ },
3674
+ resume: {
3675
+ render: (flags2) => /* @__PURE__ */ jsx15(
3676
+ Resume,
3677
+ {
3678
+ iterations: flags2.iterations,
3679
+ verbose: flags2.verbose,
3680
+ transcript: flags2.transcript
3681
+ }
3682
+ ),
3683
+ waitForExit: true
3307
3684
  },
3308
3685
  clean: {
3309
- render: (flags2) => /* @__PURE__ */ jsx14(Clean, { force: flags2.force }),
3686
+ render: (flags2) => /* @__PURE__ */ jsx15(Clean, { force: flags2.force }),
3310
3687
  waitForExit: true
3311
3688
  },
3312
3689
  config: {
3313
3690
  render: (_flags, input, version2) => {
3314
3691
  const [, subcommand, ...rest] = input;
3315
- if (!subcommand) return /* @__PURE__ */ jsx14(ConfigEditor, { version: version2 });
3692
+ if (!subcommand) return /* @__PURE__ */ jsx15(ConfigEditor, { version: version2 });
3316
3693
  if (subcommand === "set" && rest.some((arg) => arg.includes("="))) {
3317
- return /* @__PURE__ */ jsx14(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
3694
+ return /* @__PURE__ */ jsx15(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
3318
3695
  }
3319
3696
  const [configKey, value] = rest;
3320
- return /* @__PURE__ */ jsx14(
3697
+ return /* @__PURE__ */ jsx15(
3321
3698
  Config,
3322
3699
  {
3323
3700
  subcommand,
@@ -3332,12 +3709,24 @@ var commands = {
3332
3709
  };
3333
3710
  var version = cli.pkg.version ?? "0.0.0";
3334
3711
  var [command] = cli.input;
3335
- if (!command) {
3712
+ if (cli.flags.help) {
3713
+ if (!command || command in commands) {
3714
+ if (command && command in commandHelp) {
3715
+ process.stdout.write(formatCommandHelp(command, commandHelp[command]));
3716
+ } else {
3717
+ process.stdout.write(formatGlobalHelp(version));
3718
+ }
3719
+ process.exitCode = 0;
3720
+ } else {
3721
+ writeUnknownCommandError(command);
3722
+ process.exitCode = 1;
3723
+ }
3724
+ } else if (!command) {
3336
3725
  if (process.stdin.isTTY) {
3337
- const app = render(/* @__PURE__ */ jsx14(Welcome, { version }));
3726
+ const app = render(/* @__PURE__ */ jsx15(Welcome, { version }));
3338
3727
  await app.waitUntilExit();
3339
3728
  } else {
3340
- render(/* @__PURE__ */ jsx14(Help, { version })).unmount();
3729
+ process.stdout.write(formatGlobalHelp(version));
3341
3730
  }
3342
3731
  } else if (command in commands) {
3343
3732
  const entry = commands[command];
@@ -3348,6 +3737,6 @@ if (!command) {
3348
3737
  app.unmount();
3349
3738
  }
3350
3739
  } else {
3351
- render(/* @__PURE__ */ jsx14(UnknownCommand, { command })).unmount();
3740
+ writeUnknownCommandError(command);
3352
3741
  process.exitCode = 1;
3353
3742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xtiby/toby",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI-assisted development loop engine CLI",
5
5
  "repository": {
6
6
  "type": "git",