@towles/tool 0.0.108 → 0.0.110

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.
Files changed (190) hide show
  1. package/package.json +9 -4
  2. package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
  3. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
  4. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
  5. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
  6. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
  7. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
  8. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
  9. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +204 -225
  10. package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
  11. package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
  12. package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
  13. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
  14. package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
  15. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
  16. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
  17. package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
  18. package/packages/agentboard/packages/runtime/src/config.ts +80 -0
  19. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
  20. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
  21. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
  22. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
  23. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
  24. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
  25. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +2 -0
  26. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
  27. package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +1 -1
  28. package/packages/shared/package.json +15 -0
  29. package/packages/shared/src/git/exec.ts +41 -0
  30. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  31. package/packages/shared/src/index.ts +8 -0
  32. package/packages/shared/tsconfig.json +16 -0
  33. package/src/cli.ts +3 -2
  34. package/src/commands/agentboard.ts +42 -59
  35. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  36. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  37. package/src/commands/auto-claude/config-init.test.ts +137 -0
  38. package/src/commands/auto-claude/config-init.ts +159 -0
  39. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  40. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  41. package/src/commands/auto-claude/explain.test.ts +58 -0
  42. package/src/commands/auto-claude/explain.ts +97 -0
  43. package/src/commands/auto-claude/index.ts +37 -14
  44. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  45. package/src/commands/auto-claude/list.ts +5 -4
  46. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  47. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  48. package/src/commands/auto-claude/retry.test.ts +2 -2
  49. package/src/commands/auto-claude/retry.ts +5 -5
  50. package/src/commands/auto-claude/shell.ts +3 -0
  51. package/src/commands/auto-claude/status.test.ts +2 -2
  52. package/src/commands/auto-claude/status.ts +4 -4
  53. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  54. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  55. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  56. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  57. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  58. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  59. package/src/commands/config/config.test.ts +129 -0
  60. package/src/commands/config/index.ts +11 -0
  61. package/src/commands/config/reset.ts +53 -0
  62. package/src/commands/config/schema.ts +19 -0
  63. package/src/commands/{config.ts → config/show.ts} +2 -2
  64. package/src/commands/config/validate.ts +51 -0
  65. package/src/commands/doctor/checks.ts +167 -0
  66. package/src/commands/doctor/format.test.ts +63 -0
  67. package/src/commands/doctor/format.ts +5 -0
  68. package/src/commands/doctor/history.test.ts +161 -0
  69. package/src/commands/doctor/history.ts +130 -0
  70. package/src/commands/doctor.ts +80 -151
  71. package/src/commands/gh/branch-clean.ts +4 -4
  72. package/src/commands/gh/branch.test.ts +4 -5
  73. package/src/commands/gh/branch.ts +10 -5
  74. package/src/commands/gh/pr.ts +6 -7
  75. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  76. package/src/commands/graph/format.test.ts +130 -0
  77. package/src/commands/graph/format.ts +94 -0
  78. package/src/commands/graph/index.ts +69 -41
  79. package/src/{lib → commands}/graph/labels.ts +4 -4
  80. package/src/{lib → commands}/graph/server.ts +2 -2
  81. package/src/{lib → commands}/graph/types.ts +2 -0
  82. package/src/commands/graph.test.ts +1 -1
  83. package/src/commands/install.ts +6 -6
  84. package/src/commands/journal/daily-notes.ts +4 -7
  85. package/src/{lib → commands}/journal/fs.ts +1 -1
  86. package/src/commands/journal/index.ts +2 -0
  87. package/src/commands/journal/list.test.ts +174 -0
  88. package/src/commands/journal/list.ts +213 -0
  89. package/src/commands/journal/meeting.ts +4 -7
  90. package/src/commands/journal/note.ts +4 -7
  91. package/src/{lib → commands}/journal/paths.ts +1 -1
  92. package/src/commands/journal/search.test.ts +156 -0
  93. package/src/commands/journal/search.ts +256 -0
  94. package/src/{lib → commands}/journal/templates.ts +1 -1
  95. package/src/config/settings.ts +35 -26
  96. package/plugins/tt-agentboard/bun.lock +0 -444
  97. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  98. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  99. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  100. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  101. package/plugins/tt-auto-claude/commands/list.md +0 -21
  102. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  103. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  104. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  105. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  106. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  107. package/src/commands/config.test.ts +0 -9
  108. package/src/lib/auto-claude/index.ts +0 -15
  109. package/src/lib/auto-claude/shell.ts +0 -6
  110. package/src/lib/graph/index.ts +0 -24
  111. package/src/lib/journal/index.ts +0 -11
  112. package/src/utils/git/exec.ts +0 -18
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  144. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
  145. /package/{plugins/tt-core → packages/core}/README.md +0 -0
  146. /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
  147. /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
  148. /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
  149. /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
  150. /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
  151. /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
  152. /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
  153. /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
  154. /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
  155. /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
  156. /package/{src/utils → packages/shared/src}/fs.ts +0 -0
  157. /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
  158. /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
  159. /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
  160. /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
  161. /package/{src/utils → packages/shared/src}/render.ts +0 -0
  162. /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
  163. /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
  164. /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
  165. /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
  166. /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
  167. /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
  168. /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
  169. /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
  170. /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
  171. /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
  172. /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
  173. /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
  174. /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
  175. /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
  176. /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
  177. /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
  178. /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
  179. /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
  180. /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
  181. /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
  182. /package/src/{lib → commands}/graph/analyzer.ts +0 -0
  183. /package/src/{lib → commands}/graph/graph-template.html +0 -0
  184. /package/src/{lib → commands}/graph/parser.test.ts +0 -0
  185. /package/src/{lib → commands}/graph/parser.ts +0 -0
  186. /package/src/{lib → commands}/graph/render.ts +0 -0
  187. /package/src/{lib → commands}/graph/sessions.ts +0 -0
  188. /package/src/{lib → commands}/graph/tools.ts +0 -0
  189. /package/src/{lib → commands}/graph/treemap.ts +0 -0
  190. /package/src/{lib → commands}/journal/editor.ts +0 -0
