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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { detectStack, suggestConfig } from "./detect.ts";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
describe("suggestConfig", () => {
|
|
8
|
+
it("creates Next.js config with pnpm", () => {
|
|
9
|
+
const config = suggestConfig("/project", {
|
|
10
|
+
packageManager: "pnpm",
|
|
11
|
+
frameworks: ["next"],
|
|
12
|
+
devCommand: "pnpm dev",
|
|
13
|
+
language: "javascript",
|
|
14
|
+
});
|
|
15
|
+
expect(config.rows[0].size).toBe("70%");
|
|
16
|
+
expect(config.rows[0].panes.length).toBe(2);
|
|
17
|
+
const bottom = config.rows[1].panes;
|
|
18
|
+
expect(bottom[0].title).toBe("Next.js");
|
|
19
|
+
expect(bottom[0].command).toBe("pnpm dev");
|
|
20
|
+
expect(bottom[bottom.length - 1].title).toBe("Shell");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("creates Next.js + Convex config with 3 Claude panes", () => {
|
|
24
|
+
const config = suggestConfig("/project", {
|
|
25
|
+
packageManager: "pnpm",
|
|
26
|
+
frameworks: ["next", "convex"],
|
|
27
|
+
devCommand: "pnpm dev",
|
|
28
|
+
language: "javascript",
|
|
29
|
+
});
|
|
30
|
+
expect(config.rows[0].panes.length).toBe(3);
|
|
31
|
+
const bottom = config.rows[1].panes;
|
|
32
|
+
const titles = bottom.map((p) => p.title);
|
|
33
|
+
expect(titles.includes("Next.js")).toBeTruthy();
|
|
34
|
+
expect(titles.includes("Convex")).toBeTruthy();
|
|
35
|
+
expect(titles.includes("Shell")).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("creates Go config", () => {
|
|
39
|
+
const config = suggestConfig("/project", {
|
|
40
|
+
packageManager: null,
|
|
41
|
+
frameworks: ["go"],
|
|
42
|
+
devCommand: null,
|
|
43
|
+
language: "go",
|
|
44
|
+
});
|
|
45
|
+
const bottom = config.rows[1].panes;
|
|
46
|
+
expect(bottom[0].title).toBe("Go");
|
|
47
|
+
expect(bottom[0].command).toBe("go run .");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("creates Cargo config", () => {
|
|
51
|
+
const config = suggestConfig("/project", {
|
|
52
|
+
packageManager: null,
|
|
53
|
+
frameworks: ["cargo"],
|
|
54
|
+
devCommand: null,
|
|
55
|
+
language: "rust",
|
|
56
|
+
});
|
|
57
|
+
const bottom = config.rows[1].panes;
|
|
58
|
+
expect(bottom[0].title).toBe("Cargo");
|
|
59
|
+
expect(bottom[0].command).toBe("cargo watch -x run");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("falls back to dev command when no framework detected", () => {
|
|
63
|
+
const config = suggestConfig("/project", {
|
|
64
|
+
packageManager: "npm",
|
|
65
|
+
frameworks: [],
|
|
66
|
+
devCommand: "npm run dev",
|
|
67
|
+
language: "javascript",
|
|
68
|
+
});
|
|
69
|
+
const bottom = config.rows[1].panes;
|
|
70
|
+
expect(bottom[0].title).toBe("Dev Server");
|
|
71
|
+
expect(bottom[0].command).toBe("npm run dev");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("creates minimal config with just shell when nothing detected", () => {
|
|
75
|
+
const config = suggestConfig("/project", {
|
|
76
|
+
packageManager: null,
|
|
77
|
+
frameworks: [],
|
|
78
|
+
devCommand: null,
|
|
79
|
+
language: null,
|
|
80
|
+
});
|
|
81
|
+
const bottom = config.rows[1].panes;
|
|
82
|
+
expect(bottom.length).toBe(1);
|
|
83
|
+
expect(bottom[0].title).toBe("Shell");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("uses npm run for npm package manager", () => {
|
|
87
|
+
const config = suggestConfig("/project", {
|
|
88
|
+
packageManager: "npm",
|
|
89
|
+
frameworks: ["next"],
|
|
90
|
+
devCommand: "npm run dev",
|
|
91
|
+
language: "javascript",
|
|
92
|
+
});
|
|
93
|
+
const bottom = config.rows[1].panes;
|
|
94
|
+
expect(bottom[0].command).toBe("npm run dev");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses bun for bun package manager", () => {
|
|
98
|
+
const config = suggestConfig("/project", {
|
|
99
|
+
packageManager: "bun",
|
|
100
|
+
frameworks: ["vite"],
|
|
101
|
+
devCommand: "bun dev",
|
|
102
|
+
language: "javascript",
|
|
103
|
+
});
|
|
104
|
+
const bottom = config.rows[1].panes;
|
|
105
|
+
expect(bottom[0].command).toBe("bun dev");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("uses project directory basename as session name", () => {
|
|
109
|
+
const config = suggestConfig("/home/user/my-app", {
|
|
110
|
+
packageManager: null,
|
|
111
|
+
frameworks: [],
|
|
112
|
+
devCommand: null,
|
|
113
|
+
language: null,
|
|
114
|
+
});
|
|
115
|
+
expect(config.name).toBe("my-app");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("detectStack reasoning", () => {
|
|
120
|
+
it("includes reasons for detected frameworks and command choices", () => {
|
|
121
|
+
const dir = mkdtempSync(join(tmpdir(), "tmux-ide-detect-test-"));
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(dir, "package.json"),
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
dependencies: { next: "latest", convex: "latest" },
|
|
128
|
+
scripts: { dev: "next dev" },
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
writeFileSync(join(dir, "pnpm-lock.yaml"), "lockfileVersion: 9");
|
|
132
|
+
|
|
133
|
+
const detected = detectStack(dir);
|
|
134
|
+
expect(detected.packageManager).toBe("pnpm");
|
|
135
|
+
expect(detected.reasons.some((reason) => reason.includes("pnpm-lock.yaml"))).toBeTruthy();
|
|
136
|
+
expect(detected.reasons.some((reason) => reason.includes('dependency "next"'))).toBeTruthy();
|
|
137
|
+
expect(detected.reasons.some((reason) => reason.includes("dev command"))).toBeTruthy();
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { resolve, basename } from "node:path";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
-
import { writeConfig } from "./lib/yaml-io.
|
|
3
|
+
import { writeConfig } from "./lib/yaml-io.ts";
|
|
4
|
+
import type { IdeConfig } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
interface DetectedStack {
|
|
7
|
+
packageManager: string | null;
|
|
8
|
+
frameworks: string[];
|
|
9
|
+
devCommand: string | null;
|
|
10
|
+
language: string | null;
|
|
11
|
+
reasons: string[];
|
|
12
|
+
}
|
|
4
13
|
|
|
5
|
-
function fileExists(dir, name) {
|
|
14
|
+
function fileExists(dir: string, name: string): boolean {
|
|
6
15
|
return existsSync(resolve(dir, name));
|
|
7
16
|
}
|
|
8
17
|
|
|
9
|
-
function readJson(dir, name) {
|
|
18
|
+
function readJson(dir: string, name: string): Record<string, unknown> | null {
|
|
10
19
|
try {
|
|
11
|
-
return JSON.parse(readFileSync(resolve(dir, name), "utf-8"))
|
|
20
|
+
return JSON.parse(readFileSync(resolve(dir, name), "utf-8")) as Record<string, unknown>;
|
|
12
21
|
} catch {
|
|
13
22
|
return null;
|
|
14
23
|
}
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
export function detectStack(dir) {
|
|
18
|
-
const detected = {
|
|
26
|
+
export function detectStack(dir: string): DetectedStack {
|
|
27
|
+
const detected: DetectedStack = {
|
|
19
28
|
packageManager: null,
|
|
20
29
|
frameworks: [],
|
|
21
30
|
devCommand: null,
|
|
@@ -42,7 +51,10 @@ export function detectStack(dir) {
|
|
|
42
51
|
if (pkg) {
|
|
43
52
|
detected.language = "javascript";
|
|
44
53
|
detected.reasons.push('Detected JavaScript from "package.json".');
|
|
45
|
-
const deps = {
|
|
54
|
+
const deps = {
|
|
55
|
+
...(pkg.dependencies as Record<string, unknown> | undefined),
|
|
56
|
+
...(pkg.devDependencies as Record<string, unknown> | undefined),
|
|
57
|
+
};
|
|
46
58
|
|
|
47
59
|
if (deps["next"]) pushFramework(detected, "next", 'Found dependency "next".');
|
|
48
60
|
if (deps["convex"]) pushFramework(detected, "convex", 'Found dependency "convex".');
|
|
@@ -57,12 +69,13 @@ export function detectStack(dir) {
|
|
|
57
69
|
// Detect dev command
|
|
58
70
|
const pm = detected.packageManager ?? "npm";
|
|
59
71
|
const run = pm === "npm" ? "npm run" : pm;
|
|
60
|
-
|
|
72
|
+
const scripts = pkg.scripts as Record<string, unknown> | undefined;
|
|
73
|
+
if (scripts?.dev) {
|
|
61
74
|
detected.devCommand = `${run} dev`;
|
|
62
75
|
detected.reasons.push(
|
|
63
76
|
`Using dev command "${detected.devCommand}" from package.json scripts.`,
|
|
64
77
|
);
|
|
65
|
-
} else if (
|
|
78
|
+
} else if (scripts?.start) {
|
|
66
79
|
detected.devCommand = `${run} start`;
|
|
67
80
|
detected.reasons.push(
|
|
68
81
|
`Using start command "${detected.devCommand}" from package.json scripts.`,
|
|
@@ -117,13 +130,13 @@ export function detectStack(dir) {
|
|
|
117
130
|
return detected;
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
export function suggestConfig(dir, detected) {
|
|
133
|
+
export function suggestConfig(dir: string, detected: DetectedStack): IdeConfig {
|
|
121
134
|
const name = basename(dir);
|
|
122
135
|
const pm = detected.packageManager ?? "npm";
|
|
123
136
|
const run = pm === "npm" ? "npm run" : pm;
|
|
124
137
|
|
|
125
138
|
// Default: 2 claude panes + shell
|
|
126
|
-
const config = {
|
|
139
|
+
const config: IdeConfig = {
|
|
127
140
|
name,
|
|
128
141
|
rows: [
|
|
129
142
|
{
|
|
@@ -139,12 +152,12 @@ export function suggestConfig(dir, detected) {
|
|
|
139
152
|
],
|
|
140
153
|
};
|
|
141
154
|
|
|
142
|
-
const bottom = config.rows[1]
|
|
155
|
+
const bottom = config.rows[1]!.panes;
|
|
143
156
|
const frameworks = detected.frameworks;
|
|
144
157
|
|
|
145
158
|
// Add 3rd claude pane for complex stacks
|
|
146
159
|
if (frameworks.length >= 2) {
|
|
147
|
-
config.rows[0]
|
|
160
|
+
config.rows[0]!.panes.push({ title: "Claude 3", command: "claude" });
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
// Add dev servers
|
|
@@ -184,7 +197,10 @@ export function suggestConfig(dir, detected) {
|
|
|
184
197
|
return config;
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
export async function detect(
|
|
200
|
+
export async function detect(
|
|
201
|
+
targetDir: string | undefined,
|
|
202
|
+
{ json, write }: { json?: boolean; write?: boolean } = {},
|
|
203
|
+
): Promise<void> {
|
|
188
204
|
const dir = resolve(targetDir ?? ".");
|
|
189
205
|
const detected = detectStack(dir);
|
|
190
206
|
const suggested = suggestConfig(dir, detected);
|
|
@@ -224,7 +240,7 @@ export async function detect(targetDir, { json, write } = {}) {
|
|
|
224
240
|
console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
function pushFramework(detected, framework, reason) {
|
|
243
|
+
function pushFramework(detected: DetectedStack, framework: string, reason: string): void {
|
|
228
244
|
if (!detected.frameworks.includes(framework)) {
|
|
229
245
|
detected.frameworks.push(framework);
|
|
230
246
|
}
|
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { loadMission, loadGoal, loadTask } from "./lib/task-store.ts";
|
|
4
|
+
import { IdeError } from "./lib/errors.ts";
|
|
5
|
+
|
|
6
|
+
interface DispatchOptions {
|
|
7
|
+
taskId: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Print task dispatch context to stdout so agents can read it directly.
|
|
13
|
+
*
|
|
14
|
+
* 1. If a dispatch file exists (.tasks/dispatch/{id}.md), print it.
|
|
15
|
+
* 2. Otherwise, build the context from the task store (mission + goal + task).
|
|
16
|
+
* 3. In JSON mode, output a structured object.
|
|
17
|
+
* 4. Always end with the completion command hint.
|
|
18
|
+
*/
|
|
19
|
+
export async function dispatch(
|
|
20
|
+
targetDir: string | null | undefined,
|
|
21
|
+
opts: DispatchOptions,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const dir = resolve(targetDir ?? ".");
|
|
24
|
+
const { taskId, json } = opts;
|
|
25
|
+
|
|
26
|
+
if (!taskId) {
|
|
27
|
+
throw new IdeError("Missing task ID. Usage: tmux-ide dispatch <id>", {
|
|
28
|
+
code: "USAGE",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try dispatch file first
|
|
33
|
+
const dispatchFile = join(dir, ".tasks", "dispatch", `${taskId}.md`);
|
|
34
|
+
const hasDispatchFile = existsSync(dispatchFile);
|
|
35
|
+
|
|
36
|
+
// Always load the task for structured data
|
|
37
|
+
const task = loadTask(dir, taskId);
|
|
38
|
+
if (!task && !hasDispatchFile) {
|
|
39
|
+
throw new IdeError(`Task "${taskId}" not found and no dispatch file exists`, {
|
|
40
|
+
code: "TASK_NOT_FOUND",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load mission and goal context
|
|
45
|
+
const mission = loadMission(dir);
|
|
46
|
+
const goal = task?.goal ? loadGoal(dir, task.goal) : null;
|
|
47
|
+
|
|
48
|
+
if (json) {
|
|
49
|
+
const prompt = hasDispatchFile ? readFileSync(dispatchFile, "utf-8") : buildPrompt(dir, task!);
|
|
50
|
+
const result: Record<string, unknown> = {
|
|
51
|
+
task: task ?? null,
|
|
52
|
+
mission: mission ?? null,
|
|
53
|
+
goal: goal ?? null,
|
|
54
|
+
prompt,
|
|
55
|
+
};
|
|
56
|
+
console.log(JSON.stringify(result, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Text mode: print the readable prompt
|
|
61
|
+
if (hasDispatchFile) {
|
|
62
|
+
const content = readFileSync(dispatchFile, "utf-8");
|
|
63
|
+
console.log(content);
|
|
64
|
+
} else {
|
|
65
|
+
console.log(buildPrompt(dir, task!));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Always print the completion hint
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(`When done: tmux-ide task done ${taskId} --proof "what you did"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a human-readable prompt from the task store.
|
|
75
|
+
* Mirrors the logic of buildTaskPrompt() in orchestrator.ts but without
|
|
76
|
+
* requiring an orchestrator config.
|
|
77
|
+
*/
|
|
78
|
+
function buildPrompt(dir: string, task: NonNullable<ReturnType<typeof loadTask>>): string {
|
|
79
|
+
let prompt = "";
|
|
80
|
+
|
|
81
|
+
const mission = loadMission(dir);
|
|
82
|
+
if (mission) {
|
|
83
|
+
prompt += `Mission: ${mission.title}\n`;
|
|
84
|
+
if (mission.description) prompt += `${mission.description}\n`;
|
|
85
|
+
prompt += "\n";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (task.goal) {
|
|
89
|
+
const goal = loadGoal(dir, task.goal);
|
|
90
|
+
if (goal) {
|
|
91
|
+
prompt += `Goal: ${goal.title}\n`;
|
|
92
|
+
if (goal.acceptance) prompt += `Acceptance: ${goal.acceptance}\n`;
|
|
93
|
+
prompt += "\n";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
prompt += `Your Task: ${task.title}\n`;
|
|
98
|
+
if (task.description) prompt += `${task.description}\n`;
|
|
99
|
+
prompt += `\nPriority: P${task.priority}\n`;
|
|
100
|
+
if (task.tags?.length) prompt += `Tags: ${task.tags.join(", ")}\n`;
|
|
101
|
+
return prompt.trim();
|
|
102
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { doctor } from "./doctor.ts";
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
let origCwd: string;
|
|
9
|
+
let origLog: typeof console.log;
|
|
10
|
+
let logged: string[];
|
|
11
|
+
let origExitCode: number | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-doctor-test-"));
|
|
15
|
+
origCwd = process.cwd();
|
|
16
|
+
logged = [];
|
|
17
|
+
origLog = console.log;
|
|
18
|
+
origExitCode = process.exitCode;
|
|
19
|
+
console.log = (...a: string[]) => logged.push(a.join(" "));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.chdir(origCwd);
|
|
24
|
+
console.log = origLog;
|
|
25
|
+
process.exitCode = origExitCode;
|
|
26
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("doctor", () => {
|
|
30
|
+
it("reports tmux installed check", async () => {
|
|
31
|
+
process.chdir(tmpDir);
|
|
32
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
33
|
+
await doctor({ json: true });
|
|
34
|
+
const output = JSON.parse(logged[0]);
|
|
35
|
+
const tmuxCheck = output.checks.find((c) => c.label === "tmux installed");
|
|
36
|
+
expect(tmuxCheck).toBeTruthy();
|
|
37
|
+
// tmux is installed in this CI/test environment, so it should pass
|
|
38
|
+
expect(tmuxCheck.pass).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reports tmux version check", async () => {
|
|
42
|
+
process.chdir(tmpDir);
|
|
43
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
44
|
+
await doctor({ json: true });
|
|
45
|
+
const output = JSON.parse(logged[0]);
|
|
46
|
+
const versionCheck = output.checks.find((c) => c.label.includes("tmux version"));
|
|
47
|
+
expect(versionCheck).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reports Node.js version check as passing", async () => {
|
|
51
|
+
process.chdir(tmpDir);
|
|
52
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
53
|
+
await doctor({ json: true });
|
|
54
|
+
const output = JSON.parse(logged[0]);
|
|
55
|
+
const nodeCheck = output.checks.find((c) => c.label.includes("Node.js"));
|
|
56
|
+
expect(nodeCheck).toBeTruthy();
|
|
57
|
+
expect(nodeCheck.pass).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("reports ide.yml exists check as passing when present", async () => {
|
|
61
|
+
process.chdir(tmpDir);
|
|
62
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
63
|
+
await doctor({ json: true });
|
|
64
|
+
const output = JSON.parse(logged[0]);
|
|
65
|
+
const ideCheck = output.checks.find((c) => c.label.includes("ide.yml"));
|
|
66
|
+
expect(ideCheck).toBeTruthy();
|
|
67
|
+
expect(ideCheck.pass).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("reports ide.yml exists check as failing when absent", async () => {
|
|
71
|
+
process.chdir(tmpDir);
|
|
72
|
+
// No ide.yml written
|
|
73
|
+
await doctor({ json: true });
|
|
74
|
+
const output = JSON.parse(logged[0]);
|
|
75
|
+
const ideCheck = output.checks.find((c) => c.label.includes("ide.yml"));
|
|
76
|
+
expect(ideCheck).toBeTruthy();
|
|
77
|
+
expect(ideCheck.pass).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("marks agent teams check as optional", async () => {
|
|
81
|
+
process.chdir(tmpDir);
|
|
82
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
83
|
+
await doctor({ json: true });
|
|
84
|
+
const output = JSON.parse(logged[0]);
|
|
85
|
+
const teamsCheck = output.checks.find((c) => c.label.includes("agent teams"));
|
|
86
|
+
expect(teamsCheck).toBeTruthy();
|
|
87
|
+
expect(teamsCheck.optional).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("optional checks don't fail the overall result", async () => {
|
|
91
|
+
process.chdir(tmpDir);
|
|
92
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
93
|
+
// Even if agent teams env var is not set, ok should still be true
|
|
94
|
+
const origEnv = process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
95
|
+
delete process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
96
|
+
try {
|
|
97
|
+
await doctor({ json: true });
|
|
98
|
+
const output = JSON.parse(logged[0]);
|
|
99
|
+
// ok depends on required checks, not optional ones
|
|
100
|
+
const requiredFailing = output.checks.filter((c) => !c.pass && !c.optional);
|
|
101
|
+
if (requiredFailing.length === 0) {
|
|
102
|
+
expect(output.ok).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
if (origEnv !== undefined) {
|
|
106
|
+
process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = origEnv;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("prints human-readable output without --json", async () => {
|
|
112
|
+
process.chdir(tmpDir);
|
|
113
|
+
writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
|
|
114
|
+
await doctor();
|
|
115
|
+
// Should have printed colored output with ✓ or ✗
|
|
116
|
+
expect(logged.length > 0).toBeTruthy();
|
|
117
|
+
expect(logged.some((l) => l.includes("tmux installed"))).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { readConfig } from "./lib/yaml-io.ts";
|
|
5
|
+
import { loadSkills } from "./lib/skill-registry.ts";
|
|
6
|
+
|
|
7
|
+
interface CheckResult {
|
|
8
|
+
label: string;
|
|
9
|
+
pass: boolean;
|
|
10
|
+
detail: string;
|
|
11
|
+
optional: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function check(
|
|
15
|
+
label: string,
|
|
16
|
+
fn: () => string,
|
|
17
|
+
{ optional = false }: { optional?: boolean } = {},
|
|
18
|
+
): CheckResult {
|
|
19
|
+
try {
|
|
20
|
+
const result = fn();
|
|
21
|
+
return { label, pass: true, detail: result, optional };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { label, pass: false, detail: (e as Error).message, optional };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function doctor({ json }: { json?: boolean } = {}): Promise<void> {
|
|
28
|
+
const checks: CheckResult[] = [];
|
|
29
|
+
|
|
30
|
+
checks.push(
|
|
31
|
+
check("tmux installed", () => {
|
|
32
|
+
execSync("which tmux", { stdio: "ignore" });
|
|
33
|
+
return "found";
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
checks.push(
|
|
38
|
+
check("tmux version ≥ 3.0", () => {
|
|
39
|
+
const version = execSync("tmux -V", { encoding: "utf-8" }).trim();
|
|
40
|
+
const num = parseFloat(version.replace(/[^0-9.]/g, ""));
|
|
41
|
+
if (num < 3.0) throw new Error(`${version} (need ≥ 3.0)`);
|
|
42
|
+
return version;
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
checks.push(
|
|
47
|
+
check("Node.js ≥ 18", () => {
|
|
48
|
+
const major = parseInt(process.versions.node.split(".")[0]!);
|
|
49
|
+
if (major < 18) throw new Error(`Node ${process.versions.node} (need ≥ 18)`);
|
|
50
|
+
return `v${process.versions.node}`;
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
checks.push(
|
|
55
|
+
check(
|
|
56
|
+
"256-color terminal",
|
|
57
|
+
() => {
|
|
58
|
+
const term = process.env.TERM ?? "";
|
|
59
|
+
if (
|
|
60
|
+
!term.includes("256color") &&
|
|
61
|
+
!term.includes("ghostty") &&
|
|
62
|
+
!term.includes("kitty") &&
|
|
63
|
+
term !== "tmux-256color"
|
|
64
|
+
) {
|
|
65
|
+
throw new Error(`$TERM is "${term}"`);
|
|
66
|
+
}
|
|
67
|
+
return term;
|
|
68
|
+
},
|
|
69
|
+
{ optional: true },
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
checks.push(
|
|
74
|
+
check("ide.yml exists", () => {
|
|
75
|
+
const path = resolve(".", "ide.yml");
|
|
76
|
+
if (!existsSync(path)) throw new Error("not found in current directory");
|
|
77
|
+
return "found";
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
checks.push(
|
|
82
|
+
check(
|
|
83
|
+
"Claude Code agent teams",
|
|
84
|
+
() => {
|
|
85
|
+
if (process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
86
|
+
throw new Error("not set (enable with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)");
|
|
87
|
+
}
|
|
88
|
+
return "enabled";
|
|
89
|
+
},
|
|
90
|
+
{ optional: true },
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
checks.push(
|
|
95
|
+
check(
|
|
96
|
+
"pane skill references",
|
|
97
|
+
() => {
|
|
98
|
+
const dir = resolve(".");
|
|
99
|
+
const { config } = readConfig(dir);
|
|
100
|
+
const skills = loadSkills(dir);
|
|
101
|
+
const skillNames = new Set(skills.map((s) => s.name));
|
|
102
|
+
const unresolved: string[] = [];
|
|
103
|
+
for (const row of config.rows) {
|
|
104
|
+
for (const pane of row.panes) {
|
|
105
|
+
if (pane.skill && !skillNames.has(pane.skill)) {
|
|
106
|
+
unresolved.push(`${pane.title ?? "untitled"} → ${pane.skill}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (unresolved.length > 0) {
|
|
111
|
+
throw new Error(`${unresolved.length} unresolved: ${unresolved.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
const count = config.rows.flatMap((r) => r.panes).filter((p) => p.skill).length;
|
|
114
|
+
return count > 0 ? `${count} reference(s), all resolved` : "no skill references";
|
|
115
|
+
},
|
|
116
|
+
{ optional: true },
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
checks.push(
|
|
121
|
+
check(
|
|
122
|
+
"tailscale CLI",
|
|
123
|
+
() => {
|
|
124
|
+
const version = execSync("tailscale version", { encoding: "utf-8" }).trim().split("\n")[0]!;
|
|
125
|
+
return version;
|
|
126
|
+
},
|
|
127
|
+
{ optional: true },
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
checks.push(
|
|
132
|
+
check(
|
|
133
|
+
"ngrok CLI",
|
|
134
|
+
() => {
|
|
135
|
+
const version = execSync("ngrok version", { encoding: "utf-8" }).trim();
|
|
136
|
+
return version;
|
|
137
|
+
},
|
|
138
|
+
{ optional: true },
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
checks.push(
|
|
143
|
+
check(
|
|
144
|
+
"cloudflared CLI",
|
|
145
|
+
() => {
|
|
146
|
+
const version = execSync("cloudflared --version", { encoding: "utf-8" }).trim();
|
|
147
|
+
return version;
|
|
148
|
+
},
|
|
149
|
+
{ optional: true },
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const allPass = checks.every((c) => c.pass || c.optional);
|
|
154
|
+
|
|
155
|
+
if (json) {
|
|
156
|
+
console.log(JSON.stringify({ ok: allPass, checks }, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const c of checks) {
|
|
161
|
+
const icon = c.pass ? "✓" : c.optional ? "○" : "✗";
|
|
162
|
+
const color = c.pass ? "\x1b[32m" : c.optional ? "\x1b[33m" : "\x1b[31m";
|
|
163
|
+
console.log(`${color}${icon}\x1b[0m ${c.label} — ${c.detail}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!allPass) process.exitCode = 1;
|
|
167
|
+
}
|