@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.
- package/{LICENSE.md ā LICENSE} +1 -1
- package/README.md +86 -85
- package/bin/run.ts +5 -0
- package/package.json +84 -64
- package/patches/prompts.patch +34 -0
- package/src/commands/base.ts +27 -0
- package/src/commands/config.test.ts +15 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/doctor.ts +136 -0
- package/src/commands/gh/branch-clean.ts +116 -0
- package/src/commands/gh/branch.test.ts +124 -0
- package/src/commands/gh/branch.ts +135 -0
- package/src/commands/gh/pr.ts +175 -0
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +154 -0
- package/src/commands/journal/daily-notes.ts +70 -0
- package/src/commands/journal/meeting.ts +89 -0
- package/src/commands/journal/note.ts +89 -0
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/plan/list.test.ts +48 -0
- package/src/commands/ralph/plan/list.ts +99 -0
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +345 -0
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +136 -0
- package/src/lib/journal/utils.ts +399 -0
- package/src/lib/ralph/execution.ts +292 -0
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/lib/ralph/index.ts +4 -0
- package/src/lib/ralph/state.ts +166 -0
- package/src/types/journal.ts +16 -0
- package/src/utils/date-utils.test.ts +97 -0
- package/src/utils/date-utils.ts +54 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
- package/src/utils/git/gh-cli-wrapper.ts +54 -0
- package/src/utils/git/git-wrapper.test.ts +26 -0
- package/src/utils/git/git-wrapper.ts +15 -0
- package/src/utils/render.test.ts +71 -0
- package/src/utils/render.ts +34 -0
- package/dist/index.d.mts +0 -1
- 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
|
+
});
|