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/plan.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { outputError } from "./lib/output.ts";
|
|
3
|
+
import { listPlans, markPlanDone, getPlan, type PlanStatus } from "./lib/plan-store.ts";
|
|
4
|
+
|
|
5
|
+
const STATUS_ICONS: Record<PlanStatus, string> = {
|
|
6
|
+
pending: "○",
|
|
7
|
+
"in-progress": "●",
|
|
8
|
+
done: "✓",
|
|
9
|
+
archived: "▪",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const STATUS_COLORS: Record<PlanStatus, string> = {
|
|
13
|
+
pending: "\x1b[2m", // dim
|
|
14
|
+
"in-progress": "\x1b[33m", // yellow
|
|
15
|
+
done: "\x1b[32m", // green
|
|
16
|
+
archived: "\x1b[2m", // dim
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const RESET = "\x1b[0m";
|
|
20
|
+
|
|
21
|
+
export async function planCommand(
|
|
22
|
+
targetDir: string | undefined,
|
|
23
|
+
{
|
|
24
|
+
json = false,
|
|
25
|
+
sub,
|
|
26
|
+
args = [],
|
|
27
|
+
values = {},
|
|
28
|
+
}: {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
sub?: string;
|
|
31
|
+
args?: string[];
|
|
32
|
+
values?: { status?: string };
|
|
33
|
+
},
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const dir = resolve(targetDir ?? ".");
|
|
36
|
+
|
|
37
|
+
switch (sub) {
|
|
38
|
+
case "list":
|
|
39
|
+
case undefined: {
|
|
40
|
+
const statusFilter = values.status as PlanStatus | undefined;
|
|
41
|
+
const plans = listPlans(dir, statusFilter ? { status: statusFilter } : undefined);
|
|
42
|
+
|
|
43
|
+
if (json) {
|
|
44
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (plans.length === 0) {
|
|
49
|
+
console.log("No plans found.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Group by status
|
|
54
|
+
const groups: Record<string, typeof plans> = {};
|
|
55
|
+
for (const p of plans) {
|
|
56
|
+
(groups[p.status] ??= []).push(p);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const status of ["in-progress", "pending", "done", "archived"] as PlanStatus[]) {
|
|
60
|
+
const group = groups[status];
|
|
61
|
+
if (!group?.length) continue;
|
|
62
|
+
const color = STATUS_COLORS[status];
|
|
63
|
+
console.log(`\n${color}${status.toUpperCase()}${RESET}`);
|
|
64
|
+
for (const p of group) {
|
|
65
|
+
const icon = STATUS_ICONS[p.status];
|
|
66
|
+
const completed = p.completed ? ` (${p.completed})` : "";
|
|
67
|
+
console.log(` ${color}${icon}${RESET} ${p.name} ${p.title}${completed}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.log();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "show": {
|
|
75
|
+
const name = args[0];
|
|
76
|
+
if (!name) outputError("Usage: tmux-ide plan show <name|number>", "USAGE");
|
|
77
|
+
const plan = getPlan(dir, name);
|
|
78
|
+
if (!plan) outputError(`Plan not found: ${name}`, "NOT_FOUND");
|
|
79
|
+
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const icon = STATUS_ICONS[plan.status];
|
|
86
|
+
const color = STATUS_COLORS[plan.status];
|
|
87
|
+
console.log(`${color}${icon} ${plan.title}${RESET}`);
|
|
88
|
+
console.log(` Status: ${plan.status}`);
|
|
89
|
+
if (plan.effort) console.log(` Effort: ${plan.effort}`);
|
|
90
|
+
if (plan.gate) console.log(` Gate: ${plan.gate}`);
|
|
91
|
+
if (plan.completed) console.log(` Completed: ${plan.completed}`);
|
|
92
|
+
console.log(` File: ${plan.path}`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "done": {
|
|
97
|
+
const name = args[0];
|
|
98
|
+
if (!name) outputError("Usage: tmux-ide plan done <name|number>", "USAGE");
|
|
99
|
+
const result = markPlanDone(dir, name);
|
|
100
|
+
if (!result) outputError(`Plan not found: ${name}`, "NOT_FOUND");
|
|
101
|
+
|
|
102
|
+
if (json) {
|
|
103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`✓ Plan marked done: ${result.title} (${result.completed})`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
outputError(
|
|
113
|
+
`Unknown plan subcommand: ${sub}\nUsage: tmux-ide plan [list|show|done] [--status pending|in-progress|done]`,
|
|
114
|
+
"USAGE",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, 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 script = join(__dirname, "..", "scripts", "postinstall.js");
|
|
10
|
+
|
|
11
|
+
function makeHome() {
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), "tmux-ide-postinstall-test-"));
|
|
13
|
+
const home = join(root, "home");
|
|
14
|
+
mkdirSync(home, { recursive: true });
|
|
15
|
+
return { root, home };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function runPostinstall({ home, env } = {}) {
|
|
19
|
+
return spawnSync("node", [script], {
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
HOME: home,
|
|
23
|
+
...env,
|
|
24
|
+
},
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("postinstall", () => {
|
|
30
|
+
it("does not create Claude config for non-global installs", () => {
|
|
31
|
+
const { root, home } = makeHome();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = runPostinstall({ home, env: { npm_config_global: undefined } });
|
|
35
|
+
|
|
36
|
+
expect(result.status).toBe(0);
|
|
37
|
+
expect(existsSync(join(home, ".claude"))).toBe(false);
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(root, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not create Claude config when Claude is not installed", () => {
|
|
44
|
+
const { root, home } = makeHome();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = runPostinstall({ home, env: { npm_config_global: "true" } });
|
|
48
|
+
|
|
49
|
+
expect(result.status).toBe(0);
|
|
50
|
+
expect(existsSync(join(home, ".claude"))).toBe(false);
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("updates existing Claude settings for global installs", () => {
|
|
57
|
+
const { root, home } = makeHome();
|
|
58
|
+
const claudeDir = join(home, ".claude");
|
|
59
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
60
|
+
|
|
61
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
62
|
+
writeFileSync(settingsPath, `${JSON.stringify({ env: { KEEP_ME: "1" } }, null, 2)}\n`);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = runPostinstall({ home, env: { npm_config_global: "true" } });
|
|
66
|
+
|
|
67
|
+
expect(result.status).toBe(0);
|
|
68
|
+
expect(existsSync(join(claudeDir, "skills", "tmux-ide", "SKILL.md"))).toBe(true);
|
|
69
|
+
|
|
70
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
71
|
+
expect(settings.env).toEqual({
|
|
72
|
+
KEEP_ME: "1",
|
|
73
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { HQConfigSchema, RegistrationPayloadSchema } from "./lib/hq/types.ts";
|
|
3
|
+
|
|
4
|
+
describe("remote CLI contracts", () => {
|
|
5
|
+
describe("HQConfigSchema", () => {
|
|
6
|
+
it("accepts valid HQ config", () => {
|
|
7
|
+
const result = HQConfigSchema.safeParse({
|
|
8
|
+
enabled: true,
|
|
9
|
+
role: "hq",
|
|
10
|
+
});
|
|
11
|
+
expect(result.success).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts valid remote config", () => {
|
|
15
|
+
const result = HQConfigSchema.safeParse({
|
|
16
|
+
enabled: true,
|
|
17
|
+
role: "remote",
|
|
18
|
+
hq_url: "https://hq.example.com",
|
|
19
|
+
secret: "s3cr3t",
|
|
20
|
+
heartbeat_interval: 10000,
|
|
21
|
+
machine_name: "dev-1",
|
|
22
|
+
});
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects invalid role", () => {
|
|
27
|
+
const result = HQConfigSchema.safeParse({
|
|
28
|
+
enabled: true,
|
|
29
|
+
role: "invalid",
|
|
30
|
+
});
|
|
31
|
+
expect(result.success).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rejects heartbeat_interval below 1000", () => {
|
|
35
|
+
const result = HQConfigSchema.safeParse({
|
|
36
|
+
enabled: true,
|
|
37
|
+
role: "hq",
|
|
38
|
+
heartbeat_interval: 500,
|
|
39
|
+
});
|
|
40
|
+
expect(result.success).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("defaults heartbeat_interval to 15000", () => {
|
|
44
|
+
const result = HQConfigSchema.parse({
|
|
45
|
+
enabled: true,
|
|
46
|
+
role: "hq",
|
|
47
|
+
});
|
|
48
|
+
expect(result.heartbeat_interval).toBe(15000);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("defaults enabled to false", () => {
|
|
52
|
+
const result = HQConfigSchema.parse({
|
|
53
|
+
role: "remote",
|
|
54
|
+
});
|
|
55
|
+
expect(result.enabled).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("RegistrationPayloadSchema", () => {
|
|
60
|
+
it("accepts valid registration payload", () => {
|
|
61
|
+
const result = RegistrationPayloadSchema.safeParse({
|
|
62
|
+
id: "abc-123",
|
|
63
|
+
name: "dev-box",
|
|
64
|
+
url: "https://dev.example.com",
|
|
65
|
+
token: "tok-xyz",
|
|
66
|
+
});
|
|
67
|
+
expect(result.success).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects empty id", () => {
|
|
71
|
+
const result = RegistrationPayloadSchema.safeParse({
|
|
72
|
+
id: "",
|
|
73
|
+
name: "dev",
|
|
74
|
+
url: "https://dev.example.com",
|
|
75
|
+
token: "tok",
|
|
76
|
+
});
|
|
77
|
+
expect(result.success).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects invalid URL", () => {
|
|
81
|
+
const result = RegistrationPayloadSchema.safeParse({
|
|
82
|
+
id: "m1",
|
|
83
|
+
name: "dev",
|
|
84
|
+
url: "not-a-url",
|
|
85
|
+
token: "tok",
|
|
86
|
+
});
|
|
87
|
+
expect(result.success).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("IdeConfigSchema with hq field", () => {
|
|
92
|
+
it("accepts hq config in ide.yml schema", async () => {
|
|
93
|
+
const { IdeConfigSchema } = await import("./schemas/ide-config.ts");
|
|
94
|
+
const result = IdeConfigSchema.safeParse({
|
|
95
|
+
rows: [{ panes: [{ title: "Shell" }] }],
|
|
96
|
+
hq: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
role: "remote",
|
|
99
|
+
hq_url: "https://hq.example.com",
|
|
100
|
+
secret: "mykey",
|
|
101
|
+
machine_name: "laptop",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(result.success).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("allows omitting hq", async () => {
|
|
108
|
+
const { IdeConfigSchema } = await import("./schemas/ide-config.ts");
|
|
109
|
+
const result = IdeConfigSchema.safeParse({
|
|
110
|
+
rows: [{ panes: [{ title: "Shell" }] }],
|
|
111
|
+
});
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { IdeError } from "./lib/errors.ts";
|
|
3
|
+
import { HQClient } from "./lib/hq/client.ts";
|
|
4
|
+
import { HQConfigSchema } from "./lib/hq/types.ts";
|
|
5
|
+
|
|
6
|
+
interface RemoteOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
sub?: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
values?: Record<string, string | boolean | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function loadHQConfig(dir: string) {
|
|
14
|
+
try {
|
|
15
|
+
const { readConfig } = await import("./lib/yaml-io.ts");
|
|
16
|
+
const { config } = readConfig(dir);
|
|
17
|
+
if (config.hq) {
|
|
18
|
+
return HQConfigSchema.parse(config.hq);
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// config not readable
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function remoteCommand(
|
|
27
|
+
targetDir: string | null | undefined,
|
|
28
|
+
opts: RemoteOptions,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const dir = resolve(targetDir ?? ".");
|
|
31
|
+
const { json, sub } = opts;
|
|
32
|
+
|
|
33
|
+
switch (sub) {
|
|
34
|
+
case "register": {
|
|
35
|
+
const hqConfig = await loadHQConfig(dir);
|
|
36
|
+
if (!hqConfig || hqConfig.role !== "remote") {
|
|
37
|
+
throw new IdeError(
|
|
38
|
+
'HQ config not found or role is not "remote". Add hq section to ide.yml.',
|
|
39
|
+
{
|
|
40
|
+
code: "CONFIG_MISSING",
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!hqConfig.hq_url) {
|
|
45
|
+
throw new IdeError("hq_url is required for remote registration", {
|
|
46
|
+
code: "CONFIG_MISSING",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const client = new HQClient({
|
|
51
|
+
hqUrl: hqConfig.hq_url,
|
|
52
|
+
secret: hqConfig.secret ?? "",
|
|
53
|
+
machineName: hqConfig.machine_name ?? (await import("node:os")).hostname(),
|
|
54
|
+
remoteUrl: (opts.values?.url as string) ?? `http://localhost:4000`,
|
|
55
|
+
heartbeatInterval: hqConfig.heartbeat_interval,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await client.register();
|
|
59
|
+
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
ok: true,
|
|
64
|
+
remoteId: client.getRemoteId(),
|
|
65
|
+
name: client.getName(),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`Registered with HQ as ${client.getName()} (${client.getRemoteId()})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Keep alive until SIGINT/SIGTERM
|
|
73
|
+
await new Promise<void>((resolve) => {
|
|
74
|
+
const shutdown = async () => {
|
|
75
|
+
await client.destroy();
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
process.once("SIGINT", () => void shutdown());
|
|
79
|
+
process.once("SIGTERM", () => void shutdown());
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "machines": {
|
|
85
|
+
const hqConfig = await loadHQConfig(dir);
|
|
86
|
+
const hqUrl =
|
|
87
|
+
(opts.values?.["hq-url"] as string) ?? hqConfig?.hq_url ?? "http://localhost:4000";
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(`${hqUrl}/api/hq/machines`);
|
|
91
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
|
|
94
|
+
if (json) {
|
|
95
|
+
console.log(JSON.stringify(data));
|
|
96
|
+
} else {
|
|
97
|
+
const machines = (data as { machines: Array<{ id: string; name: string; url: string }> })
|
|
98
|
+
.machines;
|
|
99
|
+
if (machines.length === 0) {
|
|
100
|
+
console.log("No remote machines registered");
|
|
101
|
+
} else {
|
|
102
|
+
for (const m of machines) {
|
|
103
|
+
console.log(` ${m.name} — ${m.url} (${m.id})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new IdeError(
|
|
109
|
+
`Failed to query HQ: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
{
|
|
111
|
+
code: "HQ_ERROR",
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "status": {
|
|
119
|
+
const hqConfig = await loadHQConfig(dir);
|
|
120
|
+
|
|
121
|
+
if (json) {
|
|
122
|
+
console.log(
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
configured: !!hqConfig,
|
|
125
|
+
role: hqConfig?.role ?? null,
|
|
126
|
+
hq_url: hqConfig?.hq_url ?? null,
|
|
127
|
+
machine_name: hqConfig?.machine_name ?? null,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
if (!hqConfig) {
|
|
132
|
+
console.log("HQ: not configured");
|
|
133
|
+
} else {
|
|
134
|
+
console.log(`HQ role: ${hqConfig.role}`);
|
|
135
|
+
if (hqConfig.hq_url) console.log(`HQ URL: ${hqConfig.hq_url}`);
|
|
136
|
+
if (hqConfig.machine_name) console.log(`Machine: ${hqConfig.machine_name}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
throw new IdeError(
|
|
144
|
+
`Unknown remote subcommand: ${sub ?? "(none)"}\nUsage: tmux-ide remote register|machines|status`,
|
|
145
|
+
{ code: "USAGE" },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/restart.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { getSessionName } from "./lib/yaml-io.ts";
|
|
3
|
+
import { launch } from "./launch.ts";
|
|
4
|
+
import { killSession, stopSessionMonitor } from "./lib/tmux.ts";
|
|
5
|
+
|
|
6
|
+
export async function restart(
|
|
7
|
+
targetDir: string | undefined,
|
|
8
|
+
{ json, attach }: { json?: boolean; attach?: boolean } = {},
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const dir = resolve(targetDir ?? ".");
|
|
11
|
+
const { name: session } = getSessionName(dir);
|
|
12
|
+
|
|
13
|
+
// Stop the session monitor first — this triggers orchestrator graceful
|
|
14
|
+
// shutdown (release tasks, save state) before we kill the tmux session.
|
|
15
|
+
stopSessionMonitor(session);
|
|
16
|
+
|
|
17
|
+
const result = killSession(session);
|
|
18
|
+
|
|
19
|
+
if (result.stopped) {
|
|
20
|
+
console.log(`Stopped session "${session}"`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await launch(dir, { json, attach });
|
|
24
|
+
}
|