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/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  stripAnsi,
28
28
  waitForReadiness,
29
29
  wrapper_default
30
- } from "./index-2ec54s5t.js";
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
- const child = dependencies.spawn(argv[0], argv.slice(1), { cwd, env, stdio: [opts.prefix ? "ignore" : "inherit", "pipe", "pipe"], shell: !posix, detached: posix });
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
- if (!opts.upstreamPort && child.pid) {
214
- const rootPid = child.pid;
215
- let idle = 0;
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 (!stopping)
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
- child.on("exit", (code, signal) => {
287
- if (code !== 0 && !hinted) {
288
- const guidance = startupGuidance(recentOutput);
289
- if (guidance)
290
- console.error(`${tag}${c.yellow(["LocalDeck: " + guidance.title, ...guidance.steps].join(`
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
- leaderExit = { code, signal };
294
- stopping = true;
295
- finishAfterCleanup("SIGTERM");
296
- });
297
- child.on("error", (err) => {
298
- stopping = true;
299
- finished = true;
300
- console.error(`${tag}${c.red(`LocalDeck: failed to start "${argv[0]}": ${err.message}`)}`);
301
- ws.close();
302
- resolveDone(1);
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 readyOperation = readiness ? (async () => {
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, stop, done, ready: readyOperation };
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-hyc99xp8.js")).runMcp(rest);
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-2ec54s5t.js";
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 Logs controls.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localdeck",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Stable ports, shareable HTTPS links and request logs for your local dev servers",
6
6
  "bin": {