tmux-ide 1.2.1 → 2.0.0

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 (205) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +33 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/{attach.js → attach.ts} +7 -4
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/{detect.js → detect.ts} +31 -15
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/{inspect.js → inspect.ts} +95 -10
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/{output.js → output.ts} +18 -10
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/{session-options.js → session-options.ts} +11 -6
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/{sizes.js → sizes.ts} +4 -4
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
  103. package/src/ls.test.ts +76 -0
  104. package/src/{ls.js → ls.ts} +4 -4
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/src/config.js +0 -341
  194. package/src/doctor.js +0 -91
  195. package/src/init.js +0 -73
  196. package/src/launch.js +0 -245
  197. package/src/lib/dot-path.js +0 -16
  198. package/src/lib/errors.js +0 -35
  199. package/src/lib/launch-plan.js +0 -49
  200. package/src/lib/session-monitor.js +0 -179
  201. package/src/lib/tmux.js +0 -237
  202. package/src/restart.js +0 -16
  203. package/src/status.js +0 -35
  204. package/src/stop.js +0 -25
  205. package/src/validate.js +0 -154
@@ -0,0 +1,101 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { init } from "./init.ts";
6
+
7
+ let tmpDir;
8
+ let origCwd;
9
+ let origLog;
10
+ let logged;
11
+
12
+ beforeEach(() => {
13
+ tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-init-test-"));
14
+ origCwd = process.cwd();
15
+ process.chdir(tmpDir);
16
+ logged = [];
17
+ origLog = console.log;
18
+ console.log = (...a) => logged.push(a.join(" "));
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.chdir(origCwd);
23
+ console.log = origLog;
24
+ rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("init", () => {
28
+ it("creates ide.yml from default template when no stack detected", async () => {
29
+ await init({ json: true });
30
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
31
+ const output = JSON.parse(logged[0]);
32
+ expect(output.created).toBe(true);
33
+ expect(output.template).toBe("default");
34
+ });
35
+
36
+ it("creates ide.yml from named template", async () => {
37
+ await init({ template: "nextjs", json: true });
38
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
39
+ const content = readFileSync(join(tmpDir, "ide.yml"), "utf-8");
40
+ expect(content.includes("rows")).toBeTruthy();
41
+ const output = JSON.parse(logged[0]);
42
+ expect(output.created).toBe(true);
43
+ expect(output.template).toBe("nextjs");
44
+ });
45
+
46
+ it("creates ide.yml from detected stack", async () => {
47
+ // Create a package.json with a Next.js dependency to trigger detection
48
+ writeFileSync(
49
+ join(tmpDir, "package.json"),
50
+ JSON.stringify({
51
+ name: "test-app",
52
+ dependencies: { next: "14.0.0" },
53
+ scripts: { dev: "next dev" },
54
+ }),
55
+ );
56
+ await init({ json: true });
57
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
58
+ const output = JSON.parse(logged[0]);
59
+ expect(output.created).toBe(true);
60
+ expect(output.detected.length > 0).toBeTruthy();
61
+ });
62
+
63
+ it("rejects when ide.yml already exists", async () => {
64
+ writeFileSync(join(tmpDir, "ide.yml"), "name: existing\n");
65
+ await expect(init({ json: true })).rejects.toThrow();
66
+ });
67
+
68
+ it("rejects unknown template name", async () => {
69
+ await expect(init({ template: "nonexistent-template-xyz", json: true })).rejects.toThrow();
70
+ });
71
+
72
+ it("replaces the name field with the directory basename", async () => {
73
+ await init({ template: "default", json: true });
74
+ const content = readFileSync(join(tmpDir, "ide.yml"), "utf-8");
75
+ const dirName = tmpDir.split("/").pop();
76
+ expect(content.includes(`name: ${dirName}`)).toBeTruthy();
77
+ });
78
+
79
+ it("prints human-readable output without --json", async () => {
80
+ await init();
81
+ expect(logged.some((l) => l.includes("Created ide.yml"))).toBeTruthy();
82
+ });
83
+
84
+ it("scaffolds missions skills, library, and AGENTS.md", async () => {
85
+ await init({ template: "missions", json: true });
86
+
87
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "frontend.md"))).toBeTruthy();
88
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "backend.md"))).toBeTruthy();
89
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "reviewer.md"))).toBeTruthy();
90
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "researcher.md"))).toBeTruthy();
91
+ expect(existsSync(join(tmpDir, ".tmux-ide", "library"))).toBeTruthy();
92
+ expect(existsSync(join(tmpDir, "AGENTS.md"))).toBeTruthy();
93
+ expect(readFileSync(join(tmpDir, "AGENTS.md"), "utf-8")).toContain(
94
+ `## Project: ${join(tmpDir).split("/").pop()}`,
95
+ );
96
+
97
+ const output = JSON.parse(logged[0]);
98
+ expect(output.template).toBe("missions");
99
+ expect(output.paths.some((path: string) => path.endsWith("frontend.md"))).toBeTruthy();
100
+ });
101
+ });
package/src/init.ts ADDED
@@ -0,0 +1,144 @@
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ writeFileSync,
5
+ renameSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ copyFileSync,
9
+ } from "node:fs";
10
+ import { resolve, join, basename, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ import { detectStack, suggestConfig } from "./detect.ts";
15
+ import { outputError, printLayout } from "./lib/output.ts";
16
+ import type { IdeConfig } from "./types.ts";
17
+
18
+ function copyTemplateSkills(targetDir: string): string[] {
19
+ const created: string[] = [];
20
+ const templateSkillsDir = resolve(__dirname, "..", "templates", "skills");
21
+ if (!existsSync(templateSkillsDir)) return created;
22
+
23
+ mkdirSync(targetDir, { recursive: true });
24
+ for (const file of readdirSync(templateSkillsDir)) {
25
+ if (!file.endsWith(".md")) continue;
26
+ const destination = join(targetDir, file);
27
+ copyFileSync(join(templateSkillsDir, file), destination);
28
+ created.push(destination);
29
+ }
30
+ return created;
31
+ }
32
+
33
+ function scaffoldMissionsWorkspace(dir: string, name: string): string[] {
34
+ const created: string[] = [];
35
+ const skillsDir = join(dir, ".tmux-ide", "skills");
36
+ created.push(...copyTemplateSkills(skillsDir));
37
+
38
+ const libraryDir = join(dir, ".tmux-ide", "library");
39
+ if (!existsSync(libraryDir)) {
40
+ mkdirSync(libraryDir, { recursive: true });
41
+ created.push(libraryDir);
42
+ }
43
+
44
+ const agentsTemplatePath = resolve(__dirname, "..", "templates", "AGENTS.md");
45
+ if (existsSync(agentsTemplatePath)) {
46
+ const agentsPath = join(dir, "AGENTS.md");
47
+ const content = readFileSync(agentsTemplatePath, "utf-8").replace(/{{name}}/g, name);
48
+ writeFileSync(agentsPath, content);
49
+ created.push(agentsPath);
50
+ }
51
+
52
+ return created;
53
+ }
54
+
55
+ export async function init({
56
+ template,
57
+ json,
58
+ }: { template?: string; json?: boolean } = {}): Promise<void> {
59
+ const dir = process.cwd();
60
+ const configPath = resolve(dir, "ide.yml");
61
+
62
+ if (existsSync(configPath)) {
63
+ outputError("ide.yml already exists in this directory", "EXISTS");
64
+ }
65
+
66
+ // If a specific template is requested, use it
67
+ if (template) {
68
+ const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
69
+ if (!existsSync(templatePath)) {
70
+ outputError(`Template "${template}" not found`, "NOT_FOUND");
71
+ }
72
+
73
+ let content = readFileSync(templatePath, "utf-8");
74
+ const name = basename(dir);
75
+ content = content.replace(/^name: .+/m, `name: ${name}`);
76
+ const tmpPath = configPath + ".tmp";
77
+ writeFileSync(tmpPath, content);
78
+ renameSync(tmpPath, configPath);
79
+ const created =
80
+ template === "missions"
81
+ ? scaffoldMissionsWorkspace(dir, name)
82
+ : copyTemplateSkills(join(dir, ".tmux-ide", "skills"));
83
+
84
+ if (json) {
85
+ console.log(JSON.stringify({ created: true, template, name, paths: created }));
86
+ } else {
87
+ console.log(`Created ide.yml from "${template}" template for "${name}"`);
88
+ const yaml = (await import("js-yaml")).default;
89
+ printLayout(yaml.load(content) as IdeConfig);
90
+ for (const createdPath of created) {
91
+ console.log(`Created ${createdPath.replace(dir + "/", "")}`);
92
+ }
93
+ }
94
+ return;
95
+ }
96
+
97
+ // Smart detection
98
+ const detected = detectStack(dir);
99
+ const name = basename(dir);
100
+
101
+ if (detected.frameworks.length > 0) {
102
+ // Use detected stack to generate config
103
+ const config = suggestConfig(dir, detected);
104
+ const yaml = (await import("js-yaml")).default;
105
+ const tmpPath2 = configPath + ".tmp";
106
+ writeFileSync(tmpPath2, yaml.dump(config, { lineWidth: -1, noRefs: true, quotingType: '"' }));
107
+ renameSync(tmpPath2, configPath);
108
+
109
+ const desc = detected.frameworks.join(" + ");
110
+ if (json) {
111
+ console.log(JSON.stringify({ created: true, detected: detected.frameworks, name }));
112
+ } else {
113
+ console.log(`Detected ${desc}. Created ide.yml for "${name}".`);
114
+ printLayout(config);
115
+ console.log("Edit it to customize, then run: tmux-ide");
116
+ }
117
+ } else {
118
+ // Fallback to default template
119
+ const templatePath = resolve(__dirname, "..", "templates", "default.yml");
120
+ let content = readFileSync(templatePath, "utf-8");
121
+ content = content.replace(/^name: .+/m, `name: ${name}`);
122
+ const tmpPath3 = configPath + ".tmp";
123
+ writeFileSync(tmpPath3, content);
124
+ renameSync(tmpPath3, configPath);
125
+
126
+ if (json) {
127
+ console.log(JSON.stringify({ created: true, template: "default", name }));
128
+ } else {
129
+ console.log(`Created ide.yml for "${name}"`);
130
+ const yaml = (await import("js-yaml")).default;
131
+ printLayout(yaml.load(content) as IdeConfig);
132
+ console.log("Edit it to configure your workspace, then run: tmux-ide");
133
+ }
134
+ }
135
+
136
+ // Copy built-in skills if .tmux-ide/skills/ doesn't exist
137
+ const skillsDir = join(dir, ".tmux-ide", "skills");
138
+ if (!existsSync(skillsDir)) {
139
+ const created = copyTemplateSkills(skillsDir);
140
+ if (created.length > 0 && !json) {
141
+ console.log("Copied built-in skill templates to .tmux-ide/skills/");
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const cli = join(__dirname, "..", "bin", "cli.js");
10
+
11
+ function makeProject(config = "name: inspect-session\nrows:\n - panes:\n - title: Shell\n") {
12
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-inspect-test-"));
13
+ writeFileSync(join(dir, "ide.yml"), config);
14
+ return dir;
15
+ }
16
+
17
+ function runCli(args, cwd) {
18
+ return spawnSync("node", [cli, ...args], {
19
+ cwd,
20
+ encoding: "utf-8",
21
+ });
22
+ }
23
+
24
+ describe("inspect command", () => {
25
+ it("prints resolved config state as json", () => {
26
+ const dir = makeProject(
27
+ "name: inspect-session\nrows:\n - size: 70%\n panes:\n - title: Claude\n command: claude\n focus: true\n - title: Shell\n",
28
+ );
29
+
30
+ try {
31
+ const result = runCli(["inspect", "--json"], dir);
32
+
33
+ expect(result.status).toBe(0);
34
+ const payload = JSON.parse(result.stdout);
35
+ expect(payload.session).toBe("inspect-session");
36
+ expect(payload.valid).toBe(true);
37
+ expect(payload.summary.rows).toBe(1);
38
+ expect(payload.summary.panes).toBe(2);
39
+ expect(payload.summary.focus).toBe("rows.0.panes.0");
40
+ expect(payload.tmux.running).toBe(false);
41
+ } finally {
42
+ rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ it("reports validation errors for invalid pane arrays instead of crashing", () => {
47
+ const dir = makeProject("name: inspect-session\nrows:\n - panes: nope\n");
48
+
49
+ try {
50
+ const result = runCli(["inspect", "--json"], dir);
51
+
52
+ expect(result.status).toBe(0);
53
+ const payload = JSON.parse(result.stdout);
54
+ expect(payload.valid).toBe(false);
55
+ expect(JSON.stringify(payload.errors)).toMatch(/rows\[0\]\.panes must be an array/);
56
+ expect(payload.rows).toEqual([
57
+ {
58
+ index: 0,
59
+ size: null,
60
+ panes: [],
61
+ },
62
+ ]);
63
+ } finally {
64
+ rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ });
67
+ });
@@ -1,13 +1,75 @@
1
1
  import { resolve, basename } from "node:path";
2
- import { readConfig } from "./lib/yaml-io.js";
3
- import { validateConfig } from "./validate.js";
4
- import { outputError } from "./lib/output.js";
5
- import { getSessionState, listPanes } from "./lib/tmux.js";
2
+ import { readConfig } from "./lib/yaml-io.ts";
3
+ import { validateConfig } from "./validate.ts";
4
+ import { outputError } from "./lib/output.ts";
5
+ import { getSessionState, listPanes } from "./lib/tmux.ts";
6
+ import { loadSkills } from "./lib/skill-registry.ts";
7
+ import type { IdeConfig } from "./types.ts";
6
8
 
7
- export function buildInspection(dir, { config, configPath, running, panes }) {
9
+ interface ResolvedPane {
10
+ index: number;
11
+ title: string | null;
12
+ command: string | null;
13
+ dir: string;
14
+ size: string | null;
15
+ focus: boolean;
16
+ role: string | null;
17
+ task: string | null;
18
+ env: Record<string, unknown>;
19
+ }
20
+
21
+ interface ResolvedRow {
22
+ index: number;
23
+ size: string | null;
24
+ panes: ResolvedPane[];
25
+ }
26
+
27
+ interface SkillInfo {
28
+ name: string;
29
+ specialties: string[];
30
+ }
31
+
32
+ interface PaneSkillMapping {
33
+ pane: string;
34
+ skill: string;
35
+ resolved: boolean;
36
+ }
37
+
38
+ interface Inspection {
39
+ dir: string;
40
+ configPath: string;
41
+ valid: boolean;
42
+ errors: string[];
43
+ session: string;
44
+ before: string | null;
45
+ summary: { rows: number; panes: number; focus: string | null };
46
+ team: IdeConfig["team"] | null;
47
+ theme: IdeConfig["theme"] | null;
48
+ focus: { row: number; pane: number; title: string | null } | null;
49
+ rows: ResolvedRow[];
50
+ rawConfig: IdeConfig;
51
+ tmux: { running: boolean; panes: ReturnType<typeof listPanes> };
52
+ skills: SkillInfo[];
53
+ paneSkillMap: PaneSkillMapping[];
54
+ }
55
+
56
+ export function buildInspection(
57
+ dir: string,
58
+ {
59
+ config,
60
+ configPath,
61
+ running,
62
+ panes,
63
+ }: {
64
+ config: IdeConfig;
65
+ configPath: string;
66
+ running: boolean;
67
+ panes: ReturnType<typeof listPanes>;
68
+ },
69
+ ): Inspection {
8
70
  const errors = validateConfig(config);
9
71
  const rows = Array.isArray(config?.rows) ? config.rows : [];
10
- const resolvedRows = rows.map((row, rowIndex) => ({
72
+ const resolvedRows: ResolvedRow[] = rows.map((row, rowIndex) => ({
11
73
  index: rowIndex,
12
74
  size: row.size ?? null,
13
75
  panes: (Array.isArray(row?.panes) ? row.panes : []).map((pane, paneIndex) => ({
@@ -29,6 +91,24 @@ export function buildInspection(dir, { config, configPath, running, panes }) {
29
91
  .find(({ pane }) => pane.focus) ?? null;
30
92
  const session = config?.name ?? basename(dir);
31
93
 
94
+ // Skills
95
+ const allSkills = loadSkills(dir);
96
+ const skillNames = new Set(allSkills.map((s) => s.name));
97
+ const skills: SkillInfo[] = allSkills.map((s) => ({ name: s.name, specialties: s.specialties }));
98
+
99
+ const paneSkillMap: PaneSkillMapping[] = [];
100
+ for (const row of rows) {
101
+ for (const pane of row.panes ?? []) {
102
+ if (pane.skill) {
103
+ paneSkillMap.push({
104
+ pane: pane.title ?? "untitled",
105
+ skill: pane.skill,
106
+ resolved: skillNames.has(pane.skill),
107
+ });
108
+ }
109
+ }
110
+ }
111
+
32
112
  return {
33
113
  dir,
34
114
  configPath,
@@ -56,18 +136,23 @@ export function buildInspection(dir, { config, configPath, running, panes }) {
56
136
  running,
57
137
  panes,
58
138
  },
139
+ skills,
140
+ paneSkillMap,
59
141
  };
60
142
  }
61
143
 
62
- export async function inspect(targetDir, { json } = {}) {
144
+ export async function inspect(
145
+ targetDir: string | undefined,
146
+ { json }: { json?: boolean } = {},
147
+ ): Promise<void> {
63
148
  const dir = resolve(targetDir ?? ".");
64
149
 
65
150
  let config;
66
- let configPath;
151
+ let configPath: string;
67
152
  try {
68
153
  ({ config, configPath } = readConfig(dir));
69
154
  } catch (error) {
70
- outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
155
+ outputError(`Cannot read ide.yml: ${(error as Error).message}`, "READ_ERROR");
71
156
  return;
72
157
  }
73
158
 
@@ -107,7 +192,7 @@ export async function inspect(targetDir, { json } = {}) {
107
192
  for (const row of data.rows) {
108
193
  console.log(` Row ${row.index}${row.size ? ` (${row.size})` : ""}`);
109
194
  for (const pane of row.panes) {
110
- const parts = [];
195
+ const parts: string[] = [];
111
196
  if (pane.title) parts.push(pane.title);
112
197
  if (pane.command) parts.push(`cmd=${pane.command}`);
113
198
  if (pane.dir && pane.dir !== ".") parts.push(`dir=${pane.dir}`);