localdeck 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ import {
17
17
  loadProject,
18
18
  loadProjectFile,
19
19
  processTree,
20
+ resolveProjectSecrets,
21
+ secretOutput,
20
22
  serviceKey,
21
23
  shellCommand,
22
24
  startDaemon,
@@ -27,7 +29,7 @@ import {
27
29
  stripAnsi,
28
30
  waitForReadiness,
29
31
  wrapper_default
30
- } from "./index-3qpxpxwq.js";
32
+ } from "./index-n4sq4z00.js";
31
33
 
32
34
  // src/index.ts
33
35
  import { spawn as spawn2 } from "node:child_process";
@@ -119,15 +121,20 @@ async function startService(opts, overrides = {}) {
119
121
  throw new Error(`working directory does not exist or is not a directory: ${cwd}`);
120
122
  }
121
123
  const daemon = await dependencies.ensureDaemon();
122
- const upstreamPort = opts.upstreamPort ?? await dependencies.getFreePort();
123
- const paint = opts.color ?? c.cyan;
124
- const tag = opts.prefix ? paint(`[${opts.name}] `) : "";
125
124
  const project = opts.project ?? {
126
125
  id: "standalone",
127
126
  name: "standalone",
128
127
  slug: "standalone"
129
128
  };
130
- const commandEnv = await expandRunEnvironment(opts.env, opts.after ?? [], project.id, daemon.apiPort, dependencies.fetch);
129
+ let secretEnv = {};
130
+ const resolveSecrets = dependencies.resolveProjectSecrets ?? resolveProjectSecrets;
131
+ if (opts.secrets?.length && project.id !== "standalone")
132
+ secretEnv = resolveSecrets(project.id, opts.secrets);
133
+ const upstreamPort = opts.upstreamPort ?? await dependencies.getFreePort();
134
+ const paint = opts.color ?? c.cyan;
135
+ const tag = opts.prefix ? paint(`[${opts.name}] `) : "";
136
+ let commandEnv = await expandRunEnvironment(opts.env, opts.after ?? [], project.id, daemon.apiPort, dependencies.fetch);
137
+ commandEnv = { ...commandEnv, ...secretEnv };
131
138
  const ws = dependencies.createWebSocket(`ws://127.0.0.1:${daemon.apiPort}/api/attach`, {
132
139
  headers: { authorization: `Bearer ${daemon.controlToken}` }
133
140
  });
@@ -160,14 +167,16 @@ async function startService(opts, overrides = {}) {
160
167
  });
161
168
  const argv = opts.command.map((a) => a.replaceAll("{port}", String(upstreamPort)));
162
169
  const database = service.serviceType !== undefined && service.serviceType !== "http";
