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,261 @@
|
|
|
1
|
+
// Based on VibeTunnel (MIT) — github.com/amantus-ai/vibetunnel
|
|
2
|
+
|
|
3
|
+
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { log as logger } from "../log.ts";
|
|
7
|
+
import type { TunnelService, TunnelStatus } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
let spawnImpl: typeof nodeSpawn = nodeSpawn;
|
|
10
|
+
|
|
11
|
+
/** @internal Restore previous `spawn` with the returned function. */
|
|
12
|
+
export function _setSpawnForTesting(fn: typeof nodeSpawn): () => void {
|
|
13
|
+
const prev = spawnImpl;
|
|
14
|
+
spawnImpl = fn;
|
|
15
|
+
return () => {
|
|
16
|
+
spawnImpl = prev;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NgrokConfig {
|
|
21
|
+
authToken?: string;
|
|
22
|
+
domain?: string;
|
|
23
|
+
port: number;
|
|
24
|
+
region?: string;
|
|
25
|
+
/** Override startup wait (default 30s). Useful for unit tests. */
|
|
26
|
+
startupTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NgrokTunnel {
|
|
30
|
+
publicUrl: string;
|
|
31
|
+
proto: string;
|
|
32
|
+
name: string;
|
|
33
|
+
uri: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class NgrokService implements TunnelService {
|
|
37
|
+
private ngrokProcess: ChildProcess | null = null;
|
|
38
|
+
private currentTunnel: NgrokTunnel | null = null;
|
|
39
|
+
private isRunning = false;
|
|
40
|
+
|
|
41
|
+
constructor(private config: NgrokConfig) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if ngrok binary is available
|
|
45
|
+
*/
|
|
46
|
+
private async checkNgrokBinary(): Promise<string | null> {
|
|
47
|
+
const possiblePaths = [
|
|
48
|
+
"ngrok", // Global PATH
|
|
49
|
+
"/usr/local/bin/ngrok",
|
|
50
|
+
"/opt/homebrew/bin/ngrok",
|
|
51
|
+
path.join(os.homedir(), ".local", "bin", "ngrok"),
|
|
52
|
+
// Windows paths
|
|
53
|
+
"C:\\Program Files\\ngrok\\ngrok.exe",
|
|
54
|
+
path.join(os.homedir(), "AppData", "Local", "ngrok", "ngrok.exe"),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const ngrokPath of possiblePaths) {
|
|
58
|
+
try {
|
|
59
|
+
const result = await new Promise<boolean>((resolve) => {
|
|
60
|
+
const proc = spawnImpl(ngrokPath, ["version"], { stdio: "ignore" });
|
|
61
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
62
|
+
proc.on("error", () => resolve(false));
|
|
63
|
+
// Timeout after 2 seconds
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
proc.kill();
|
|
66
|
+
resolve(false);
|
|
67
|
+
}, 2000);
|
|
68
|
+
});
|
|
69
|
+
if (result) {
|
|
70
|
+
logger.debug(`Found ngrok at: ${ngrokPath}`);
|
|
71
|
+
return ngrokPath;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Continue checking other paths
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start ngrok tunnel
|
|
83
|
+
*/
|
|
84
|
+
async start(): Promise<NgrokTunnel> {
|
|
85
|
+
if (this.isRunning) {
|
|
86
|
+
logger.warn("Ngrok tunnel is already running");
|
|
87
|
+
if (this.currentTunnel) {
|
|
88
|
+
return this.currentTunnel;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ngrokPath = await this.checkNgrokBinary();
|
|
93
|
+
if (!ngrokPath) {
|
|
94
|
+
throw new Error("ngrok binary not found. Please install ngrok: https://ngrok.com/download");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build ngrok command arguments
|
|
98
|
+
const args = ["http", String(this.config.port), "--log=stdout", "--log-format=json"];
|
|
99
|
+
|
|
100
|
+
if (this.config.authToken) {
|
|
101
|
+
args.push("--authtoken", this.config.authToken);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.config.domain) {
|
|
105
|
+
args.push("--domain", this.config.domain);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.config.region) {
|
|
109
|
+
args.push("--region", this.config.region);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger.log(`Starting ngrok tunnel on port ${this.config.port}...`);
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
this.ngrokProcess = spawnImpl(ngrokPath, args, {
|
|
116
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let startupTimeout: NodeJS.Timeout;
|
|
120
|
+
let resolved = false;
|
|
121
|
+
|
|
122
|
+
const cleanup = () => {
|
|
123
|
+
if (startupTimeout) clearTimeout(startupTimeout);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleOutput = (data: Buffer) => {
|
|
127
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
try {
|
|
130
|
+
const record = JSON.parse(line) as {
|
|
131
|
+
msg?: string;
|
|
132
|
+
url?: string;
|
|
133
|
+
lvl?: string;
|
|
134
|
+
err?: unknown;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Look for tunnel started message
|
|
138
|
+
if (record.msg === "started tunnel" && record.url && !resolved) {
|
|
139
|
+
resolved = true;
|
|
140
|
+
this.currentTunnel = {
|
|
141
|
+
publicUrl: record.url,
|
|
142
|
+
proto: "http",
|
|
143
|
+
name: "command_line",
|
|
144
|
+
uri: `http://localhost:${this.config.port}`,
|
|
145
|
+
};
|
|
146
|
+
this.isRunning = true;
|
|
147
|
+
cleanup();
|
|
148
|
+
logger.log(`Ngrok tunnel started: ${record.url}`);
|
|
149
|
+
resolve(this.currentTunnel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Log errors
|
|
153
|
+
if (record.lvl === "error" || record.err) {
|
|
154
|
+
logger.error("Ngrok error:", record.err || record.msg);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Not JSON, log as debug
|
|
158
|
+
logger.debug("Ngrok output:", line);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
this.ngrokProcess.stdout?.on("data", handleOutput);
|
|
164
|
+
this.ngrokProcess.stderr?.on("data", (data) => {
|
|
165
|
+
logger.error("Ngrok stderr:", data.toString());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.ngrokProcess.on("error", (error) => {
|
|
169
|
+
if (!resolved) {
|
|
170
|
+
resolved = true;
|
|
171
|
+
this.isRunning = false;
|
|
172
|
+
cleanup();
|
|
173
|
+
reject(new Error(`Failed to start ngrok: ${error.message}`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.ngrokProcess.on("close", (code) => {
|
|
178
|
+
this.isRunning = false;
|
|
179
|
+
this.currentTunnel = null;
|
|
180
|
+
if (code !== 0 && code !== null) {
|
|
181
|
+
logger.error(`Ngrok process exited with code ${code}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Timeout if tunnel doesn't start
|
|
186
|
+
startupTimeout = setTimeout(() => {
|
|
187
|
+
if (!resolved) {
|
|
188
|
+
resolved = true;
|
|
189
|
+
this.stop().catch(() => {});
|
|
190
|
+
reject(new Error("Ngrok startup timeout - tunnel failed to start"));
|
|
191
|
+
}
|
|
192
|
+
}, this.config.startupTimeoutMs ?? 30_000);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Stop ngrok tunnel
|
|
198
|
+
*/
|
|
199
|
+
async stop(): Promise<void> {
|
|
200
|
+
if (!this.ngrokProcess) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.log("Stopping ngrok tunnel...");
|
|
205
|
+
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
if (!this.ngrokProcess) {
|
|
208
|
+
resolve();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const killTimeout = setTimeout(() => {
|
|
213
|
+
if (this.ngrokProcess) {
|
|
214
|
+
logger.warn("Ngrok process did not exit gracefully, forcing kill");
|
|
215
|
+
this.ngrokProcess.kill("SIGKILL");
|
|
216
|
+
}
|
|
217
|
+
resolve();
|
|
218
|
+
}, 5000);
|
|
219
|
+
|
|
220
|
+
this.ngrokProcess.on("close", () => {
|
|
221
|
+
clearTimeout(killTimeout);
|
|
222
|
+
this.ngrokProcess = null;
|
|
223
|
+
this.currentTunnel = null;
|
|
224
|
+
this.isRunning = false;
|
|
225
|
+
logger.log("Ngrok tunnel stopped");
|
|
226
|
+
resolve();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
this.ngrokProcess.kill("SIGTERM");
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get current tunnel information
|
|
235
|
+
*/
|
|
236
|
+
getTunnel(): NgrokTunnel | null {
|
|
237
|
+
return this.currentTunnel;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if tunnel is running
|
|
242
|
+
*/
|
|
243
|
+
isActive(): boolean {
|
|
244
|
+
return this.isRunning;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get public URL if available
|
|
249
|
+
*/
|
|
250
|
+
getPublicUrl(): string | null {
|
|
251
|
+
return this.currentTunnel?.publicUrl || null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async status(): Promise<TunnelStatus> {
|
|
255
|
+
return {
|
|
256
|
+
running: this.isActive(),
|
|
257
|
+
publicUrl: this.getPublicUrl(),
|
|
258
|
+
port: this.config.port,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uses `_setSpawnForTesting` only — `tailscale.ts` does not call `execFileSync` (no stub needed).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import type { ChildProcess } from "node:child_process";
|
|
7
|
+
import {
|
|
8
|
+
TailscaleServeServiceImpl,
|
|
9
|
+
_setSpawnForTesting,
|
|
10
|
+
_setTestSkipTailscaleFsPaths,
|
|
11
|
+
} from "./tailscale.ts";
|
|
12
|
+
|
|
13
|
+
function makeChild(emitOnSpawn: (c: EventEmitter & Partial<ChildProcess>) => void): ChildProcess {
|
|
14
|
+
const stdout = new EventEmitter();
|
|
15
|
+
const stderr = new EventEmitter();
|
|
16
|
+
const proc = new EventEmitter() as EventEmitter & Partial<ChildProcess>;
|
|
17
|
+
proc.stdout = stdout as NodeJS.ReadableStream;
|
|
18
|
+
proc.stderr = stderr as NodeJS.ReadableStream;
|
|
19
|
+
proc.pid = 4242;
|
|
20
|
+
proc.killed = false;
|
|
21
|
+
proc.kill = () => {
|
|
22
|
+
proc.killed = true;
|
|
23
|
+
queueMicrotask(() => proc.emit("exit", 0, null));
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
queueMicrotask(() => emitOnSpawn(proc));
|
|
27
|
+
return proc as ChildProcess;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("TailscaleServeServiceImpl", () => {
|
|
31
|
+
let restoreSpawn: () => void;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
_setTestSkipTailscaleFsPaths(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
_setTestSkipTailscaleFsPaths(false);
|
|
39
|
+
restoreSpawn?.();
|
|
40
|
+
restoreSpawn = () => {};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects when Tailscale is not installed (which exits non-zero)", async () => {
|
|
44
|
+
const mockSpawn = mock((_cmd: string, args: string[]) => {
|
|
45
|
+
return makeChild((proc) => {
|
|
46
|
+
if (_cmd === "which" && args[0] === "tailscale") {
|
|
47
|
+
proc.emit("exit", 1, null);
|
|
48
|
+
} else {
|
|
49
|
+
proc.emit("exit", 0, null);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
restoreSpawn = _setSpawnForTesting(mockSpawn as typeof import("node:child_process").spawn);
|
|
54
|
+
|
|
55
|
+
const svc = new TailscaleServeServiceImpl();
|
|
56
|
+
await expect(svc.start(4020, false)).rejects.toThrow(/Tailscale command not found/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("start completes when serve --bg exits 0 and stop resets serve", async () => {
|
|
60
|
+
const mockSpawn = mock((cmd: string, args: string[]) => {
|
|
61
|
+
return makeChild((proc) => {
|
|
62
|
+
if (cmd === "which") {
|
|
63
|
+
proc.emit("exit", 0, null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (args[0] === "serve" && args[1] === "reset") {
|
|
67
|
+
proc.emit("exit", 0, null);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (args[0] === "serve" && args[1] === "--bg") {
|
|
71
|
+
proc.emit("exit", 0, null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
proc.emit("exit", 0, null);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
restoreSpawn = _setSpawnForTesting(mockSpawn as typeof import("node:child_process").spawn);
|
|
78
|
+
|
|
79
|
+
const svc = new TailscaleServeServiceImpl();
|
|
80
|
+
await svc.start(4020, false);
|
|
81
|
+
// With `serve --bg`, the CLI exits after configuring; `cleanup()` clears `currentPort` (VibeTunnel behavior).
|
|
82
|
+
await svc.stop();
|
|
83
|
+
const serveResets = mockSpawn.mock.calls.filter(
|
|
84
|
+
(c) => Array.isArray(c[1]) && c[1][0] === "serve" && c[1][1] === "reset",
|
|
85
|
+
);
|
|
86
|
+
expect(serveResets.length).toBeGreaterThanOrEqual(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("status() returns a TunnelStatus-shaped object", async () => {
|
|
90
|
+
const mockSpawn = mock(() =>
|
|
91
|
+
makeChild((proc) => {
|
|
92
|
+
proc.emit("exit", 0, null);
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
restoreSpawn = _setSpawnForTesting(mockSpawn as typeof import("node:child_process").spawn);
|
|
96
|
+
|
|
97
|
+
const svc = new TailscaleServeServiceImpl();
|
|
98
|
+
await svc.start(4020, false);
|
|
99
|
+
const st = await svc.status();
|
|
100
|
+
expect(typeof st.running).toBe("boolean");
|
|
101
|
+
expect(st).toHaveProperty("meta");
|
|
102
|
+
});
|
|
103
|
+
});
|