@towles/tool 0.0.109 → 0.0.111
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 +9 -4
- package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +206 -226
- package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
- package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
- package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
- package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
- package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
- package/packages/agentboard/packages/runtime/src/config.ts +80 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +7 -2
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/git/exec.ts +41 -0
- package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
- package/packages/shared/src/index.ts +8 -0
- package/packages/shared/tsconfig.json +16 -0
- package/src/cli.ts +1 -1
- package/src/commands/agentboard.ts +51 -67
- package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
- package/src/commands/auto-claude/config-init-helpers.ts +79 -0
- package/src/commands/auto-claude/config-init.test.ts +137 -0
- package/src/commands/auto-claude/config-init.ts +159 -0
- package/src/{lib → commands}/auto-claude/config.ts +4 -8
- package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
- package/src/commands/auto-claude/explain.test.ts +58 -0
- package/src/commands/auto-claude/explain.ts +97 -0
- package/src/commands/auto-claude/index.ts +37 -14
- package/src/{lib → commands}/auto-claude/labels.ts +1 -1
- package/src/commands/auto-claude/list.ts +5 -4
- package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
- package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
- package/src/commands/auto-claude/retry.test.ts +2 -2
- package/src/commands/auto-claude/retry.ts +5 -5
- package/src/commands/auto-claude/shell.ts +3 -0
- package/src/commands/auto-claude/status.test.ts +2 -2
- package/src/commands/auto-claude/status.ts +4 -4
- package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
- package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
- package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
- package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
- package/src/{lib → commands}/auto-claude/utils.ts +10 -4
- package/src/{lib/install → commands}/claude-settings.ts +1 -1
- package/src/commands/config/config.test.ts +129 -0
- package/src/commands/config/index.ts +11 -0
- package/src/commands/config/reset.ts +53 -0
- package/src/commands/config/schema.ts +19 -0
- package/src/commands/{config.ts → config/show.ts} +2 -2
- package/src/commands/config/validate.ts +51 -0
- package/src/commands/doctor/checks.ts +167 -0
- package/src/commands/doctor/format.test.ts +63 -0
- package/src/commands/doctor/format.ts +5 -0
- package/src/commands/doctor/history.test.ts +161 -0
- package/src/commands/doctor/history.ts +130 -0
- package/src/commands/doctor.ts +80 -151
- package/src/commands/gh/branch-clean.ts +4 -4
- package/src/commands/gh/branch.test.ts +4 -5
- package/src/commands/gh/branch.ts +10 -5
- package/src/commands/gh/pr.ts +6 -7
- package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
- package/src/commands/graph/format.test.ts +130 -0
- package/src/commands/graph/format.ts +94 -0
- package/src/commands/graph/index.ts +69 -41
- package/src/{lib → commands}/graph/labels.ts +4 -4
- package/src/{lib → commands}/graph/server.ts +2 -2
- package/src/{lib → commands}/graph/types.ts +2 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +6 -6
- package/src/commands/journal/daily-notes.ts +4 -7
- package/src/{lib → commands}/journal/fs.ts +1 -1
- package/src/commands/journal/index.ts +2 -0
- package/src/commands/journal/list.test.ts +174 -0
- package/src/commands/journal/list.ts +213 -0
- package/src/commands/journal/meeting.ts +4 -7
- package/src/commands/journal/note.ts +4 -7
- package/src/{lib → commands}/journal/paths.ts +1 -1
- package/src/commands/journal/search.test.ts +156 -0
- package/src/commands/journal/search.ts +256 -0
- package/src/{lib → commands}/journal/templates.ts +1 -1
- package/src/config/settings.ts +35 -26
- package/plugins/tt-agentboard/bun.lock +0 -444
- package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
- package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
- package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
- package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
- package/plugins/tt-auto-claude/commands/list.md +0 -21
- package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
- package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
- package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
- package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
- package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
- package/src/commands/config.test.ts +0 -9
- package/src/lib/auto-claude/index.ts +0 -15
- package/src/lib/auto-claude/shell.ts +0 -6
- package/src/lib/graph/index.ts +0 -24
- package/src/lib/journal/index.ts +0 -11
- package/src/utils/git/exec.ts +0 -18
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
- /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +0 -0
- /package/{plugins/tt-core → packages/core}/README.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
- /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
- /package/{src/utils → packages/shared/src}/fs.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.ts +0 -0
- /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
- /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
- /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
- /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
- /package/src/{lib → commands}/graph/analyzer.ts +0 -0
- /package/src/{lib → commands}/graph/graph-template.html +0 -0
- /package/src/{lib → commands}/graph/parser.test.ts +0 -0
- /package/src/{lib → commands}/graph/parser.ts +0 -0
- /package/src/{lib → commands}/graph/render.ts +0 -0
- /package/src/{lib → commands}/graph/sessions.ts +0 -0
- /package/src/{lib → commands}/graph/tools.ts +0 -0
- /package/src/{lib → commands}/graph/treemap.ts +0 -0
- /package/src/{lib → commands}/journal/editor.ts +0 -0
package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts
RENAMED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import type { AgentStatus } from "../contracts/agent";
|
|
5
|
+
import { TERMINAL_STATUSES } from "../contracts/agent";
|
|
5
6
|
import type { SidebarPane } from "../contracts/mux";
|
|
6
7
|
import type { ServerContext, PaneAgentPresence } from "./context";
|
|
7
8
|
import { shell } from "./git-info";
|
|
@@ -98,6 +99,11 @@ function resolveClaudeCodePaneInfo(
|
|
|
98
99
|
const threadId: string | undefined = data.sessionId;
|
|
99
100
|
if (!threadId) return {};
|
|
100
101
|
const journalInfo = resolveClaudeCodeJournalInfo(threadId);
|
|
102
|
+
// Process is alive (found via process tree), so terminal journal status
|
|
103
|
+
// is a between-turn artifact — override to running.
|
|
104
|
+
if (journalInfo.status && TERMINAL_STATUSES.has(journalInfo.status)) {
|
|
105
|
+
journalInfo.status = "running";
|
|
106
|
+
}
|
|
101
107
|
return { threadId, ...journalInfo };
|
|
102
108
|
} catch {
|
|
103
109
|
return {};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { AgentStatus, AgentEvent } from "./contracts/agent";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
3
|
+
export const DEFAULT_SERVER_PORT = 4201;
|
|
4
|
+
export const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
5
|
+
|
|
6
|
+
export const SERVER_PORT: number = Number(process.env.TT_AGENTBOARD_PORT) || DEFAULT_SERVER_PORT;
|
|
7
|
+
export const SERVER_HOST: string = process.env.TT_AGENTBOARD_HOST || DEFAULT_SERVER_HOST;
|
|
5
8
|
export const PID_FILE = "/tmp/agentboard.pid";
|
|
6
9
|
export const SERVER_IDLE_TIMEOUT_MS = 30_000;
|
|
7
10
|
export const STUCK_RUNNING_TIMEOUT_MS = 3 * 60 * 1000;
|
|
11
|
+
export const JOURNAL_IDLE_TIMEOUT_MS = 120_000;
|
|
8
12
|
|
|
9
13
|
export interface SessionData {
|
|
10
14
|
name: string;
|
|
@@ -35,6 +39,7 @@ export interface ServerState {
|
|
|
35
39
|
currentSession: string | null;
|
|
36
40
|
theme: string | undefined;
|
|
37
41
|
sidebarWidth: number;
|
|
42
|
+
preferredEditor: string;
|
|
38
43
|
ts: number;
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@towles/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"consola": "^3.4.2"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"typescript": "^5.8.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface XResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface XOptions {
|
|
8
|
+
throwOnError?: boolean;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function run(cmd: string, args: string[] = [], options?: XOptions): Promise<XResult> {
|
|
13
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
14
|
+
const proc = Bun.spawn([cmd, ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
15
|
+
const exitCode = await proc.exited;
|
|
16
|
+
const [stdout, stderr] = await Promise.all([
|
|
17
|
+
new Response(proc.stdout).text(),
|
|
18
|
+
new Response(proc.stderr).text(),
|
|
19
|
+
]);
|
|
20
|
+
if (options?.throwOnError && exitCode !== 0) {
|
|
21
|
+
throw new Error(`Command failed (exit ${exitCode}): ${cmd} ${args.join(" ")}\n${stderr}`);
|
|
22
|
+
}
|
|
23
|
+
return { stdout, stderr, exitCode };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function exec(cmd: string, args: string[]): Promise<string> {
|
|
27
|
+
const result = await run(cmd, args, { throwOnError: true });
|
|
28
|
+
return result.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function execSafe(
|
|
32
|
+
cmd: string,
|
|
33
|
+
args: string[],
|
|
34
|
+
): Promise<{ stdout: string; ok: boolean }> {
|
|
35
|
+
const result = await run(cmd, args);
|
|
36
|
+
return { stdout: result.stdout.trim(), ok: result.exitCode === 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function git(args: string[]): Promise<string> {
|
|
40
|
+
return exec("git", args);
|
|
41
|
+
}
|
|
@@ -1,36 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type { Output } from "tinyexec";
|
|
4
|
-
|
|
5
|
-
import { execSafe } from "./exec.js";
|
|
1
|
+
import { exec, execSafe, run as defaultX } from "./exec.js";
|
|
2
|
+
import type { XResult } from "./exec.js";
|
|
6
3
|
|
|
7
4
|
export type XFn = (
|
|
8
5
|
cmd: string,
|
|
9
6
|
args?: string[],
|
|
10
7
|
opts?: Record<string, unknown>,
|
|
11
|
-
) => PromiseLike<
|
|
8
|
+
) => PromiseLike<XResult>;
|
|
12
9
|
|
|
13
|
-
export async function isGithubCliInstalled(
|
|
10
|
+
export async function isGithubCliInstalled(execFn: XFn = defaultX): Promise<boolean> {
|
|
14
11
|
try {
|
|
15
|
-
const proc = await
|
|
12
|
+
const proc = await execFn("gh", ["--version"]);
|
|
16
13
|
return proc.stdout.includes("https://github.com/cli/cli");
|
|
17
14
|
} catch {
|
|
18
|
-
// gh CLI not installed or not accessible
|
|
19
15
|
return false;
|
|
20
16
|
}
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export async function gh<T = unknown>(args: string[]): Promise<T> {
|
|
24
|
-
const
|
|
25
|
-
return JSON.parse(
|
|
20
|
+
const stdout = await exec("gh", args);
|
|
21
|
+
return JSON.parse(stdout) as T;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
export async function ghRaw(
|
|
29
25
|
args: string[],
|
|
30
|
-
|
|
26
|
+
execFn?: (cmd: string, args: string[]) => Promise<{ stdout: string; ok: boolean }>,
|
|
31
27
|
): Promise<string> {
|
|
32
|
-
const
|
|
33
|
-
const result = await
|
|
28
|
+
const fn = execFn ?? execSafe;
|
|
29
|
+
const result = await fn("gh", args);
|
|
34
30
|
return result.stdout;
|
|
35
31
|
}
|
|
36
32
|
|
|
@@ -48,7 +44,7 @@ export async function getIssues({
|
|
|
48
44
|
assignedToMe,
|
|
49
45
|
cwd,
|
|
50
46
|
label,
|
|
51
|
-
exec =
|
|
47
|
+
exec: execFn = defaultX,
|
|
52
48
|
}: {
|
|
53
49
|
assignedToMe?: boolean;
|
|
54
50
|
cwd: string;
|
|
@@ -65,9 +61,8 @@ export async function getIssues({
|
|
|
65
61
|
args.push("--label", label);
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
const result = await
|
|
69
|
-
|
|
70
|
-
const stripped = stripAnsi(result.stdout);
|
|
64
|
+
const result = await execFn("gh", args);
|
|
65
|
+
const stripped = Bun.stripANSI(result.stdout);
|
|
71
66
|
|
|
72
67
|
try {
|
|
73
68
|
return JSON.parse(stripped) as Issue[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ensureDir, fileExists, readFile, writeFile } from "./fs.js";
|
|
2
|
+
export { getTerminalColumns, limitText, printWithHexColor } from "./render.js";
|
|
3
|
+
export { formatDate, generateJournalFilename, getMondayOfWeek, getWeekInfo } from "./date-utils.js";
|
|
4
|
+
export { exec, execSafe, git, run } from "./git/exec.js";
|
|
5
|
+
export type { XResult, XOptions } from "./git/exec.js";
|
|
6
|
+
export { gh, ghRaw, getIssues, isGithubCliInstalled } from "./git/gh-cli-wrapper.js";
|
|
7
|
+
export type { Issue, XFn } from "./git/gh-cli-wrapper.js";
|
|
8
|
+
export { createBranchNameFromIssue } from "./git/branch-name.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"strictNullChecks": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"types": ["bun-types"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { version } from "../package.json";
|
|
|
4
4
|
export const main = defineCommand({
|
|
5
5
|
meta: { name: "tt", version, description: "towles-tool — personal CLI utilities" },
|
|
6
6
|
subCommands: {
|
|
7
|
-
config: () => import("./commands/config.js").then((m) => m.default),
|
|
7
|
+
config: () => import("./commands/config/index.js").then((m) => m.default),
|
|
8
8
|
doctor: () => import("./commands/doctor.js").then((m) => m.default),
|
|
9
9
|
install: () => import("./commands/install.js").then((m) => m.default),
|
|
10
10
|
agentboard: () => import("./commands/agentboard.js").then((m) => m.default),
|
|
@@ -6,10 +6,8 @@ import consola from "consola";
|
|
|
6
6
|
import { colors } from "consola/utils";
|
|
7
7
|
import { debugArg } from "./shared.js";
|
|
8
8
|
|
|
9
|
-
const SERVER_HOST = "127.0.0.1";
|
|
10
|
-
const SERVER_PORT = 4201;
|
|
11
|
-
|
|
12
|
-
const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
|
|
9
|
+
const SERVER_HOST = process.env.TT_AGENTBOARD_HOST || "127.0.0.1";
|
|
10
|
+
const SERVER_PORT = Number(process.env.TT_AGENTBOARD_PORT) || 4201;
|
|
13
11
|
|
|
14
12
|
// Keybinding defaults
|
|
15
13
|
const DEFAULT_KEY = "a";
|
|
@@ -20,37 +18,18 @@ const MARKER = "# agentboard";
|
|
|
20
18
|
function findTmuxConf(): string | null {
|
|
21
19
|
const candidates = [resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf")];
|
|
22
20
|
for (const path of candidates) {
|
|
23
|
-
|
|
24
|
-
const real = existsSync(path) ? path : null;
|
|
25
|
-
if (real) return real;
|
|
26
|
-
} catch {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
21
|
+
if (existsSync(path)) return path;
|
|
29
22
|
}
|
|
30
23
|
return null;
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
function
|
|
26
|
+
function ensureBun(): void {
|
|
34
27
|
try {
|
|
35
28
|
execSync("bun --version", { stdio: "pipe" });
|
|
36
29
|
} catch {
|
|
37
30
|
consola.error("bun is required but not found. Install: https://bun.sh");
|
|
38
31
|
process.exit(1);
|
|
39
32
|
}
|
|
40
|
-
|
|
41
|
-
if (!existsSync(PLUGIN_DIR)) {
|
|
42
|
-
consola.error(`Agentboard plugin directory not found: ${PLUGIN_DIR}`);
|
|
43
|
-
consola.error(
|
|
44
|
-
"If installed globally, ensure the 'plugins' directory is included in the package.",
|
|
45
|
-
);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const runtimeNodeModules = resolve(PLUGIN_DIR, "packages/runtime/node_modules");
|
|
50
|
-
if (!existsSync(runtimeNodeModules)) {
|
|
51
|
-
consola.info("Installing agentboard dependencies...");
|
|
52
|
-
execSync("bun install", { cwd: PLUGIN_DIR, stdio: "inherit" });
|
|
53
|
-
}
|
|
54
33
|
}
|
|
55
34
|
|
|
56
35
|
function reloadTmux(): void {
|
|
@@ -111,8 +90,6 @@ function showKeys(): void {
|
|
|
111
90
|
}
|
|
112
91
|
|
|
113
92
|
function setup(): void {
|
|
114
|
-
ensureDeps();
|
|
115
|
-
|
|
116
93
|
const confPath = findTmuxConf();
|
|
117
94
|
if (!confPath) {
|
|
118
95
|
consola.warn("No tmux.conf found. Add this line manually:");
|
|
@@ -169,14 +146,14 @@ function uninstall(): void {
|
|
|
169
146
|
}
|
|
170
147
|
|
|
171
148
|
const content = readFileSync(editPath, "utf8");
|
|
172
|
-
if (!content.includes(
|
|
149
|
+
if (!content.includes(MARKER) && !content.includes(RUN_SHELL_LINE)) {
|
|
173
150
|
consola.info("agentboard not found in tmux.conf");
|
|
174
151
|
return;
|
|
175
152
|
}
|
|
176
153
|
|
|
177
154
|
const newContent = content
|
|
178
155
|
.split("\n")
|
|
179
|
-
.filter((line) => !line.includes(
|
|
156
|
+
.filter((line) => !line.includes(MARKER) && !line.includes(RUN_SHELL_LINE))
|
|
180
157
|
.join("\n")
|
|
181
158
|
.replace(/\n{3,}/g, "\n\n");
|
|
182
159
|
|
|
@@ -186,18 +163,15 @@ function uninstall(): void {
|
|
|
186
163
|
}
|
|
187
164
|
|
|
188
165
|
function startServer(): void {
|
|
189
|
-
|
|
166
|
+
ensureBun();
|
|
190
167
|
|
|
191
|
-
const
|
|
168
|
+
const agentboardDir = resolve(import.meta.dirname, "../../packages/agentboard");
|
|
169
|
+
const serverEntry = resolve(agentboardDir, "apps/server/src/main.ts");
|
|
192
170
|
consola.info("Starting agentboard server (foreground, Ctrl+C to stop)...");
|
|
193
171
|
|
|
194
172
|
execSync(`bun run ${serverEntry}`, {
|
|
195
173
|
stdio: "inherit",
|
|
196
|
-
cwd:
|
|
197
|
-
env: {
|
|
198
|
-
...process.env,
|
|
199
|
-
AGENTBOARD_DIR: PLUGIN_DIR,
|
|
200
|
-
},
|
|
174
|
+
cwd: agentboardDir,
|
|
201
175
|
});
|
|
202
176
|
}
|
|
203
177
|
|
|
@@ -215,13 +189,10 @@ async function serverAlive(): Promise<boolean> {
|
|
|
215
189
|
async function ensureServerUp(): Promise<boolean> {
|
|
216
190
|
if (await serverAlive()) return true;
|
|
217
191
|
|
|
218
|
-
const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
|
|
219
192
|
consola.info("Starting agentboard server...");
|
|
220
|
-
const child = spawn("
|
|
193
|
+
const child = spawn("tt", ["agentboard", "server"], {
|
|
221
194
|
stdio: "ignore",
|
|
222
|
-
cwd: PLUGIN_DIR,
|
|
223
195
|
detached: true,
|
|
224
|
-
env: { ...process.env, AGENTBOARD_DIR: PLUGIN_DIR },
|
|
225
196
|
});
|
|
226
197
|
child.unref();
|
|
227
198
|
|
|
@@ -262,7 +233,9 @@ function findSidebarPane(windowId: string): string | null {
|
|
|
262
233
|
const [paneId, title] = line.split(" ", 2);
|
|
263
234
|
if (title === "agentboard-sidebar" && paneId) return paneId;
|
|
264
235
|
}
|
|
265
|
-
} catch {
|
|
236
|
+
} catch (err) {
|
|
237
|
+
consola.debug(`findSidebarPane failed for window ${windowId}:`, err);
|
|
238
|
+
}
|
|
266
239
|
return null;
|
|
267
240
|
}
|
|
268
241
|
|
|
@@ -360,12 +333,13 @@ async function runFocus(): Promise<void> {
|
|
|
360
333
|
return;
|
|
361
334
|
}
|
|
362
335
|
|
|
363
|
-
// Otherwise, ensure server +
|
|
336
|
+
// Otherwise, ensure server + open sidebar
|
|
364
337
|
if (!(await ensureServerUp())) process.exit(0);
|
|
365
338
|
const ctx = tmuxContext();
|
|
366
|
-
await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/
|
|
367
|
-
|
|
368
|
-
|
|
339
|
+
await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/ensure-sidebar`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: ctx,
|
|
342
|
+
}).catch(() => {});
|
|
369
343
|
|
|
370
344
|
// Wait for sidebar pane to appear
|
|
371
345
|
for (let i = 0; i < 20; i++) {
|
|
@@ -381,7 +355,7 @@ async function runFocus(): Promise<void> {
|
|
|
381
355
|
}
|
|
382
356
|
|
|
383
357
|
async function restart(): Promise<void> {
|
|
384
|
-
|
|
358
|
+
ensureBun();
|
|
385
359
|
|
|
386
360
|
// 1. Kill stash sessions left over from hidden sidebars
|
|
387
361
|
try {
|
|
@@ -407,35 +381,47 @@ async function restart(): Promise<void> {
|
|
|
407
381
|
}
|
|
408
382
|
consola.success("Server is running");
|
|
409
383
|
|
|
410
|
-
// 3.
|
|
384
|
+
// 3. Bootstrap sidebars — refresh session list, then ensure-sidebar for
|
|
385
|
+
// each active client's window so sidebars appear without interaction.
|
|
386
|
+
const base = `http://${SERVER_HOST}:${SERVER_PORT}`;
|
|
387
|
+
await fetch(`${base}/refresh`, { method: "POST", signal: AbortSignal.timeout(2000) }).catch(
|
|
388
|
+
() => {},
|
|
389
|
+
);
|
|
390
|
+
|
|
411
391
|
try {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
392
|
+
const clients = spawnSync(
|
|
393
|
+
"tmux",
|
|
394
|
+
["list-clients", "-F", "#{client_tty}|#{session_name}|#{window_id}"],
|
|
395
|
+
{
|
|
396
|
+
encoding: "utf8",
|
|
397
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
const lines = (clients.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
401
|
+
await Promise.allSettled(
|
|
402
|
+
lines.map((ctx) =>
|
|
403
|
+
fetch(`${base}/ensure-sidebar`, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
body: ctx,
|
|
406
|
+
signal: AbortSignal.timeout(2000),
|
|
407
|
+
}),
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
consola.success(`Sidebars ensured for ${lines.length} client(s)`);
|
|
422
411
|
} catch (err) {
|
|
423
|
-
consola.error("Failed to
|
|
412
|
+
consola.error("Failed to bootstrap sidebars:", err);
|
|
424
413
|
}
|
|
425
414
|
}
|
|
426
415
|
|
|
427
416
|
function startTui(): void {
|
|
428
|
-
|
|
417
|
+
ensureBun();
|
|
429
418
|
|
|
430
|
-
const
|
|
419
|
+
const agentboardDir = resolve(import.meta.dirname, "../../packages/agentboard");
|
|
420
|
+
const tuiEntry = resolve(agentboardDir, "apps/tui/src/index.tsx");
|
|
431
421
|
|
|
432
422
|
execSync(`bun run ${tuiEntry}`, {
|
|
433
423
|
stdio: "inherit",
|
|
434
|
-
cwd: resolve(
|
|
435
|
-
env: {
|
|
436
|
-
...process.env,
|
|
437
|
-
AGENTBOARD_DIR: PLUGIN_DIR,
|
|
438
|
-
},
|
|
424
|
+
cwd: resolve(agentboardDir, "apps/tui"),
|
|
439
425
|
});
|
|
440
426
|
}
|
|
441
427
|
|
|
@@ -463,8 +449,6 @@ export default defineCommand({
|
|
|
463
449
|
startServer();
|
|
464
450
|
break;
|
|
465
451
|
case "tui":
|
|
466
|
-
startTui();
|
|
467
|
-
break;
|
|
468
452
|
case "start":
|
|
469
453
|
startTui();
|
|
470
454
|
break;
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
|
|
7
|
-
import { readFile } from "
|
|
7
|
+
import { readFile } from "@towles/shared";
|
|
8
8
|
import { getConfig } from "./config.js";
|
|
9
9
|
import { sleep } from "./shell.js";
|
|
10
10
|
import { spawnClaude as defaultSpawnClaude } from "./spawn-claude.js";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AutoClaudeConfigSchema } from "./config.js";
|
|
2
|
+
import type { AutoClaudeConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export const CONFIG_DIR = ".auto-claude";
|
|
5
|
+
export const CONFIG_FILENAME = "config.json";
|
|
6
|
+
export const CONFIG_PATH = `${CONFIG_DIR}/${CONFIG_FILENAME}`;
|
|
7
|
+
|
|
8
|
+
export interface ConfigInitInput {
|
|
9
|
+
triggerLabel: string;
|
|
10
|
+
mainBranch: string;
|
|
11
|
+
scopePath: string;
|
|
12
|
+
model: string;
|
|
13
|
+
repo: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a full AutoClaudeConfig from user inputs, applying schema defaults.
|
|
18
|
+
*/
|
|
19
|
+
export function buildConfig(input: ConfigInitInput): AutoClaudeConfig {
|
|
20
|
+
return AutoClaudeConfigSchema.parse({
|
|
21
|
+
triggerLabel: input.triggerLabel,
|
|
22
|
+
mainBranch: input.mainBranch,
|
|
23
|
+
scopePath: input.scopePath,
|
|
24
|
+
model: input.model,
|
|
25
|
+
repo: input.repo,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate that a trigger label is non-empty and contains no spaces.
|
|
31
|
+
*/
|
|
32
|
+
export function validateTriggerLabel(label: string): string | true {
|
|
33
|
+
const trimmed = label.trim();
|
|
34
|
+
if (trimmed.length === 0) return "Trigger label cannot be empty";
|
|
35
|
+
if (/\s/.test(trimmed)) return "Trigger label cannot contain spaces";
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate that a branch name is non-empty.
|
|
41
|
+
*/
|
|
42
|
+
export function validateBranchName(name: string): string | true {
|
|
43
|
+
const trimmed = name.trim();
|
|
44
|
+
if (trimmed.length === 0) return "Branch name cannot be empty";
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate that a scope path is non-empty.
|
|
50
|
+
*/
|
|
51
|
+
export function validateScopePath(path: string): string | true {
|
|
52
|
+
const trimmed = path.trim();
|
|
53
|
+
if (trimmed.length === 0) return "Scope path cannot be empty";
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format the config as a human-readable summary for confirmation.
|
|
59
|
+
*/
|
|
60
|
+
export function formatConfigSummary(config: AutoClaudeConfig): string {
|
|
61
|
+
return [
|
|
62
|
+
` repo: ${config.repo}`,
|
|
63
|
+
` triggerLabel: ${config.triggerLabel}`,
|
|
64
|
+
` mainBranch: ${config.mainBranch}`,
|
|
65
|
+
` scopePath: ${config.scopePath}`,
|
|
66
|
+
` model: ${config.model}`,
|
|
67
|
+
` remote: ${config.remote}`,
|
|
68
|
+
` maxIterations: ${config.maxImplementIterations}`,
|
|
69
|
+
` maxReviewRetries: ${config.maxReviewRetries}`,
|
|
70
|
+
` loopInterval: ${config.loopIntervalMinutes}min`,
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Serialize config to JSON for writing to disk.
|
|
76
|
+
*/
|
|
77
|
+
export function serializeConfig(config: AutoClaudeConfig): string {
|
|
78
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
79
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildConfig,
|
|
5
|
+
formatConfigSummary,
|
|
6
|
+
serializeConfig,
|
|
7
|
+
validateBranchName,
|
|
8
|
+
validateScopePath,
|
|
9
|
+
validateTriggerLabel,
|
|
10
|
+
} from "./config-init-helpers";
|
|
11
|
+
|
|
12
|
+
describe("validateTriggerLabel", () => {
|
|
13
|
+
it("accepts a valid label", () => {
|
|
14
|
+
expect(validateTriggerLabel("auto-claude")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("rejects empty string", () => {
|
|
18
|
+
expect(validateTriggerLabel("")).toBe("Trigger label cannot be empty");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects whitespace-only", () => {
|
|
22
|
+
expect(validateTriggerLabel(" ")).toBe("Trigger label cannot be empty");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects label with spaces", () => {
|
|
26
|
+
expect(validateTriggerLabel("auto claude")).toBe("Trigger label cannot contain spaces");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("validateBranchName", () => {
|
|
31
|
+
it("accepts a valid branch name", () => {
|
|
32
|
+
expect(validateBranchName("main")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rejects empty string", () => {
|
|
36
|
+
expect(validateBranchName("")).toBe("Branch name cannot be empty");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("validateScopePath", () => {
|
|
41
|
+
it("accepts a valid path", () => {
|
|
42
|
+
expect(validateScopePath(".")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("accepts a subdirectory", () => {
|
|
46
|
+
expect(validateScopePath("src/lib")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("rejects empty string", () => {
|
|
50
|
+
expect(validateScopePath("")).toBe("Scope path cannot be empty");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("buildConfig", () => {
|
|
55
|
+
it("builds config with provided values", () => {
|
|
56
|
+
const config = buildConfig({
|
|
57
|
+
triggerLabel: "my-label",
|
|
58
|
+
mainBranch: "develop",
|
|
59
|
+
scopePath: "src/",
|
|
60
|
+
model: "sonnet",
|
|
61
|
+
repo: "owner/repo",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(config.triggerLabel).toBe("my-label");
|
|
65
|
+
expect(config.mainBranch).toBe("develop");
|
|
66
|
+
expect(config.scopePath).toBe("src/");
|
|
67
|
+
expect(config.model).toBe("sonnet");
|
|
68
|
+
expect(config.repo).toBe("owner/repo");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("applies schema defaults for fields not in input", () => {
|
|
72
|
+
const config = buildConfig({
|
|
73
|
+
triggerLabel: "auto-claude",
|
|
74
|
+
mainBranch: "main",
|
|
75
|
+
scopePath: ".",
|
|
76
|
+
model: "opus",
|
|
77
|
+
repo: "owner/repo",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(config.remote).toBe("origin");
|
|
81
|
+
expect(config.maxImplementIterations).toBe(5);
|
|
82
|
+
expect(config.maxReviewRetries).toBe(2);
|
|
83
|
+
expect(config.loopIntervalMinutes).toBe(30);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("formatConfigSummary", () => {
|
|
88
|
+
it("includes all key fields", () => {
|
|
89
|
+
const config = buildConfig({
|
|
90
|
+
triggerLabel: "auto-claude",
|
|
91
|
+
mainBranch: "main",
|
|
92
|
+
scopePath: ".",
|
|
93
|
+
model: "opus",
|
|
94
|
+
repo: "owner/repo",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const summary = formatConfigSummary(config);
|
|
98
|
+
expect(summary).toContain("owner/repo");
|
|
99
|
+
expect(summary).toContain("auto-claude");
|
|
100
|
+
expect(summary).toContain("main");
|
|
101
|
+
expect(summary).toContain("opus");
|
|
102
|
+
expect(summary).toContain("origin");
|
|
103
|
+
expect(summary).toContain("30min");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("serializeConfig", () => {
|
|
108
|
+
it("returns valid JSON with trailing newline", () => {
|
|
109
|
+
const config = buildConfig({
|
|
110
|
+
triggerLabel: "auto-claude",
|
|
111
|
+
mainBranch: "main",
|
|
112
|
+
scopePath: ".",
|
|
113
|
+
model: "opus",
|
|
114
|
+
repo: "owner/repo",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const json = serializeConfig(config);
|
|
118
|
+
expect(json.endsWith("\n")).toBe(true);
|
|
119
|
+
|
|
120
|
+
const parsed = JSON.parse(json);
|
|
121
|
+
expect(parsed.repo).toBe("owner/repo");
|
|
122
|
+
expect(parsed.triggerLabel).toBe("auto-claude");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("is pretty-printed with 2-space indent", () => {
|
|
126
|
+
const config = buildConfig({
|
|
127
|
+
triggerLabel: "test",
|
|
128
|
+
mainBranch: "main",
|
|
129
|
+
scopePath: ".",
|
|
130
|
+
model: "opus",
|
|
131
|
+
repo: "o/r",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const json = serializeConfig(config);
|
|
135
|
+
expect(json).toContain(' "repo"');
|
|
136
|
+
});
|
|
137
|
+
});
|