@towles/tool 0.0.62 → 0.0.64
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/package.json +50 -57
- package/src/commands/agentboard.ts +176 -0
- package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
- package/src/commands/auto-claude/list.ts +114 -0
- package/src/commands/auto-claude/retry.test.ts +138 -0
- package/src/commands/auto-claude/retry.ts +139 -0
- package/src/commands/auto-claude/status.test.ts +147 -0
- package/src/commands/auto-claude/status.ts +123 -0
- package/src/commands/base.ts +7 -2
- package/src/commands/config.ts +5 -7
- package/src/commands/doctor.ts +111 -12
- package/src/commands/gh/branch.ts +4 -4
- package/src/commands/gh/pr.ts +1 -0
- package/src/commands/graph/index.ts +169 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +40 -68
- package/src/commands/journal/daily-notes.ts +3 -3
- package/src/commands/journal/meeting.ts +3 -3
- package/src/commands/journal/note.ts +3 -3
- package/src/lib/auto-claude/claude-cli.ts +183 -0
- package/src/lib/auto-claude/config.test.ts +6 -8
- package/src/lib/auto-claude/config.ts +3 -4
- package/src/lib/auto-claude/index.ts +2 -3
- package/src/lib/auto-claude/labels.test.ts +85 -0
- package/src/lib/auto-claude/labels.ts +42 -0
- package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
- package/src/lib/auto-claude/pipeline.test.ts +2 -2
- package/src/lib/auto-claude/pipeline.ts +120 -36
- package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
- package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
- package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
- package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
- package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
- package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
- package/src/lib/auto-claude/run-claude.test.ts +48 -68
- package/src/lib/auto-claude/shell.ts +6 -0
- package/src/lib/auto-claude/steps/create-pr.ts +89 -25
- package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
- package/src/lib/auto-claude/steps/implement.ts +9 -16
- package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
- package/src/lib/auto-claude/steps/steps.test.ts +68 -63
- package/src/lib/auto-claude/templates.test.ts +91 -0
- package/src/lib/auto-claude/templates.ts +34 -0
- package/src/lib/auto-claude/test-helpers.ts +2 -1
- package/src/lib/auto-claude/utils-execution.test.ts +9 -57
- package/src/lib/auto-claude/utils.test.ts +5 -9
- package/src/lib/auto-claude/utils.ts +27 -253
- package/src/lib/graph/analyzer.test.ts +451 -0
- package/src/lib/graph/analyzer.ts +165 -0
- package/src/lib/graph/index.ts +24 -0
- package/src/lib/graph/labels.ts +87 -0
- package/src/lib/graph/parser.test.ts +150 -0
- package/src/lib/graph/parser.ts +65 -0
- package/src/lib/graph/render.ts +25 -0
- package/src/lib/graph/server.ts +70 -0
- package/src/lib/graph/sessions.ts +104 -0
- package/src/lib/graph/tools.ts +90 -0
- package/src/lib/graph/treemap.ts +211 -0
- package/src/lib/graph/types.ts +80 -0
- package/src/lib/install/claude-settings.ts +64 -0
- package/src/lib/journal/editor.ts +33 -0
- package/src/lib/journal/fs.ts +13 -0
- package/src/lib/journal/index.ts +11 -0
- package/src/lib/journal/paths.ts +106 -0
- package/src/lib/journal/{utils.ts → templates.ts} +3 -151
- package/src/utils/fs.ts +19 -0
- package/src/utils/git/exec.ts +18 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
- package/src/utils/git/gh-cli-wrapper.ts +31 -19
- package/src/utils/render.ts +3 -1
- package/src/commands/graph.ts +0 -970
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
- package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
- package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
- package/src/lib/auto-claude/steps/plan.ts +0 -14
- package/src/lib/auto-claude/steps/refresh.ts +0 -114
- package/src/lib/auto-claude/steps/remove-label.ts +0 -22
- package/src/lib/auto-claude/steps/research.ts +0 -21
- package/src/lib/auto-claude/steps/review.ts +0 -14
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { Flags } from "@oclif/core";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { colors } from "consola/utils";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
|
|
9
|
+
import { BaseCommand } from "../base.js";
|
|
10
|
+
import { initConfig } from "../../lib/auto-claude/index.js";
|
|
11
|
+
import { LABELS, removeLabel, setLabel } from "../../lib/auto-claude/labels.js";
|
|
12
|
+
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
13
|
+
import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Core retry logic: swap labels on failed issues to re-trigger the pipeline.
|
|
17
|
+
* Extracted for testability.
|
|
18
|
+
*/
|
|
19
|
+
export async function retryIssues(
|
|
20
|
+
repo: string,
|
|
21
|
+
triggerLabel: string,
|
|
22
|
+
selected: Issue[],
|
|
23
|
+
clean: boolean,
|
|
24
|
+
): Promise<number> {
|
|
25
|
+
for (const issue of selected) {
|
|
26
|
+
consola.info(`Retrying issue #${issue.number}: ${issue.title}`);
|
|
27
|
+
|
|
28
|
+
await removeLabel(repo, issue.number, LABELS.failed);
|
|
29
|
+
consola.success(` Removed '${LABELS.failed}' label`);
|
|
30
|
+
|
|
31
|
+
await setLabel(repo, issue.number, triggerLabel);
|
|
32
|
+
consola.success(` Added '${triggerLabel}' label`);
|
|
33
|
+
|
|
34
|
+
if (clean) {
|
|
35
|
+
const artifactDir = join(process.cwd(), `.auto-claude/issue-${issue.number}`);
|
|
36
|
+
try {
|
|
37
|
+
rmSync(artifactDir, { recursive: true, force: true });
|
|
38
|
+
consola.success(` Cleaned artifacts: ${artifactDir}`);
|
|
39
|
+
} catch {
|
|
40
|
+
consola.warn(` Could not clean artifacts: ${artifactDir}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return selected.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default class AutoClaudeRetry extends BaseCommand {
|
|
49
|
+
static override description = "Retry failed auto-claude issues by swapping labels";
|
|
50
|
+
|
|
51
|
+
static override examples = [
|
|
52
|
+
{
|
|
53
|
+
description: "Interactively pick failed issues to retry",
|
|
54
|
+
command: "<%= config.bin %> auto-claude retry",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
description: "Retry a specific issue",
|
|
58
|
+
command: "<%= config.bin %> auto-claude retry --issue 42",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
description: "Retry and clean local artifacts",
|
|
62
|
+
command: "<%= config.bin %> auto-claude retry --issue 42 --clean",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
static override flags = {
|
|
67
|
+
...BaseCommand.baseFlags,
|
|
68
|
+
issue: Flags.integer({
|
|
69
|
+
char: "i",
|
|
70
|
+
description: "Issue number to retry",
|
|
71
|
+
}),
|
|
72
|
+
clean: Flags.boolean({
|
|
73
|
+
description: "Delete local .auto-claude/issue-{N}/ artifacts",
|
|
74
|
+
default: false,
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
async run(): Promise<void> {
|
|
79
|
+
const { flags } = await this.parse(AutoClaudeRetry);
|
|
80
|
+
|
|
81
|
+
const cfg = await initConfig();
|
|
82
|
+
|
|
83
|
+
const cliInstalled = await isGithubCliInstalled();
|
|
84
|
+
if (!cliInstalled) {
|
|
85
|
+
this.error("GitHub CLI (gh) is not installed");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const failedIssues = await getIssues({ cwd: process.cwd(), label: LABELS.failed });
|
|
89
|
+
|
|
90
|
+
let selected: Issue[];
|
|
91
|
+
|
|
92
|
+
if (flags.issue) {
|
|
93
|
+
const match = failedIssues.find((i) => i.number === flags.issue);
|
|
94
|
+
if (!match) {
|
|
95
|
+
this.error(`Issue #${flags.issue} not found with '${LABELS.failed}' label`);
|
|
96
|
+
}
|
|
97
|
+
selected = [match];
|
|
98
|
+
} else {
|
|
99
|
+
if (failedIssues.length === 0) {
|
|
100
|
+
consola.info(`No open issues with '${LABELS.failed}' label`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
consola.info(colors.yellow(`${failedIssues.length} failed issue(s) found`));
|
|
105
|
+
|
|
106
|
+
const choices = failedIssues.map((issue) => ({
|
|
107
|
+
title: `#${issue.number} ${issue.title}`,
|
|
108
|
+
value: issue.number,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const result = await prompts(
|
|
112
|
+
{
|
|
113
|
+
name: "selected",
|
|
114
|
+
message: "Select issues to retry:",
|
|
115
|
+
type: "multiselect",
|
|
116
|
+
choices,
|
|
117
|
+
instructions: false,
|
|
118
|
+
hint: "- Space to select, Enter to confirm",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
onCancel: () => {
|
|
122
|
+
consola.info(colors.dim("Canceled"));
|
|
123
|
+
this.exit(0);
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!result.selected || result.selected.length === 0) {
|
|
129
|
+
consola.info(colors.dim("No issues selected"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
selected = failedIssues.filter((i) => result.selected.includes(i.number));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const count = await retryIssues(cfg.repo, cfg.triggerLabel, selected, flags.clean);
|
|
137
|
+
consola.box(`Retried ${count} issue(s)`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
|
|
8
|
+
import { LABELS } from "../../lib/auto-claude/labels.js";
|
|
9
|
+
import { checkArtifacts, findAcLabel, formatIssueStatus } from "./status.js";
|
|
10
|
+
|
|
11
|
+
// ── Test fixtures ──
|
|
12
|
+
|
|
13
|
+
function makeIssue(overrides: Partial<Issue> = {}): Issue {
|
|
14
|
+
return {
|
|
15
|
+
number: 1,
|
|
16
|
+
title: "Test issue",
|
|
17
|
+
state: "open",
|
|
18
|
+
labels: [{ name: "auto-claude", color: "000000" }],
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── checkArtifacts ──
|
|
24
|
+
|
|
25
|
+
describe("checkArtifacts", () => {
|
|
26
|
+
let tmpDir: string;
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
tmpDir = mkdtempSync(join(tmpdir(), "ac-status-test-"));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns all false when no artifacts exist", () => {
|
|
37
|
+
const result = checkArtifacts(99, tmpDir);
|
|
38
|
+
expect(result).toHaveLength(4);
|
|
39
|
+
for (const a of result) {
|
|
40
|
+
expect(a.exists).toBe(false);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects existing artifacts", () => {
|
|
45
|
+
const issueDir = join(tmpDir, ".auto-claude/issue-42");
|
|
46
|
+
mkdirSync(issueDir, { recursive: true });
|
|
47
|
+
writeFileSync(join(issueDir, "plan.md"), "# Plan");
|
|
48
|
+
writeFileSync(join(issueDir, "review.md"), "# Review");
|
|
49
|
+
|
|
50
|
+
const result = checkArtifacts(42, tmpDir);
|
|
51
|
+
const planArtifact = result.find((a) => a.name === "plan.md");
|
|
52
|
+
const reviewArtifact = result.find((a) => a.name === "review.md");
|
|
53
|
+
const summaryArtifact = result.find((a) => a.name === "completed-summary.md");
|
|
54
|
+
|
|
55
|
+
expect(planArtifact?.exists).toBe(true);
|
|
56
|
+
expect(reviewArtifact?.exists).toBe(true);
|
|
57
|
+
expect(summaryArtifact?.exists).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── findAcLabel ──
|
|
62
|
+
|
|
63
|
+
describe("findAcLabel", () => {
|
|
64
|
+
it("returns specific status label when present", () => {
|
|
65
|
+
const issue = makeIssue({
|
|
66
|
+
labels: [
|
|
67
|
+
{ name: "auto-claude", color: "000000" },
|
|
68
|
+
{ name: LABELS.inProgress, color: "ffff00" },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
expect(findAcLabel(issue)).toBe(LABELS.inProgress);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("falls back to auto-claude when no status label", () => {
|
|
75
|
+
const issue = makeIssue({
|
|
76
|
+
labels: [{ name: "auto-claude", color: "000000" }],
|
|
77
|
+
});
|
|
78
|
+
expect(findAcLabel(issue)).toBe("auto-claude");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("prefers in-progress over other labels", () => {
|
|
82
|
+
const issue = makeIssue({
|
|
83
|
+
labels: [
|
|
84
|
+
{ name: LABELS.inProgress, color: "ffff00" },
|
|
85
|
+
{ name: LABELS.success, color: "00ff00" },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
expect(findAcLabel(issue)).toBe(LABELS.inProgress);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── formatIssueStatus ──
|
|
93
|
+
|
|
94
|
+
describe("formatIssueStatus", () => {
|
|
95
|
+
it("includes issue number and title", () => {
|
|
96
|
+
const issue = makeIssue({ number: 7, title: "Fix widget" });
|
|
97
|
+
const artifacts = [
|
|
98
|
+
{ name: "plan.md", exists: false },
|
|
99
|
+
{ name: "completed-summary.md", exists: false },
|
|
100
|
+
{ name: "simplify-summary.md", exists: false },
|
|
101
|
+
{ name: "review.md", exists: false },
|
|
102
|
+
];
|
|
103
|
+
const output = formatIssueStatus(issue, artifacts);
|
|
104
|
+
expect(output).toContain("#7");
|
|
105
|
+
expect(output).toContain("Fix widget");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("includes status tag", () => {
|
|
109
|
+
const issue = makeIssue({
|
|
110
|
+
labels: [{ name: LABELS.failed, color: "ff0000" }],
|
|
111
|
+
});
|
|
112
|
+
const artifacts = [
|
|
113
|
+
{ name: "plan.md", exists: false },
|
|
114
|
+
{ name: "completed-summary.md", exists: false },
|
|
115
|
+
{ name: "simplify-summary.md", exists: false },
|
|
116
|
+
{ name: "review.md", exists: false },
|
|
117
|
+
];
|
|
118
|
+
const output = formatIssueStatus(issue, artifacts);
|
|
119
|
+
expect(output).toContain("failed");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("shows completed artifacts", () => {
|
|
123
|
+
const issue = makeIssue();
|
|
124
|
+
const artifacts = [
|
|
125
|
+
{ name: "plan.md", exists: true },
|
|
126
|
+
{ name: "completed-summary.md", exists: true },
|
|
127
|
+
{ name: "simplify-summary.md", exists: false },
|
|
128
|
+
{ name: "review.md", exists: false },
|
|
129
|
+
];
|
|
130
|
+
const output = formatIssueStatus(issue, artifacts);
|
|
131
|
+
expect(output).toContain("plan.md");
|
|
132
|
+
expect(output).toContain("completed-summary.md");
|
|
133
|
+
expect(output).not.toContain("simplify-summary.md");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("omits artifacts line when none exist", () => {
|
|
137
|
+
const issue = makeIssue();
|
|
138
|
+
const artifacts = [
|
|
139
|
+
{ name: "plan.md", exists: false },
|
|
140
|
+
{ name: "completed-summary.md", exists: false },
|
|
141
|
+
{ name: "simplify-summary.md", exists: false },
|
|
142
|
+
{ name: "review.md", exists: false },
|
|
143
|
+
];
|
|
144
|
+
const output = formatIssueStatus(issue, artifacts);
|
|
145
|
+
expect(output).not.toContain("artifacts:");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
import { colors } from "consola/utils";
|
|
6
|
+
|
|
7
|
+
import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
|
|
8
|
+
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
9
|
+
import { ARTIFACTS } from "../../lib/auto-claude/prompt-templates/index.js";
|
|
10
|
+
import { LABELS } from "../../lib/auto-claude/labels.js";
|
|
11
|
+
import { BaseCommand } from "../base.js";
|
|
12
|
+
|
|
13
|
+
/** All labels that indicate an issue is part of the auto-claude pipeline. */
|
|
14
|
+
const ALL_AC_LABELS = ["auto-claude", ...Object.values(LABELS)] as const;
|
|
15
|
+
|
|
16
|
+
/** Display config per label: short name + color function. */
|
|
17
|
+
const LABEL_DISPLAY: Record<string, { status: string; color: (t: string) => string }> = {
|
|
18
|
+
[LABELS.inProgress]: { status: "in-progress", color: colors.yellow },
|
|
19
|
+
[LABELS.success]: { status: "success", color: colors.green },
|
|
20
|
+
[LABELS.failed]: { status: "failed", color: colors.red },
|
|
21
|
+
[LABELS.review]: { status: "review", color: colors.blue },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DEFAULT_DISPLAY = { status: "queued", color: colors.dim };
|
|
25
|
+
|
|
26
|
+
interface ArtifactStatus {
|
|
27
|
+
name: string;
|
|
28
|
+
exists: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Pipeline artifacts to check, in pipeline order. */
|
|
32
|
+
const CHECKED_ARTIFACTS = [
|
|
33
|
+
ARTIFACTS.plan,
|
|
34
|
+
ARTIFACTS.completedSummary,
|
|
35
|
+
ARTIFACTS.simplifySummary,
|
|
36
|
+
ARTIFACTS.review,
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/** Check which pipeline artifacts exist locally for an issue. */
|
|
40
|
+
export function checkArtifacts(issueNumber: number, cwd: string): ArtifactStatus[] {
|
|
41
|
+
const issueDir = join(cwd, `.auto-claude/issue-${issueNumber}`);
|
|
42
|
+
return CHECKED_ARTIFACTS.map((name) => ({ name, exists: existsSync(join(issueDir, name)) }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Find the most specific auto-claude label on an issue. */
|
|
46
|
+
export function findAcLabel(issue: Issue): string {
|
|
47
|
+
const labelNames = issue.labels.map((l) => l.name);
|
|
48
|
+
// Prefer the most specific status label; fall back to generic "auto-claude"
|
|
49
|
+
for (const label of Object.values(LABELS)) {
|
|
50
|
+
if (labelNames.includes(label)) return label;
|
|
51
|
+
}
|
|
52
|
+
return "auto-claude";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Format a single issue for display. */
|
|
56
|
+
export function formatIssueStatus(issue: Issue, artifacts: ArtifactStatus[]): string {
|
|
57
|
+
const label = findAcLabel(issue);
|
|
58
|
+
const { status, color } = LABEL_DISPLAY[label] ?? DEFAULT_DISPLAY;
|
|
59
|
+
const statusTag = color(`[${status}]`);
|
|
60
|
+
|
|
61
|
+
const parts: string[] = [`#${issue.number} ${issue.title} ${statusTag}`];
|
|
62
|
+
|
|
63
|
+
const completedSteps = artifacts.filter((a) => a.exists).map((a) => a.name);
|
|
64
|
+
if (completedSteps.length > 0) {
|
|
65
|
+
parts.push(colors.dim(` artifacts: ${completedSteps.join(", ")}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parts.join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Fetch issues across all auto-claude labels, deduplicating by issue number. */
|
|
72
|
+
export async function fetchAllAcIssues(cwd: string): Promise<Issue[]> {
|
|
73
|
+
const issueMap = new Map<number, Issue>();
|
|
74
|
+
|
|
75
|
+
const results = await Promise.all(ALL_AC_LABELS.map((label) => getIssues({ cwd, label })));
|
|
76
|
+
|
|
77
|
+
for (const issues of results) {
|
|
78
|
+
for (const issue of issues) {
|
|
79
|
+
if (!issueMap.has(issue.number)) {
|
|
80
|
+
issueMap.set(issue.number, issue);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sort by issue number ascending
|
|
86
|
+
return [...issueMap.values()].sort((a, b) => a.number - b.number);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default class AutoClaudeStatus extends BaseCommand {
|
|
90
|
+
static override description = "Show pipeline status for auto-claude issues";
|
|
91
|
+
|
|
92
|
+
static override aliases = ["ac:status"];
|
|
93
|
+
|
|
94
|
+
static override examples = [
|
|
95
|
+
{
|
|
96
|
+
description: "Show status of all auto-claude issues",
|
|
97
|
+
command: "<%= config.bin %> auto-claude status",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
async run(): Promise<void> {
|
|
102
|
+
const cliInstalled = await isGithubCliInstalled();
|
|
103
|
+
if (!cliInstalled) {
|
|
104
|
+
this.error("GitHub CLI (gh) is not installed");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
const issues = await fetchAllAcIssues(cwd);
|
|
109
|
+
|
|
110
|
+
if (issues.length === 0) {
|
|
111
|
+
consola.info("No auto-claude issues found");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
consola.info(colors.bold(`Auto-Claude Pipeline Status (${issues.length} issue(s))`));
|
|
116
|
+
consola.log("");
|
|
117
|
+
|
|
118
|
+
for (const issue of issues) {
|
|
119
|
+
const artifacts = checkArtifacts(issue.number, cwd);
|
|
120
|
+
consola.log(formatIssueStatus(issue, artifacts));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/commands/base.ts
CHANGED
|
@@ -15,13 +15,18 @@ export abstract class BaseCommand extends Command {
|
|
|
15
15
|
}),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
protected
|
|
18
|
+
protected settingsFile!: SettingsFile;
|
|
19
|
+
|
|
20
|
+
/** Shortcut to avoid `this.settingsFile.settings.X` stutter */
|
|
21
|
+
protected get userSettings() {
|
|
22
|
+
return this.settingsFile.settings;
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Called before run(). Loads user settings.
|
|
22
27
|
*/
|
|
23
28
|
async init(): Promise<void> {
|
|
24
29
|
await super.init();
|
|
25
|
-
this.
|
|
30
|
+
this.settingsFile = await loadSettings();
|
|
26
31
|
}
|
|
27
32
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -18,18 +18,16 @@ export default class Config extends BaseCommand {
|
|
|
18
18
|
consola.info("Configuration");
|
|
19
19
|
consola.log("");
|
|
20
20
|
|
|
21
|
-
consola.info(`Settings File: ${this.
|
|
21
|
+
consola.info(`Settings File: ${this.settingsFile.path}`);
|
|
22
22
|
consola.log("");
|
|
23
23
|
|
|
24
24
|
consola.warn("User Config:");
|
|
25
|
+
consola.log(` Daily Path Template: ${this.userSettings.journalSettings.dailyPathTemplate}`);
|
|
25
26
|
consola.log(
|
|
26
|
-
`
|
|
27
|
+
` Meeting Path Template: ${this.userSettings.journalSettings.meetingPathTemplate}`,
|
|
27
28
|
);
|
|
28
|
-
consola.log(
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
consola.log(` Note Path Template: ${this.settings.settings.journalSettings.notePathTemplate}`);
|
|
32
|
-
consola.log(` Editor: ${this.settings.settings.preferredEditor}`);
|
|
29
|
+
consola.log(` Note Path Template: ${this.userSettings.journalSettings.notePathTemplate}`);
|
|
30
|
+
consola.log(` Editor: ${this.userSettings.preferredEditor}`);
|
|
33
31
|
consola.log("");
|
|
34
32
|
|
|
35
33
|
consola.warn("Working Directory:");
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import consola from "consola";
|
|
1
4
|
import { x } from "tinyexec";
|
|
2
|
-
import
|
|
5
|
+
import { colors } from "consola/utils";
|
|
3
6
|
import { BaseCommand } from "./base.js";
|
|
4
7
|
|
|
5
8
|
interface CheckResult {
|
|
@@ -31,25 +34,32 @@ export default class Doctor extends BaseCommand {
|
|
|
31
34
|
this.checkCommand("node", ["--version"], /v?([\d.]+)/),
|
|
32
35
|
this.checkCommand("bun", ["--version"], /([\d.]+)/),
|
|
33
36
|
this.checkCommand("pnpm", ["--version"], /([\d.]+)/),
|
|
37
|
+
this.checkCommand("claude", ["--version"], /([\d.]+)/),
|
|
38
|
+
this.checkCommand("tmux", ["-V"], /tmux ([\d.]+)/),
|
|
39
|
+
this.checkCommand("ttyd", ["--version"], /ttyd version ([\d.]+)/, true),
|
|
34
40
|
]);
|
|
35
41
|
|
|
36
42
|
// Display results
|
|
37
43
|
for (const check of checks) {
|
|
38
|
-
const icon = check.ok
|
|
44
|
+
const icon = check.ok
|
|
45
|
+
? colors.green("✓")
|
|
46
|
+
: check.warning
|
|
47
|
+
? colors.yellow("⚠")
|
|
48
|
+
: colors.red("✗");
|
|
39
49
|
const version = check.version ?? "not found";
|
|
40
50
|
this.log(`${icon} ${check.name}: ${version}`);
|
|
41
51
|
if (check.warning) {
|
|
42
|
-
this.log(` ${
|
|
52
|
+
this.log(` ${colors.yellow("⚠")} ${check.warning}`);
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// Check gh auth
|
|
47
57
|
this.log("");
|
|
48
58
|
const ghAuth = await this.checkGhAuth();
|
|
49
|
-
const authIcon = ghAuth.ok ?
|
|
59
|
+
const authIcon = ghAuth.ok ? colors.green("✓") : colors.yellow("⚠");
|
|
50
60
|
this.log(`${authIcon} gh auth: ${ghAuth.ok ? "authenticated" : "not authenticated"}`);
|
|
51
61
|
if (!ghAuth.ok) {
|
|
52
|
-
this.log(` ${
|
|
62
|
+
this.log(` ${colors.dim("Run: gh auth login")}`);
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
// Node version check
|
|
@@ -58,7 +68,7 @@ export default class Doctor extends BaseCommand {
|
|
|
58
68
|
const major = Number.parseInt(nodeCheck.version.split(".")[0], 10);
|
|
59
69
|
if (major < 18) {
|
|
60
70
|
this.log("");
|
|
61
|
-
this.log(`${
|
|
71
|
+
this.log(`${colors.yellow("⚠")} Node.js 18+ recommended (found ${nodeCheck.version})`);
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -66,21 +76,41 @@ export default class Doctor extends BaseCommand {
|
|
|
66
76
|
this.log("");
|
|
67
77
|
const pluginChecks = await this.checkClaudePlugins();
|
|
68
78
|
for (const check of pluginChecks) {
|
|
69
|
-
const icon = check.ok ?
|
|
79
|
+
const icon = check.ok ? colors.green("✓") : colors.red("✗");
|
|
70
80
|
const status = check.ok ? "installed" : "not installed";
|
|
71
81
|
this.log(`${icon} claude plugin ${check.name}: ${status}`);
|
|
72
82
|
if (!check.ok && check.installHint) {
|
|
73
|
-
this.log(` ${
|
|
83
|
+
this.log(` ${colors.dim(check.installHint)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// AgentBoard checks
|
|
88
|
+
this.log("");
|
|
89
|
+
this.log(colors.bold("AgentBoard:"));
|
|
90
|
+
const agentboardChecks = this.checkAgentBoard();
|
|
91
|
+
for (const check of agentboardChecks) {
|
|
92
|
+
const icon = check.ok
|
|
93
|
+
? colors.green("✓")
|
|
94
|
+
: check.warning
|
|
95
|
+
? colors.yellow("⚠")
|
|
96
|
+
: colors.red("✗");
|
|
97
|
+
this.log(`${icon} ${check.name}: ${check.value}`);
|
|
98
|
+
if (check.hint) {
|
|
99
|
+
this.log(` ${colors.dim(check.hint)}`);
|
|
74
100
|
}
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
// Summary
|
|
78
|
-
const allOk =
|
|
104
|
+
const allOk =
|
|
105
|
+
checks.every((c) => c.ok || !!c.warning) &&
|
|
106
|
+
ghAuth.ok &&
|
|
107
|
+
pluginChecks.every((c) => c.ok) &&
|
|
108
|
+
agentboardChecks.every((c) => c.ok || !!c.warning);
|
|
79
109
|
this.log("");
|
|
80
110
|
if (allOk) {
|
|
81
|
-
this.log(
|
|
111
|
+
this.log(colors.green("All checks passed!"));
|
|
82
112
|
} else {
|
|
83
|
-
this.log(
|
|
113
|
+
this.log(colors.yellow("Some checks failed. See above for details."));
|
|
84
114
|
}
|
|
85
115
|
}
|
|
86
116
|
|
|
@@ -88,6 +118,7 @@ export default class Doctor extends BaseCommand {
|
|
|
88
118
|
name: string,
|
|
89
119
|
args: string[],
|
|
90
120
|
versionPattern: RegExp,
|
|
121
|
+
optional = false,
|
|
91
122
|
): Promise<CheckResult> {
|
|
92
123
|
try {
|
|
93
124
|
// tinyexec is safe - uses execFile internally, no shell injection risk
|
|
@@ -100,7 +131,13 @@ export default class Doctor extends BaseCommand {
|
|
|
100
131
|
ok: true,
|
|
101
132
|
};
|
|
102
133
|
} catch {
|
|
103
|
-
|
|
134
|
+
consola.debug(`Tool check failed for "${name}"`);
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
version: null,
|
|
138
|
+
ok: optional,
|
|
139
|
+
warning: optional ? "optional, not installed" : undefined,
|
|
140
|
+
};
|
|
104
141
|
}
|
|
105
142
|
}
|
|
106
143
|
|
|
@@ -110,10 +147,71 @@ export default class Doctor extends BaseCommand {
|
|
|
110
147
|
const result = await x("gh", ["auth", "status"]);
|
|
111
148
|
return { ok: result.exitCode === 0 };
|
|
112
149
|
} catch {
|
|
150
|
+
consola.debug("GitHub CLI auth check failed");
|
|
113
151
|
return { ok: false };
|
|
114
152
|
}
|
|
115
153
|
}
|
|
116
154
|
|
|
155
|
+
private checkAgentBoard(): {
|
|
156
|
+
name: string;
|
|
157
|
+
value: string;
|
|
158
|
+
ok: boolean;
|
|
159
|
+
warning?: string;
|
|
160
|
+
hint?: string;
|
|
161
|
+
}[] {
|
|
162
|
+
const results: { name: string; value: string; ok: boolean; warning?: string; hint?: string }[] =
|
|
163
|
+
[];
|
|
164
|
+
|
|
165
|
+
const defaultDataDir = resolve(
|
|
166
|
+
process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
|
|
167
|
+
"towles-tool",
|
|
168
|
+
"agentboard",
|
|
169
|
+
);
|
|
170
|
+
const dataDir = process.env.AGENTBOARD_DATA_DIR ?? defaultDataDir;
|
|
171
|
+
const dbPath = join(dataDir, "agentboard.db");
|
|
172
|
+
const configPath = join(dataDir, "config.json");
|
|
173
|
+
|
|
174
|
+
// DB exists
|
|
175
|
+
const dbExists = existsSync(dbPath);
|
|
176
|
+
results.push({
|
|
177
|
+
name: "database",
|
|
178
|
+
value: dbExists ? dbPath : "not found",
|
|
179
|
+
ok: dbExists,
|
|
180
|
+
hint: dbExists ? undefined : "Run: tt ag (starts server and creates DB automatically)",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Config exists with repoPaths
|
|
184
|
+
let repoPaths: string[] = [];
|
|
185
|
+
if (existsSync(configPath)) {
|
|
186
|
+
try {
|
|
187
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
188
|
+
repoPaths = config.repoPaths ?? [];
|
|
189
|
+
} catch {
|
|
190
|
+
// Corrupted config
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
results.push({
|
|
195
|
+
name: "scan paths",
|
|
196
|
+
value: repoPaths.length > 0 ? repoPaths.join(", ") : "none configured",
|
|
197
|
+
ok: repoPaths.length > 0,
|
|
198
|
+
warning: repoPaths.length === 0 ? "no scan paths" : undefined,
|
|
199
|
+
hint:
|
|
200
|
+
repoPaths.length === 0
|
|
201
|
+
? "Run: tt ag → open Workspaces → run the onboarding wizard"
|
|
202
|
+
: undefined,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Data directory
|
|
206
|
+
results.push({
|
|
207
|
+
name: "data dir",
|
|
208
|
+
value: dataDir,
|
|
209
|
+
ok: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
|
|
117
215
|
private async checkClaudePlugins(): Promise<
|
|
118
216
|
{ name: string; ok: boolean; installHint?: string }[]
|
|
119
217
|
> {
|
|
@@ -136,6 +234,7 @@ export default class Doctor extends BaseCommand {
|
|
|
136
234
|
installHint: installedIds.has(p.id) ? undefined : `Run: ${p.installCmd}`,
|
|
137
235
|
}));
|
|
138
236
|
} catch {
|
|
237
|
+
consola.debug("Failed to list Claude plugins");
|
|
139
238
|
return requiredPlugins.map((p) => ({
|
|
140
239
|
name: p.name,
|
|
141
240
|
ok: false,
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
+
import consola from "consola";
|
|
2
3
|
import prompts from "prompts";
|
|
3
4
|
import type { Choice } from "prompts";
|
|
4
5
|
import { colors } from "consola/utils";
|
|
5
6
|
import { Fzf } from "fzf";
|
|
6
|
-
import consola from "consola";
|
|
7
|
-
|
|
8
|
-
import { exec } from "tinyexec";
|
|
9
7
|
|
|
10
8
|
import { BaseCommand } from "../base.js";
|
|
11
9
|
import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
|
|
12
10
|
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
11
|
+
import { git } from "../../utils/git/exec.js";
|
|
13
12
|
import { createBranchNameFromIssue } from "../../utils/git/branch-name.js";
|
|
14
13
|
import { getTerminalColumns, limitText, printWithHexColor } from "../../utils/render.js";
|
|
15
14
|
|
|
@@ -131,8 +130,9 @@ export default class GhBranch extends BaseCommand {
|
|
|
131
130
|
);
|
|
132
131
|
|
|
133
132
|
const branchName = createBranchNameFromIssue(selectedIssue);
|
|
134
|
-
await
|
|
133
|
+
await git(["checkout", "-b", branchName]);
|
|
135
134
|
} catch {
|
|
135
|
+
consola.debug("Branch checkout failed");
|
|
136
136
|
this.exit(1);
|
|
137
137
|
}
|
|
138
138
|
}
|