@towles/tool 0.0.53 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +82 -72
  2. package/package.json +8 -7
  3. package/src/commands/auto-claude.ts +219 -0
  4. package/src/commands/doctor.ts +1 -34
  5. package/src/config/settings.ts +0 -10
  6. package/src/lib/auto-claude/config.test.ts +53 -0
  7. package/src/lib/auto-claude/config.ts +68 -0
  8. package/src/lib/auto-claude/index.ts +14 -0
  9. package/src/lib/auto-claude/pipeline.test.ts +14 -0
  10. package/src/lib/auto-claude/pipeline.ts +64 -0
  11. package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
  12. package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
  13. package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
  14. package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
  15. package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
  16. package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
  17. package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
  18. package/src/lib/auto-claude/prompt-templates/index.test.ts +145 -0
  19. package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
  20. package/src/lib/auto-claude/steps/create-pr.ts +93 -0
  21. package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
  22. package/src/lib/auto-claude/steps/implement.ts +63 -0
  23. package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
  24. package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
  25. package/src/lib/auto-claude/steps/plan.ts +14 -0
  26. package/src/lib/auto-claude/steps/refresh.ts +114 -0
  27. package/src/lib/auto-claude/steps/remove-label.ts +22 -0
  28. package/src/lib/auto-claude/steps/research.ts +21 -0
  29. package/src/lib/auto-claude/steps/review.ts +14 -0
  30. package/src/lib/auto-claude/utils.test.ts +136 -0
  31. package/src/lib/auto-claude/utils.ts +334 -0
  32. package/src/commands/ralph/plan/add.ts +0 -69
  33. package/src/commands/ralph/plan/done.ts +0 -82
  34. package/src/commands/ralph/plan/list.test.ts +0 -48
  35. package/src/commands/ralph/plan/list.ts +0 -100
  36. package/src/commands/ralph/plan/remove.ts +0 -71
  37. package/src/commands/ralph/run.test.ts +0 -607
  38. package/src/commands/ralph/run.ts +0 -362
  39. package/src/commands/ralph/show.ts +0 -88
  40. package/src/lib/ralph/execution.ts +0 -292
  41. package/src/lib/ralph/formatter.ts +0 -240
  42. package/src/lib/ralph/index.ts +0 -4
  43. package/src/lib/ralph/state.ts +0 -201