@@ -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 stripAnsi from "strip-ansi";
2
- import { x } from "tinyexec";
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<Output>;
8
+ ) => PromiseLike<XResult>;
12
9
 
13
- export async function isGithubCliInstalled(exec: XFn = x as XFn): Promise<boolean> {
10
+ export async function isGithubCliInstalled(execFn: XFn = defaultX): Promise<boolean> {
14
11
  try {
15
- const proc = await exec("gh", ["--version"]);
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 result = await x("gh", args, { nodeOptions: { cwd: process.cwd() }, throwOnError: true });
25
- return JSON.parse(result.stdout.trim()) as T;
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
- exec?: (cmd: string, args: string[]) => Promise<{ stdout: string; ok: boolean }>,
26
+ execFn?: (cmd: string, args: string[]) => Promise<{ stdout: string; ok: boolean }>,
31
27
  ): Promise<string> {
32
- const execFn = exec ?? execSafe;
33
- const result = await execFn("gh", args);
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 = x as XFn,
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 exec("gh", args);
69
- // Setting NO_COLOR=1 didn't remove colors so had to use stripAnsi
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
@@ -1,9 +1,10 @@
1
1
  import { defineCommand } from "citty";
2
+ import { version } from "../package.json";
2
3
 
3
4
  export const main = defineCommand({
4
- meta: { name: "tt", description: "towles-tool — personal CLI utilities" },
5
+ meta: { name: "tt", version, description: "towles-tool — personal CLI utilities" },
5
6
  subCommands: {
6
- config: () => import("./commands/config.js").then((m) => m.default),
7
+ config: () => import("./commands/config/index.js").then((m) => m.default),
7
8
  doctor: () => import("./commands/doctor.js").then((m) => m.default),
8
9
  install: () => import("./commands/install.js").then((m) => m.default),
9
10
  agentboard: () => import("./commands/agentboard.js").then((m) => m.default),
@@ -9,8 +9,6 @@ import { debugArg } from "./shared.js";
9
9
  const SERVER_HOST = "127.0.0.1";
10
10
  const SERVER_PORT = 4201;
11
11
 
12
- const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
13
-
14
12
  // Keybinding defaults
15
13
  const DEFAULT_KEY = "a";
16
14
  const TMUX_BINDINGS = { toggle: "t", focus: "s" } as const;
@@ -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
- try {
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 ensureDeps(): void {
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:");
@@ -186,18 +163,15 @@ function uninstall(): void {
186
163
  }
187
164
 
188
165
  function startServer(): void {
189
- ensureDeps();
166
+ ensureBun();
190
167
 
191
- const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
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: PLUGIN_DIR,
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("bun", ["run", serverEntry], {
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
 
@@ -381,7 +354,7 @@ async function runFocus(): Promise<void> {
381
354
  }
382
355
 
383
356
  async function restart(): Promise<void> {
384
- ensureDeps();
357
+ ensureBun();
385
358
 
386
359
  // 1. Kill stash sessions left over from hidden sidebars
387
360
  try {
@@ -407,35 +380,47 @@ async function restart(): Promise<void> {
407
380
  }
408
381
  consola.success("Server is running");
409
382
 
410
- // 3. Toggle sidebar on (the server spawns sidebars in all active windows)
383
+ // 3. Bootstrap sidebars refresh session list, then ensure-sidebar for
384
+ // each active client's window so sidebars appear without interaction.
385
+ const base = `http://${SERVER_HOST}:${SERVER_PORT}`;
386
+ await fetch(`${base}/refresh`, { method: "POST", signal: AbortSignal.timeout(2000) }).catch(
387
+ () => {},
388
+ );
389
+
411
390
  try {
412
- const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, {
413
- method: "POST",
414
- body: "",
415
- signal: AbortSignal.timeout(2000),
416
- });
417
- if (res.ok) {
418
- consola.success("Sidebar toggled on for all sessions");
419
- } else {
420
- consola.warn(`Toggle returned: ${res.status}`);
421
- }
391
+ const clients = spawnSync(
392
+ "tmux",
393
+ ["list-clients", "-F", "#{client_tty}|#{session_name}|#{window_id}"],
394
+ {
395
+ encoding: "utf8",
396
+ stdio: ["pipe", "pipe", "pipe"],
397
+ },
398
+ );
399
+ const lines = (clients.stdout ?? "").trim().split("\n").filter(Boolean);
400
+ await Promise.allSettled(
401
+ lines.map((ctx) =>
402
+ fetch(`${base}/ensure-sidebar`, {
403
+ method: "POST",
404
+ body: ctx,
405
+ signal: AbortSignal.timeout(2000),
406
+ }),
407
+ ),
408
+ );
409
+ consola.success(`Sidebars ensured for ${lines.length} client(s)`);
422
410
  } catch (err) {
423
- consola.error("Failed to toggle sidebar:", err);
411
+ consola.error("Failed to bootstrap sidebars:", err);
424
412
  }
425
413
  }
426
414
 
427
415
  function startTui(): void {
428
- ensureDeps();
416
+ ensureBun();
429
417
 
430
- const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
418
+ const agentboardDir = resolve(import.meta.dirname, "../../packages/agentboard");
419
+ const tuiEntry = resolve(agentboardDir, "apps/tui/src/index.tsx");
431
420
 
432
421
  execSync(`bun run ${tuiEntry}`, {
433
422
  stdio: "inherit",
434
- cwd: resolve(PLUGIN_DIR, "apps/tui"),
435
- env: {
436
- ...process.env,
437
- AGENTBOARD_DIR: PLUGIN_DIR,
438
- },
423
+ cwd: resolve(agentboardDir, "apps/tui"),
439
424
  });
440
425
  }
441
426
 
@@ -463,8 +448,6 @@ export default defineCommand({
463
448
  startServer();
464
449
  break;
465
450
  case "tui":
466
- startTui();
467
- break;
468
451
  case "start":
469
452
  startTui();
470
453
  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 "../../utils/fs.js";
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
+ });