@towles/tool 0.0.96 → 0.0.103

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.
@@ -1,58 +1,87 @@
1
- import { Flags } from "@oclif/core";
1
+ import { defineCommand } from "citty";
2
2
  import { x } from "tinyexec";
3
3
  import consola from "consola";
4
4
  import { colors } from "consola/utils";
5
5
 
6
- import { BaseCommand } from "../base.js";
6
+ import { debugArg } from "../shared.js";
7
7
  import { isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
8
8
 
9
- /**
10
- * Create a pull request from the current branch
11
- * Note: Uses tinyexec which is safe (execFile-based, no shell injection)
12
- */
13
- export default class Pr extends BaseCommand {
14
- static override aliases = ["pr"];
15
- static override description = "Create a pull request from the current branch";
16
-
17
- static override examples = [
18
- {
19
- description: "Create PR from current branch",
20
- command: "<%= config.bin %> <%= command.id %>",
21
- },
22
- { description: "Create draft PR", command: "<%= config.bin %> <%= command.id %> --draft" },
23
- {
24
- description: "PR against develop branch",
25
- command: "<%= config.bin %> <%= command.id %> --base develop",
26
- },
27
- ];
9
+ function generatePrContent(branch: string, commits: string[]): { title: string; body: string } {
10
+ // Extract issue number from branch name if present (e.g., feature/123-some-feature)
11
+ const issueMatch = branch.match(/(\d+)/);
12
+ const issueNumber = issueMatch ? issueMatch[1] : null;
13
+
14
+ // Generate title from first commit or branch name
15
+ let title: string;
16
+ if (commits.length === 1) {
17
+ title = commits[0];
18
+ } else {
19
+ // Use branch name, cleaned up
20
+ title = branch
21
+ .replace(/^(feature|fix|bugfix|hotfix|chore|refactor)\//, "")
22
+ .replace(/^\d+-/, "")
23
+ .replace(/-/g, " ")
24
+ .replace(/\b\w/g, (c) => c.toUpperCase());
25
+ }
26
+
27
+ // Generate body
28
+ const lines: string[] = ["## Summary", ""];
29
+
30
+ if (commits.length === 1) {
31
+ lines.push(`- ${commits[0]}`);
32
+ } else {
33
+ for (const commit of commits.slice(0, 10)) {
34
+ lines.push(`- ${commit}`);
35
+ }
36
+ if (commits.length > 10) {
37
+ lines.push(`- ... and ${commits.length - 10} more commits`);
38
+ }
39
+ }
28
40
 
29
- static override flags = {
30
- ...BaseCommand.baseFlags,
31
- draft: Flags.boolean({
32
- char: "D",
41
+ lines.push("");
42
+
43
+ if (issueNumber) {
44
+ lines.push(`Closes #${issueNumber}`);
45
+ lines.push("");
46
+ }
47
+
48
+ lines.push("## Test plan");
49
+ lines.push("");
50
+ lines.push("- [ ] Tests pass");
51
+ lines.push("- [ ] Manual testing");
52
+
53
+ return { title, body: lines.join("\n") };
54
+ }
55
+
56
+ export default defineCommand({
57
+ meta: { name: "pr", description: "Create a pull request from the current branch" },
58
+ args: {
59
+ debug: debugArg,
60
+ draft: {
61
+ type: "boolean",
62
+ alias: "D",
33
63
  description: "Create as draft PR",
34
64
  default: false,
35
- }),
36
- base: Flags.string({
37
- char: "b",
65
+ },
66
+ base: {
67
+ type: "string",
68
+ alias: "b",
38
69
  description: "Base branch for the PR",
39
70
  default: "main",
40
- }),
41
- yes: Flags.boolean({
42
- char: "y",
71
+ },
72
+ yes: {
73
+ type: "boolean",
74
+ alias: "y",
43
75
  description: "Skip confirmation prompt",
44
76
  default: false,
45
- }),
46
- };
47
-
48
- async run(): Promise<void> {
49
- const { flags } = await this.parse(Pr);
50
-
77
+ },
78
+ },
79
+ async run({ args }) {
51
80
  // Check prerequisites
52
81
  const cliInstalled = await isGithubCliInstalled();
53
82
  if (!cliInstalled) {
54
83
  consola.error("GitHub CLI not installed");
55
- this.exit(1);
84
+ process.exit(1);
56
85
  }
57
86
 
58
87
  // Get current branch
@@ -61,31 +90,31 @@ export default class Pr extends BaseCommand {
61
90
 
62
91
  if (!currentBranch) {
63
92
  consola.error("Not on a branch (detached HEAD?)");
64
- this.exit(1);
93
+ process.exit(1);
65
94
  }
66
95
 
67
- if (currentBranch === flags.base) {
68
- consola.error(`Already on base branch ${flags.base}`);
69
- this.exit(1);
96
+ if (currentBranch === args.base) {
97
+ consola.error(`Already on base branch ${args.base}`);
98
+ process.exit(1);
70
99
  }
71
100
 
72
101
  consola.info(`Current branch: ${colors.cyan(currentBranch)}`);
73
- consola.info(`Base branch: ${colors.cyan(flags.base)}`);
102
+ consola.info(`Base branch: ${colors.cyan(args.base)}`);
74
103
 
75
104
  // Get commits between base and current branch
76
- const logResult = await x("git", ["log", `${flags.base}..HEAD`, "--pretty=format:%s"]);
105
+ const logResult = await x("git", ["log", `${args.base}..HEAD`, "--pretty=format:%s"]);
77
106
 
78
107
  const commits = logResult.stdout.trim().split("\n").filter(Boolean);
79
108
 
80
109
  if (commits.length === 0) {
81
- consola.error(`No commits between ${flags.base} and ${currentBranch}`);
82
- this.exit(1);
110
+ consola.error(`No commits between ${args.base} and ${currentBranch}`);
111
+ process.exit(1);
83
112
  }
84
113
 
85
114
  consola.info(`Found ${colors.green(commits.length.toString())} commits`);
86
115
 
87
116
  // Generate PR title and body
88
- const { title, body } = this.generatePrContent(currentBranch, commits);
117
+ const { title, body } = generatePrContent(currentBranch, commits);
89
118
 
90
119
  consola.box({
91
120
  title: "PR Preview",
@@ -93,7 +122,7 @@ export default class Pr extends BaseCommand {
93
122
  });
94
123
 
95
124
  // Confirm unless --yes
96
- if (!flags.yes) {
125
+ if (!args.yes) {
97
126
  const confirmed = await consola.prompt("Create this PR?", {
98
127
  type: "confirm",
99
128
  initial: true,
@@ -101,7 +130,7 @@ export default class Pr extends BaseCommand {
101
130
 
102
131
  if (!confirmed) {
103
132
  consola.info(colors.dim("Canceled"));
104
- this.exit(0);
133
+ process.exit(0);
105
134
  }
106
135
  }
107
136
 
@@ -115,9 +144,9 @@ export default class Pr extends BaseCommand {
115
144
  }
116
145
 
117
146
  // Create PR
118
- const prArgs = ["pr", "create", "--title", title, "--body", body, "--base", flags.base];
147
+ const prArgs = ["pr", "create", "--title", title, "--body", body, "--base", args.base];
119
148
 
120
- if (flags.draft) {
149
+ if (args.draft) {
121
150
  prArgs.push("--draft");
122
151
  }
123
152
 
@@ -125,52 +154,5 @@ export default class Pr extends BaseCommand {
125
154
  const prUrl = prResult.stdout.trim();
126
155
 
127
156
  consola.success(`PR created: ${colors.cyan(prUrl)}`);
128
- }
129
-
130
- private generatePrContent(branch: string, commits: string[]): { title: string; body: string } {
131
- // Extract issue number from branch name if present (e.g., feature/123-some-feature)
132
- const issueMatch = branch.match(/(\d+)/);
133
- const issueNumber = issueMatch ? issueMatch[1] : null;
134
-
135
- // Generate title from first commit or branch name
136
- let title: string;
137
- if (commits.length === 1) {
138
- title = commits[0];
139
- } else {
140
- // Use branch name, cleaned up
141
- title = branch
142
- .replace(/^(feature|fix|bugfix|hotfix|chore|refactor)\//, "")
143
- .replace(/^\d+-/, "")
144
- .replace(/-/g, " ")
145
- .replace(/\b\w/g, (c) => c.toUpperCase());
146
- }
147
-
148
- // Generate body
149
- const lines: string[] = ["## Summary", ""];
150
-
151
- if (commits.length === 1) {
152
- lines.push(`- ${commits[0]}`);
153
- } else {
154
- for (const commit of commits.slice(0, 10)) {
155
- lines.push(`- ${commit}`);
156
- }
157
- if (commits.length > 10) {
158
- lines.push(`- ... and ${commits.length - 10} more commits`);
159
- }
160
- }
161
-
162
- lines.push("");
163
-
164
- if (issueNumber) {
165
- lines.push(`Closes #${issueNumber}`);
166
- lines.push("");
167
- }
168
-
169
- lines.push("## Test plan");
170
- lines.push("");
171
- lines.push("- [ ] Tests pass");
172
- lines.push("- [ ] Manual testing");
173
-
174
- return { title, body: lines.join("\n") };
175
- }
176
- }
157
+ },
158
+ });
@@ -1,10 +1,12 @@
1
- import { Flags } from "@oclif/core";
1
+ import { defineCommand } from "citty";
2
2
  import { DateTime } from "luxon";
3
3
  import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { x } from "tinyexec";
7
- import { BaseCommand } from "../base.js";
7
+ import consola from "consola";
8
+
9
+ import { debugArg } from "../shared.js";
8
10
  import {
9
11
  buildAllSessionsTreemap,
10
12
  buildBarChartData,
@@ -40,86 +42,73 @@ export type {
40
42
  TreemapNode,
41
43
  } from "../../lib/graph/index.js";
42
44
 
43
- /**
44
- * Generate interactive HTML treemap from session token data
45
- */
46
- export default class Graph extends BaseCommand {
47
- static override description = "Generate interactive HTML treemap from session token data";
48
-
49
- static override examples = [
50
- {
51
- description: "Generate treemap for all recent sessions",
52
- command: "<%= config.bin %> <%= command.id %>",
53
- },
54
- {
55
- description: "Generate treemap for a specific session",
56
- command: "<%= config.bin %> <%= command.id %> --session abc123",
57
- },
58
- {
59
- description: "Generate and auto-open in browser",
60
- command: "<%= config.bin %> <%= command.id %> --open",
61
- },
62
- ];
63
-
64
- static override flags = {
65
- ...BaseCommand.baseFlags,
66
- session: Flags.string({
67
- char: "s",
45
+ export default defineCommand({
46
+ meta: { name: "graph", description: "Generate interactive HTML treemap from session token data" },
47
+ args: {
48
+ debug: debugArg,
49
+ session: {
50
+ type: "string" as const,
51
+ alias: "s",
68
52
  description: "Session ID to analyze (shows all sessions if not provided)",
69
- }),
70
- open: Flags.boolean({
71
- char: "o",
53
+ },
54
+ open: {
55
+ type: "boolean" as const,
56
+ alias: "o",
72
57
  description: "Open treemap in browser after generating",
73
58
  default: true,
74
- allowNo: true,
75
- }),
76
- serve: Flags.boolean({
59
+ },
60
+ serve: {
61
+ type: "boolean" as const,
77
62
  description: "Start local HTTP server to serve treemap (default: true)",
78
63
  default: true,
79
- allowNo: true,
80
- }),
81
- port: Flags.integer({
82
- char: "p",
83
- description: "Port for local server",
84
- default: 8765,
85
- }),
86
- days: Flags.integer({
87
- description: "Filter to sessions from last N days (0=no limit)",
88
- default: 7,
89
- }),
90
- };
91
-
92
- async run(): Promise<void> {
93
- const { flags } = await this.parse(Graph);
64
+ },
65
+ port: {
66
+ type: "string" as const,
67
+ alias: "p",
68
+ description: "Port for local server (default: 8765)",
69
+ default: "8765",
70
+ },
71
+ days: {
72
+ type: "string" as const,
73
+ description: "Filter to sessions from last N days (0=no limit, default: 7)",
74
+ default: "7",
75
+ },
76
+ },
77
+ async run({ args }) {
78
+ const port = Number(args.port);
79
+ const days = Number(args.days);
94
80
 
95
81
  const projectsDir = path.join(os.homedir(), ".claude", "projects");
96
82
  if (!fs.existsSync(projectsDir)) {
97
- this.error("No Claude projects directory found at ~/.claude/projects/");
83
+ consola.error("No Claude projects directory found at ~/.claude/projects/");
84
+ process.exit(1);
98
85
  }
99
86
 
100
- const sessionId = flags.session;
87
+ const sessionId = args.session;
101
88
  let treemapData;
102
89
  let barChartData = { days: [] as any[] };
103
90
 
104
91
  if (!sessionId) {
105
92
  // All sessions mode
106
- const sessions = findRecentSessions(projectsDir, 500, flags.days);
93
+ const sessions = findRecentSessions(projectsDir, 500, days);
107
94
  if (sessions.length === 0) {
108
- this.error("No sessions found");
95
+ consola.error("No sessions found");
96
+ process.exit(1);
109
97
  }
110
98
 
111
- const daysMsg = flags.days > 0 ? ` (last ${flags.days} days)` : "";
112
- this.log(`šŸ“Š Generating treemap for ${sessions.length} sessions${daysMsg}...`);
99
+ const daysMsg = days > 0 ? ` (last ${days} days)` : "";
100
+ consola.info(`šŸ“Š Generating treemap for ${sessions.length} sessions${daysMsg}...`);
113
101
  treemapData = buildAllSessionsTreemap(sessions);
114
102
  barChartData = buildBarChartData(sessions);
115
103
  } else {
116
104
  // Single session mode
117
105
  const sessionPath = findSessionPath(projectsDir, sessionId);
118
106
  if (!sessionPath) {
119
- this.error(`Session ${sessionId} not found`);
107
+ consola.error(`Session ${sessionId} not found`);
108
+ process.exit(1);
120
109
  }
121
110
 
122
- this.log(`šŸ“Š Generating treemap for session ${sessionId}...`);
111
+ consola.info(`šŸ“Š Generating treemap for session ${sessionId}...`);
123
112
  const entries = parseJsonl(sessionPath);
124
113
  treemapData = buildSessionTreemap(sessionId, entries);
125
114
  // Bar chart not meaningful for single session, leave empty
@@ -135,35 +124,35 @@ export default class Graph extends BaseCommand {
135
124
  }
136
125
 
137
126
  const timestamp = DateTime.now().toFormat("yyyy-MM-dd'T'HH-mmZZZ");
138
- const daysLabel = flags.days > 0 ? `${flags.days}d` : "all";
127
+ const daysLabel = days > 0 ? `${days}d` : "all";
139
128
  const filename = sessionId
140
129
  ? `treemap-${sessionId.slice(0, 8)}-${timestamp}.html`
141
130
  : `treemap-${daysLabel}-${timestamp}.html`;
142
131
  const outputPath = path.join(reportsDir, filename);
143
132
 
144
133
  fs.writeFileSync(outputPath, html);
145
- this.log(`āœ“ Saved to ${outputPath}`);
134
+ consola.info(`āœ“ Saved to ${outputPath}`);
146
135
 
147
- if (flags.serve) {
148
- const { server, port: actualPort } = await startServer(html, filename, flags.port);
136
+ if (args.serve) {
137
+ const { server, port: actualPort } = await startServer(html, filename, port);
149
138
  const url = `http://localhost:${actualPort}/`;
150
- if (actualPort !== flags.port) {
151
- this.log(`\nāš ļø Port ${flags.port} in use, using ${actualPort}`);
139
+ if (actualPort !== port) {
140
+ consola.info(`\nāš ļø Port ${port} in use, using ${actualPort}`);
152
141
  }
153
- this.log(`🌐 Server running at ${url}`);
154
- this.log(" Press Ctrl+C to stop\n");
142
+ consola.info(`🌐 Server running at ${url}`);
143
+ consola.info(" Press Ctrl+C to stop\n");
155
144
 
156
- if (flags.open) {
145
+ if (args.open) {
157
146
  openInBrowser(url);
158
147
  }
159
148
 
160
149
  // Keep server running until Ctrl+C
161
150
  await waitForShutdown(server);
162
- this.log("\nšŸ‘‹ Stopping server...");
163
- } else if (flags.open) {
164
- this.log("\nšŸ“ˆ Opening treemap...");
151
+ consola.info("\nšŸ‘‹ Stopping server...");
152
+ } else if (args.open) {
153
+ consola.info("\nšŸ“ˆ Opening treemap...");
165
154
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
166
155
  await x(openCmd, [outputPath]);
167
156
  }
168
- }
169
- }
157
+ },
158
+ });