@@ -0,0 +1,334 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import consola from "consola";
6
+ import { x } from "tinyexec";
7
+
8
+ import { getConfig } from "./config.js";
9
+ import { ARTIFACTS } from "./prompt-templates/index.js";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ export const TEMPLATES_DIR = join(__dirname, "prompt-templates");
13
+
14
+ // ── Shell helpers ──
15
+
16
+ async function exec(cmd: string, args: string[]): Promise<string> {
17
+ const result = await x(cmd, args, { nodeOptions: { cwd: process.cwd() }, throwOnError: true });
18
+ return result.stdout.trim();
19
+ }
20
+
21
+ export async function execSafe(
22
+ cmd: string,
23
+ args: string[],
24
+ ): Promise<{ stdout: string; ok: boolean }> {
25
+ const result = await x(cmd, args, { nodeOptions: { cwd: process.cwd() }, throwOnError: false });
26
+ return { stdout: (result.stdout ?? "").trim(), ok: result.exitCode === 0 };
27
+ }
28
+
29
+ export async function gh<T = unknown>(args: string[]): Promise<T> {
30
+ const out = await exec("gh", args);
31
+ return JSON.parse(out) as T;
32
+ }
33
+
34
+ export async function ghRaw(args: string[]): Promise<string> {
35
+ const result = await execSafe("gh", args);
36
+ return result.stdout;
37
+ }
38
+
39
+ export async function git(args: string[]): Promise<string> {
40
+ return exec("git", args);
41
+ }
42
+
43
+ export function sleep(ms: number): Promise<void> {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ // ── Claude CLI ──
48
+
49
+ export interface ClaudeResult {
50
+ result: string;
51
+ is_error: boolean;
52
+ total_cost_usd: number;
53
+ num_turns: number;
54
+ }
55
+
56
+ export async function runClaude(opts: {
57
+ promptFile: string;
58
+ permissionMode: "plan" | "acceptEdits";
59
+ maxTurns?: number;
60
+ retry?: boolean;
61
+ }): Promise<ClaudeResult> {
62
+ const args = [
63
+ "-p",
64
+ "--output-format",
65
+ "json",
66
+ "--permission-mode",
67
+ opts.permissionMode,
68
+ ...(opts.maxTurns ? ["--max-turns", String(opts.maxTurns)] : []),
69
+ `@${opts.promptFile}`,
70
+ ];
71
+
72
+ const cfg = getConfig();
73
+ let retryDelay = cfg.retryDelayMs;
74
+ let retries = 0;
75
+
76
+ while (true) {
77
+ try {
78
+ const proc = await x("claude", args, {
79
+ nodeOptions: { cwd: process.cwd(), stdio: ["ignore", "pipe", "inherit"] },
80
+ throwOnError: true,
81
+ });
82
+ const stdout = proc.stdout;
83
+
84
+ try {
85
+ const parsed = JSON.parse(stdout) as ClaudeResult;
86
+ consola.success(`Done — $${parsed.total_cost_usd.toFixed(4)} | ${parsed.num_turns} turns`);
87
+ if (parsed.result) {
88
+ consola.box(parsed.result);
89
+ }
90
+ return parsed;
91
+ } catch {
92
+ consola.warn("Done — failed to parse Claude output");
93
+ if (stdout.trim()) {
94
+ consola.box(stdout.trim());
95
+ }
96
+ return { result: stdout.trim(), is_error: false, total_cost_usd: 0, num_turns: 0 };
97
+ }
98
+ } catch (e) {
99
+ const shouldRetry = opts.retry ?? cfg.loopRetryEnabled ?? false;
100
+ if (!shouldRetry) throw e;
101
+
102
+ retries++;
103
+ if (retries >= cfg.maxRetries) {
104
+ throw new Error(`Claude failed after ${cfg.maxRetries} retries: ${e}`);
105
+ }
106
+
107
+ consola.warn(`Claude process error (attempt ${retries}/${cfg.maxRetries}): ${e}`);
108
+ consola.info(`Retrying in ${retryDelay / 1000}s...`);
109
+ await sleep(retryDelay);
110
+ retryDelay = Math.min(retryDelay * 2, cfg.maxRetryDelayMs);
111
+ }
112
+ }
113
+ }
114
+
115
+ // ── Template resolution ──
116
+
117
+ export interface TokenValues {
118
+ SCOPE_PATH: string;
119
+ ISSUE_DIR: string;
120
+ MAIN_BRANCH: string;
121
+ }
122
+
123
+ export function resolveTemplate(
124
+ templateName: string,
125
+ tokens: TokenValues,
126
+ issueDir: string,
127
+ ): string {
128
+ const templatePath = join(TEMPLATES_DIR, templateName);
129
+ let template = readFileSync(templatePath, "utf-8");
130
+
131
+ for (const [key, value] of Object.entries(tokens)) {
132
+ template = template.replaceAll(`{{${key}}}`, value);
133
+ }
134
+
135
+ const resolvedPath = join(issueDir, templateName);
136
+ ensureDir(dirname(resolvedPath));
137
+ writeFileSync(resolvedPath, template, "utf-8");
138
+
139
+ return relative(process.cwd(), resolvedPath);
140
+ }
141
+
142
+ // ── File helpers ──
143
+
144
+ export function ensureDir(dir: string): void {
145
+ mkdirSync(dir, { recursive: true });
146
+ }
147
+
148
+ export function fileExists(path: string): boolean {
149
+ return existsSync(path);
150
+ }
151
+
152
+ export function readFile(path: string): string {
153
+ return readFileSync(path, "utf-8");
154
+ }
155
+
156
+ export function writeFile(path: string, content: string): void {
157
+ ensureDir(dirname(path));
158
+ writeFileSync(path, content, "utf-8");
159
+ }
160
+
161
+ // ── Git helpers ──
162
+
163
+ export async function commitArtifacts(ctx: IssueContext, message: string): Promise<void> {
164
+ await git(["add", ctx.issueDirRel]);
165
+ const staged = await execSafe("git", ["diff", "--cached", "--name-only"]);
166
+ if (staged.ok && staged.stdout.length > 0) {
167
+ await git(["commit", "-m", message]);
168
+ }
169
+ }
170
+
171
+ // ── Issue context ──
172
+
173
+ export interface IssueContext {
174
+ number: number;
175
+ title: string;
176
+ body: string;
177
+ repo: string;
178
+ scopePath: string;
179
+ issueDir: string;
180
+ issueDirRel: string;
181
+ branch: string;
182
+ }
183
+
184
+ export function buildIssueContext(
185
+ issue: { number: number; title: string; body: string },
186
+ repo: string,
187
+ scopePath: string,
188
+ ): IssueContext {
189
+ const issueDirRel = `.auto-claude/issue-${issue.number}`;
190
+ return {
191
+ number: issue.number,
192
+ title: issue.title,
193
+ body: issue.body,
194
+ repo,
195
+ scopePath,
196
+ issueDir: join(process.cwd(), issueDirRel),
197
+ issueDirRel,
198
+ branch: `auto-claude/issue-${issue.number}`,
199
+ };
200
+ }
201
+
202
+ export function buildTokens(ctx: IssueContext): TokenValues {
203
+ return {
204
+ SCOPE_PATH: ctx.scopePath,
205
+ ISSUE_DIR: ctx.issueDirRel,
206
+ MAIN_BRANCH: getConfig().mainBranch,
207
+ };
208
+ }
209
+
210
+ export function buildContextFromArtifacts(issueNumber: number): IssueContext {
211
+ const cfg = getConfig();
212
+ const issueDirRel = `.auto-claude/issue-${issueNumber}`;
213
+ const ramblingsPath = join(process.cwd(), issueDirRel, ARTIFACTS.initialRamblings);
214
+
215
+ if (!fileExists(ramblingsPath)) {
216
+ throw new Error(`No artifacts found at ${issueDirRel}. Run the pipeline first.`);
217
+ }
218
+
219
+ const content = readFile(ramblingsPath);
220
+ const titleMatch = content.match(/^# (.+)$/m);
221
+ const title = titleMatch?.[1] ?? `Issue #${issueNumber}`;
222
+
223
+ // Body starts after the second blank line (past the title and metadata lines)
224
+ const secondBlankIdx = findNthBlankLine(content.split("\n"), 2);
225
+ const body = content
226
+ .split("\n")
227
+ .slice(secondBlankIdx + 1)
228
+ .join("\n")
229
+ .trim();
230
+
231
+ return buildIssueContext({ number: issueNumber, title, body }, cfg.repo, cfg.scopePath);
232
+ }
233
+
234
+ function findNthBlankLine(lines: string[], n: number): number {
235
+ let count = 0;
236
+ for (let i = 0; i < lines.length; i++) {
237
+ if (lines[i].trim() === "") {
238
+ count++;
239
+ if (count === n) return i;
240
+ }
241
+ }
242
+ return 0;
243
+ }
244
+
245
+ export function log(msg: string): void {
246
+ consola.info(`[auto-claude] ${msg}`);
247
+ }
248
+
249
+ export function logStep(step: string, ctx: IssueContext, skipped = false): void {
250
+ const tag = skipped ? "SKIP" : "RUN";
251
+ consola.box({ title: `[${tag}] ${step}`, message: `${ctx.repo}#${ctx.number} — ${ctx.title}` });
252
+ }
253
+
254
+ // ── Git branch helpers ──
255
+
256
+ export async function ensureBranch(branch: string): Promise<void> {
257
+ const { mainBranch, remote } = getConfig();
258
+
259
+ try {
260
+ const branches = await git(["branch", "--list", branch]);
261
+ if (branches.includes(branch)) {
262
+ await git(["checkout", branch]);
263
+ return;
264
+ }
265
+ } catch {
266
+ /* ignore */
267
+ }
268
+
269
+ try {
270
+ await git(["fetch", remote, branch]);
271
+ await git(["checkout", branch]);
272
+ return;
273
+ } catch {
274
+ /* doesn't exist remotely */
275
+ }
276
+
277
+ await git(["checkout", mainBranch]);
278
+ await git(["pull", remote, mainBranch]);
279
+ await git(["checkout", "-b", branch]);
280
+ }
281
+
282
+ // ── Common step runner ──
283
+
284
+ export interface StepRunnerOptions {
285
+ stepName: string;
286
+ ctx: IssueContext;
287
+ artifactPath: string;
288
+ templateName: string;
289
+ artifactValidator?: (path: string) => boolean;
290
+ }
291
+
292
+ /**
293
+ * Runs a standard pipeline step: check artifact (skip if exists), run Claude
294
+ * with a template, validate the artifact was produced, and commit.
295
+ *
296
+ * Used by plan, plan-implementation, review, and similar steps that follow
297
+ * the same pattern.
298
+ */
299
+ export async function runStepWithArtifact(opts: StepRunnerOptions): Promise<boolean> {
300
+ const { stepName, ctx, artifactPath, templateName, artifactValidator } = opts;
301
+
302
+ const isValid = artifactValidator ?? fileExists;
303
+ if (isValid(artifactPath)) {
304
+ logStep(stepName, ctx, true);
305
+ return true;
306
+ }
307
+
308
+ logStep(stepName, ctx);
309
+
310
+ const tokens = buildTokens(ctx);
311
+ const promptFile = resolveTemplate(templateName, tokens, ctx.issueDir);
312
+
313
+ const result = await runClaude({
314
+ promptFile,
315
+ permissionMode: "acceptEdits",
316
+ maxTurns: getConfig().maxTurns,
317
+ });
318
+
319
+ if (result.is_error) {
320
+ consola.error(`${stepName} step failed: ${result.result}`);
321
+ return false;
322
+ }
323
+
324
+ if (!isValid(artifactPath)) {
325
+ consola.error(`${stepName} step did not produce expected artifact`);
326
+ return false;
327
+ }
328
+
329
+ await commitArtifacts(
330
+ ctx,
331
+ `chore(auto-claude): ${stepName.toLowerCase()} for ${ctx.repo}#${ctx.number}`,
332
+ );
333
+ return true;
334
+ }
@@ -1,69 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { Args, Flags } from "@oclif/core";
4
- import consola from "consola";
5
- import { colors } from "consola/utils";
6
- import { BaseCommand } from "../../base.js";
7
- import {
8
- DEFAULT_STATE_FILE,
9
- loadState,
10
- saveState,
11
- createInitialState,
12
- addPlanToState,
13
- resolveRalphPath,
14
- } from "../../../lib/ralph/state.js";
15
-
16
- /**
17
- * Add a new plan to ralph state from a file
18
- */
19
- export default class PlanAdd extends BaseCommand {
20
- static override description = "Add a new plan from a file";
21
-
22
- static override examples = [
23
- {
24
- description: "Add a plan from a markdown file",
25
- command: "<%= config.bin %> <%= command.id %> docs/plans/2025-01-18-feature.md",
26
- },
27
- ];
28
-
29
- static override args = {
30
- file: Args.string({
31
- description: "Path to plan file (markdown)",
32
- required: true,
33
- }),
34
- };
35
-
36
- static override flags = {
37
- ...BaseCommand.baseFlags,
38
- stateFile: Flags.string({
39
- char: "s",
40
- description: `State file path (default: ${DEFAULT_STATE_FILE})`,
41
- }),
42
- };
43
-
44
- async run(): Promise<void> {
45
- const { args, flags } = await this.parse(PlanAdd);
46
- const ralphSettings = this.settings.settings.ralphSettings;
47
- const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
48
-
49
- const planFilePath = resolve(args.file);
50
-
51
- if (!existsSync(planFilePath)) {
52
- this.error(`Plan file not found: ${planFilePath}`);
53
- }
54
-
55
- let state = loadState(stateFile);
56
-
57
- if (!state) {
58
- state = createInitialState();
59
- }
60
-
61
- const newPlan = addPlanToState(state, planFilePath);
62
- saveState(state, stateFile);
63
-
64
- consola.log(colors.green(`✓ Added plan #${newPlan.id}`));
65
- consola.log(colors.dim(` File: ${planFilePath}`));
66
- consola.log(colors.dim(`State saved to: ${stateFile}`));
67
- consola.log(colors.dim(`Total plans: ${state.plans.length}`));
68
- }
69
- }
@@ -1,82 +0,0 @@
1
- import { Args, Flags } from "@oclif/core";
2
- import consola from "consola";
3
- import { colors } from "consola/utils";
4
- import { BaseCommand } from "../../base.js";
5
- import {
6
- DEFAULT_STATE_FILE,
7
- loadState,
8
- saveState,
9
- resolveRalphPath,
10
- } from "../../../lib/ralph/state.js";
11
-
12
- /**
13
- * Mark a ralph plan as done
14
- */
15
- export default class PlanDone extends BaseCommand {
16
- static override description = "Mark a plan as done by ID";
17
-
18
- static override examples = [
19
- { description: "Mark plan #1 as done", command: "<%= config.bin %> <%= command.id %> 1" },
20
- {
21
- description: "Mark done using custom state file",
22
- command: "<%= config.bin %> <%= command.id %> 5 --stateFile custom-state.json",
23
- },
24
- ];
25
-
26
- static override args = {
27
- id: Args.integer({
28
- description: "Plan ID to mark done",
29
- required: true,
30
- }),
31
- };
32
-
33
- static override flags = {
34
- ...BaseCommand.baseFlags,
35
- stateFile: Flags.string({
36
- char: "s",
37
- description: `State file path (default: ${DEFAULT_STATE_FILE})`,
38
- }),
39
- };
40
-
41
- async run(): Promise<void> {
42
- const { args, flags } = await this.parse(PlanDone);
43
- const ralphSettings = this.settings.settings.ralphSettings;
44
- const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
45
-
46
- const planId = args.id;
47
-
48
- if (planId < 1) {
49
- this.error("Invalid plan ID");
50
- }
51
-
52
- const state = loadState(stateFile);
53
-
54
- if (!state) {
55
- this.error(`No state file found at: ${stateFile}`);
56
- }
57
-
58
- const plan = state.plans.find((p) => p.id === planId);
59
-
60
- if (!plan) {
61
- this.error(`Plan #${planId} not found. Use: tt ralph plan list`);
62
- }
63
-
64
- if (plan.status === "done") {
65
- consola.log(colors.yellow(`Plan #${planId} is already done.`));
66
- return;
67
- }
68
-
69
- plan.status = "done";
70
- plan.completedAt = new Date().toISOString();
71
- saveState(state, stateFile);
72
-
73
- consola.log(colors.green(`✓ Marked plan #${planId} as done: ${plan.planFilePath}`));
74
-
75
- const remaining = state.plans.filter((p) => p.status !== "done").length;
76
- if (remaining === 0) {
77
- consola.log(colors.bold(colors.green("All plans complete!")));
78
- } else {
79
- consola.log(colors.dim(`Remaining plans: ${remaining}`));
80
- }
81
- }
82
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Integration tests for oclif ralph plan list command
3
- * Note: --help output goes through oclif's own routing
4
- */
5
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
6
- import { runCommand } from "@oclif/test";
7
- import { join } from "node:path";
8
- import { tmpdir } from "node:os";
9
- import { writeFileSync, unlinkSync, existsSync } from "node:fs";
10
-
11
- describe("ralph plan list command", () => {
12
- const tempStateFile = join(tmpdir(), `ralph-test-list-${Date.now()}.json`);
13
-
14
- beforeAll(() => {
15
- // Create state file with one plan to minimize output during tests
16
- writeFileSync(
17
- tempStateFile,
18
- JSON.stringify({
19
- version: 1,
20
- iteration: 0,
21
- maxIterations: 10,
22
- status: "running",
23
- tasks: [{ id: 1, description: "test", status: "done", addedAt: new Date().toISOString() }],
24
- startedAt: new Date().toISOString(),
25
- }),
26
- );
27
- });
28
-
29
- afterAll(() => {
30
- if (existsSync(tempStateFile)) unlinkSync(tempStateFile);
31
- });
32
-
33
- it("runs task list without error", async () => {
34
- const { error } = await runCommand(["ralph:plan:list", "-s", tempStateFile]);
35
- expect(error).toBeUndefined();
36
- });
37
-
38
- it("supports --format flag", async () => {
39
- const { error } = await runCommand([
40
- "ralph:plan:list",
41
- "--format",
42
- "markdown",
43
- "-s",
44
- tempStateFile,
45
- ]);
46
- expect(error).toBeUndefined();
47
- });
48
- });
@@ -1,100 +0,0 @@
1
- import { Flags } from "@oclif/core";
2
- import pc from "picocolors";
3
- import { BaseCommand } from "../../base.js";
4
- import { DEFAULT_STATE_FILE, loadState, resolveRalphPath } from "../../../lib/ralph/state.js";
5
- import { formatPlansAsMarkdown } from "../../../lib/ralph/formatter.js";
6
-
7
- /**
8
- * List all ralph plans
9
- */
10
- export default class PlanList extends BaseCommand {
11
- static override description = "List all plans";
12
-
13
- static override examples = [
14
- { description: "List all plans", command: "<%= config.bin %> <%= command.id %>" },
15
- {
16
- description: "Output as markdown",
17
- command: "<%= config.bin %> <%= command.id %> --format markdown",
18
- },
19
- ];
20
-
21
- static override flags = {
22
- ...BaseCommand.baseFlags,
23
- stateFile: Flags.string({
24
- char: "s",
25
- description: `State file path (default: ${DEFAULT_STATE_FILE})`,
26
- }),
27
- format: Flags.string({
28
- char: "f",
29
- description: "Output format: default, markdown",
30
- default: "default",
31
- options: ["default", "markdown"],
32
- }),
33
- };
34
-
35
- async run(): Promise<void> {
36
- const { flags } = await this.parse(PlanList);
37
- const ralphSettings = this.settings.settings.ralphSettings;
38
- const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
39
-
40
- const state = loadState(stateFile);
41
-
42
- if (!state) {
43
- this.log(pc.yellow(`No state file found at: ${stateFile}`));
44
- return;
45
- }
46
-
47
- const plans = state.plans;
48
-
49
- if (plans.length === 0) {
50
- this.log(pc.yellow("No plans in state file."));
51
- this.log(pc.dim('Use: tt ralph plan add "description"'));
52
- return;
53
- }
54
-
55
- if (flags.format === "markdown") {
56
- this.log(formatPlansAsMarkdown(plans));
57
- return;
58
- }
59
-
60
- // Default format output - compact with truncation
61
- const ready = plans.filter((p) => p.status === "ready");
62
- const done = plans.filter((p) => p.status === "done");
63
-
64
- const truncate = (s: string, len: number) => (s.length > len ? s.slice(0, len - 1) + "…" : s);
65
- const termWidth = process.stdout.columns || 120;
66
-
67
- // Summary header
68
- this.log(
69
- pc.bold("\nPlans: ") +
70
- pc.green(`${done.length} done`) +
71
- pc.dim(" / ") +
72
- pc.yellow(`${ready.length} ready`),
73
- );
74
- this.log();
75
-
76
- // Show ready plans first (these are actionable)
77
- // Reserve ~10 chars for " ○ #XX " prefix
78
- const pathWidth = Math.max(40, termWidth - 12);
79
-
80
- if (ready.length > 0) {
81
- for (const plan of ready) {
82
- const icon = pc.dim("○");
83
- const id = pc.cyan(`#${plan.id}`);
84
- const filePath = truncate(plan.planFilePath, pathWidth);
85
- const errorSuffix = plan.error ? pc.red(` ⚠ ${truncate(plan.error, 30)}`) : "";
86
- this.log(` ${icon} ${id} ${filePath}${errorSuffix}`);
87
- }
88
- }
89
-
90
- // Show done plans collapsed
91
- if (done.length > 0) {
92
- this.log(pc.dim(` ─── ${done.length} completed ───`));
93
- for (const plan of done) {
94
- const filePath = truncate(plan.planFilePath, pathWidth - 5);
95
- this.log(pc.dim(` ✓ #${plan.id} ${filePath}`));
96
- }
97
- }
98
- this.log();
99
- }
100
- }
@@ -1,71 +0,0 @@
1
- import { Args, Flags } from "@oclif/core";
2
- import consola from "consola";
3
- import { colors } from "consola/utils";
4
- import { BaseCommand } from "../../base.js";
5
- import {
6
- DEFAULT_STATE_FILE,
7
- loadState,
8
- saveState,
9
- resolveRalphPath,
10
- } from "../../../lib/ralph/state.js";
11
-
12
- /**
13
- * Remove a ralph plan by ID
14
- */
15
- export default class PlanRemove extends BaseCommand {
16
- static override description = "Remove a plan by ID";
17
-
18
- static override examples = [
19
- { description: "Remove plan #1", command: "<%= config.bin %> <%= command.id %> 1" },
20
- {
21
- description: "Remove from custom state file",
22
- command: "<%= config.bin %> <%= command.id %> 5 --stateFile custom-state.json",
23
- },
24
- ];
25
-
26
- static override args = {
27
- id: Args.integer({
28
- description: "Plan ID to remove",
29
- required: true,
30
- }),
31
- };
32
-
33
- static override flags = {
34
- ...BaseCommand.baseFlags,
35
- stateFile: Flags.string({
36
- char: "s",
37
- description: `State file path (default: ${DEFAULT_STATE_FILE})`,
38
- }),
39
- };
40
-
41
- async run(): Promise<void> {
42
- const { args, flags } = await this.parse(PlanRemove);
43
- const ralphSettings = this.settings.settings.ralphSettings;
44
- const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
45
-
46
- const planId = args.id;
47
-
48
- if (planId < 1) {
49
- this.error("Invalid plan ID");
50
- }
51
-
52
- const state = loadState(stateFile);
53
-
54
- if (!state) {
55
- this.error(`No state file found at: ${stateFile}`);
56
- }
57
-
58
- const planIndex = state.plans.findIndex((p) => p.id === planId);
59
-
60
- if (planIndex === -1) {
61
- this.error(`Plan #${planId} not found. Use: tt ralph plan list`);
62
- }
63
-
64
- const removedPlan = state.plans[planIndex];
65
- state.plans.splice(planIndex, 1);
66
- saveState(state, stateFile);
67
-
68
- consola.log(colors.green(`✓ Removed plan #${planId}: ${removedPlan.planFilePath}`));
69
- consola.log(colors.dim(`Remaining plans: ${state.plans.length}`));
70
- }
71
- }