localdeck 1.0.1 → 1.0.2
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/CHANGELOG.md +26 -0
- package/dashboard/assets/index-78KgNO32.js +20 -0
- package/dashboard/assets/{index-CWTMVR9T.css → index-BSKA3LqW.css} +1 -1
- package/dashboard/index.html +2 -2
- package/dist/{index-2ec54s5t.js → index-eyb4mz5b.js} +185 -8
- package/dist/index.js +112 -55
- package/dist/{mcp-hyc99xp8.js → mcp-4dnef2t7.js} +1 -1
- package/docs/dashboard.md +4 -4
- package/package.json +1 -1
- package/dashboard/assets/index-joMglKUf.js +0 -20
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
stripAnsi,
|
|
28
28
|
waitForReadiness,
|
|
29
29
|
wrapper_default
|
|
30
|
-
} from "./index-
|
|
30
|
+
} from "./index-eyb4mz5b.js";
|
|
31
31
|
|
|
32
32
|
// src/index.ts
|
|
33
33
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -175,9 +175,7 @@ async function startService(opts, overrides = {}) {
|
|
|
175
175
|
console.log(`${tag}${c.bold("LocalDeck")} ${c.dim("›")} ${paint(service.name)} ${c.dim("→")} ${c.green(appUrl)} ${c.dim(`(also ${service.urls[1]} · dashboard http://localhost:${daemon.apiPort})`)}`);
|
|
176
176
|
}
|
|
177
177
|
const posix = process.platform !== "win32";
|
|
178
|
-
|
|
179
|
-
if (child.pid)
|
|
180
|
-
send({ type: "pid", pid: child.pid });
|
|
178
|
+
let child;
|
|
181
179
|
const pipe = (stream, name, out) => {
|
|
182
180
|
stream.on("data", (chunk) => {
|
|
183
181
|
if (opts.prefix) {
|
|
@@ -192,8 +190,6 @@ async function startService(opts, overrides = {}) {
|
|
|
192
190
|
send({ type: "log", stream: name, line: part });
|
|
193
191
|
});
|
|
194
192
|
};
|
|
195
|
-
pipe(child.stdout, "stdout", process.stdout);
|
|
196
|
-
pipe(child.stderr, "stderr", process.stderr);
|
|
197
193
|
let hinted = false;
|
|
198
194
|
let recentOutput = "";
|
|
199
195
|
const hintOnPortClash = (chunk) => {
|
|
@@ -206,34 +202,11 @@ async function startService(opts, overrides = {}) {
|
|
|
206
202
|
console.error(`${tag}${c.yellow(` Fix: run without --port (LocalDeck finds the tool's own port automatically), or pick a different stable port, e.g. --port ${service.stablePort + 1000}.`)}`);
|
|
207
203
|
}
|
|
208
204
|
};
|
|
209
|
-
child.stdout.on("data", hintOnPortClash);
|
|
210
|
-
child.stderr.on("data", hintOnPortClash);
|
|
211
205
|
let detectTimer;
|
|
212
206
|
let reported;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const tick = async () => {
|
|
217
|
-
if (child.exitCode !== null || child.signalCode)
|
|
218
|
-
return;
|
|
219
|
-
try {
|
|
220
|
-
const listeners = await listeningPorts(await processTree(rootPid));
|
|
221
|
-
const pick = chooseUpstream(listeners, upstreamPort);
|
|
222
|
-
if (pick) {
|
|
223
|
-
const key = `${connectHost(pick)}:${pick.port}`;
|
|
224
|
-
if (key !== reported) {
|
|
225
|
-
reported = key;
|
|
226
|
-
send({ type: "upstream", port: pick.port, host: connectHost(pick) });
|
|
227
|
-
if (pick.port !== upstreamPort)
|
|
228
|
-
console.log(`${tag}${c.dim(`LocalDeck: process is listening on ${key} (ignored $PORT) — ${service.urls[0]} now points there`)}`);
|
|
229
|
-
}
|
|
230
|
-
idle++;
|
|
231
|
-
}
|
|
232
|
-
} catch {}
|
|
233
|
-
detectTimer = setTimeout(tick, reported ? Math.min(2000 + idle * 500, 1e4) : 400);
|
|
234
|
-
};
|
|
235
|
-
detectTimer = setTimeout(tick, 300);
|
|
236
|
-
}
|
|
207
|
+
let retryTimer;
|
|
208
|
+
let retryAttempts = 0;
|
|
209
|
+
let finalizing = false;
|
|
237
210
|
let stopping = false;
|
|
238
211
|
let cleanupPromise;
|
|
239
212
|
let leaderExit;
|
|
@@ -244,20 +217,49 @@ async function startService(opts, overrides = {}) {
|
|
|
244
217
|
});
|
|
245
218
|
const cleanup = (signal) => cleanupPromise ??= cleanupTerminalTree(child, signal);
|
|
246
219
|
const finishAfterCleanup = async (signal) => {
|
|
247
|
-
if (finished || !leaderExit)
|
|
220
|
+
if (finished || finalizing || !leaderExit)
|
|
248
221
|
return;
|
|
222
|
+
finalizing = true;
|
|
249
223
|
if (!await cleanup(signal)) {
|
|
250
224
|
cleanupPromise = undefined;
|
|
225
|
+
finalizing = false;
|
|
251
226
|
stopping = false;
|
|
252
227
|
console.error(`${tag}${c.red(`LocalDeck: process descendants survived cleanup; retry Stop`)}`);
|
|
253
228
|
return;
|
|
254
229
|
}
|
|
255
230
|
if (finished)
|
|
256
231
|
return;
|
|
257
|
-
finished = true;
|
|
258
232
|
if (detectTimer)
|
|
259
233
|
clearTimeout(detectTimer);
|
|
260
234
|
send({ type: "log", stream: "stderr", line: `process exited (${leaderExit.signal ?? `code ${leaderExit.code}`})` });
|
|
235
|
+
const interrupted = ["SIGINT", "SIGTERM", "SIGHUP"].includes(leaderExit.signal ?? "") || [130, 143, 129].includes(leaderExit.code ?? -1);
|
|
236
|
+
if (!stopping && !interrupted && retryAttempts < 3 && ws.readyState === wrapper_default.OPEN) {
|
|
237
|
+
const message = `LocalDeck: restarting in ${(dependencies.retryDelayMs ?? 3000) / 1000} seconds (attempt ${++retryAttempts}/3)`;
|
|
238
|
+
console.log(`${tag}${c.yellow(message)}`);
|
|
239
|
+
send({ type: "log", stream: "stderr", line: message });
|
|
240
|
+
send({ type: "readiness", status: "checking", message });
|
|
241
|
+
retryTimer = setTimeout(() => {
|
|
242
|
+
retryTimer = undefined;
|
|
243
|
+
if (stopping || finished)
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
launch();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
finished = true;
|
|
249
|
+
console.error(`${tag}${c.red(`LocalDeck: restart failed: ${error instanceof Error ? error.message : error}`)}`);
|
|
250
|
+
ws.close();
|
|
251
|
+
resolveDone(1);
|
|
252
|
+
}
|
|
253
|
+
}, dependencies.retryDelayMs ?? 3000);
|
|
254
|
+
finalizing = false;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!stopping && !interrupted && retryAttempts === 3) {
|
|
258
|
+
const message = "LocalDeck: automatic restart limit reached (3/3). Start the service manually to try again.";
|
|
259
|
+
console.error(`${tag}${c.red(message)}`);
|
|
260
|
+
send({ type: "log", stream: "stderr", line: message });
|
|
261
|
+
}
|
|
262
|
+
finished = true;
|
|
261
263
|
setTimeout(() => {
|
|
262
264
|
ws.close();
|
|
263
265
|
resolveDone(leaderExit.code ?? (leaderExit.signal ? 1 : 0));
|
|
@@ -267,6 +269,10 @@ async function startService(opts, overrides = {}) {
|
|
|
267
269
|
if (finished || stopping)
|
|
268
270
|
return;
|
|
269
271
|
stopping = true;
|
|
272
|
+
if (retryTimer) {
|
|
273
|
+
clearTimeout(retryTimer);
|
|
274
|
+
retryTimer = undefined;
|
|
275
|
+
}
|
|
270
276
|
cleanup(signal).then(() => finishAfterCleanup(signal));
|
|
271
277
|
};
|
|
272
278
|
ws.on("message", (raw) => {
|
|
@@ -280,29 +286,77 @@ async function startService(opts, overrides = {}) {
|
|
|
280
286
|
}
|
|
281
287
|
});
|
|
282
288
|
ws.on("close", () => {
|
|
283
|
-
if (
|
|
289
|
+
if (retryTimer) {
|
|
290
|
+
stop();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!stopping && !finished)
|
|
284
294
|
console.error(`${tag}${c.yellow("LocalDeck: lost connection to daemon; the stable port is no longer proxied. Process keeps running.")}`);
|
|
285
295
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
function launch() {
|
|
297
|
+
leaderExit = undefined;
|
|
298
|
+
cleanupPromise = undefined;
|
|
299
|
+
finalizing = false;
|
|
300
|
+
reported = undefined;
|
|
301
|
+
recentOutput = "";
|
|
302
|
+
hinted = false;
|
|
303
|
+
child = dependencies.spawn(argv[0], argv.slice(1), { cwd, env, stdio: [opts.prefix ? "ignore" : "inherit", "pipe", "pipe"], shell: !posix, detached: posix });
|
|
304
|
+
if (child.pid)
|
|
305
|
+
send({ type: "pid", pid: child.pid });
|
|
306
|
+
pipe(child.stdout, "stdout", process.stdout);
|
|
307
|
+
pipe(child.stderr, "stderr", process.stderr);
|
|
308
|
+
child.stdout.on("data", hintOnPortClash);
|
|
309
|
+
child.stderr.on("data", hintOnPortClash);
|
|
310
|
+
if (!opts.upstreamPort && child.pid) {
|
|
311
|
+
const detectedChild = child;
|
|
312
|
+
const rootPid = child.pid;
|
|
313
|
+
let idle = 0;
|
|
314
|
+
const tick = async () => {
|
|
315
|
+
if (child !== detectedChild || child.exitCode !== null || child.signalCode)
|
|
316
|
+
return;
|
|
317
|
+
try {
|
|
318
|
+
const listeners = await listeningPorts(await processTree(rootPid));
|
|
319
|
+
const pick = chooseUpstream(listeners, upstreamPort);
|
|
320
|
+
if (pick) {
|
|
321
|
+
const key = `${connectHost(pick)}:${pick.port}`;
|
|
322
|
+
if (key !== reported) {
|
|
323
|
+
reported = key;
|
|
324
|
+
send({ type: "upstream", port: pick.port, host: connectHost(pick) });
|
|
325
|
+
if (pick.port !== upstreamPort)
|
|
326
|
+
console.log(`${tag}${c.dim(`LocalDeck: process is listening on ${key} (ignored $PORT) — ${service.urls[0]} now points there`)}`);
|
|
327
|
+
}
|
|
328
|
+
idle++;
|
|
329
|
+
}
|
|
330
|
+
} catch {}
|
|
331
|
+
if (child !== detectedChild || child.exitCode !== null || stopping || leaderExit)
|
|
332
|
+
return;
|
|
333
|
+
detectTimer = setTimeout(tick, reported ? Math.min(2000 + idle * 500, 1e4) : 400);
|
|
334
|
+
};
|
|
335
|
+
detectTimer = setTimeout(tick, 300);
|
|
292
336
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
child.on("exit", (code, signal) => {
|
|
338
|
+
if (code !== 0 && !hinted) {
|
|
339
|
+
const guidance = startupGuidance(recentOutput);
|
|
340
|
+
if (guidance)
|
|
341
|
+
console.error(`${tag}${c.yellow(["LocalDeck: " + guidance.title, ...guidance.steps].join(`
|
|
342
|
+
`))}`);
|
|
343
|
+
}
|
|
344
|
+
leaderExit = { code, signal };
|
|
345
|
+
finishAfterCleanup("SIGTERM");
|
|
346
|
+
});
|
|
347
|
+
child.on("error", (err) => {
|
|
348
|
+
stopping = true;
|
|
349
|
+
finished = true;
|
|
350
|
+
console.error(`${tag}${c.red(`LocalDeck: failed to start "${argv[0]}": ${err.message}`)}`);
|
|
351
|
+
ws.close();
|
|
352
|
+
resolveDone(1);
|
|
353
|
+
});
|
|
354
|
+
if (retryAttempts > 0)
|
|
355
|
+
checkReadiness().catch(() => stop());
|
|
356
|
+
}
|
|
357
|
+
launch();
|
|
304
358
|
const readiness = opts.ready;
|
|
305
|
-
const
|
|
359
|
+
const checkReadiness = () => readiness ? (async () => {
|
|
306
360
|
send({ type: "readiness", status: "checking", message: readiness.type === "tcp" ? "Waiting for the service to listen" : "Running configured readiness check" });
|
|
307
361
|
try {
|
|
308
362
|
const ready = await waitForReadiness({
|
|
@@ -326,8 +380,11 @@ async function startService(opts, overrides = {}) {
|
|
|
326
380
|
throw error;
|
|
327
381
|
}
|
|
328
382
|
})() : Promise.resolve(service);
|
|
383
|
+
const readyOperation = checkReadiness();
|
|
329
384
|
readyOperation.catch(() => {});
|
|
330
|
-
return { name: opts.name, service, child
|
|
385
|
+
return { name: opts.name, service, get child() {
|
|
386
|
+
return child;
|
|
387
|
+
}, stop, done, ready: readyOperation };
|
|
331
388
|
}
|
|
332
389
|
async function cleanupTerminalTree(child, signal) {
|
|
333
390
|
await killProcessTree(child, signal);
|
|
@@ -824,7 +881,7 @@ async function main(argv, overrides = {}) {
|
|
|
824
881
|
case "init":
|
|
825
882
|
return cmdInit(rest, dependencies);
|
|
826
883
|
case "mcp":
|
|
827
|
-
return (await import("./mcp-
|
|
884
|
+
return (await import("./mcp-4dnef2t7.js")).runMcp(rest);
|
|
828
885
|
case "doctor":
|
|
829
886
|
return cmdDoctor(rest);
|
|
830
887
|
case "forward":
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ensureDaemon,
|
|
8
8
|
environmentValues,
|
|
9
9
|
redactDiagnostic
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-eyb4mz5b.js";
|
|
11
11
|
|
|
12
12
|
// ../../node_modules/.bun/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
|
|
13
13
|
var require_code = __commonJS((exports) => {
|
package/docs/dashboard.md
CHANGED
|
@@ -7,16 +7,16 @@ Open the dashboard with `localdeck open`. It runs on `http://localhost:7777` by
|
|
|
7
7
|
## Pages and projects
|
|
8
8
|
|
|
9
9
|
- **Projects** (`/projects`) lists projects remembered through the CLI. Selecting one opens `/projects/<id>`.
|
|
10
|
-
- **All Services** (`/services`) lists configured and running services across projects, with search, Start/Stop, Share, and
|
|
10
|
+
- **All Services** (`/services`) lists configured and running services across projects, with search, Start/Stop, Share, View logs, and View requests controls.
|
|
11
11
|
- **Settings** (`/settings`) contains Clean URLs and the log display limit.
|
|
12
12
|
- **About** (`/about`) contains setup and command information.
|
|
13
13
|
- **Changelog** (`/changelog`) lists release features and changes.
|
|
14
14
|
|
|
15
15
|
Routes support refresh, direct links, and browser Back/Forward. LocalDeck remembers registered project files; it does not scan your disk. Missing or invalid projects remain visible with recovery actions until fixed or forgotten.
|
|
16
16
|
|
|
17
|
-
Project pages expose individual Start/Stop, View logs, and Share controls, plus Start all and Stop all. Start includes prerequisites and waits for readiness; Stop all reverses dependency order. Local and shared URLs appear below each service. Failed services keep their own errors so other services remain usable.
|
|
17
|
+
Project pages expose individual Start/Stop, View logs, View requests, and Share controls, plus Start all and Stop all. Start includes prerequisites and waits for readiness; Stop all reverses dependency order. Local and shared URLs appear below each service. Failed services keep their own errors so other services remain usable.
|
|
18
18
|
|
|
19
|
-
Dashboard-started services run in the background under the daemon. Terminal services stay attached to their original CLI session; stopping them is supported, but restart them from their terminal. Shutting down the daemon stops its services and shares.
|
|
19
|
+
Dashboard-started services run in the background under the daemon. Terminal services stay attached to their original CLI session; stopping them is supported, but restart them from their terminal. Unexpected process exits trigger up to three automatic restarts, each after a three-second delay. Logs show the attempt number; after the third failed retry, start the service manually to try again. The retry budget lasts until the next manual start. Stop, Stop all, Ctrl-C, and daemon shutdown cancel pending retries. Shutting down the daemon stops its services and shares.
|
|
20
20
|
|
|
21
21
|
## Project settings
|
|
22
22
|
|
|
@@ -68,7 +68,7 @@ Windows CI covers native detection and script syntax. Administrator installation
|
|
|
68
68
|
|
|
69
69
|
Project diagnostics include configuration, readiness errors, and recent logs. Environment values and recognized credential formats are redacted; review the preview because application output may still contain private data. Nothing is uploaded automatically.
|
|
70
70
|
|
|
71
|
-
Request inspection is available through CLI/API/MCP. Recorded GET/HEAD replay sends a new request to the current local upstream, without copying cookies, authorization, or request bodies. It does not follow redirects and has bounded response size and duration.
|
|
71
|
+
Choose **View requests** on a service, then expand a request row to inspect its method, path, status, timing, size, and local/share source. Details and replay responses expand directly below the selected row. Click the request again to collapse it. Service tabs and filtering are available in this view. **Replay request** sends eligible GET/HEAD requests and displays the new response body. New HTTP requests include request and response headers, request cookies, Set-Cookie values, and query parameters. Headers retain duplicate entries and are capped at 100 entries or 16 KiB per direction. These details, including cookie and authorization values, are stored in local request history. Older records cannot recover headers that were not captured. New HTTP requests also retain original request and response body previews, limited to 16 KiB per direction. Text is shown as UTF-8; binary or undecodable compressed content is shown as Base64. Complete gzip, deflate and Brotli bodies are decoded within the same preview limit. Older records cannot recover bodies that were not captured. Request inspection is also available through CLI/API/MCP. Recorded GET/HEAD replay sends a new request to the current local upstream, without copying cookies, authorization, or request bodies. It does not follow redirects and has bounded response size and duration.
|
|
72
72
|
|
|
73
73
|
For an occupied startup port, service errors show recovery guidance with the raw output under Technical details. Vite guidance covers `strictPort` and reading `PORT`; LocalDeck does not change your application configuration or terminate unrelated listeners.
|
|
74
74
|
|