163
- const env = {
170
+ let env = {
164
171
  ...process.env,
165
172
  ...commandEnv,
173
+ ...secretEnv,
166
174
  PORT: String(upstreamPort),
167
175
  LOCALDECK_PORT: String(service.stablePort),
168
176
  LOCALDECK_SERVICE: service.name,
169
177
  ...database ? {} : { LOCALDECK_URL: `http://localhost:${service.stablePort}` }
170
178
  };
179
+ const redactionValues = new Set(Object.values(secretEnv));
171
180
  if (database) {
172
181
  console.log(`${tag}${c.bold("LocalDeck")} ${c.dim("›")} ${paint(service.name)} ${c.dim("→")} ${c.green(`database localhost:${service.upstreamPort}`)} ${c.dim("(published port · no HTTP proxy)")}`);
173
182
  } else {
@@ -177,7 +186,8 @@ async function startService(opts, overrides = {}) {
177
186
  const posix = process.platform !== "win32";
178
187
  let child;
179
188
  const pipe = (stream, name, out) => {
180
- stream.on("data", (chunk) => {
189
+ const output = redactionValues.size ? stream.pipe(secretOutput(redactionValues)) : stream;
190
+ output.on("data", (chunk) => {
181
191
  if (opts.prefix) {
182
192
  const text = stripAnsi(chunk.toString());
183
193
  recentOutput = (recentOutput + text).slice(-16000);
@@ -185,7 +195,7 @@ async function startService(opts, overrides = {}) {
185
195
  } else
186
196
  out.write(chunk);
187
197
  });
188
- streamLines(stream, (line) => {
198
+ streamLines(output, (line) => {
189
199
  for (const part of splitAttachLogLine(stripAnsi(line)))
190
200
  send({ type: "log", stream: name, line: part });
191
201
  });
@@ -294,6 +304,13 @@ async function startService(opts, overrides = {}) {
294
304
  console.error(`${tag}${c.yellow("LocalDeck: lost connection to daemon; the stable port is no longer proxied. Process keeps running.")}`);
295
305
  });
296
306
  function launch() {
307
+ if (retryAttempts > 0 && opts.secrets?.length && project.id !== "standalone") {
308
+ secretEnv = resolveSecrets(project.id, opts.secrets);
309
+ for (const value of Object.values(secretEnv))
310
+ redactionValues.add(value);
311
+ commandEnv = { ...commandEnv, ...secretEnv };
312
+ env = { ...process.env, ...commandEnv, PORT: String(upstreamPort), LOCALDECK_PORT: String(service.stablePort), LOCALDECK_SERVICE: service.name, ...database ? {} : { LOCALDECK_URL: `http://localhost:${service.stablePort}` } };
313
+ }
297
314
  leaderExit = undefined;
298
315
  cleanupPromise = undefined;
299
316
  finalizing = false;
@@ -880,13 +897,13 @@ async function main(argv, overrides = {}) {
880
897
  case "--update":
881
898
  if (rest.length)
882
899
  throw new Error("Usage: localdeck --update");
883
- return (await import("./update-2f1pgdst.js")).updateLocalDeck();
900
+ return (await import("./update-gj01sqte.js")).updateLocalDeck();
884
901
  case "up":
885
902
  return cmdUp(rest, dependencies);
886
903
  case "init":
887
904
  return cmdInit(rest, dependencies);
888
905
  case "mcp":
889
- return (await import("./mcp-tgg2n05x.js")).runMcp(rest);
906
+ return (await import("./mcp-arq8ad4g.js")).runMcp(rest);
890
907
  case "doctor":
891
908
  return cmdDoctor(rest);
892
909
  case "forward":
@@ -956,6 +973,7 @@ function toRunOptions(p, project, remembered, extra = {}) {
956
973
  after: p.after,
957
974
  ready: p.ready,
958
975
  ...p.env ? { env: p.env } : {},
976
+ ...p.secrets ? { secrets: p.secrets } : {},
959
977
  ...p.serviceType ? { serviceType: p.serviceType } : {},
960
978
  ...extra
961
979
  };
@@ -6,8 +6,10 @@ import {
6
6
  __toESM,
7
7
  ensureDaemon,
8
8
  environmentValues,
9
- redactDiagnostic
10
- } from "./index-3qpxpxwq.js";
9
+ listProjectSecrets,
10
+ redactDiagnostic,
11
+ resolveProjectSecrets
12
+ } from "./index-n4sq4z00.js";
11
13
 
12
14
  // ../../node_modules/.bun/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
13
15
  var require_code = __commonJS((exports) => {
@@ -34008,6 +34010,8 @@ function createMcpServer(options) {
34008
34010
  scope(projectId);
34009
34011
  const details = await api2.project(projectId);
34010
34012
  const directories = new Set(details.directory ? [details.directory] : []);
34013
+ const stored = options.projectSecrets ? options.projectSecrets(details.id) : /^[a-f0-9]{16}$/.test(details.id) ? resolveProjectSecrets(details.id, listProjectSecrets(details.id)) : {};
34014
+ secrets.push(...Object.values(stored));
34011
34015
  for (const command of details.commands) {
34012
34016
  for (const [name, value] of Object.entries(command.env ?? {})) {
34013
34017
  if (/password|secret|token|api[_-]?key|credential/i.test(name))
@@ -2,7 +2,7 @@ import {
2
2
  Api,
3
3
  STATE_FILE,
4
4
  findDaemon
5
- } from "./index-3qpxpxwq.js";
5
+ } from "./index-n4sq4z00.js";
6
6
 
7
7
  // src/update.ts
8
8
  import { spawn } from "node:child_process";
@@ -22,7 +22,12 @@ JSON at the project root. `localdeck init` generates one from `package.json`; ed
22
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
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
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.
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.
26
+ - `secrets` — optional list of environment names whose values are managed in Project → Secrets. Names are declared here, while values are stored outside the project at `$LOCALDECK_HOME/secrets/<project-id>.json` (by default `~/.localdeck/secrets/`). Values are plaintext on this computer and never written to `.localdeck`.
27
+
28
+ For example: `{ "name": "web", "run": "npm run dev", "secrets": ["CLOUDFLARE_API", "CLOUDFLARE_ID"] }`. Each machine needs its own secret values. A missing value blocks launch with instructions to add it. Saved changes apply the next time the service starts, including automatic retries; editing a value never restarts a running service.
29
+
30
+ Secrets are stored in plaintext on this computer, outside your project. They are not encrypted. Anyone with access to the file can read them. Values persist across daemon restarts, updates, and reboots. File permissions restrict access where supported; they do not make the file readable only by LocalDeck.
26
31
 
27
32
  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
33
 
package/docs/dashboard.md CHANGED
@@ -26,9 +26,11 @@ A nickname such as `platform` produces `http://platform.web.localhost:7777`. Wit
26
26
 
27
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
28
 
29
+ The Secrets button beside Settings on the project page opens a dedicated Secrets page at `/projects/<project-id>/secrets`. It lists declared names and whether each value is configured. Use Add secret, Replace, or Set value to open the editor popover; click outside or press Escape to dismiss it. Remove deletes a saved value. Password fields start empty; saved values are never shown again. Secrets are stored in plaintext on this computer, outside your project. They are not encrypted. Anyone with access to the file can read them. Changes apply when the service next starts. Secret values are per machine and must be configured separately on each machine.
30
+
29
31
  ## Logs
30
32
 
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.
33
+ 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. Use the trash icon beside a service tab to clear that service’s logs. Clearing keeps the service running and new output continues to appear.
32
34
 
33
35
  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
36
 
@@ -70,7 +72,9 @@ Project diagnostics include configuration, readiness errors, and recent logs. En
70
72
 
71
73
  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
74
 
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.
75
+ 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. The trash icon beside each service tab clears only its requests. Clears apply to connected dashboards and survive refreshes and daemon restarts. **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. Expand **Response** to load the full captured response. Full bodies use a shared 64 MiB cache, which evicts older entries as it fills and resets when the daemon restarts. The inspector identifies expired bodies and incomplete transfers. Original previews remain in saved history. 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.
76
+
77
+ Traffic must pass through a LocalDeck service URL to be captured. Calls directly to Wrangler at `localhost:8787`, or through Worker service bindings, bypass the proxy. Configure your frontend’s API base URL to use the API service’s LocalDeck URL.
74
78
 
75
79
  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.
76
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localdeck",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Stable ports, shareable HTTPS links and request logs for your local dev servers",
6
6
  "bin": {