localdeck 1.0.1 → 1.0.3
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 +41 -0
- package/README.md +4 -0
- package/dashboard/assets/index-qPKQBj-D.js +20 -0
- package/dashboard/assets/{index-CWTMVR9T.css → index-sKTai3O1.css} +1 -1
- package/dashboard/index.html +2 -2
- package/dist/{index-2ec54s5t.js → index-3qpxpxwq.js} +186 -9
- package/dist/index.js +117 -55
- package/dist/{mcp-hyc99xp8.js → mcp-tgg2n05x.js} +1 -1
- package/dist/update-2f1pgdst.js +127 -0
- package/docs/commands.md +12 -0
- package/docs/dashboard.md +6 -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-3qpxpxwq.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);
|
|
@@ -775,6 +832,7 @@ ${c.bold("Usage")}
|
|
|
775
832
|
localdeck daemon status|start|stop|run
|
|
776
833
|
localdeck --help | --h | -h show help
|
|
777
834
|
localdeck --version | --v | -v show the installed version
|
|
835
|
+
localdeck --update update the global npm install and restart the daemon
|
|
778
836
|
|
|
779
837
|
${c.bold("How it works")}
|
|
780
838
|
LocalDeck holds a stable port for each service (e.g. 4000) and proxies to
|
|
@@ -819,12 +877,16 @@ async function main(argv, overrides = {}) {
|
|
|
819
877
|
case "--help":
|
|
820
878
|
console.log(HELP);
|
|
821
879
|
return;
|
|
880
|
+
case "--update":
|
|
881
|
+
if (rest.length)
|
|
882
|
+
throw new Error("Usage: localdeck --update");
|
|
883
|
+
return (await import("./update-2f1pgdst.js")).updateLocalDeck();
|
|
822
884
|
case "up":
|
|
823
885
|
return cmdUp(rest, dependencies);
|
|
824
886
|
case "init":
|
|
825
887
|
return cmdInit(rest, dependencies);
|
|
826
888
|
case "mcp":
|
|
827
|
-
return (await import("./mcp-
|
|
889
|
+
return (await import("./mcp-tgg2n05x.js")).runMcp(rest);
|
|
828
890
|
case "doctor":
|
|
829
891
|
return cmdDoctor(rest);
|
|
830
892
|
case "forward":
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ensureDaemon,
|
|
8
8
|
environmentValues,
|
|
9
9
|
redactDiagnostic
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-3qpxpxwq.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) => {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api,
|
|
3
|
+
STATE_FILE,
|
|
4
|
+
findDaemon
|
|
5
|
+
} from "./index-3qpxpxwq.js";
|
|
6
|
+
|
|
7
|
+
// src/update.ts
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
function installationPrefix(packageRoot, globalRoot, platform = process.platform) {
|
|
13
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
14
|
+
const root = paths.normalize(packageRoot);
|
|
15
|
+
const modules = paths.dirname(root);
|
|
16
|
+
const equal = (a, b) => platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
17
|
+
if (paths.basename(root) !== "localdeck" || !equal(paths.basename(modules), "node_modules")) {
|
|
18
|
+
throw new Error("--update requires a global npm installation. Source checkouts and linked development builds must be updated manually.");
|
|
19
|
+
}
|
|
20
|
+
if (equal(modules, paths.normalize(globalRoot))) {
|
|
21
|
+
return platform === "win32" ? paths.dirname(modules) : paths.dirname(paths.dirname(modules));
|
|
22
|
+
}
|
|
23
|
+
if (platform !== "win32" && paths.basename(paths.dirname(modules)) === "lib")
|
|
24
|
+
return paths.dirname(paths.dirname(modules));
|
|
25
|
+
throw new Error("This is not the active global npm installation. Update it with the package manager and prefix used to install it.");
|
|
26
|
+
}
|
|
27
|
+
function npmCommand() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.resolve(path.dirname(process.execPath), "node_modules/npm/bin/npm-cli.js"),
|
|
30
|
+
path.resolve(path.dirname(process.execPath), "../lib/node_modules/npm/bin/npm-cli.js")
|
|
31
|
+
];
|
|
32
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
33
|
+
if (!directory)
|
|
34
|
+
continue;
|
|
35
|
+
candidates.push(path.join(directory, "node_modules/npm/bin/npm-cli.js"));
|
|
36
|
+
try {
|
|
37
|
+
const executable = fs.realpathSync(path.join(directory, "npm"));
|
|
38
|
+
if (path.basename(executable) === "npm-cli.js")
|
|
39
|
+
candidates.push(executable);
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
const cli = candidates.find((candidate) => fs.existsSync(candidate));
|
|
43
|
+
if (!cli)
|
|
44
|
+
throw new Error("npm was not found. Install Node.js with npm, then run localdeck --update again.");
|
|
45
|
+
return [process.execPath, cli];
|
|
46
|
+
}
|
|
47
|
+
async function execute(command, capture = false) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(command[0], command.slice(1), { stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit", shell: false });
|
|
50
|
+
let output = "";
|
|
51
|
+
child.stdout?.on("data", (chunk) => {
|
|
52
|
+
output += chunk.toString();
|
|
53
|
+
});
|
|
54
|
+
child.once("error", reject);
|
|
55
|
+
child.once("close", (code, signal) => code === 0 ? resolve(output.trim()) : reject(new Error(`Command failed (${signal ?? `exit ${code}`}).`)));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function applyUpdate(steps) {
|
|
59
|
+
await steps.install();
|
|
60
|
+
const version = await steps.validate();
|
|
61
|
+
await steps.stop();
|
|
62
|
+
await steps.start();
|
|
63
|
+
return version;
|
|
64
|
+
}
|
|
65
|
+
async function updateLocalDeck() {
|
|
66
|
+
const root = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
67
|
+
const npm = npmCommand();
|
|
68
|
+
const globalRoot = await execute([...npm, "root", "--global"], true);
|
|
69
|
+
const prefix = installationPrefix(root, globalRoot);
|
|
70
|
+
const bin = path.join(root, "bin/localdeck.js");
|
|
71
|
+
console.log(`Updating LocalDeck from npm in ${prefix}…`);
|
|
72
|
+
console.log("After installation, the daemon will restart. Managed services and public shares will stop; start them again when ready.");
|
|
73
|
+
let installedVersion = "";
|
|
74
|
+
const version = await applyUpdate({
|
|
75
|
+
install: async () => {
|
|
76
|
+
try {
|
|
77
|
+
await execute([...npm, "install", "--global", "--prefix", prefix, "localdeck@latest", "--registry=https://registry.npmjs.org/"]);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`npm update failed. The daemon was not restarted. Check the npm error above; the installation prefix must be writable. ${error instanceof Error ? error.message : error}`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
validate: async () => {
|
|
83
|
+
const version2 = await execute([process.execPath, bin, "--version"], true);
|
|
84
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(version2))
|
|
85
|
+
throw new Error("The updated CLI could not be verified. The daemon was not restarted.");
|
|
86
|
+
installedVersion = version2;
|
|
87
|
+
return version2;
|
|
88
|
+
},
|
|
89
|
+
stop: async () => {
|
|
90
|
+
const running = await findDaemon();
|
|
91
|
+
if (!running)
|
|
92
|
+
return;
|
|
93
|
+
await new Api(running.apiPort, globalThis.fetch, 30000).shutdown();
|
|
94
|
+
const deadline = Date.now() + 30000;
|
|
95
|
+
const ownsState = () => {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8")).pid === running.pid;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error.code === "ENOENT")
|
|
100
|
+
return false;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
while (ownsState()) {
|
|
105
|
+
if (Date.now() >= deadline)
|
|
106
|
+
throw new Error("Package updated, but the daemon did not stop within 30 seconds. Restart it manually.");
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
start: async () => {
|
|
111
|
+
try {
|
|
112
|
+
await execute([process.execPath, bin, "daemon", "start"]);
|
|
113
|
+
const running = await findDaemon();
|
|
114
|
+
if (running?.version !== installedVersion)
|
|
115
|
+
throw new Error("Unexpected daemon version");
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error("Package updated, but the daemon could not restart. Run localdeck daemon start to retry.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
console.log(`LocalDeck ${version} installed. The daemon has restarted.`);
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
updateLocalDeck,
|
|
125
|
+
installationPrefix,
|
|
126
|
+
applyUpdate
|
|
127
|
+
};
|
package/docs/commands.md
CHANGED
|
@@ -92,3 +92,15 @@ Child services receive `PORT` (upstream), `LOCALDECK_PORT` (stable/published por
|
|
|
92
92
|
## MCP client connection
|
|
93
93
|
|
|
94
94
|
`localdeck mcp` runs the stdio MCP server for service inspection, controls, recorded GET/HEAD replay, and quick sharing. `--project <id>` restricts every tool to one remembered project; `--help` prints usage without connecting. A tool call can start the daemon, but connecting never starts application services. Closing the client leaves services running. See [MCP setup and every tool](mcp.md).
|
|
95
|
+
|
|
96
|
+
## Update LocalDeck
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
localdeck --update
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Available starting with 1.0.3. Installs the latest public npm release into the current global npm prefix, verifies the updated CLI, and restarts the daemon. Restarting stops daemon-managed services and public shares. Start services again afterward; terminal sessions may need to be rerun to reconnect.
|
|
103
|
+
|
|
104
|
+
A failed installation leaves the daemon running. The npm prefix must be writable; LocalDeck never invokes sudo. Source checkouts, linked builds, local installs, npx caches, and unsupported package-manager layouts must be updated manually. Custom Unix prefixes are preserved. On Windows the installation must match npm's active global root.
|
|
105
|
+
|
|
106
|
+
Older versions without this command can upgrade with `npm install -g localdeck@latest` using their existing npm prefix.
|
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,9 @@ 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
|
-
|
|
71
|
+
Successful asset and development-module requests are hidden by default. Enable **Show assets & dev traffic** to include them; this preference is saved in the browser. Failed requests, page navigations, and API calls remain visible. Filtering affects the dashboard view, not capture.
|
|
72
|
+
|
|
73
|
+
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
74
|
|
|
73
75
|
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
76
|
|