@0xtiby/toby 1.0.1 → 1.2.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 +457 -165
  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 Text13 } 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";
@@ -80,6 +80,7 @@ var LOCAL_TOBY_DIR = ".toby";
80
80
  var DEFAULT_SPECS_DIR = "specs";
81
81
  var STATUS_FILE = "status.json";
82
82
  var CONFIG_FILE = "config.json";
83
+ var TRANSCRIPTS_DIR = "transcripts";
83
84
  function getGlobalDir() {
84
85
  return path.join(os.homedir(), GLOBAL_TOBY_DIR);
85
86
  }
@@ -111,6 +112,220 @@ function ensureLocalDir(cwd) {
111
112
  return dir;
112
113
  }
113
114
 
115
+ // src/lib/help.ts
116
+ var SPEC_FLAGS = [
117
+ { name: "--spec=<query>", description: "Target spec(s) by name, slug, number, or list" },
118
+ { name: "--specs=<names>", description: "Alias for --spec" },
119
+ { name: "--all", description: "Process all matching specs" },
120
+ { name: "--iterations=<n>", description: "Override iteration count" },
121
+ { name: "--verbose", description: "Show full CLI output" },
122
+ { name: "--transcript", description: "Save session transcript to file" },
123
+ { name: "--cli=<name>", description: "Override AI CLI (claude, codex, opencode)" },
124
+ { name: "--session=<name>", description: "Name the session for branch/PR naming" }
125
+ ];
126
+ var commandHelp = {
127
+ plan: {
128
+ summary: "Plan specs with AI loop engine",
129
+ usage: ["$ toby plan [options]"],
130
+ flags: SPEC_FLAGS,
131
+ examples: [
132
+ {
133
+ command: "toby plan --spec=auth --cli=claude --session=auth-feature",
134
+ description: 'Plan the auth spec using Claude, naming the session "auth-feature"'
135
+ },
136
+ {
137
+ command: "toby plan --spec=auth,payments --iterations=3 --verbose",
138
+ description: "Plan auth and payments specs with 3 iterations, showing full output"
139
+ },
140
+ {
141
+ command: "toby plan --all --transcript",
142
+ description: "Plan all pending specs and save a transcript of the session"
143
+ }
144
+ ]
145
+ },
146
+ build: {
147
+ summary: "Build tasks one-per-spawn with AI",
148
+ usage: ["$ toby build [options]"],
149
+ flags: SPEC_FLAGS,
150
+ examples: [
151
+ {
152
+ command: "toby build --spec=auth --cli=claude --session=auth-feature",
153
+ description: 'Build the auth spec using Claude, resuming "auth-feature"'
154
+ },
155
+ {
156
+ command: "toby build --all --iterations=5 --transcript",
157
+ description: "Build all planned specs with up to 5 iterations, saving transcripts"
158
+ },
159
+ {
160
+ command: "toby build --spec=2 --verbose",
161
+ description: "Build spec #2 with full CLI output visible"
162
+ }
163
+ ]
164
+ },
165
+ init: {
166
+ summary: "Initialize toby in current project",
167
+ usage: ["$ toby init [options]"],
168
+ flags: [
169
+ {
170
+ name: "--plan-cli=<name>",
171
+ description: "Set plan CLI (claude, codex, opencode)"
172
+ },
173
+ { name: "--plan-model=<id>", description: "Set plan model" },
174
+ {
175
+ name: "--build-cli=<name>",
176
+ description: "Set build CLI (claude, codex, opencode)"
177
+ },
178
+ { name: "--build-model=<id>", description: "Set build model" },
179
+ { name: "--specs-dir=<path>", description: "Set specs directory" },
180
+ {
181
+ name: "--verbose",
182
+ description: "Enable verbose output in config"
183
+ }
184
+ ],
185
+ examples: [
186
+ {
187
+ command: "toby init",
188
+ description: "Launch the interactive setup wizard"
189
+ },
190
+ {
191
+ command: "toby init --plan-cli=claude --build-cli=claude --specs-dir=specs",
192
+ description: "Non-interactive init with required flags (for CI/agents)"
193
+ },
194
+ {
195
+ command: "toby init --plan-cli=codex --build-cli=codex --specs-dir=specs --verbose",
196
+ description: "Initialize with Codex for both phases, verbose enabled"
197
+ }
198
+ ]
199
+ },
200
+ status: {
201
+ summary: "Show project status",
202
+ usage: ["$ toby status [options]"],
203
+ flags: [
204
+ {
205
+ name: "--spec=<query>",
206
+ description: "Show status for a specific spec by name, slug, or number"
207
+ }
208
+ ],
209
+ examples: [
210
+ {
211
+ command: "toby status",
212
+ description: "Show status overview for all specs in the project"
213
+ },
214
+ {
215
+ command: "toby status --spec=auth",
216
+ description: "Show detailed status for the auth spec"
217
+ }
218
+ ]
219
+ },
220
+ config: {
221
+ summary: "Manage configuration",
222
+ usage: [
223
+ "$ toby config Interactive config editor",
224
+ "$ toby config get <key> Show a config value (dot-notation)",
225
+ "$ toby config set <key> <value> Set a config value",
226
+ "$ toby config set <k>=<v> [<k>=<v>...] Batch set values"
227
+ ],
228
+ flags: [],
229
+ examples: [
230
+ {
231
+ command: "toby config",
232
+ description: "Open the interactive config editor"
233
+ },
234
+ {
235
+ command: "toby config get plan.cli",
236
+ description: "Show the configured plan CLI"
237
+ },
238
+ {
239
+ command: "toby config set plan.cli=claude build.iterations=5",
240
+ description: "Batch set plan CLI to claude and build iterations to 5"
241
+ }
242
+ ]
243
+ },
244
+ clean: {
245
+ summary: "Delete session transcripts",
246
+ usage: ["$ toby clean [options]"],
247
+ flags: [
248
+ {
249
+ name: "--force",
250
+ description: "Skip confirmation prompt (required in non-TTY)"
251
+ }
252
+ ],
253
+ examples: [
254
+ {
255
+ command: "toby clean",
256
+ description: "Delete all transcripts with confirmation prompt"
257
+ },
258
+ {
259
+ command: "toby clean --force",
260
+ description: "Delete all transcripts without confirmation (for CI/agents)"
261
+ }
262
+ ]
263
+ }
264
+ };
265
+ function formatCommandHelp(command2, help) {
266
+ const lines = [];
267
+ lines.push(`toby ${command2} \u2014 ${help.summary}`);
268
+ lines.push("");
269
+ lines.push("Usage");
270
+ for (const usage of help.usage) {
271
+ lines.push(` ${usage}`);
272
+ }
273
+ if (help.flags.length > 0) {
274
+ lines.push("");
275
+ lines.push("Options");
276
+ const maxName = Math.max(...help.flags.map((f) => f.name.length));
277
+ for (const flag of help.flags) {
278
+ lines.push(` ${flag.name.padEnd(maxName)} ${flag.description}`);
279
+ }
280
+ }
281
+ lines.push("");
282
+ lines.push("Examples");
283
+ const exampleBlocks = help.examples.map(
284
+ (ex) => ` $ ${ex.command}
285
+ ${ex.description}`
286
+ );
287
+ lines.push(exampleBlocks.join("\n\n"));
288
+ lines.push("");
289
+ return lines.join("\n");
290
+ }
291
+ function formatErrorWithHint(message, validValues, example) {
292
+ const lines = [];
293
+ if (validValues) {
294
+ lines.push(`\u2717 ${message}. Valid options: ${validValues.join(", ")}`);
295
+ } else {
296
+ lines.push(`\u2717 ${message}`);
297
+ }
298
+ if (example) {
299
+ lines.push("");
300
+ lines.push("Example:");
301
+ lines.push(` $ ${example}`);
302
+ }
303
+ lines.push("");
304
+ return lines.join("\n");
305
+ }
306
+ function formatGlobalHelp(version2) {
307
+ const maxCmd = Math.max(
308
+ ...Object.keys(commandHelp).map((c) => c.length)
309
+ );
310
+ const cmdLines = Object.entries(commandHelp).map(([name, h]) => ` ${name.padEnd(maxCmd)} ${h.summary}`).join("\n");
311
+ return [
312
+ `toby v${version2} \u2014 AI-assisted development loop engine`,
313
+ "",
314
+ "Usage",
315
+ " $ toby <command> [options]",
316
+ "",
317
+ "Commands",
318
+ cmdLines,
319
+ "",
320
+ "Options",
321
+ " --help Show help (use with a command for details)",
322
+ " --version Show version",
323
+ "",
324
+ "Run toby <command> --help for command-specific options and examples.",
325
+ ""
326
+ ].join("\n");
327
+ }
328
+
114
329
  // src/lib/config.ts
