@towles/tool 0.0.20 → 0.0.48

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 (45) hide show
  1. package/{LICENSE.md → LICENSE} +1 -1
  2. package/README.md +86 -85
  3. package/bin/run.ts +5 -0
  4. package/package.json +84 -64
  5. package/patches/prompts.patch +34 -0
  6. package/src/commands/base.ts +27 -0
  7. package/src/commands/config.test.ts +15 -0
  8. package/src/commands/config.ts +44 -0
  9. package/src/commands/doctor.ts +136 -0
  10. package/src/commands/gh/branch-clean.ts +116 -0
  11. package/src/commands/gh/branch.test.ts +124 -0
  12. package/src/commands/gh/branch.ts +135 -0
  13. package/src/commands/gh/pr.ts +175 -0
  14. package/src/commands/graph-template.html +1214 -0
  15. package/src/commands/graph.test.ts +176 -0
  16. package/src/commands/graph.ts +970 -0
  17. package/src/commands/install.ts +154 -0
  18. package/src/commands/journal/daily-notes.ts +70 -0
  19. package/src/commands/journal/meeting.ts +89 -0
  20. package/src/commands/journal/note.ts +89 -0
  21. package/src/commands/ralph/plan/add.ts +75 -0
  22. package/src/commands/ralph/plan/done.ts +82 -0
  23. package/src/commands/ralph/plan/list.test.ts +48 -0
  24. package/src/commands/ralph/plan/list.ts +99 -0
  25. package/src/commands/ralph/plan/remove.ts +71 -0
  26. package/src/commands/ralph/run.test.ts +521 -0
  27. package/src/commands/ralph/run.ts +345 -0
  28. package/src/commands/ralph/show.ts +88 -0
  29. package/src/config/settings.ts +136 -0
  30. package/src/lib/journal/utils.ts +399 -0
  31. package/src/lib/ralph/execution.ts +292 -0
  32. package/src/lib/ralph/formatter.ts +238 -0
  33. package/src/lib/ralph/index.ts +4 -0
  34. package/src/lib/ralph/state.ts +166 -0
  35. package/src/types/journal.ts +16 -0
  36. package/src/utils/date-utils.test.ts +97 -0
  37. package/src/utils/date-utils.ts +54 -0
  38. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  39. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  40. package/src/utils/git/git-wrapper.test.ts +26 -0
  41. package/src/utils/git/git-wrapper.ts +15 -0
  42. package/src/utils/render.test.ts +71 -0
  43. package/src/utils/render.ts +34 -0
  44. package/dist/index.d.mts +0 -1
  45. package/dist/index.mjs +0 -805
