@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,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve, join } from "node:path";
3
+ import { defineCommand } from "citty";
3
4
  import consola from "consola";
4
5
  import { x } from "tinyexec";
5
6
  import { colors } from "consola/utils";
6
- import { BaseCommand } from "./base.js";
7
+ import { debugArg } from "./shared.js";
7
8
 
8
9
  interface CheckResult {
9
10
  name: string;
@@ -12,35 +13,146 @@ interface CheckResult {
12
13
  warning?: string;
13
14
  }
14
15
 
15
- /**
16
- * Check system dependencies and environment
17
- */
18
- export default class Doctor extends BaseCommand {
19
- static override description = "Check system dependencies and environment";
16
+ async function checkCommand(
17
+ name: string,
18
+ args: string[],
19
+ versionPattern: RegExp,
20
+ optional = false,
21
+ ): Promise<CheckResult> {
22
+ try {
23
+ const result = await x(name, args);
24
+ const output = result.stdout + result.stderr;
25
+ const match = output.match(versionPattern);
26
+ return {
27
+ name,
28
+ version: match?.[1] ?? output.trim().slice(0, 20),
29
+ ok: true,
30
+ };
31
+ } catch {
32
+ consola.debug(`Tool check failed for "${name}"`);
33
+ return {
34
+ name,
35
+ version: null,
36
+ ok: optional,
37
+ warning: optional ? "optional, not installed" : undefined,
38
+ };
39
+ }
40
+ }
41
+
42
+ async function checkGhAuth(): Promise<{ ok: boolean }> {
43
+ try {
44
+ const result = await x("gh", ["auth", "status"]);
45
+ return { ok: result.exitCode === 0 };
46
+ } catch {
47
+ consola.debug("GitHub CLI auth check failed");
48
+ return { ok: false };
49
+ }
50
+ }
51
+
52
+ function checkAgentBoard(): {
53
+ name: string;
54
+ value: string;
55
+ ok: boolean;
56
+ warning?: string;
57
+ hint?: string;
58
+ }[] {
59
+ const results: { name: string; value: string; ok: boolean; warning?: string; hint?: string }[] =
60
+ [];
61
+
62
+ const defaultDataDir = resolve(
63
+ process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
64
+ "towles-tool",
65
+ "agentboard",
66
+ );
67
+ const dataDir = process.env.AGENTBOARD_DATA_DIR ?? defaultDataDir;
68
+ const dbPath = join(dataDir, "agentboard.db");
69
+ const configPath = join(dataDir, "config.json");
70
+
71
+ const dbExists = existsSync(dbPath);
72
+ results.push({
73
+ name: "database",
74
+ value: dbExists ? dbPath : "not found",
75
+ ok: dbExists,
76
+ hint: dbExists ? undefined : "Run: tt ag (starts server and creates DB automatically)",
77
+ });
78
+
79
+ let repoPaths: string[] = [];
80
+ if (existsSync(configPath)) {
81
+ try {
82
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
83
+ repoPaths = config.repoPaths ?? [];
84
+ } catch {
85
+ // Corrupted config
86
+ }
87
+ }
20
88
 
21
- static override examples = [
22
- { description: "Check system dependencies", command: "<%= config.bin %> <%= command.id %>" },
23
- { description: "Verify environment after setup", command: "<%= config.bin %> doctor" },
89
+ results.push({
90
+ name: "scan paths",
91
+ value: repoPaths.length > 0 ? repoPaths.join(", ") : "none configured",
92
+ ok: repoPaths.length > 0,
93
+ warning: repoPaths.length === 0 ? "no scan paths" : undefined,
94
+ hint:
95
+ repoPaths.length === 0
96
+ ? "Run: tt ag → open Workspaces → run the onboarding wizard"
97
+ : undefined,
98
+ });
99
+
100
+ results.push({
101
+ name: "data dir",
102
+ value: dataDir,
103
+ ok: true,
104
+ });
105
+
106
+ return results;
107
+ }
108
+
109
+ async function checkClaudePlugins(): Promise<
110
+ { name: string; ok: boolean; installHint?: string }[]
111
+ > {
112
+ const requiredPlugins = [
113
+ {
114
+ id: "code-simplifier@claude-plugins-official",
115
+ name: "code-simplifier",
116
+ installCmd: "claude plugin install code-simplifier@claude-plugins-official --scope user",
117
+ },
24
118
  ];
25
119
 
26
- async run(): Promise<void> {
27
- await this.parse(Doctor);
120
+ try {
121
+ const result = await x("claude", ["plugin", "list", "--json"]);
122
+ const plugins: { id: string }[] = JSON.parse(result.stdout);
123
+ const installedIds = new Set(plugins.map((p) => p.id));
124
+
125
+ return requiredPlugins.map((p) => ({
126
+ name: p.name,
127
+ ok: installedIds.has(p.id),
128
+ installHint: installedIds.has(p.id) ? undefined : `Run: ${p.installCmd}`,
129
+ }));
130
+ } catch {
131
+ consola.debug("Failed to list Claude plugins");
132
+ return requiredPlugins.map((p) => ({
133
+ name: p.name,
134
+ ok: false,
135
+ installHint: `Run: ${p.installCmd}`,
136
+ }));
137
+ }
138
+ }
28
139
 
29
- this.log("Checking dependencies...\n");
140
+ export default defineCommand({
141
+ meta: { name: "doctor", description: "Check system dependencies and environment" },
142
+ args: { debug: debugArg },
143
+ async run() {
144
+ consola.info("Checking dependencies...\n");
30
145
 
31
146
  const checks: CheckResult[] = await Promise.all([
32
- this.checkCommand("git", ["--version"], /git version ([\d.]+)/),
33
- this.checkCommand("gh", ["--version"], /gh version ([\d.]+)/),
34
- this.checkCommand("node", ["--version"], /v?([\d.]+)/),
35
- this.checkCommand("bun", ["--version"], /([\d.]+)/),
36
- this.checkCommand("pnpm", ["--version"], /([\d.]+)/),
37
- this.checkCommand("tsx", ["--version"], /([\d.]+)/),
38
- this.checkCommand("claude", ["--version"], /([\d.]+)/),
39
- this.checkCommand("tmux", ["-V"], /tmux ([\d.]+)/),
40
- this.checkCommand("ttyd", ["--version"], /ttyd version ([\d.]+)/, true),
147
+ checkCommand("git", ["--version"], /git version ([\d.]+)/),
148
+ checkCommand("gh", ["--version"], /gh version ([\d.]+)/),
149
+ checkCommand("node", ["--version"], /v?([\d.]+)/),
150
+ checkCommand("bun", ["--version"], /([\d.]+)/),
151
+ checkCommand("claude", ["--version"], /([\d.]+)/),
152
+ checkCommand("tmux", ["-V"], /tmux ([\d.]+)/),
153
+ checkCommand("ttyd", ["--version"], /ttyd version ([\d.]+)/, true),
41
154
  ]);
42
155
 
43
- // Display results
44
156
  for (const check of checks) {
45
157
  const icon = check.ok
46
158
  ? colors.green("✓")
@@ -48,199 +160,65 @@ export default class Doctor extends BaseCommand {
48
160
  ? colors.yellow("⚠")
49
161
  : colors.red("✗");
50
162
  const version = check.version ?? "not found";
51
- this.log(`${icon} ${check.name}: ${version}`);
163
+ consola.log(`${icon} ${check.name}: ${version}`);
52
164
  if (check.warning) {
53
- this.log(` ${colors.yellow("⚠")} ${check.warning}`);
165
+ consola.log(` ${colors.yellow("⚠")} ${check.warning}`);
54
166
  }
55
167
  }
56
168
 
57
- // Check gh auth
58
- this.log("");
59
- const ghAuth = await this.checkGhAuth();
169
+ consola.log("");
170
+ const ghAuth = await checkGhAuth();
60
171
  const authIcon = ghAuth.ok ? colors.green("✓") : colors.yellow("⚠");
61
- this.log(`${authIcon} gh auth: ${ghAuth.ok ? "authenticated" : "not authenticated"}`);
172
+ consola.log(`${authIcon} gh auth: ${ghAuth.ok ? "authenticated" : "not authenticated"}`);
62
173
  if (!ghAuth.ok) {
63
- this.log(` ${colors.dim("Run: gh auth login")}`);
174
+ consola.log(` ${colors.dim("Run: gh auth login")}`);
64
175
  }
65
176
 
66
- // Node version check
67
177
  const nodeCheck = checks.find((c) => c.name === "node");
68
178
  if (nodeCheck?.version) {
69
179
  const major = Number.parseInt(nodeCheck.version.split(".")[0], 10);
70
180
  if (major < 18) {
71
- this.log("");
72
- this.log(`${colors.yellow("⚠")} Node.js 18+ recommended (found ${nodeCheck.version})`);
181
+ consola.log("");
182
+ consola.log(`${colors.yellow("⚠")} Node.js 18+ recommended (found ${nodeCheck.version})`);
73
183
  }
74
184
  }
75
185
 
76
- // Claude plugin checks
77
- this.log("");
78
- const pluginChecks = await this.checkClaudePlugins();
186
+ consola.log("");
187
+ const pluginChecks = await checkClaudePlugins();
79
188
  for (const check of pluginChecks) {
80
189
  const icon = check.ok ? colors.green("✓") : colors.red("✗");
81
190
  const status = check.ok ? "installed" : "not installed";
82
- this.log(`${icon} claude plugin ${check.name}: ${status}`);
191
+ consola.log(`${icon} claude plugin ${check.name}: ${status}`);
83
192
  if (!check.ok && check.installHint) {
84
- this.log(` ${colors.dim(check.installHint)}`);
193
+ consola.log(` ${colors.dim(check.installHint)}`);
85
194
  }
86
195
  }
87
196
 
88
- // AgentBoard checks
89
- this.log("");
90
- this.log(colors.bold("AgentBoard:"));
91
- const agentboardChecks = this.checkAgentBoard();
197
+ consola.log("");
198
+ consola.log(colors.bold("AgentBoard:"));
199
+ const agentboardChecks = checkAgentBoard();
92
200
  for (const check of agentboardChecks) {
93
201
  const icon = check.ok
94
202
  ? colors.green("✓")
95
203
  : check.warning
96
204
  ? colors.yellow("⚠")
97
205
  : colors.red("✗");
98
- this.log(`${icon} ${check.name}: ${check.value}`);
206
+ consola.log(`${icon} ${check.name}: ${check.value}`);
99
207
  if (check.hint) {
100
- this.log(` ${colors.dim(check.hint)}`);
208
+ consola.log(` ${colors.dim(check.hint)}`);
101
209
  }
102
210
  }
103
211
 
104
- // Summary
105
212
  const allOk =
106
213
  checks.every((c) => c.ok || !!c.warning) &&
107
214
  ghAuth.ok &&
108
215
  pluginChecks.every((c) => c.ok) &&
109
216
  agentboardChecks.every((c) => c.ok || !!c.warning);
110
- this.log("");
217
+ consola.log("");
111
218
  if (allOk) {
112
- this.log(colors.green("All checks passed!"));
219
+ consola.log(colors.green("All checks passed!"));
113
220
  } else {
114
- this.log(colors.yellow("Some checks failed. See above for details."));
221
+ consola.log(colors.yellow("Some checks failed. See above for details."));
115
222
  }
116
- }
117
-
118
- private async checkCommand(
119
- name: string,
120
- args: string[],
121
- versionPattern: RegExp,
122
- optional = false,
123
- ): Promise<CheckResult> {
124
- try {
125
- // tinyexec is safe - uses execFile internally, no shell injection risk
126
- const result = await x(name, args);
127
- const output = result.stdout + result.stderr;
128
- const match = output.match(versionPattern);
129
- return {
130
- name,
131
- version: match?.[1] ?? output.trim().slice(0, 20),
132
- ok: true,
133
- };
134
- } catch {
135
- consola.debug(`Tool check failed for "${name}"`);
136
- return {
137
- name,
138
- version: null,
139
- ok: optional,
140
- warning: optional ? "optional, not installed" : undefined,
141
- };
142
- }
143
- }
144
-
145
- private async checkGhAuth(): Promise<{ ok: boolean }> {
146
- try {
147
- // tinyexec is safe - uses execFile internally, no shell injection risk
148
- const result = await x("gh", ["auth", "status"]);
149
- return { ok: result.exitCode === 0 };
150
- } catch {
151
- consola.debug("GitHub CLI auth check failed");
152
- return { ok: false };
153
- }
154
- }
155
-
156
- private checkAgentBoard(): {
157
- name: string;
158
- value: string;
159
- ok: boolean;
160
- warning?: string;
161
- hint?: string;
162
- }[] {
163
- const results: { name: string; value: string; ok: boolean; warning?: string; hint?: string }[] =
164
- [];
165
-
166
- const defaultDataDir = resolve(
167
- process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
168
- "towles-tool",
169
- "agentboard",
170
- );
171
- const dataDir = process.env.AGENTBOARD_DATA_DIR ?? defaultDataDir;
172
- const dbPath = join(dataDir, "agentboard.db");
173
- const configPath = join(dataDir, "config.json");
174
-
175
- // DB exists
176
- const dbExists = existsSync(dbPath);
177
- results.push({
178
- name: "database",
179
- value: dbExists ? dbPath : "not found",
180
- ok: dbExists,
181
- hint: dbExists ? undefined : "Run: tt ag (starts server and creates DB automatically)",
182
- });
183
-
184
- // Config exists with repoPaths
185
- let repoPaths: string[] = [];
186
- if (existsSync(configPath)) {
187
- try {
188
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
189
- repoPaths = config.repoPaths ?? [];
190
- } catch {
191
- // Corrupted config
192
- }
193
- }
194
-
195
- results.push({
196
- name: "scan paths",
197
- value: repoPaths.length > 0 ? repoPaths.join(", ") : "none configured",
198
- ok: repoPaths.length > 0,
199
- warning: repoPaths.length === 0 ? "no scan paths" : undefined,
200
- hint:
201
- repoPaths.length === 0
202
- ? "Run: tt ag → open Workspaces → run the onboarding wizard"
203
- : undefined,
204
- });
205
-
206
- // Data directory
207
- results.push({
208
- name: "data dir",
209
- value: dataDir,
210
- ok: true,
211
- });
212
-
213
- return results;
214
- }
215
-
216
- private async checkClaudePlugins(): Promise<
217
- { name: string; ok: boolean; installHint?: string }[]
218
- > {
219
- const requiredPlugins = [
220
- {
221
- id: "code-simplifier@claude-plugins-official",
222
- name: "code-simplifier",
223
- installCmd: "claude plugin install code-simplifier@claude-plugins-official --scope user",
224
- },
225
- ];
226
-
227
- try {
228
- const result = await x("claude", ["plugin", "list", "--json"]);
229
- const plugins: { id: string }[] = JSON.parse(result.stdout);
230
- const installedIds = new Set(plugins.map((p) => p.id));
231
-
232
- return requiredPlugins.map((p) => ({
233
- name: p.name,
234
- ok: installedIds.has(p.id),
235
- installHint: installedIds.has(p.id) ? undefined : `Run: ${p.installCmd}`,
236
- }));
237
- } catch {
238
- consola.debug("Failed to list Claude plugins");
239
- return requiredPlugins.map((p) => ({
240
- name: p.name,
241
- ok: false,
242
- installHint: `Run: ${p.installCmd}`,
243
- }));
244
- }
245
- }
246
- }
223
+ },
224
+ });
@@ -1,52 +1,37 @@
1
- import { Flags } from "@oclif/core";
1
+ import { defineCommand } from "citty";
2
2
  import { colors } from "consola/utils";
3
3
  import consola from "consola";
4
4
  import { x } from "tinyexec";
5
5
 
6
- import { BaseCommand } from "../base.js";
7
-
8
- /**
9
- * Clean up merged branches
10
- */
11
- export default class BranchClean extends BaseCommand {
12
- static override description = "Delete local branches that have been merged into main";
13
-
14
- static override examples = [
15
- { description: "Clean merged branches", command: "<%= config.bin %> <%= command.id %>" },
16
- {
17
- description: "Preview without deleting",
18
- command: "<%= config.bin %> <%= command.id %> --dry-run",
19
- },
20
- { description: "Skip confirmation", command: "<%= config.bin %> <%= command.id %> --force" },
21
- {
22
- description: "Check against develop",
23
- command: "<%= config.bin %> <%= command.id %> --base develop",
24
- },
25
- ];
26
-
27
- static override flags = {
28
- ...BaseCommand.baseFlags,
29
- force: Flags.boolean({
30
- char: "f",
6
+ import { debugArg } from "../shared.js";
7
+
8
+ export default defineCommand({
9
+ meta: {
10
+ name: "branch-clean",
11
+ description: "Delete local branches that have been merged into main",
12
+ },
13
+ args: {
14
+ debug: debugArg,
15
+ force: {
16
+ type: "boolean",
17
+ alias: "f",
31
18
  description: "Skip confirmation prompt",
32
19
  default: false,
33
- }),
34
- "dry-run": Flags.boolean({
35
- char: "d",
20
+ },
21
+ dryRun: {
22
+ type: "boolean",
36
23
  description: "Preview branches without deleting",
37
24
  default: false,
38
- }),
39
- base: Flags.string({
40
- char: "b",
25
+ },
26
+ base: {
27
+ type: "string",
28
+ alias: "b",
41
29
  description: "Base branch to check against",
42
30
  default: "main",
43
- }),
44
- };
45
-
46
- async run(): Promise<void> {
47
- const { flags } = await this.parse(BranchClean);
48
- const baseBranch = flags.base;
49
- const dryRun = flags["dry-run"];
31
+ },
32
+ },
33
+ async run({ args }) {
34
+ const baseBranch = args.base;
50
35
 
51
36
  // Get current branch
52
37
  const currentResult = await x("git", ["branch", "--show-current"]);
@@ -73,12 +58,12 @@ export default class BranchClean extends BaseCommand {
73
58
  consola.log(` - ${branch}`);
74
59
  }
75
60
 
76
- if (dryRun) {
61
+ if (args.dryRun) {
77
62
  consola.info(colors.yellow("Dry run - no branches deleted"));
78
63
  return;
79
64
  }
80
65
 
81
- if (!flags.force) {
66
+ if (!args.force) {
82
67
  const answer = await consola.prompt(`Delete ${toDelete.length} branch(es)?`, {
83
68
  type: "confirm",
84
69
  initial: false,
@@ -112,5 +97,5 @@ export default class BranchClean extends BaseCommand {
112
97
  if (failed > 0) {
113
98
  consola.warn(colors.yellow(`Failed to delete ${failed} branch(es)`));
114
99
  }
115
- }
116
- }
100
+ },
101
+ });
@@ -1,11 +1,11 @@
1
- import { Flags } from "@oclif/core";
1
+ import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
3
  import prompts from "prompts";
4
4
  import type { Choice } from "prompts";
5
5
  import { colors } from "consola/utils";
6
6
  import { Fzf } from "fzf";
7
7
 
8
- import { BaseCommand } from "../base.js";
8
+ import { debugArg } from "../shared.js";
9
9
  import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
10
10
  import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
11
11
  import { git } from "../../utils/git/exec.js";
@@ -46,46 +46,31 @@ export function buildIssueChoices(issues: Issue[], layout: ColumnLayout): Choice
46
46
  return choices;
47
47
  }
48
48
 
49
- /**
50
- * Create a git branch from a GitHub issue
51
- */
52
- export default class GhBranch extends BaseCommand {
53
- static override description = "Create a git branch from a GitHub issue";
54
-
55
- static override examples = [
56
- { description: "Browse all open issues", command: "<%= config.bin %> <%= command.id %>" },
57
- {
58
- description: "Only issues assigned to me",
59
- command: "<%= config.bin %> <%= command.id %> --assignedToMe",
60
- },
61
- { description: "Short flag for assigned", command: "<%= config.bin %> <%= command.id %> -a" },
62
- ];
63
-
64
- static override flags = {
65
- ...BaseCommand.baseFlags,
66
- assignedToMe: Flags.boolean({
67
- char: "a",
49
+ export default defineCommand({
50
+ meta: { name: "branch", description: "Create a git branch from a GitHub issue" },
51
+ args: {
52
+ debug: debugArg,
53
+ assignedToMe: {
54
+ type: "boolean",
55
+ alias: "a",
68
56
  description: "Only show issues assigned to me",
69
57
  default: false,
70
- }),
71
- };
72
-
73
- async run(): Promise<void> {
74
- const { flags } = await this.parse(GhBranch);
75
-
58
+ },
59
+ },
60
+ async run({ args }) {
76
61
  // Check prerequisites
77
62
  const cliInstalled = await isGithubCliInstalled();
78
63
  if (!cliInstalled) {
79
- consola.log("Github CLI not installed");
80
- this.exit(1);
64
+ consola.error("Github CLI not installed");
65
+ process.exit(1);
81
66
  }
82
67
 
83
- consola.log("Assigned to me:", flags.assignedToMe);
68
+ consola.log("Assigned to me:", args.assignedToMe);
84
69
 
85
- const currentIssues = await getIssues({ assignedToMe: flags.assignedToMe, cwd: process.cwd() });
70
+ const currentIssues = await getIssues({ assignedToMe: args.assignedToMe, cwd: process.cwd() });
86
71
  if (currentIssues.length === 0) {
87
72
  consola.log(colors.yellow("No issues found, check assignments"));
88
- this.exit(1);
73
+ process.exit(1);
89
74
  } else {
90
75
  consola.log(colors.green(`${currentIssues.length} Issues found assigned to you`));
91
76
  }
@@ -114,14 +99,14 @@ export default class GhBranch extends BaseCommand {
114
99
  {
115
100
  onCancel: () => {
116
101
  consola.info(colors.dim("Canceled"));
117
- this.exit(0);
102
+ process.exit(0);
118
103
  },
119
104
  },
120
105
  );
121
106
 
122
107
  if (result.issueNumber === "cancel") {
123
108
  consola.log(colors.dim("Canceled"));
124
- this.exit(0);
109
+ process.exit(0);
125
110
  }
126
111
 
127
112
  const selectedIssue = currentIssues.find((i) => i.number === result.issueNumber)!;
@@ -133,7 +118,7 @@ export default class GhBranch extends BaseCommand {
133
118
  await git(["checkout", "-b", branchName]);
134
119
  } catch {
135
120
  consola.debug("Branch checkout failed");
136
- this.exit(1);
121
+ process.exit(1);
137
122
  }
138
- }
139
- }
123
+ },
124
+ });
@@ -0,0 +1,10 @@
1
+ import { defineCommand } from "citty";
2
+
3
+ export default defineCommand({
4
+ meta: { name: "gh", description: "GitHub utilities" },
5
+ subCommands: {
6
+ branch: () => import("./branch.js").then((m) => m.default),
7
+ "branch-clean": () => import("./branch-clean.js").then((m) => m.default),
8
+ pr: () => import("./pr.js").then((m) => m.default),
9
+ },
10
+ });