115
330
  function readConfigFile(filePath) {
116
331
  try {
@@ -153,7 +368,13 @@ function writeConfig(config, filePath) {
153
368
  }
154
369
  function validateCliName(cli2) {
155
370
  if (cli2 && !CLI_NAMES.includes(cli2)) {
156
- throw new Error(`Unknown CLI: ${cli2}. Must be one of: ${CLI_NAMES.join(", ")}`);
371
+ throw new Error(
372
+ formatErrorWithHint(
373
+ `Unknown CLI: ${cli2}`,
374
+ [...CLI_NAMES],
375
+ "toby plan --cli=claude --spec=auth"
376
+ )
377
+ );
157
378
  }
158
379
  }
159
380
  function resolveCommandConfig(config, command2, flags2 = {}) {
@@ -567,7 +788,6 @@ var AbortError = class extends Error {
567
788
  // src/lib/transcript.ts
568
789
  import fs6 from "fs";
569
790
  import path6 from "path";
570
- var TRANSCRIPTS_DIR = "transcripts";
571
791
  function formatTimestamp() {
572
792
  const now = /* @__PURE__ */ new Date();
573
793
  const pad2 = (n, len = 2) => String(n).padStart(len, "0");
@@ -2582,13 +2802,127 @@ Usage: toby config set <key> <value>` });
2582
2802
  ] });
2583
2803
  }
2584
2804
 
2805
+ // src/commands/clean.tsx
2806
+ import { useState as useState8, useEffect as useEffect7, useMemo as useMemo4 } from "react";
2807
+ import { Text as Text9, useApp as useApp4, useInput as useInput2 } from "ink";
2808
+
2809
+ // src/lib/clean.ts
2810
+ import path9 from "path";
2811
+ import fs10 from "fs";
2812
+ function listTranscripts(cwd) {
2813
+ const dir = path9.join(getLocalDir(cwd), TRANSCRIPTS_DIR);
2814
+ let entries;
2815
+ try {
2816
+ entries = fs10.readdirSync(dir, { withFileTypes: true });
2817
+ } catch {
2818
+ return [];
2819
+ }
2820
+ return entries.filter((e) => e.isFile()).map((e) => path9.join(dir, e.name));
2821
+ }
2822
+ function deleteTranscripts(files) {
2823
+ let deleted = 0;
2824
+ for (const file of files) {
2825
+ try {
2826
+ fs10.unlinkSync(file);
2827
+ deleted++;
2828
+ } catch {
2829
+ }
2830
+ }
2831
+ return deleted;
2832
+ }
2833
+ function executeClean(cwd) {
2834
+ const files = listTranscripts(cwd);
2835
+ if (files.length === 0) {
2836
+ return { deleted: 0, failed: 0, total: 0 };
2837
+ }
2838
+ const deleted = deleteTranscripts(files);
2839
+ return { deleted, failed: files.length - deleted, total: files.length };
2840
+ }
2841
+
2842
+ // src/commands/clean.tsx
2843
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2844
+ function computeInitial(force) {
2845
+ const files = listTranscripts();
2846
+ if (files.length === 0) {
2847
+ return { phase: "empty", fileCount: 0, result: null };
2848
+ }
2849
+ if (!process.stdin.isTTY && !force) {
2850
+ return { phase: "error", fileCount: files.length, result: null };
2851
+ }
2852
+ if (force) {
2853
+ const r = executeClean();
2854
+ return { phase: "done", fileCount: files.length, result: r };
2855
+ }
2856
+ return { phase: "confirming", fileCount: files.length, result: null };
2857
+ }
2858
+ function Clean({ force }) {
2859
+ const { exit } = useApp4();
2860
+ const initial = useMemo4(() => computeInitial(force), [force]);
2861
+ useEffect7(() => {
2862
+ if (initial.phase === "error") {
2863
+ process.exitCode = 1;
2864
+ }
2865
+ }, [initial.phase]);
2866
+ const [phase, setPhase] = useState8(initial.phase);
2867
+ const [result, setResult] = useState8(initial.result);
2868
+ useEffect7(() => {
2869
+ if (phase === "empty" || phase === "done" || phase === "cancelled" || phase === "error") {
2870
+ const timer = setTimeout(() => exit(), 100);
2871
+ return () => clearTimeout(timer);
2872
+ }
2873
+ }, [phase, exit]);
2874
+ useInput2((input, key) => {
2875
+ if (phase !== "confirming") return;
2876
+ if (input === "y" || key.return) {
2877
+ const r = executeClean();
2878
+ setResult(r);
2879
+ setPhase("done");
2880
+ } else if (input === "n" || key.escape) {
2881
+ setPhase("cancelled");
2882
+ }
2883
+ });
2884
+ if (phase === "error") {
2885
+ return /* @__PURE__ */ jsx9(Text9, { color: "red", children: "Error: Use --force to clean transcripts in non-interactive mode." });
2886
+ }
2887
+ if (phase === "empty") {
2888
+ return /* @__PURE__ */ jsx9(Text9, { children: "No transcripts to clean." });
2889
+ }
2890
+ if (phase === "confirming") {
2891
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
2892
+ "Found ",
2893
+ initial.fileCount,
2894
+ " transcript files. Delete all? [Y/n]"
2895
+ ] });
2896
+ }
2897
+ if (phase === "done" && result) {
2898
+ if (result.failed > 0) {
2899
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
2900
+ "Deleted ",
2901
+ result.deleted,
2902
+ " transcript files. Failed to delete ",
2903
+ result.failed,
2904
+ " files."
2905
+ ] });
2906
+ }
2907
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
2908
+ "Deleted ",
2909
+ result.deleted,
2910
+ " transcript files."
2911
+ ] });
2912
+ }
2913
+ if (phase === "cancelled") {
2914
+ return /* @__PURE__ */ jsx9(Text9, { children: "Clean cancelled." });
2915
+ }
2916
+ return null;
2917
+ }
2918
+
2585
2919
  // src/components/Welcome.tsx
2586
- import { useState as useState9, useEffect as useEffect8, useMemo as useMemo5 } from "react";
2587
- import { Box as Box12, Text as Text12, useApp as useApp4, useStdout as useStdout2 } from "ink";
2920
+ import { useState as useState10, useEffect as useEffect9, useMemo as useMemo6 } from "react";
2921
+ import { Box as Box12, Text as Text13, useApp as useApp5, useStdout as useStdout2 } from "ink";
2588
2922
 
2589
2923
  // src/components/hamster/HamsterWheel.tsx
2590
- import { useState as useState8, useEffect as useEffect7, useMemo as useMemo4 } from "react";
2591
- import { Box as Box9, Text as Text9, useStdout } from "ink";
2924
+ import { useState as useState9, useEffect as useEffect8, useMemo as useMemo5 } from "react";
2925
+ import { Box as Box9, Text as Text10, useStdout } from "ink";
2592
2926
 
2593
2927
  // src/components/hamster/palette.ts
2594
2928
  var PALETTE = {
@@ -2747,7 +3081,7 @@ function generateWheelPixels(cx, cy, outerRadius, innerRadius, spokeAngle, aspec
2747
3081
  }
2748
3082
 
2749
3083
  // src/components/hamster/HamsterWheel.tsx
2750
- import { jsx as jsx9 } from "react/jsx-runtime";
3084
+ import { jsx as jsx10 } from "react/jsx-runtime";
2751
3085
  function buildGrid(width, height) {
2752
3086
  return Array.from(
2753
3087
  { length: height },
@@ -2830,9 +3164,9 @@ function HamsterWheel({
2830
3164
  heightProp,
2831
3165
  columns
2832
3166
  );
2833
- const [frame, setFrame] = useState8(0);
2834
- const [spokeAngle, setSpokeAngle] = useState8(0);
2835
- useEffect7(() => {
3167
+ const [frame, setFrame] = useState9(0);
3168
+ const [spokeAngle, setSpokeAngle] = useState9(0);
3169
+ useEffect8(() => {
2836
3170
  if (speed === 0 || isStatic) return;
2837
3171
  const interval = computeInterval(HAMSTER_BASE_INTERVAL, speed);
2838
3172
  const id = setInterval(() => {
@@ -2840,7 +3174,7 @@ function HamsterWheel({
2840
3174
  }, interval);
2841
3175
  return () => clearInterval(id);
2842
3176
  }, [speed, isStatic]);
2843
- useEffect7(() => {
3177
+ useEffect8(() => {
2844
3178
  if (speed === 0 || isStatic) return;
2845
3179
  const interval = computeInterval(WHEEL_BASE_INTERVAL, speed);
2846
3180
  const id = setInterval(() => {
@@ -2848,7 +3182,7 @@ function HamsterWheel({
2848
3182
  }, interval);
2849
3183
  return () => clearInterval(id);
2850
3184
  }, [speed, isStatic]);
2851
- const renderedRows = useMemo4(() => {
3185
+ const renderedRows = useMemo5(() => {
2852
3186
  if (isStatic) return [];
2853
3187
  const grid = buildGrid(resolvedWidth, resolvedHeight);
2854
3188
  const { cx, cy, outerRadius, innerRadius } = computeWheelGeometry(
@@ -2880,43 +3214,43 @@ function HamsterWheel({
2880
3214
  return buildColorRuns(grid, resolvedWidth, resolvedHeight);
2881
3215
  }, [resolvedWidth, resolvedHeight, frame, spokeAngle, isStatic]);
2882
3216
  if (isStatic) {
2883
- return /* @__PURE__ */ jsx9(Text9, { children: " \u{1F439} toby" });
3217
+ return /* @__PURE__ */ jsx10(Text10, { children: " \u{1F439} toby" });
2884
3218
  }
2885
- return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: renderedRows.map((runs, y) => /* @__PURE__ */ jsx9(Text9, { children: runs.map((run, i) => /* @__PURE__ */ jsx9(Text9, { color: run.fg, backgroundColor: run.bg, children: run.char.repeat(run.length) }, i)) }, y)) });
3219
+ 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)) });
2886
3220
  }
2887
3221
 
2888
3222
  // src/components/InfoPanel.tsx
2889
- import { Box as Box10, Text as Text10 } from "ink";
2890
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
3223
+ import { Box as Box10, Text as Text11 } from "ink";
3224
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2891
3225
  var formatTokens = (n) => new Intl.NumberFormat().format(n);
2892
3226
  function StatRow({ label, value }) {
2893
- return /* @__PURE__ */ jsxs8(Box10, { children: [
2894
- /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
3227
+ return /* @__PURE__ */ jsxs9(Box10, { children: [
3228
+ /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
2895
3229
  String(label).padStart(9),
2896
3230
  " "
2897
3231
  ] }),
2898
- /* @__PURE__ */ jsx10(Text10, { children: value })
3232
+ /* @__PURE__ */ jsx11(Text11, { children: value })
2899
3233
  ] });
2900
3234
  }
2901
3235
  function InfoPanel({ version: version2, stats }) {
2902
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", children: [
2903
- /* @__PURE__ */ jsxs8(Text10, { bold: true, color: "#f0a030", children: [
3236
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
3237
+ /* @__PURE__ */ jsxs9(Text11, { bold: true, color: "#f0a030", children: [
2904
3238
  "toby v",
2905
3239
  version2
2906
3240
  ] }),
2907
- stats !== null && /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", marginTop: 1, children: [
2908
- /* @__PURE__ */ jsx10(StatRow, { label: "Specs", value: stats.totalSpecs }),
2909
- /* @__PURE__ */ jsx10(StatRow, { label: "Planned", value: stats.planned }),
2910
- /* @__PURE__ */ jsx10(StatRow, { label: "Done", value: stats.done }),
2911
- /* @__PURE__ */ jsx10(StatRow, { label: "Tokens", value: formatTokens(stats.totalTokens) })
3241
+ stats !== null && /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
3242
+ /* @__PURE__ */ jsx11(StatRow, { label: "Specs", value: stats.totalSpecs }),
3243
+ /* @__PURE__ */ jsx11(StatRow, { label: "Planned", value: stats.planned }),
3244
+ /* @__PURE__ */ jsx11(StatRow, { label: "Done", value: stats.done }),
3245
+ /* @__PURE__ */ jsx11(StatRow, { label: "Tokens", value: formatTokens(stats.totalTokens) })
2912
3246
  ] })
2913
3247
  ] });
2914
3248
  }
2915
3249
 
2916
3250
  // src/components/MainMenu.tsx
2917
- import { Text as Text11, Box as Box11 } from "ink";
3251
+ import { Text as Text12, Box as Box11 } from "ink";
2918
3252
  import SelectInput3 from "ink-select-input";
2919
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3253
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2920
3254
  var MENU_ITEMS = [
2921
3255
  { label: "plan", value: "plan", description: "Plan specs with AI loop engine" },
2922
3256
  { label: "build", value: "build", description: "Build tasks one-per-spawn with AI" },
@@ -2924,16 +3258,16 @@ var MENU_ITEMS = [
2924
3258
  { label: "config", value: "config", description: "Manage configuration" }
2925
3259
  ];
2926
3260
  function MenuItem({ isSelected = false, label, description }) {
2927
- return /* @__PURE__ */ jsxs9(Box11, { children: [
2928
- /* @__PURE__ */ jsx11(Text11, { color: isSelected ? "blue" : void 0, children: label.padEnd(10) }),
2929
- description && /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
3261
+ return /* @__PURE__ */ jsxs10(Box11, { children: [
3262
+ /* @__PURE__ */ jsx12(Text12, { color: isSelected ? "blue" : void 0, children: label.padEnd(10) }),
3263
+ description && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
2930
3264
  "\u2014 ",
2931
3265
  description
2932
3266
  ] })
2933
3267
  ] });
2934
3268
  }
2935
3269
  function MainMenu({ onSelect }) {
2936
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx11(
3270
+ return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx12(
2937
3271
  SelectInput3,
2938
3272
  {
2939
3273
  items: MENU_ITEMS,
@@ -2944,9 +3278,9 @@ function MainMenu({ onSelect }) {
2944
3278
  }
2945
3279
 
2946
3280
  // src/lib/stats.ts
2947
- import fs10 from "fs";
3281
+ import fs11 from "fs";
2948
3282
  function computeProjectStats(cwd) {
2949
- if (!fs10.existsSync(getLocalDir(cwd))) {
3283
+ if (!fs11.existsSync(getLocalDir(cwd))) {
2950
3284
  return null;
2951
3285
  }
2952
3286
  let statusData;
@@ -2988,157 +3322,99 @@ function computeProjectStats(cwd) {
2988
3322
  }
2989
3323
 
2990
3324
  // src/components/Welcome.tsx
2991
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3325
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2992
3326
  var NARROW_THRESHOLD = 60;
2993
3327
  function Welcome({ version: version2 }) {
2994
- const { exit } = useApp4();
3328
+ const { exit } = useApp5();
2995
3329
  const { stdout } = useStdout2();
2996
- const [selectedCommand, setSelectedCommand] = useState9(null);
2997
- const stats = useMemo5(() => computeProjectStats(), []);
3330
+ const [selectedCommand, setSelectedCommand] = useState10(null);
3331
+ const stats = useMemo6(() => computeProjectStats(), []);
2998
3332
  const isNarrow = (stdout.columns ?? 80) < NARROW_THRESHOLD;
2999
- useEffect8(() => {
3333
+ useEffect9(() => {
3000
3334
  if (selectedCommand === "status") {
3001
3335
  const timer = setTimeout(() => exit(), 0);
3002
3336
  return () => clearTimeout(timer);
3003
3337
  }
3004
3338
  }, [selectedCommand, exit]);
3005
3339
  if (selectedCommand === "plan") {
3006
- return /* @__PURE__ */ jsx12(Plan, {});
3340
+ return /* @__PURE__ */ jsx13(Plan, {});
3007
3341
  }
3008
3342
  if (selectedCommand === "build") {
3009
- return /* @__PURE__ */ jsx12(Build, {});
3343
+ return /* @__PURE__ */ jsx13(Build, {});
3010
3344
  }
3011
3345
  if (selectedCommand === "status") {
3012
- return /* @__PURE__ */ jsx12(Status, { version: version2 });
3346
+ return /* @__PURE__ */ jsx13(Status, { version: version2 });
3013
3347
  }
3014
3348
  if (selectedCommand === "config") {
3015
- return /* @__PURE__ */ jsx12(ConfigEditor, { version: version2 });
3349
+ return /* @__PURE__ */ jsx13(ConfigEditor, { version: version2 });
3016
3350
  }
3017
- return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", gap: 1, children: [
3018
- isNarrow ? /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", children: [
3019
- /* @__PURE__ */ jsxs10(Text12, { bold: true, color: "#f0a030", children: [
3351
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
3352
+ isNarrow ? /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", children: [
3353
+ /* @__PURE__ */ jsxs11(Text13, { bold: true, color: "#f0a030", children: [
3020
3354
  "\u{1F439} toby v",
3021
3355
  version2
3022
3356
  ] }),
3023
- stats !== null && /* @__PURE__ */ jsxs10(Text12, { children: [
3024
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Specs: " }),
3025
- /* @__PURE__ */ jsx12(Text12, { children: stats.totalSpecs }),
3026
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: " \xB7 Planned: " }),
3027
- /* @__PURE__ */ jsx12(Text12, { children: stats.planned }),
3028
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: " \xB7 Done: " }),
3029
- /* @__PURE__ */ jsx12(Text12, { children: stats.done }),
3030
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: " \xB7 Tokens: " }),
3031
- /* @__PURE__ */ jsx12(Text12, { children: formatTokens(stats.totalTokens) })
3357
+ stats !== null && /* @__PURE__ */ jsxs11(Text13, { children: [
3358
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Specs: " }),
3359
+ /* @__PURE__ */ jsx13(Text13, { children: stats.totalSpecs }),
3360
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Planned: " }),
3361
+ /* @__PURE__ */ jsx13(Text13, { children: stats.planned }),
3362
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Done: " }),
3363
+ /* @__PURE__ */ jsx13(Text13, { children: stats.done }),
3364
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \xB7 Tokens: " }),
3365
+ /* @__PURE__ */ jsx13(Text13, { children: formatTokens(stats.totalTokens) })
3032
3366
  ] })
3033
- ] }) : /* @__PURE__ */ jsxs10(Box12, { flexDirection: "row", gap: 2, children: [
3034
- /* @__PURE__ */ jsx12(HamsterWheel, {}),
3035
- /* @__PURE__ */ jsx12(InfoPanel, { version: version2, stats })
3367
+ ] }) : /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 2, children: [
3368
+ /* @__PURE__ */ jsx13(HamsterWheel, {}),
3369
+ /* @__PURE__ */ jsx13(InfoPanel, { version: version2, stats })
3036
3370
  ] }),
3037
- /* @__PURE__ */ jsx12(MainMenu, { onSelect: setSelectedCommand })
3371
+ /* @__PURE__ */ jsx13(MainMenu, { onSelect: setSelectedCommand })
3038
3372
  ] });
3039
3373
  }
3040
3374
 
3041
- // src/cli.tsx
3042
- import { jsx as jsx13 } from "react/jsx-runtime";
3043
- function Help({ version: version2 }) {
3044
- return /* @__PURE__ */ jsx13(Text13, { children: `toby v${version2} \u2014 AI-assisted development loop engine
3045
-
3046
- Usage
3047
- $ toby <command> [options]
3048
-
3049
- Commands
3050
- plan Plan specs with AI loop engine
3051
- build Build tasks one-per-spawn with AI
3052
- init Initialize toby in current project
3053
- status Show project status
3054
- config Manage configuration
3055
-
3056
- Options
3057
- --help Show this help
3058
- --version Show version
3059
-
3060
- Spec Selection
3061
- --spec=<name> Single spec or comma-separated (e.g. --spec=auth,payments)
3062
- --specs=<names> Alias for --spec` });
3063
- }
3064
- function UnknownCommand({ command: command2 }) {
3065
- return /* @__PURE__ */ jsx13(Text13, { color: "red", children: `Unknown command: ${command2}
3066
- Run "toby --help" for available commands.` });
3067
- }
3068
- var cli = meow(
3069
- `
3070
- Usage
3071
- $ toby <command> [options]
3072
-
3073
- Commands
3074
- plan Plan specs with AI loop engine
3075
- build Build tasks one-per-spawn with AI
3076
- init Initialize toby in current project
3077
- status Show project status
3078
- config Manage configuration
3079
-
3080
- Plan Options
3081
- --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3082
- --specs=<names> Alias for --spec with comma-separated specs
3083
- --all Plan all pending specs
3084
- --iterations=<n> Override iteration count
3085
- --verbose Show full CLI output
3086
- --transcript Save session transcript to file
3087
- --cli=<name> Override AI CLI (claude, codex, opencode)
3088
- --session=<name> Name the session for branch/PR naming
3089
-
3090
- Build Options
3091
- --spec=<query> Target spec(s) by name, slug, number, or comma-separated list
3092
- --specs=<names> Alias for --spec with comma-separated specs
3093
- --all Build all planned specs in order
3094
- --iterations=<n> Override max iteration count
3095
- --verbose Show full CLI output
3096
- --transcript Save session transcript to file
3097
- --cli=<name> Override AI CLI (claude, codex, opencode)
3098
- --session=<name> Name the session for branch/PR naming
3099
-
3100
- Status Options
3101
- --spec=<query> Show status for a spec by name, slug, or number
3102
-
3103
- Init Options
3104
- --plan-cli=<name> Set plan CLI (claude, codex, opencode)
3105
- --plan-model=<id> Set plan model
3106
- --build-cli=<name> Set build CLI (claude, codex, opencode)
3107
- --build-model=<id> Set build model
3108
- --specs-dir=<path> Set specs directory
3109
- --verbose Enable verbose output in config
3375
+ // src/lib/cli-meta.ts
3376
+ var COMMAND_NAMES = Object.keys(commandHelp);
3377
+ var MEOW_FLAGS = {
3378
+ help: { type: "boolean", default: false },
3379
+ spec: { type: "string" },
3380
+ specs: { type: "string" },
3381
+ all: { type: "boolean", default: false },
3382
+ iterations: { type: "number" },
3383
+ verbose: { type: "boolean", default: false },
3384
+ transcript: { type: "boolean" },
3385
+ cli: { type: "string" },
3386
+ planCli: { type: "string" },
3387
+ planModel: { type: "string" },
3388
+ buildCli: { type: "string" },
3389
+ buildModel: { type: "string" },
3390
+ specsDir: { type: "string" },
3391
+ session: { type: "string" },
3392
+ force: { type: "boolean", default: false }
3393
+ };
3394
+ var MEOW_FLAG_NAMES = Object.keys(MEOW_FLAGS);
3110
3395
 
3111
- Config Subcommands
3112
- config Interactive config editor
3113
- config get <key> Show a config value (dot-notation)
3114
- config set <key> <value> Set a config value
3115
- config set <k>=<v> [<k>=<v>...] Batch set config values
3116
- `,
3117
- {
3118
- importMeta: import.meta,
3119
- flags: {
3120
- spec: { type: "string" },
3121
- specs: { type: "string" },
3122
- all: { type: "boolean", default: false },
3123
- iterations: { type: "number" },
3124
- verbose: { type: "boolean", default: false },
3125
- transcript: { type: "boolean" },
3126
- cli: { type: "string" },
3127
- planCli: { type: "string" },
3128
- planModel: { type: "string" },
3129
- buildCli: { type: "string" },
3130
- buildModel: { type: "string" },
3131
- specsDir: { type: "string" },
3132
- session: { type: "string" }
3133
- }
3134
- }
3135
- );
3396
+ // src/cli.tsx
3397
+ import { jsx as jsx14 } from "react/jsx-runtime";
3398
+ function writeUnknownCommandError(command2) {
3399
+ process.stderr.write(
3400
+ formatErrorWithHint(
3401
+ `Unknown command: ${command2}`,
3402
+ COMMAND_NAMES,
3403
+ "toby --help"
3404
+ )
3405
+ );
3406
+ }
3407
+ var cli = meow("", {
3408
+ importMeta: import.meta,
3409
+ autoHelp: false,
3410
+ flags: MEOW_FLAGS
3411
+ });
3136
3412
  ensureGlobalDir();
3137
3413
  var resolvedSpec = cli.flags.specs ?? cli.flags.spec;
3138
3414
  var flags = { ...cli.flags, spec: resolvedSpec };
3139
3415
  var commands = {
3140
3416
  plan: {
3141
- render: (flags2) => /* @__PURE__ */ jsx13(
3417
+ render: (flags2) => /* @__PURE__ */ jsx14(
3142
3418
  Plan,
3143
3419
  {
3144
3420
  spec: flags2.spec,
@@ -3153,7 +3429,7 @@ var commands = {
3153
3429
  waitForExit: true
3154
3430
  },
3155
3431
  build: {
3156
- render: (flags2) => /* @__PURE__ */ jsx13(
3432
+ render: (flags2) => /* @__PURE__ */ jsx14(
3157
3433
  Build,
3158
3434
  {
3159
3435
  spec: flags2.spec,
@@ -3168,7 +3444,7 @@ var commands = {
3168
3444
  waitForExit: true
3169
3445
  },
3170
3446
  init: {
3171
- render: (flags2, _input, version2) => /* @__PURE__ */ jsx13(
3447
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx14(
3172
3448
  Init,
3173
3449
  {
3174
3450
  version: version2,
@@ -3183,17 +3459,21 @@ var commands = {
3183
3459
  waitForExit: true
3184
3460
  },
3185
3461
  status: {
3186
- render: (flags2, _input, version2) => /* @__PURE__ */ jsx13(Status, { spec: flags2.spec, version: version2 })
3462
+ render: (flags2, _input, version2) => /* @__PURE__ */ jsx14(Status, { spec: flags2.spec, version: version2 })
3463
+ },
3464
+ clean: {
3465
+ render: (flags2) => /* @__PURE__ */ jsx14(Clean, { force: flags2.force }),
3466
+ waitForExit: true
3187
3467
  },
3188
3468
  config: {
3189
3469
  render: (_flags, input, version2) => {
3190
3470
  const [, subcommand, ...rest] = input;
3191
- if (!subcommand) return /* @__PURE__ */ jsx13(ConfigEditor, { version: version2 });
3471
+ if (!subcommand) return /* @__PURE__ */ jsx14(ConfigEditor, { version: version2 });
3192
3472
  if (subcommand === "set" && rest.some((arg) => arg.includes("="))) {
3193
- return /* @__PURE__ */ jsx13(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
3473
+ return /* @__PURE__ */ jsx14(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
3194
3474
  }
3195
3475
  const [configKey, value] = rest;
3196
- return /* @__PURE__ */ jsx13(
3476
+ return /* @__PURE__ */ jsx14(
3197
3477
  Config,
3198
3478
  {
3199
3479
  subcommand,
@@ -3208,12 +3488,24 @@ var commands = {
3208
3488
  };
3209
3489
  var version = cli.pkg.version ?? "0.0.0";
3210
3490
  var [command] = cli.input;
3211
- if (!command) {
3491
+ if (cli.flags.help) {
3492
+ if (!command || command in commands) {
3493
+ if (command && command in commandHelp) {
3494
+ process.stdout.write(formatCommandHelp(command, commandHelp[command]));
3495
+ } else {
3496
+ process.stdout.write(formatGlobalHelp(version));
3497
+ }
3498
+ process.exitCode = 0;
3499
+ } else {
3500
+ writeUnknownCommandError(command);
3501
+ process.exitCode = 1;
3502
+ }
3503
+ } else if (!command) {
3212
3504
  if (process.stdin.isTTY) {
3213
- const app = render(/* @__PURE__ */ jsx13(Welcome, { version }));
3505
+ const app = render(/* @__PURE__ */ jsx14(Welcome, { version }));
3214
3506
  await app.waitUntilExit();
3215
3507
  } else {
3216
- render(/* @__PURE__ */ jsx13(Help, { version })).unmount();
3508
+ process.stdout.write(formatGlobalHelp(version));
3217
3509
  }
3218
3510
  } else if (command in commands) {
3219
3511
  const entry = commands[command];
@@ -3224,6 +3516,6 @@ if (!command) {
3224
3516
  app.unmount();
3225
3517
  }
3226
3518
  } else {
3227
- render(/* @__PURE__ */ jsx13(UnknownCommand, { command })).unmount();
3519
+ writeUnknownCommandError(command);
3228
3520
  process.exitCode = 1;
3229
3521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xtiby/toby",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "AI-assisted development loop engine CLI",
5
5
  "repository": {
6
6
  "type": "git",