@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.
- package/dist/cli.js +627 -238
- 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
|
|
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
|
-
|
|
71
|
-
|
|
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(
|
|
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
|
|
1458
|
+
function resolveResumeSessionId(specEntry, currentCli, sessionCli) {
|
|
1459
|
+
if (currentCli !== sessionCli) return void 0;
|
|
1184
1460
|
const lastIteration = specEntry?.iterations.at(-1);
|
|
1185
|
-
|
|
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
|
-
|
|
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
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
|
2701
|
-
import { Box as
|
|
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
|
|
2705
|
-
import { Box as
|
|
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
|
|
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] =
|
|
2948
|
-
const [spokeAngle, setSpokeAngle] =
|
|
2949
|
-
|
|
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
|
-
|
|
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__ */
|
|
3423
|
+
return /* @__PURE__ */ jsx11(Text11, { children: " \u{1F439} toby" });
|
|
2998
3424
|
}
|
|
2999
|
-
return /* @__PURE__ */
|
|
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
|
|
3004
|
-
import { jsx as
|
|
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__ */
|
|
3008
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3438
|
+
/* @__PURE__ */ jsx12(Text12, { children: value })
|
|
3013
3439
|
] });
|
|
3014
3440
|
}
|
|
3015
3441
|
function InfoPanel({ version: version2, stats }) {
|
|
3016
|
-
return /* @__PURE__ */
|
|
3017
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3022
|
-
/* @__PURE__ */
|
|
3023
|
-
/* @__PURE__ */
|
|
3024
|
-
/* @__PURE__ */
|
|
3025
|
-
/* @__PURE__ */
|
|
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
|
|
3457
|
+
import { Text as Text13, Box as Box12 } from "ink";
|
|
3032
3458
|
import SelectInput3 from "ink-select-input";
|
|
3033
|
-
import { jsx as
|
|
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__ */
|
|
3042
|
-
/* @__PURE__ */
|
|
3043
|
-
description && /* @__PURE__ */
|
|
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__ */
|
|
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
|
|
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 } =
|
|
3535
|
+
const { exit } = useApp6();
|
|
3109
3536
|
const { stdout } = useStdout2();
|
|
3110
|
-
const [selectedCommand, setSelectedCommand] =
|
|
3537
|
+
const [selectedCommand, setSelectedCommand] = useState11(null);
|
|
3111
3538
|
const stats = useMemo6(() => computeProjectStats(), []);
|
|
3112
3539
|
const isNarrow = (stdout.columns ?? 80) < NARROW_THRESHOLD;
|
|
3113
|
-
|
|
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__ */
|
|
3547
|
+
return /* @__PURE__ */ jsx14(Plan, {});
|
|
3121
3548
|
}
|
|
3122
3549
|
if (selectedCommand === "build") {
|
|
3123
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
3556
|
+
return /* @__PURE__ */ jsx14(Status, { version: version2 });
|
|
3127
3557
|
}
|
|
3128
3558
|
if (selectedCommand === "config") {
|
|
3129
|
-
return /* @__PURE__ */
|
|
3559
|
+
return /* @__PURE__ */ jsx14(ConfigEditor, { version: version2 });
|
|
3130
3560
|
}
|
|
3131
|
-
return /* @__PURE__ */
|
|
3132
|
-
isNarrow ? /* @__PURE__ */
|
|
3133
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3138
|
-
/* @__PURE__ */
|
|
3139
|
-
/* @__PURE__ */
|
|
3140
|
-
/* @__PURE__ */
|
|
3141
|
-
/* @__PURE__ */
|
|
3142
|
-
/* @__PURE__ */
|
|
3143
|
-
/* @__PURE__ */
|
|
3144
|
-
/* @__PURE__ */
|
|
3145
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3148
|
-
/* @__PURE__ */
|
|
3149
|
-
/* @__PURE__ */
|
|
3577
|
+
] }) : /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 2, children: [
|
|
3578
|
+
/* @__PURE__ */ jsx14(HamsterWheel, {}),
|
|
3579
|
+
/* @__PURE__ */ jsx14(InfoPanel, { version: version2, stats })
|
|
3150
3580
|
] }),
|
|
3151
|
-
/* @__PURE__ */
|
|
3581
|
+
/* @__PURE__ */ jsx14(MainMenu, { onSelect: setSelectedCommand })
|
|
3152
3582
|
] });
|
|
3153
3583
|
}
|
|
3154
3584
|
|
|
3155
|
-
// src/cli.
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
3692
|
+
if (!subcommand) return /* @__PURE__ */ jsx15(ConfigEditor, { version: version2 });
|
|
3316
3693
|
if (subcommand === "set" && rest.some((arg) => arg.includes("="))) {
|
|
3317
|
-
return /* @__PURE__ */
|
|
3694
|
+
return /* @__PURE__ */ jsx15(ConfigSetBatch, { pairs: rest.filter((arg) => arg.includes("=")) });
|
|
3318
3695
|
}
|
|
3319
3696
|
const [configKey, value] = rest;
|
|
3320
|
-
return /* @__PURE__ */
|
|
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 (
|
|
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__ */
|
|
3726
|
+
const app = render(/* @__PURE__ */ jsx15(Welcome, { version }));
|
|
3338
3727
|
await app.waitUntilExit();
|
|
3339
3728
|
} else {
|
|
3340
|
-
|
|
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
|
-
|
|
3740
|
+
writeUnknownCommandError(command);
|
|
3352
3741
|
process.exitCode = 1;
|
|
3353
3742
|
}
|