@@ -0,0 +1,154 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { Flags } from "@oclif/core";
5
+ import pc from "picocolors";
6
+ import consola from "consola";
7
+ import { BaseCommand } from "./base.js";
8
+
9
+ const CLAUDE_SETTINGS_PATH = path.join(homedir(), ".claude", "settings.json");
10
+
11
+ interface ClaudeSettings {
12
+ cleanupPeriodDays?: number;
13
+ alwaysThinkingEnabled?: boolean;
14
+ hooks?: Record<string, unknown[]>;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ /**
19
+ * Install and configure towles-tool with Claude Code
20
+ */
21
+ export default class Install extends BaseCommand {
22
+ static override description =
23
+ "Configure Claude Code settings and optionally enable observability";
24
+
25
+ static override examples = [
26
+ {
27
+ description: "Configure Claude Code settings",
28
+ command: "<%= config.bin %> <%= command.id %>",
29
+ },
30
+ {
31
+ description: "Include OTEL setup instructions",
32
+ command: "<%= config.bin %> <%= command.id %> --observability",
33
+ },
34
+ ];
35
+
36
+ static override flags = {
37
+ ...BaseCommand.baseFlags,
38
+ observability: Flags.boolean({
39
+ char: "o",
40
+ description: "Show OTEL setup instructions and configure SubagentStop hook",
41
+ default: false,
42
+ }),
43
+ };
44
+
45
+ async run(): Promise<void> {
46
+ const { flags } = await this.parse(Install);
47
+
48
+ this.log(pc.bold("\nšŸ”§ towles-tool install\n"));
49
+
50
+ // Load or create Claude settings
51
+ let claudeSettings: ClaudeSettings = {};
52
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
53
+ try {
54
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
55
+ claudeSettings = JSON.parse(content);
56
+ this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
57
+ } catch {
58
+ this.log(
59
+ pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
60
+ );
61
+ }
62
+ } else {
63
+ this.log(pc.dim(`No Claude settings file found, will create one`));
64
+ }
65
+
66
+ // Configure recommended settings
67
+ let modified = false;
68
+
69
+ // Prevent log deletion (set to ~274 years)
70
+ if (claudeSettings.cleanupPeriodDays !== 99999) {
71
+ claudeSettings.cleanupPeriodDays = 99999;
72
+ modified = true;
73
+ this.log(pc.green("āœ“ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
74
+ } else {
75
+ this.log(pc.dim("āœ“ cleanupPeriodDays already set to 99999"));
76
+ }
77
+
78
+ // Enable thinking by default
79
+ if (claudeSettings.alwaysThinkingEnabled !== true) {
80
+ claudeSettings.alwaysThinkingEnabled = true;
81
+ modified = true;
82
+ this.log(pc.green("āœ“ Set alwaysThinkingEnabled: true"));
83
+ } else {
84
+ this.log(pc.dim("āœ“ alwaysThinkingEnabled already set to true"));
85
+ }
86
+
87
+ // Save settings if modified
88
+ if (modified) {
89
+ this.saveClaudeSettings(claudeSettings);
90
+ this.log(pc.green(`\nāœ“ Saved Claude settings to ${CLAUDE_SETTINGS_PATH}`));
91
+ }
92
+
93
+ // Show observability setup if requested
94
+ if (flags.observability) {
95
+ this.log(pc.bold("\nšŸ“Š Observability Setup\n"));
96
+ this.showOtelInstructions();
97
+ }
98
+
99
+ this.log(pc.bold(pc.green("\nāœ… Installation complete!\n")));
100
+
101
+ // Offer to install plugins from marketplace
102
+ this.log(pc.cyan("To install plugins from the Claude Code marketplace:"));
103
+ this.log(
104
+ pc.dim(" claude /plugins marketplace add https://github.com/ChrisTowles/towles-tool"),
105
+ );
106
+ this.log("");
107
+
108
+ const answer = await consola.prompt("Install tt-core plugin from marketplace now?", {
109
+ type: "confirm",
110
+ initial: true,
111
+ });
112
+
113
+ if (answer) {
114
+ const { x } = await import("tinyexec");
115
+ this.log("");
116
+
117
+ const result = await x("claude", ["plugin", "install", "tt@towles-tool", "--scope", "user"]);
118
+ if (result.stdout) this.log(result.stdout);
119
+ if (result.stderr) this.log(pc.dim(result.stderr));
120
+ if (result.exitCode === 0) {
121
+ this.log(pc.green("āœ“ tt-core plugin installed"));
122
+ } else {
123
+ this.log(pc.yellow(`Command exited with code ${result.exitCode}`));
124
+ }
125
+ }
126
+
127
+ this.log("");
128
+ }
129
+
130
+ private saveClaudeSettings(settings: ClaudeSettings): void {
131
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
132
+ if (!fs.existsSync(dir)) {
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ }
135
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
136
+ }
137
+
138
+ private showOtelInstructions(): void {
139
+ this.log(pc.cyan("Add these environment variables to your shell profile:\n"));
140
+
141
+ consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
142
+ export OTEL_METRICS_EXPORTER=otlp
143
+ export OTEL_LOGS_EXPORTER=otlp
144
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
145
+
146
+ this.log("");
147
+ this.log(
148
+ pc.dim("For more info, see: https://github.com/anthropics/claude-code-monitoring-guide"),
149
+ );
150
+ this.log("");
151
+ this.log(pc.cyan("Quick cost analysis (no setup required):"));
152
+ this.log(pc.dim(" npx ccusage@latest --breakdown"));
153
+ }
154
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import consola from "consola";
5
+ import { colors } from "consola/utils";
6
+ import { BaseCommand } from "../base.js";
7
+ import { JOURNAL_TYPES } from "../../types/journal.js";
8
+ import {
9
+ createJournalContent,
10
+ ensureDirectoryExists,
11
+ ensureTemplatesExist,
12
+ generateJournalFileInfoByType,
13
+ openInEditor,
14
+ } from "../../lib/journal/utils.js";
15
+
16
+ /**
17
+ * Create or open daily notes journal file
18
+ */
19
+ export default class DailyNotes extends BaseCommand {
20
+ static override aliases = ["today"];
21
+ static override description = "Weekly files with daily sections for ongoing work and notes";
22
+
23
+ static override examples = [
24
+ {
25
+ description: "Open weekly notes for today",
26
+ command: "<%= config.bin %> <%= command.id %>",
27
+ },
28
+ { description: "Using alias", command: "<%= config.bin %> today" },
29
+ ];
30
+
31
+ async run(): Promise<void> {
32
+ await this.parse(DailyNotes);
33
+
34
+ try {
35
+ const journalSettings = this.settings.settings.journalSettings;
36
+ const templateDir = journalSettings.templateDir;
37
+
38
+ // Ensure templates exist on first run
39
+ ensureTemplatesExist(templateDir);
40
+
41
+ const currentDate = new Date();
42
+ const fileInfo = generateJournalFileInfoByType({
43
+ journalSettings,
44
+ date: currentDate,
45
+ type: JOURNAL_TYPES.DAILY_NOTES,
46
+ title: "",
47
+ });
48
+
49
+ // Ensure journal directory exists
50
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
51
+
52
+ if (existsSync(fileInfo.fullPath)) {
53
+ consola.info(`Opening existing daily-notes file: ${colors.cyan(fileInfo.fullPath)}`);
54
+ } else {
55
+ const content = createJournalContent({ mondayDate: fileInfo.mondayDate, templateDir });
56
+ consola.info(`Creating new daily-notes file: ${colors.cyan(fileInfo.fullPath)}`);
57
+ writeFileSync(fileInfo.fullPath, content, "utf8");
58
+ }
59
+
60
+ await openInEditor({
61
+ editor: this.settings.settings.preferredEditor,
62
+ filePath: fileInfo.fullPath,
63
+ folderPath: journalSettings.baseFolder,
64
+ });
65
+ } catch (error) {
66
+ consola.warn(`Error creating daily-notes file:`, error);
67
+ process.exit(1);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,89 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { Args } from "@oclif/core";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import { BaseCommand } from "../base.js";
8
+ import { JOURNAL_TYPES } from "../../types/journal.js";
9
+ import {
10
+ createMeetingContent,
11
+ ensureDirectoryExists,
12
+ ensureTemplatesExist,
13
+ generateJournalFileInfoByType,
14
+ openInEditor,
15
+ } from "../../lib/journal/utils.js";
16
+
17
+ /**
18
+ * Create or open meeting notes file
19
+ */
20
+ export default class Meeting extends BaseCommand {
21
+ static override description = "Structured meeting notes with agenda and action items";
22
+
23
+ static override args = {
24
+ title: Args.string({
25
+ description: "Meeting title",
26
+ required: false,
27
+ }),
28
+ };
29
+
30
+ static override examples = [
31
+ {
32
+ description: "Create meeting note (prompts for title)",
33
+ command: "<%= config.bin %> <%= command.id %>",
34
+ },
35
+ {
36
+ description: "Create with title",
37
+ command: '<%= config.bin %> <%= command.id %> "Sprint Planning"',
38
+ },
39
+ { description: "Using alias", command: '<%= config.bin %> m "Standup"' },
40
+ ];
41
+
42
+ async run(): Promise<void> {
43
+ const { args } = await this.parse(Meeting);
44
+
45
+ try {
46
+ const journalSettings = this.settings.settings.journalSettings;
47
+ const templateDir = journalSettings.templateDir;
48
+
49
+ // Ensure templates exist on first run
50
+ ensureTemplatesExist(templateDir);
51
+
52
+ // Prompt for title if not provided
53
+ let title = args.title || "";
54
+ if (title.trim().length === 0) {
55
+ title = await consola.prompt(`Enter meeting title:`, {
56
+ type: "text",
57
+ });
58
+ }
59
+
60
+ const currentDate = new Date();
61
+ const fileInfo = generateJournalFileInfoByType({
62
+ journalSettings,
63
+ date: currentDate,
64
+ type: JOURNAL_TYPES.MEETING,
65
+ title,
66
+ });
67
+
68
+ // Ensure journal directory exists
69
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
70
+
71
+ if (existsSync(fileInfo.fullPath)) {
72
+ consola.info(`Opening existing meeting file: ${colors.cyan(fileInfo.fullPath)}`);
73
+ } else {
74
+ const content = createMeetingContent({ title, date: currentDate, templateDir });
75
+ consola.info(`Creating new meeting file: ${colors.cyan(fileInfo.fullPath)}`);
76
+ writeFileSync(fileInfo.fullPath, content, "utf8");
77
+ }
78
+
79
+ await openInEditor({
80
+ editor: this.settings.settings.preferredEditor,
81
+ filePath: fileInfo.fullPath,
82
+ folderPath: journalSettings.baseFolder,
83
+ });
84
+ } catch (error) {
85
+ consola.warn(`Error creating meeting file:`, error);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,89 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { Args } from "@oclif/core";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import { BaseCommand } from "../base.js";
8
+ import { JOURNAL_TYPES } from "../../types/journal.js";
9
+ import {
10
+ createNoteContent,
11
+ ensureDirectoryExists,
12
+ ensureTemplatesExist,
13
+ generateJournalFileInfoByType,
14
+ openInEditor,
15
+ } from "../../lib/journal/utils.js";
16
+
17
+ /**
18
+ * Create or open general-purpose note file
19
+ */
20
+ export default class Note extends BaseCommand {
21
+ static override description = "General-purpose notes with structured sections";
22
+
23
+ static override args = {
24
+ title: Args.string({
25
+ description: "Note title",
26
+ required: false,
27
+ }),
28
+ };
29
+
30
+ static override examples = [
31
+ {
32
+ description: "Create note (prompts for title)",
33
+ command: "<%= config.bin %> <%= command.id %>",
34
+ },
35
+ {
36
+ description: "Create with title",
37
+ command: '<%= config.bin %> <%= command.id %> "Research Notes"',
38
+ },
39
+ { description: "Using alias", command: '<%= config.bin %> n "Ideas"' },
40
+ ];
41
+
42
+ async run(): Promise<void> {
43
+ const { args } = await this.parse(Note);
44
+
45
+ try {
46
+ const journalSettings = this.settings.settings.journalSettings;
47
+ const templateDir = journalSettings.templateDir;
48
+
49
+ // Ensure templates exist on first run
50
+ ensureTemplatesExist(templateDir);
51
+
52
+ // Prompt for title if not provided
53
+ let title = args.title || "";
54
+ if (title.trim().length === 0) {
55
+ title = await consola.prompt(`Enter note title:`, {
56
+ type: "text",
57
+ });
58
+ }
59
+
60
+ const currentDate = new Date();
61
+ const fileInfo = generateJournalFileInfoByType({
62
+ journalSettings,
63
+ date: currentDate,
64
+ type: JOURNAL_TYPES.NOTE,
65
+ title,
66
+ });
67
+
68
+ // Ensure journal directory exists
69
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
70
+
71
+ if (existsSync(fileInfo.fullPath)) {
72
+ consola.info(`Opening existing note file: ${colors.cyan(fileInfo.fullPath)}`);
73
+ } else {
74
+ const content = createNoteContent({ title, date: currentDate, templateDir });
75
+ consola.info(`Creating new note file: ${colors.cyan(fileInfo.fullPath)}`);
76
+ writeFileSync(fileInfo.fullPath, content, "utf8");
77
+ }
78
+
79
+ await openInEditor({
80
+ editor: this.settings.settings.preferredEditor,
81
+ filePath: fileInfo.fullPath,
82
+ folderPath: journalSettings.baseFolder,
83
+ });
84
+ } catch (error) {
85
+ consola.warn(`Error creating note file:`, error);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { 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 %> --file docs/plans/2025-01-18-feature.md",
26
+ },
27
+ ];
28
+
29
+ static override flags = {
30
+ ...BaseCommand.baseFlags,
31
+ file: Flags.string({
32
+ char: "f",
33
+ description: "Path to plan file (markdown)",
34
+ required: true,
35
+ }),
36
+ stateFile: Flags.string({
37
+ char: "s",
38
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
39
+ }),
40
+ };
41
+
42
+ async run(): Promise<void> {
43
+ const { flags } = await this.parse(PlanAdd);
44
+ const ralphSettings = this.settings.settings.ralphSettings;
45
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
46
+
47
+ const filePath = resolve(flags.file);
48
+
49
+ if (!existsSync(filePath)) {
50
+ this.error(`Plan file not found: ${filePath}`);
51
+ }
52
+
53
+ const description = readFileSync(filePath, "utf-8").trim();
54
+
55
+ if (!description || description.length < 3) {
56
+ this.error("Plan file is empty or too short (min 3 chars)");
57
+ }
58
+
59
+ let state = loadState(stateFile);
60
+
61
+ if (!state) {
62
+ state = createInitialState();
63
+ }
64
+
65
+ const newPlan = addPlanToState(state, description);
66
+ saveState(state, stateFile);
67
+
68
+ // Show truncated description for display
69
+ const displayDesc = description.length > 80 ? `${description.slice(0, 80)}...` : description;
70
+ consola.log(colors.green(`āœ“ Added plan #${newPlan.id} from ${flags.file}`));
71
+ consola.log(colors.dim(` ${displayDesc.split("\n")[0]}`));
72
+ consola.log(colors.dim(`State saved to: ${stateFile}`));
73
+ consola.log(colors.dim(`Total plans: ${state.plans.length}`));
74
+ }
75
+ }
@@ -0,0 +1,82 @@
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.description}`));
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
+ }
@@ -0,0 +1,48 @@
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
+ });