tmux-ide 1.3.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 +27 -14
- package/src/__tests__/support.ts +186 -0
- package/src/attach.ts +20 -0
- 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.ts +248 -0
- 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.ts +212 -0
- 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.ts +76 -0
- 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.ts +100 -0
- 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.ts +36 -0
- 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.ts +27 -0
- package/src/ls.test.ts +76 -0
- package/src/ls.ts +40 -0
- 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/dist/attach.d.ts +0 -3
- package/dist/attach.js +0 -14
- package/dist/config.d.ts +0 -5
- package/dist/config.js +0 -269
- package/dist/detect.d.ts +0 -15
- package/dist/detect.js +0 -228
- package/dist/doctor.d.ts +0 -3
- package/dist/doctor.js +0 -66
- package/dist/init.d.ts +0 -4
- package/dist/init.js +0 -67
- package/dist/inspect.d.ts +0 -54
- package/dist/inspect.js +0 -118
- package/dist/launch.d.ts +0 -22
- package/dist/launch.js +0 -174
- package/dist/lib/dot-path.d.ts +0 -2
- package/dist/lib/dot-path.js +0 -17
- package/dist/lib/errors.d.ts +0 -29
- package/dist/lib/errors.js +0 -37
- package/dist/lib/launch-plan.d.ts +0 -6
- package/dist/lib/launch-plan.js +0 -40
- package/dist/lib/output.d.ts +0 -9
- package/dist/lib/output.js +0 -65
- package/dist/lib/session-monitor.d.ts +0 -12
- package/dist/lib/session-monitor.js +0 -160
- package/dist/lib/session-options.d.ts +0 -13
- package/dist/lib/session-options.js +0 -85
- package/dist/lib/sizes.d.ts +0 -16
- package/dist/lib/sizes.js +0 -36
- package/dist/lib/tmux.d.ts +0 -42
- package/dist/lib/tmux.js +0 -225
- package/dist/lib/yaml-io.d.ts +0 -10
- package/dist/lib/yaml-io.js +0 -24
- package/dist/ls.d.ts +0 -3
- package/dist/ls.js +0 -35
- package/dist/restart.d.ts +0 -4
- package/dist/restart.js +0 -13
- package/dist/status.d.ts +0 -3
- package/dist/status.js +0 -29
- package/dist/stop.d.ts +0 -3
- package/dist/stop.js +0 -21
- package/dist/types.d.ts +0 -41
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +0 -4
- package/dist/validate.js +0 -172
|
@@ -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
|
+
});
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
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
|
+
}
|
|
13
|
+
|
|
14
|
+
function fileExists(dir: string, name: string): boolean {
|
|
15
|
+
return existsSync(resolve(dir, name));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJson(dir: string, name: string): Record<string, unknown> | null {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(resolve(dir, name), "utf-8")) as Record<string, unknown>;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function detectStack(dir: string): DetectedStack {
|
|
27
|
+
const detected: DetectedStack = {
|
|
28
|
+
packageManager: null,
|
|
29
|
+
frameworks: [],
|
|
30
|
+
devCommand: null,
|
|
31
|
+
language: null,
|
|
32
|
+
reasons: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Detect package manager from lockfile
|
|
36
|
+
if (fileExists(dir, "pnpm-lock.yaml")) {
|
|
37
|
+
detected.packageManager = "pnpm";
|
|
38
|
+
detected.reasons.push('Detected pnpm from "pnpm-lock.yaml".');
|
|
39
|
+
} else if (fileExists(dir, "bun.lockb") || fileExists(dir, "bun.lock")) {
|
|
40
|
+
detected.packageManager = "bun";
|
|
41
|
+
detected.reasons.push('Detected bun from "bun.lockb" or "bun.lock".');
|
|
42
|
+
} else if (fileExists(dir, "yarn.lock")) {
|
|
43
|
+
detected.packageManager = "yarn";
|
|
44
|
+
detected.reasons.push('Detected yarn from "yarn.lock".');
|
|
45
|
+
} else if (fileExists(dir, "package-lock.json")) {
|
|
46
|
+
detected.packageManager = "npm";
|
|
47
|
+
detected.reasons.push('Detected npm from "package-lock.json".');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pkg = readJson(dir, "package.json");
|
|
51
|
+
if (pkg) {
|
|
52
|
+
detected.language = "javascript";
|
|
53
|
+
detected.reasons.push('Detected JavaScript from "package.json".');
|
|
54
|
+
const deps = {
|
|
55
|
+
...(pkg.dependencies as Record<string, unknown> | undefined),
|
|
56
|
+
...(pkg.devDependencies as Record<string, unknown> | undefined),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (deps["next"]) pushFramework(detected, "next", 'Found dependency "next".');
|
|
60
|
+
if (deps["convex"]) pushFramework(detected, "convex", 'Found dependency "convex".');
|
|
61
|
+
if (deps["vite"]) pushFramework(detected, "vite", 'Found dependency "vite".');
|
|
62
|
+
if (deps["remix"] || deps["@remix-run/node"])
|
|
63
|
+
pushFramework(detected, "remix", "Found Remix dependency.");
|
|
64
|
+
if (deps["nuxt"]) pushFramework(detected, "nuxt", 'Found dependency "nuxt".');
|
|
65
|
+
if (deps["astro"]) pushFramework(detected, "astro", 'Found dependency "astro".');
|
|
66
|
+
if (deps["svelte"] || deps["@sveltejs/kit"])
|
|
67
|
+
pushFramework(detected, "svelte", "Found Svelte dependency.");
|
|
68
|
+
|
|
69
|
+
// Detect dev command
|
|
70
|
+
const pm = detected.packageManager ?? "npm";
|
|
71
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
72
|
+
const scripts = pkg.scripts as Record<string, unknown> | undefined;
|
|
73
|
+
if (scripts?.dev) {
|
|
74
|
+
detected.devCommand = `${run} dev`;
|
|
75
|
+
detected.reasons.push(
|
|
76
|
+
`Using dev command "${detected.devCommand}" from package.json scripts.`,
|
|
77
|
+
);
|
|
78
|
+
} else if (scripts?.start) {
|
|
79
|
+
detected.devCommand = `${run} start`;
|
|
80
|
+
detected.reasons.push(
|
|
81
|
+
`Using start command "${detected.devCommand}" from package.json scripts.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Python
|
|
87
|
+
if (fileExists(dir, "pyproject.toml") || fileExists(dir, "requirements.txt")) {
|
|
88
|
+
detected.language = detected.language ?? "python";
|
|
89
|
+
detected.reasons.push('Detected Python from "pyproject.toml" or "requirements.txt".');
|
|
90
|
+
try {
|
|
91
|
+
const pyproject = readFileSync(resolve(dir, "pyproject.toml"), "utf-8");
|
|
92
|
+
if (pyproject.includes("fastapi"))
|
|
93
|
+
pushFramework(detected, "fastapi", 'Found "fastapi" in pyproject.toml.');
|
|
94
|
+
else if (pyproject.includes("django"))
|
|
95
|
+
pushFramework(detected, "django", 'Found "django" in pyproject.toml.');
|
|
96
|
+
else if (pyproject.includes("flask"))
|
|
97
|
+
pushFramework(detected, "flask", 'Found "flask" in pyproject.toml.');
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore missing or unreadable pyproject metadata.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rust
|
|
104
|
+
if (fileExists(dir, "Cargo.toml")) {
|
|
105
|
+
detected.language = detected.language ?? "rust";
|
|
106
|
+
detected.reasons.push('Detected Rust from "Cargo.toml".');
|
|
107
|
+
pushFramework(detected, "cargo", 'Using Cargo workflow from "Cargo.toml".');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Go
|
|
111
|
+
if (fileExists(dir, "go.mod")) {
|
|
112
|
+
detected.language = detected.language ?? "go";
|
|
113
|
+
detected.reasons.push('Detected Go from "go.mod".');
|
|
114
|
+
pushFramework(detected, "go", 'Using Go workflow from "go.mod".');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Docker
|
|
118
|
+
if (fileExists(dir, "docker-compose.yml") || fileExists(dir, "docker-compose.yaml")) {
|
|
119
|
+
pushFramework(
|
|
120
|
+
detected,
|
|
121
|
+
"docker",
|
|
122
|
+
'Detected Docker from "docker-compose.yml" or "docker-compose.yaml".',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (detected.reasons.length === 0) {
|
|
127
|
+
detected.reasons.push("No framework-specific signals found; using the generic layout.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return detected;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function suggestConfig(dir: string, detected: DetectedStack): IdeConfig {
|
|
134
|
+
const name = basename(dir);
|
|
135
|
+
const pm = detected.packageManager ?? "npm";
|
|
136
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
137
|
+
|
|
138
|
+
// Default: 2 claude panes + shell
|
|
139
|
+
const config: IdeConfig = {
|
|
140
|
+
name,
|
|
141
|
+
rows: [
|
|
142
|
+
{
|
|
143
|
+
size: "70%",
|
|
144
|
+
panes: [
|
|
145
|
+
{ title: "Claude 1", command: "claude" },
|
|
146
|
+
{ title: "Claude 2", command: "claude" },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
panes: [],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const bottom = config.rows[1]!.panes;
|
|
156
|
+
const frameworks = detected.frameworks;
|
|
157
|
+
|
|
158
|
+
// Add 3rd claude pane for complex stacks
|
|
159
|
+
if (frameworks.length >= 2) {
|
|
160
|
+
config.rows[0]!.panes.push({ title: "Claude 3", command: "claude" });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add dev servers
|
|
164
|
+
if (frameworks.includes("next")) {
|
|
165
|
+
bottom.push({ title: "Next.js", command: `${run} dev` });
|
|
166
|
+
} else if (frameworks.includes("vite")) {
|
|
167
|
+
bottom.push({ title: "Vite", command: `${run} dev` });
|
|
168
|
+
} else if (frameworks.includes("nuxt")) {
|
|
169
|
+
bottom.push({ title: "Nuxt", command: `${run} dev` });
|
|
170
|
+
} else if (frameworks.includes("remix")) {
|
|
171
|
+
bottom.push({ title: "Remix", command: `${run} dev` });
|
|
172
|
+
} else if (frameworks.includes("astro")) {
|
|
173
|
+
bottom.push({ title: "Astro", command: `${run} dev` });
|
|
174
|
+
} else if (frameworks.includes("svelte")) {
|
|
175
|
+
bottom.push({ title: "SvelteKit", command: `${run} dev` });
|
|
176
|
+
} else if (frameworks.includes("fastapi")) {
|
|
177
|
+
bottom.push({ title: "FastAPI", command: "uvicorn main:app --reload" });
|
|
178
|
+
} else if (frameworks.includes("django")) {
|
|
179
|
+
bottom.push({ title: "Django", command: "python manage.py runserver" });
|
|
180
|
+
} else if (frameworks.includes("flask")) {
|
|
181
|
+
bottom.push({ title: "Flask", command: "flask run --reload" });
|
|
182
|
+
} else if (frameworks.includes("cargo")) {
|
|
183
|
+
bottom.push({ title: "Cargo", command: "cargo watch -x run" });
|
|
184
|
+
} else if (frameworks.includes("go")) {
|
|
185
|
+
bottom.push({ title: "Go", command: "go run ." });
|
|
186
|
+
} else if (detected.devCommand) {
|
|
187
|
+
bottom.push({ title: "Dev Server", command: detected.devCommand });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (frameworks.includes("convex")) {
|
|
191
|
+
bottom.push({ title: "Convex", command: "npx convex dev" });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Always add shell
|
|
195
|
+
bottom.push({ title: "Shell" });
|
|
196
|
+
|
|
197
|
+
return config;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function detect(
|
|
201
|
+
targetDir: string | undefined,
|
|
202
|
+
{ json, write }: { json?: boolean; write?: boolean } = {},
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const dir = resolve(targetDir ?? ".");
|
|
205
|
+
const detected = detectStack(dir);
|
|
206
|
+
const suggested = suggestConfig(dir, detected);
|
|
207
|
+
|
|
208
|
+
if (write) {
|
|
209
|
+
writeConfig(dir, suggested);
|
|
210
|
+
if (json) {
|
|
211
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested, written: true }, null, 2));
|
|
212
|
+
} else {
|
|
213
|
+
const desc =
|
|
214
|
+
detected.frameworks.length > 0
|
|
215
|
+
? detected.frameworks.join(" + ")
|
|
216
|
+
: (detected.language ?? "generic project");
|
|
217
|
+
console.log(`Detected ${desc}. Created ide.yml.`);
|
|
218
|
+
console.log("\nWhy this layout:");
|
|
219
|
+
for (const reason of detected.reasons) {
|
|
220
|
+
console.log(` - ${reason}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (json) {
|
|
227
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested }, null, 2));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log("Detected stack:");
|
|
232
|
+
if (detected.packageManager) console.log(` Package manager: ${detected.packageManager}`);
|
|
233
|
+
if (detected.language) console.log(` Language: ${detected.language}`);
|
|
234
|
+
if (detected.frameworks.length) console.log(` Frameworks: ${detected.frameworks.join(", ")}`);
|
|
235
|
+
if (detected.devCommand) console.log(` Dev command: ${detected.devCommand}`);
|
|
236
|
+
console.log("\nReasoning:");
|
|
237
|
+
for (const reason of detected.reasons) {
|
|
238
|
+
console.log(` - ${reason}`);
|
|
239
|
+
}
|
|
240
|
+
console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function pushFramework(detected: DetectedStack, framework: string, reason: string): void {
|
|
244
|
+
if (!detected.frameworks.includes(framework)) {
|
|
245
|
+
detected.frameworks.push(framework);
|
|
246
|
+
}
|
|
247
|
+
detected.reasons.push(reason);
|
|
248
|
+
}
|
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
|
+
});
|