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