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.
- package/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/package.json +33 -14
- package/src/__tests__/support.ts +186 -0
- package/src/{attach.js → attach.ts} +7 -4
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +818 -0
- package/src/command-center/server.ts +1426 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/{detect.js → detect.ts} +31 -15
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/{inspect.js → inspect.ts} +95 -10
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +222 -0
- package/src/launch.ts +494 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +417 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1792 -0
- package/src/lib/{output.js → output.ts} +18 -10
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/{session-options.js → session-options.ts} +11 -6
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/{sizes.js → sizes.ts} +4 -4
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
- package/src/ls.test.ts +76 -0
- package/src/{ls.js → ls.ts} +4 -4
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +176 -0
- package/src/stop.ts +47 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +192 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/src/config.js +0 -341
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/tmux.js +0 -237
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
package/src/init.test.ts
ADDED
|
@@ -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.
|
|
3
|
-
import { validateConfig } from "./validate.
|
|
4
|
-
import { outputError } from "./lib/output.
|
|
5
|
-
import { getSessionState, listPanes } from "./lib/tmux.
|
|
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
|
-
|
|
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(
|
|
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}`);
|