localdeck 1.0.0 → 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-3npyta7k.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-s6gapf6v.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-3npyta7k.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) => {
@@ -0,0 +1,94 @@
1
+ # Command reference
2
+
3
+ [Quick start](../README.md) · Development scripts · [Configuration](configuration.md)
4
+
5
+ Run project commands from your app's folder or any descendant. LocalDeck searches upwards for `.localdeck`, `.localdeck.json`, or `localdeck.json`.
6
+
7
+ ## Set up and inspect a project
8
+
9
+ | Command | What it does |
10
+ | --- | --- |
11
+ | `localdeck` | Shows help and, when a configuration exists, lists and remembers the project. May start the daemon to remember it. |
12
+ | `localdeck help`, `-h`, `--h`, `--help` | Shows CLI help without starting a service. |
13
+ | `localdeck -v`, `--v`, `--version` | Prints the installed version. |
14
+ | `localdeck commands` | Lists configured service names, ports, dependencies, tags, and commands; remembers the project. Alias: `cmds`. |
15
+ | `localdeck init` | Detects package-manager scripts, writes `.localdeck`, and remembers the project. Prefers `dev*` scripts per package; falls back to `start*`, `serve*`, and `preview*`. |
16
+ | `localdeck init --dry-run` | Prints the proposed file and services without writing or starting the daemon. |
17
+ | `localdeck init --force` | Replaces an existing configuration with the newly detected configuration. |
18
+ | `localdeck init --name web --command 'your command'` | Creates a service using an explicit command, including non-JavaScript projects. Combine with `--dry-run` or `--force`. |
19
+ | `localdeck init .localdeck.json` | Selects the alternate output filename. `localdeck.json` is also accepted; replacing existing configuration requires `--force`. |
20
+ | `localdeck doctor` | Checks Node, package manager, installed dependencies, configured directories, environment variable names, stable ports, and required database tools. Does not start your services. Exits with status 1 if a check fails. |
21
+ | `localdeck doctor --json` | Produces the same diagnostic report as JSON. |
22
+
23
+ Initialization uses the nearest `package.json` directory, or the current folder when none exists. `doctor` checks whether explicitly configured stable ports are free; a port held by a service you already started is reported as occupied too.
24
+
25
+ ## Run services
26
+
27
+ | Command | What it does |
28
+ | --- | --- |
29
+ | `localdeck web` | Runs the configured `web` service and its transitive prerequisites in the terminal. |
30
+ | `localdeck up` | Runs all configured services, starting each after its own prerequisites become ready. |
31
+ | `localdeck up frontend api` | Runs services matching any supplied name or tag, plus their prerequisites. |
32
+ | `localdeck up --profile app` | Runs the names in the saved `app` profile and their prerequisites. Additional name/tag selectors must stay within the profile. |
33
+ | `localdeck run web` | Runs the configured `web` service; an alternative to `localdeck web`. |
34
+ | `localdeck run api -- node server.js` | Runs a one-off command without requiring a configuration. |
35
+ | `localdeck run api --port 4000 --upstream 8787 -- node server.js` | Reserves stable port 4000 and explicitly proxies to upstream port 8787. Omit these flags for automatic allocation and detection. |
36
+
37
+ `run` accepts `-p` for `--port`, `-u` for `--upstream`, and `--port=4000`/`--upstream=8787`. Ports must be integers from 1 to 65535. Put LocalDeck options before the application command; `--` separates the two. `localdeck run web --port 4000` overrides the configured web service's stable port for that terminal run.
38
+
39
+ `{port}` inside a configured command expands to the assigned **upstream** port in both terminal and dashboard launches. `$PORT` also contains that port. The stable port is held by LocalDeck, so avoid assigning it the same port your tool insists on using.
40
+
41
+ Once an automatic stable port has been remembered, an occupied port causes startup to fail with the port number. LocalDeck does not silently move it or overwrite the remembered mapping. Stop the conflicting listener or explicitly select a different stable port.
42
+
43
+ Foreground services stop on Ctrl-C, including their descendant processes. Restart terminal-owned services from that terminal; dashboard Restart is reserved for daemon-owned active services. Dashboard-launched services are owned by the daemon and continue in the background. Starting a service may offer clean-URL setup during interactive use; cancellation keeps normal port-number URLs working.
44
+
45
+ ## Observe, stop, and share
46
+
47
+ Here `<service>` may be a unique active name (`web`), a qualified reference (`my-project/web`), or an exact runtime key printed by `ps`. Project references accept an ID, name, or slug. Use an exact key to inspect retained history for a stopped service.
48
+
49
+ | Command | What it does |
50
+ | --- | --- |
51
+ | `localdeck ps` | Lists registered services with status, project, key, stable/upstream ports, PID, request counts, share URL, age, and command. Aliases: `ls`, `ports`. Stopped entries may remain visible. |
52
+ | `localdeck port <service>` | Prints only the stable port, for use in scripts. Database services return their published port. |
53
+ | `localdeck logs <service>` | Shows the latest 100 captured output lines. |
54
+ | `localdeck logs <service> -f -n 200` | Shows 200 lines, then follows live output. Long options: `--follow`, `--lines`. Ctrl-C exits the log viewer. |
55
+ | `localdeck requests <service> -n 50` | Shows recent proxied HTTP requests: time, status, method, path, latency, origin, and errors. Defaults to 50; `--lines` is equivalent to `-n`. |
56
+ | `localdeck stop <service>` | Stops the service process group and tears down its share. Does not automatically stop its dependencies or dependents. |
57
+ | `localdeck open` | Starts the daemon if needed and opens the dashboard. |
58
+ | `localdeck open <service>` | Opens a service's stable URL. Database endpoints require a database client. |
59
+ | `localdeck share <service>` | Starts a public Cloudflare HTTPS tunnel with an automatically generated password and prints both. Requires `cloudflared`. |
60
+ | `localdeck share <service> --password xyz` | Shares using your chosen password. |
61
+ | `localdeck share <service> --public` | Shares without a password. Cannot be combined with `--password`. |
62
+ | `localdeck unshare <service>` | Closes the service's public tunnel without stopping the local service. |
63
+
64
+ History limits are integers from 1 to 5000. Only requests through LocalDeck's proxy are recorded. An interrupted client response is recorded once with status 499 and a disconnect error; an upstream response failure is recorded as a proxy error. Database services cannot be opened in a browser or shared. Sharing verifies public connectivity for up to two minutes. Quick Tunnel DNS delays can prevent startup; inspect the error before retrying. Sharing remains active until unshared, stopped, restarted, or the daemon shuts down.
65
+
66
+ ## Daemon and optional forwarding
67
+
68
+ | Command | What it does |
69
+ | --- | --- |
70
+ | `localdeck daemon status` | Prints daemon status, PID, API URL, uptime, and data directory. |
71
+ | `localdeck daemon start` | Starts the daemon in the background, or reuses the existing daemon. |
72
+ | `localdeck daemon run` | Runs the daemon in the foreground; useful for development and debugging. Ctrl-C shuts it down. |
73
+ | `localdeck daemon stop` | Requests daemon shutdown, including cleanup of managed services and shares. |
74
+ | `localdeck forward` | Runs a foreground TCP relay from `127.0.0.1:80` to port 7777. Ctrl-C stops the relay. Does not start the daemon. |
75
+ | `localdeck forward --to-port 8888` | Relays port 80 to a custom daemon port. Requires port 80 to be available and permission to bind it. |
76
+
77
+ Normal startup handles clean URLs automatically where supported. Manual `forward` is an advanced alternative; see [clean URLs](dashboard.md#clean-urls).
78
+
79
+ ## Environment variables
80
+
81
+ | Variable | Meaning |
82
+ | --- | --- |
83
+ | `LOCALDECK_HOME` | Overrides the data directory (default `~/.localdeck`), including state, remembered projects, ports, and logs. Useful for isolated tests. |
84
+ | `LOCALDECK_PORT` when starting LocalDeck | Overrides the daemon API port (default 7777). Use the same value for the Vite dashboard development proxy. |
85
+ | `LOCALDECK_DEV_ORIGINS` | Comma-separated exact loopback HTTP origins allowed to call the daemon from a browser. Read when the daemon starts. |
86
+ | `LOCALDECK_CLOUDFLARED` | Overrides the `cloudflared` executable used for sharing. |
87
+ | `NO_COLOR` | Disables CLI colors. |
88
+ | `CI` | Suppresses interactive clean-URL preparation when set. |
89
+
90
+ Child services receive `PORT` (upstream), `LOCALDECK_PORT` (stable/published port), and `LOCALDECK_SERVICE` (service name). HTTP services also receive `LOCALDECK_URL`, using a stable numeric localhost URL. These names and other `LOCALDECK_*` names are reserved in configuration `env`; see [environment wiring](configuration.md#profiles-and-environment-wiring).
91
+
92
+ ## MCP client connection
93
+
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).
@@ -0,0 +1,64 @@
1
+ # Project configuration
2
+
3
+ [Back to quick start](../README.md) · [Commands](commands.md)
4
+
5
+ JSON at the project root. `localdeck init` generates one from `package.json`; edit freely.
6
+
7
+ ```json
8
+ [
9
+ { "name": "database", "run": "docker compose up postgres", "serviceType": "postgres", "upstream": 5432, "ready": { "type": "compose", "service": "postgres", "engine": "postgres" } },
10
+ { "name": "api", "run": "npm run api", "after": "database", "ready": "/health", "tag": "backend" },
11
+ { "name": "web", "run": "npm run dev:web", "after": "api", "port": 4000, "tag": "frontend" },
12
+ { "name": "docs", "run": "npm run dev", "cwd": "packages/docs" }
13
+ ]
14
+ ```
15
+
16
+ - `name` — what you type: `localdeck api`. Running services receive a project-qualified hostname; the shorter `api.localhost:7777` alias works only while it is unambiguous.
17
+ - `run` — the command, exactly as you'd type it in the terminal.
18
+ - `port` — optional. The stable port other things should use. Without it, LocalDeck picks one (4000+) the first time and remembers it across runs.
19
+ - `tag` — optional, string or array. `localdeck up backend` runs everything tagged `backend`.
20
+ - `cwd` — optional, relative to the file.
21
+ - `upstream` — optional. Only for the rare tool whose port LocalDeck can't detect; pins where to proxy.
22
+ - `after` — optional, one service name or an array. LocalDeck starts those prerequisites first and waits until they are ready. `localdeck web` and `localdeck up frontend` automatically include transitive prerequisites.
23
+ - `ready` — optional advanced readiness check. Without it, LocalDeck waits for its ownership-verified upstream port. Use `"/health"` for an HTTP endpoint, or one of the advanced objects below.
24
+ - `serviceType` — `http` (default), `postgres`, `redis`, or `compose`. Database services require `upstream`, use that published port directly, and cannot set an HTTP proxy `port`. They have no browser/share links: LocalDeck does not proxy PostgreSQL or Redis traffic.
25
+ - `env` — optional environment overrides. `PORT` and `LOCALDECK_*` are reserved. Use `{service:api:url}` or `{service:database:port}` to reference a service explicitly listed in `after`. Database port references use the actual published port; database URL references are rejected. Keep secrets in your environment, not committed config.
26
+
27
+ The dashboard shows each relationship as **Starts after** and lets you edit it with checkboxes—no graph terminology or manual JSON is required. Missing names, self-dependencies, and cycles are rejected before the file changes.
28
+
29
+ Advanced readiness examples:
30
+
31
+ ```json
32
+ [
33
+ { "name": "database", "run": "docker compose up postgres", "serviceType": "postgres", "upstream": 5432, "ready": { "type": "compose", "service": "postgres", "engine": "postgres" } },
34
+ { "name": "api", "run": "npm run api", "after": "database", "ready": { "type": "http", "path": "/health", "status": 200 } },
35
+ { "name": "web", "run": "npm run web", "after": "api" }
36
+ ]
37
+ ```
38
+
39
+ Readiness defaults to a 60-second timeout and a 400ms retry interval. A command check receives `PORT`, `LOCALDECK_PORT`, `LOCALDECK_SERVICE`, and `LOCALDECK_URL`; `{upstream}` expands to the process port and `{port}` to the stable port. If a prerequisite times out, LocalDeck stops it, skips its dependents with a named error, and continues unrelated branches.
40
+
41
+ For Docker Compose databases, set `upstream` to the host-published database port as shown above. Container listeners are not descendants of the `docker compose` client process, so LocalDeck cannot safely infer their ownership from the local process tree.
42
+
43
+ Native database presets are `ready: "postgres"` (requires `pg_isready`) and `ready: "redis"` (requires `redis-cli`). The matching `serviceType` selects this check by default. Compose checks use tools inside the named container: `{ "type": "compose", "service": "redis", "engine": "redis" }`. Compose service names and engines are validated; the host still needs Docker. **Listening** means TCP connections are accepted; **ready** means the configured readiness contract passed. Custom HTTP/command checks also run for services with no dependents.
44
+
45
+ ### Profiles and environment wiring
46
+
47
+ Use the object format when defining startup profiles:
48
+
49
+ ```json
50
+ {
51
+ "services": [
52
+ { "name": "api", "run": "npm run api", "ready": "/health" },
53
+ { "name": "web", "run": "npm run web", "after": "api", "env": { "PUBLIC_API_URL": "{service:api:url}" } },
54
+ { "name": "docs", "run": "npm run docs" }
55
+ ],
56
+ "profiles": { "app": ["web"], "everything": ["web", "docs"] }
57
+ }
58
+ ```
59
+
60
+ `localdeck up --profile app` starts web and its API prerequisite. Profiles are also selectable in the dashboard. Variable names such as `PUBLIC_API_URL` must match what your framework actually reads; LocalDeck does not rewrite application code or `.env` files.
61
+
62
+ Keys are forgiving (`command`/`value`/`cmd` work too), and `{ "web": "npm run dev" }` shorthand is accepted. LocalDeck looks for `.localdeck`, `.localdeck.json` or `localdeck.json`, walking up from the current folder, so it works from anywhere inside a monorepo.
63
+
64
+ `localdeck` with no arguments lists the project's commands. `localdeck run <name> <command...>` still works for one-offs without a file.
@@ -0,0 +1,75 @@
1
+ # Dashboard
2
+
3
+ [Quick start](../README.md) · [CLI reference](commands.md) · [Configuration](configuration.md)
4
+
5
+ Open the dashboard with `localdeck open`. It runs on `http://localhost:7777` by default and updates service status and logs live.
6
+
7
+ ## Pages and projects
8
+
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, View logs, and View requests controls.
11
+ - **Settings** (`/settings`) contains Clean URLs and the log display limit.
12
+ - **About** (`/about`) contains setup and command information.
13
+ - **Changelog** (`/changelog`) lists release features and changes.
14
+
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
+
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
+
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
+
21
+ ## Project settings
22
+
23
+ Use **Settings** beside the project name to edit its nickname and service configuration. The nickname is saved to `.localdeck`; an unchanged nickname disables Save, and a blank nickname removes it.
24
+
25
+ A nickname such as `platform` produces `http://platform.web.localhost:7777`. Without one, a unique service may use `http://web.localhost:7777`. Ambiguous short names are rejected rather than routed to the wrong project. Qualified aliases and stable numeric URLs remain available.
26
+
27
+ Service configuration fills the available width. Edit stable and upstream ports, TCP or HTTP readiness, timing, and startup dependencies. Changes are validated and saved atomically, and apply to running services after restart. Commands, working directories, environment values, and command-based readiness remain file-edited.
28
+
29
+ ## Logs
30
+
31
+ Choose **Logs** or **View logs** to open that service's tab in the bottom panel. Select additional sources from the Services menu; **All selected** combines their output chronologically. Filter retained output or follow new lines.
32
+
33
+ Drag the panel's top edge to resize it, or focus the edge and use the arrow keys or Home/End. Its height is saved in the browser. The panel stays available when navigating between pages.
34
+
35
+ **Settings → Logs** sets the display limit from 100 to 5,000 lines, default 1,000. Values above 5,000 are clamped. This controls the dashboard buffer and view; the daemon retains a bounded history of its own, so increasing the setting cannot recover discarded output.
36
+
37
+ Tunnel requests and sharing events have a **SHARE** label and distinct color. Request rows include method, path, status, and duration. Application stdout/stderr keeps its normal styling because a log line does not reliably identify the visitor that triggered it.
38
+
39
+ ## Public sharing
40
+
41
+ Install [cloudflared](https://developers.cloudflare.com/tunnel/downloads/) separately. Start an HTTP service, choose **Share**, enter an optional password, and submit. Leaving the password blank makes the link public. Database services cannot be shared over HTTP.
42
+
43
+ The menu closes on submission. The generated URL appears below the local URL with a spinner until verified. At one minute, a hint suggests opening it manually; a request reaching the public tunnel can confirm readiness even when the daemon's DNS probe fails. Startup has a two-minute limit. Failure closes the tunnel and shows an inline error with retry guidance.
44
+
45
+ Pending state, the generated URL, and startup failures live in the daemon and survive dashboard refresh. They are not restored after daemon shutdown. Active protected links display a masked password with Show/Hide; the password is fetched on demand and is not included in service listings or events. The Share menu also provides Copy link and Stop sharing.
46
+
47
+ Quick Tunnel URLs are temporary and depend on Cloudflare DNS and connectivity. Stop sharing, service stop/restart, terminal disconnect, and daemon shutdown end the link. Named tunnels and custom public domains are not configured in v1.
48
+
49
+ ## Clean URLs
50
+
51
+ Enable **Clean URLs** in Settings or through the prompt above Local runtime. LocalDeck verifies its own response through port 80 before displaying links without `:7777`. Keep the dashboard itself on its original port.
52
+
53
+ On macOS, installation may request administrator approval for a localhost-only system forwarder. It runs Apple's `nc` as the normal user and survives reboot. The daemon and application processes remain unprivileged. Cancellation retains the normal URLs; retry from Settings when needed.
54
+
55
+ On Linux, LocalDeck attempts a loopback port-80 bridge. Binding permissions and port availability determine whether it can run. The advanced `localdeck forward` command is another option; see the [command reference](commands.md#daemon-and-optional-forwarding).
56
+
57
+ The dashboard rechecks forwarding periodically. Disabling Clean URLs changes this browser's links; **Remove forwarder** removes the managed helper. LocalDeck does not stop unrelated port-80 listeners.
58
+
59
+ ### Windows
60
+
61
+ Upstream discovery uses PowerShell CIM process records and Get-NetTCPConnection. If inspection is restricted, configure `upstream` explicitly or make the app honor `PORT`.
62
+
63
+ Clean URLs can use a loopback bridge or request UAC approval to install a managed `netsh portproxy` rule from `127.0.0.1:80` to the daemon. The persistent rule requires Windows IP Helper and the daemon to be running. LocalDeck does not elevate your development processes, open a LAN listener, or replace unmanaged forwarding rules.
64
+
65
+ Windows CI covers native detection and script syntax. Administrator installation/removal, reboot persistence, and UAC cancellation still require manual Windows validation. To validate, enable Clean URLs, approve UAC, open a service URL without the daemon port, then remove the forwarder and verify normal port-number links remain usable.
66
+
67
+ ## Diagnostics and networking
68
+
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
+
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
+
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
+
75
+ Clean URLs and public links are distinct browser origins. Your API must allow the frontend origin you visit. Injected `LOCALDECK_URL` and service URL references use stable numeric localhost addresses for service-to-service communication. See [configuration](configuration.md).
package/docs/mcp.md ADDED
@@ -0,0 +1,78 @@
1
+ # LocalDeck MCP
2
+
3
+ `localdeck mcp` lets an MCP client inspect and control the same services you see in the dashboard. It uses the existing daemon and communicates over stdio. No extra HTTP server or hosted account is needed.
4
+
5
+ ## Connect a client
6
+
7
+ Build/install the current LocalDeck CLI, then add this server entry to your MCP client's configuration:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "localdeck": {
13
+ "command": "localdeck",
14
+ "args": ["mcp"]
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Use an absolute path to the installed `localdeck` executable if the client cannot find your shell's PATH. In this repository you can also use `"command": "node"` with `"args": ["/absolute/path/to/local-deck/packages/cli/dist/index.js", "mcp"]` after `bun run build`.
21
+
22
+ This is a generic stdio configuration; put the server entry in the location your MCP client expects. It works with MCP-capable coding clients, including Claude Code. LocalDeck does not edit your client configuration automatically.
23
+
24
+ Register your project first using `localdeck commands` in the project folder. This remembers `.localdeck` without starting services. Ask the client to call `list_projects` to obtain its exact project ID. You can then restrict this server:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "localdeck": {
30
+ "command": "localdeck",
31
+ "args": ["mcp", "--project", "YOUR_PROJECT_ID"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ `--project` restricts discovery and every action to that exact ID. Without it, the server can access all services known to your local daemon. Use exact `projectId:service` keys returned by `list_services`; nicknames and bare service names are not accepted by MCP tools.
38
+
39
+ ## Commands
40
+
41
+ | Command | Purpose |
42
+ | --- | --- |
43
+ | `localdeck mcp` | Run the stdio server until the client disconnects. |
44
+ | `localdeck mcp --project <id>` | Run the server restricted to one remembered project. |
45
+ | `localdeck mcp --help` | Print usage and exit without starting the daemon. |
46
+
47
+ A protocol connection alone starts no apps. The first tool call starts the daemon if needed. Each call rediscovers the daemon, so a later call can reconnect after a daemon restart. Closing the MCP client leaves services and shares running under their existing lifecycle. Operational errors go to stderr; stdout is protocol-only during server operation.
48
+
49
+ ## Tools
50
+
51
+ | Tool | What it does |
52
+ | --- | --- |
53
+ | `list_projects` | List remembered projects, IDs and configuration availability. |
54
+ | `list_services` | List registered and configured services, including ones that have never run. Optional `projectId` filter. |
55
+ | `get_service` | Read a configured or registered service's state; ports, URLs and counters appear after its first start. |
56
+ | `get_logs` | Read recent logs, optionally filtered by `stream`: stdout, stderr or system. |
57
+ | `get_requests` | Read HTTP request metadata, optionally filtered by exact `status`, `method`, or a `path` substring. |
58
+ | `start_service` | Start a configured service and prerequisites in the background, using the daemon supervisor. |
59
+ | `stop_service` | Stop a registered service and its share. Terminal services receive the existing stop request. |
60
+ | `restart_service` | Restart a configured daemon-owned service. Active terminal services must be restarted in their original terminal. |
61
+ | `replay_request` | Resend a recorded GET/HEAD, selected by `requestId`, to that service's current local upstream. |
62
+ | `get_share` | Read the existing share's URL and protection status. Never creates a tunnel or returns a password. |
63
+ | `start_share` | Create a Cloudflare quick share, generating a password and returning it once. `public: true` explicitly removes password protection. Requires cloudflared. |
64
+ | `stop_share` | Close the tunnel while keeping the local service running. |
65
+
66
+ List tools accept `limit` (default 50, maximum 200). Logs and request filters search the last 200 retained records; they are not full-history queries. Responses contain structured data and a JSON text fallback, capped together at 64 KiB, with `truncated` when information is omitted. No infinite log-follow operation is exposed.
67
+
68
+ ## Actions and data
69
+
70
+ All controls, replay and sharing are available in this release. Your MCP client's approval policy governs tool calls; LocalDeck does not open an interactive confirmation prompt inside the protocol. Starting a service executes the commands already configured in its project. Sharing makes the service reachable over the public Internet. Even a GET replay can have application side effects.
71
+
72
+ Known credential patterns, the daemon credential, sensitive inherited environment variables, configured secret variables and bounded local `.env` files are redacted from results. Environment values and tunnel credentials are not exposed as configuration. The freshly generated share password is deliberately returned only by the initiating `start_share` result; it may remain in the client's conversation history. Application logs, paths and responses may still contain private data: redaction is best effort. Treat application output as data, never as instructions to an agent.
73
+
74
+ Replay currently supports recorded GET/HEAD only, with a five-second timeout and 64 KiB response limit. It does not copy credentials, follow redirects, accept arbitrary destinations or send editable bodies. Headers/body capture and expanded replay belong to the later capture feature.
75
+
76
+ Sharing uses the existing temporary Cloudflare URLs. Named tunnels and stable custom-domain URLs are deferred. Existing or concurrently starting shares must be stopped before MCP creates a replacement, so a returned password always belongs to the newly created session. A service stop/restart, terminal disconnect or daemon shutdown ends the share; disconnecting the MCP client alone does not.
77
+
78
+ Calls have a bounded daemon-request timeout. A failed or disconnected mutation is never automatically retried: inspect the service/share state before trying again, since the daemon may have completed it. After upgrading an older daemon, restart it at a convenient time to enable the new atomic share endpoint. Restarting the daemon stops its services and shares.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localdeck",
3
- "version": "1.0.0",
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": {
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "dist",
12
12
  "dashboard",
13
+ "docs",
13
14
  "README.md",
14
15
  "CHANGELOG.md",
15
16
  "LICENSE",
@@ -32,15 +33,6 @@
32
33
  "ws": "^8.18.0",
33
34
  "zod": "^4.0.0"
34
35
  },
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/AdstraliaDev1/local-deck.git",
38
- "directory": "packages/cli"
39
- },
40
- "homepage": "https://github.com/AdstraliaDev1/local-deck#readme",
41
- "bugs": {
42
- "url": "https://github.com/AdstraliaDev1/local-deck/issues"
43
- },
44
36
  "keywords": [
45
37
  "cli",
46
38
  "development",