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.
@@ -3226,6 +3226,22 @@ function normalize(raw, file) {
3226
3226
  Object.defineProperty(env, key, { value: value2, enumerable: true, writable: true, configurable: true });
3227
3227
  }
3228
3228
  }
3229
+ let secrets;
3230
+ if (entry.secrets !== undefined) {
3231
+ if (!Array.isArray(entry.secrets) || !entry.secrets.every((value2) => typeof value2 === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value2))) {
3232
+ throw new Error(`${path2.basename(file)}: secrets must contain valid environment names`);
3233
+ }
3234
+ secrets = [];
3235
+ for (const secret of entry.secrets) {
3236
+ if (secret === "PORT" || secret.startsWith("LOCALDECK_"))
3237
+ throw new Error(`${path2.basename(file)}: ${secret} is reserved by LocalDeck`);
3238
+ if (env && Object.prototype.hasOwnProperty.call(env, secret))
3239
+ throw new Error(`${path2.basename(file)}: ${secret} cannot be both a secret and env setting`);
3240
+ if (secrets.includes(secret))
3241
+ throw new Error(`${path2.basename(file)}: duplicate secret ${secret}`);
3242
+ secrets.push(secret);
3243
+ }
3244
+ }
3229
3245
  out.push({
3230
3246
  name,
3231
3247
  run,
@@ -3236,7 +3252,8 @@ function normalize(raw, file) {
3236
3252
  after: [...new Set(after)],
3237
3253
  ready: parseReadiness(entry.ready ?? (serviceType === "postgres" || serviceType === "redis" ? serviceType : undefined), file, index),
3238
3254
  ...serviceType ? { serviceType } : {},
3239
- ...env ? { env } : {}
3255
+ ...env ? { env } : {},
3256
+ ...secrets ? { secrets } : {}
3240
3257
  });
3241
3258
  }
3242
3259
  validateDependencies(out, file);
@@ -3392,9 +3409,9 @@ function stopOrder(commands) {
3392
3409
  }
3393
3410
 
3394
3411
  // ../daemon/src/index.ts
3395
- import fs8 from "node:fs";
3412
+ import fs9 from "node:fs";
3396
3413
  import crypto2 from "node:crypto";
3397
- import path9 from "node:path";
3414
+ import path10 from "node:path";
3398
3415
 
3399
3416
  // ../daemon/src/api.ts
3400
3417
  import http5 from "node:http";
@@ -3687,34 +3704,55 @@ class CleanUrls {
3687
3704
  }
3688
3705
 
3689
3706
  // ../daemon/src/api.ts
3690
- import fs5 from "node:fs";
3691
- import path6 from "node:path";
3707
+ import fs6 from "node:fs";
3708
+ import path7 from "node:path";
3692
3709
 
3693
3710
  // ../daemon/src/request-body.ts
3694
3711
  import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
3695
3712
  var MAX_BODY_CAPTURE_BYTES = 16384;
3696
3713
 
3697
3714
  class BodyCapture {
3715
+ limit;
3698
3716
  chunks = [];
3699
3717
  retained = 0;
3700
3718
  bytes = 0;
3719
+ tailUsed = 0;
3720
+ constructor(limit = MAX_BODY_CAPTURE_BYTES) {
3721
+ this.limit = limit;
3722
+ }
3701
3723
  append(chunk) {
3702
3724
  this.bytes += chunk.length;
3703
- const count = Math.min(chunk.length, MAX_BODY_CAPTURE_BYTES - this.retained);
3704
- if (count > 0) {
3705
- this.chunks.push(Buffer.from(chunk.subarray(0, count)));
3706
- this.retained += count;
3725
+ const count = Math.min(chunk.length, this.limit - this.retained);
3726
+ let offset = 0;
3727
+ while (offset < count) {
3728
+ let tail = this.chunks.at(-1);
3729
+ if (!tail || this.tailUsed === tail.length) {
3730
+ if (tail && tail.length < 4096) {
3731
+ const grown = Buffer.allocUnsafe(Math.min(4096, this.limit - this.retained + this.tailUsed, Math.max(tail.length * 2, count - offset)));
3732
+ tail.copy(grown, 0, 0, this.tailUsed);
3733
+ this.chunks[this.chunks.length - 1] = tail = grown;
3734
+ } else {
3735
+ tail = Buffer.allocUnsafe(Math.min(4096, this.limit - this.retained, Math.max(64, count - offset)));
3736
+ this.chunks.push(tail);
3737
+ this.tailUsed = 0;
3738
+ }
3739
+ }
3740
+ const size = Math.min(count - offset, tail.length - this.tailUsed);
3741
+ chunk.copy(tail, this.tailUsed, offset, offset + size);
3742
+ this.tailUsed += size;
3743
+ this.retained += size;
3744
+ offset += size;
3707
3745
  }
3708
3746
  }
3709
3747
  snapshot(complete, contentEncoding) {
3710
- let buffer = Buffer.concat(this.chunks, this.retained);
3748
+ let buffer = Buffer.concat(this.chunks.map((chunk, index) => index === this.chunks.length - 1 ? chunk.subarray(0, this.tailUsed) : chunk), this.retained);
3711
3749
  let encoded = Boolean(contentEncoding && contentEncoding !== "identity");
3712
3750
  const truncated = this.bytes > this.retained;
3713
3751
  if (encoded && complete && !truncated && buffer.length) {
3714
3752
  const decode = { gzip: gunzipSync, deflate: inflateSync, br: brotliDecompressSync }[contentEncoding];
3715
3753
  try {
3716
3754
  if (decode) {
3717
- buffer = decode(buffer, { maxOutputLength: MAX_BODY_CAPTURE_BYTES });
3755
+ buffer = decode(buffer, { maxOutputLength: this.limit });
3718
3756
  encoded = false;
3719
3757
  }
3720
3758
  } catch {}
@@ -3786,6 +3824,11 @@ function proxyRequest(req, res, target, hooks) {
3786
3824
  const requestHeaders = captureHeaders(req.rawHeaders);
3787
3825
  const requestBody = new BodyCapture;
3788
3826
  const responseBody = new BodyCapture;
3827
+ const fullResponse = hooks.responseBodies?.start(target.service);
3828
+ const captureResponse = (chunk) => {
3829
+ responseBody.append(chunk);
3830
+ fullResponse?.append(chunk);
3831
+ };
3789
3832
  let responseComplete = false;
3790
3833
  let responseEncoding;
3791
3834
  let responseHeaders;
@@ -3795,6 +3838,7 @@ function proxyRequest(req, res, target, hooks) {
3795
3838
  if (recorded)
3796
3839
  return;
3797
3840
  recorded = true;
3841
+ fullResponse?.finish(responseComplete, responseEncoding);
3798
3842
  hooks.onRequest({
3799
3843
  ts: started,
3800
3844
  service: target.service,
@@ -3808,14 +3852,15 @@ function proxyRequest(req, res, target, hooks) {
3808
3852
  requestHeaders,
3809
3853
  responseHeaders,
3810
3854
  requestBody: requestBody.snapshot(req.readableEnded, req.headers["content-encoding"]),
3811
- responseBody: responseBody.snapshot(responseComplete, responseEncoding)
3855
+ responseBody: responseBody.snapshot(responseComplete, responseEncoding),
3856
+ responseBodyId: fullResponse?.id
3812
3857
  });
3813
3858
  };
3814
3859
  if (!target.upstreamReady) {
3815
3860
  responseHeaders = captureHeaders(["content-type", "text/html; charset=utf-8", "retry-after", "2"]);
3816
3861
  res.writeHead(503, { "content-type": "text/html; charset=utf-8", "retry-after": "2" });
3817
3862
  const body = waitingPage(target);
3818
- responseBody.append(Buffer.from(body));
3863
+ captureResponse(Buffer.from(body));
3819
3864
  responseComplete = true;
3820
3865
  res.end(body);
3821
3866
  record(503, "upstream not ready");
@@ -3847,7 +3892,7 @@ function proxyRequest(req, res, target, hooks) {
3847
3892
  res.writeHead(up.statusCode ?? 502, up.statusMessage, headers);
3848
3893
  up.on("data", (chunk) => {
3849
3894
  bytes += chunk.length;
3850
- responseBody.append(chunk);
3895
+ captureResponse(chunk);
3851
3896
  });
3852
3897
  up.pipe(res);
3853
3898
  });
@@ -3857,7 +3902,7 @@ function proxyRequest(req, res, target, hooks) {
3857
3902
  if (!res.headersSent && !res.destroyed) {
3858
3903
  res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
3859
3904
  const body = `LocalDeck: upstream ${target.service} (${target.upstreamHost}:${target.upstreamPort}) error: ${err.code ?? err.message}`;
3860
- responseBody.append(Buffer.from(body));
3905
+ captureResponse(Buffer.from(body));
3861
3906
  responseComplete = true;
3862
3907
  res.end(body);
3863
3908
  } else
@@ -4423,13 +4468,13 @@ class ShareManager {
4423
4468
  res.writeHead(401, { "content-type": "text/html; charset=utf-8" }).end(loginPage(name, false));
4424
4469
  return;
4425
4470
  }
4426
- proxyRequest(req, res, target, { onRequest: (r) => this.store.request(r) });
4471
+ proxyRequest(req, res, target, { responseBodies: this.store.responseBodies, onRequest: (r) => this.store.request(r) });
4427
4472
  });
4428
4473
  gate.on("upgrade", (req, socket, head) => {
4429
4474
  const target = this.registry.target(key, "share");
4430
4475
  if (!target || !authed(req))
4431
4476
  return socket.destroy();
4432
- proxyUpgrade(req, socket, head, target, { onRequest: (r) => this.store.request(r) });
4477
+ proxyUpgrade(req, socket, head, target, { responseBodies: this.store.responseBodies, onRequest: (r) => this.store.request(r) });
4433
4478
  });
4434
4479
  const sockets = new Set;
4435
4480
  gate.on("connection", (socket) => {
@@ -5005,6 +5050,98 @@ function replayRequest(service, record, options = {}) {
5005
5050
  });
5006
5051
  }
5007
5052
 
5053
+ // ../daemon/src/project-secrets.ts
5054
+ import fs5 from "node:fs";
5055
+ import path6 from "node:path";
5056
+ var ID = /^[a-f0-9]{16}$/;
5057
+ var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5058
+
5059
+ class ProjectSecretError extends Error {
5060
+ }
5061
+ function validateName(name) {
5062
+ if (!NAME.test(name) || name === "PORT" || name.startsWith("LOCALDECK_")) {
5063
+ throw new ProjectSecretError("invalid secret name");
5064
+ }
5065
+ }
5066
+ function createProjectSecretStore(home = HOME_DIR) {
5067
+ const directory = path6.join(home, "secrets");
5068
+ function secretPath(projectId2) {
5069
+ if (!ID.test(projectId2))
5070
+ throw new ProjectSecretError("invalid project ID");
5071
+ return path6.join(directory, `${projectId2}.json`);
5072
+ }
5073
+ function read(projectId2) {
5074
+ const file = secretPath(projectId2);
5075
+ let text;
5076
+ try {
5077
+ text = fs5.readFileSync(file, "utf8");
5078
+ } catch (error) {
5079
+ if (error.code === "ENOENT")
5080
+ return Object.create(null);
5081
+ throw new ProjectSecretError("Could not read stored secrets");
5082
+ }
5083
+ let parsed;
5084
+ try {
5085
+ parsed = JSON.parse(text);
5086
+ } catch {
5087
+ throw new ProjectSecretError("stored secrets are corrupt");
5088
+ }
5089
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5090
+ throw new ProjectSecretError("stored secrets are corrupt");
5091
+ }
5092
+ const values = Object.create(null);
5093
+ for (const [name, value] of Object.entries(parsed)) {
5094
+ validateName(name);
5095
+ if (typeof value !== "string" || !value || value.includes("\x00")) {
5096
+ throw new ProjectSecretError("stored secrets are corrupt");
5097
+ }
5098
+ values[name] = value;
5099
+ }
5100
+ return values;
5101
+ }
5102
+ function write(projectId2, values) {
5103
+ const file = secretPath(projectId2);
5104
+ fs5.mkdirSync(directory, { recursive: true, mode: 448 });
5105
+ if (process.platform !== "win32")
5106
+ fs5.chmodSync(directory, 448);
5107
+ writePrivateJson(file, values);
5108
+ }
5109
+ function listProjectSecrets(projectId2) {
5110
+ return Object.keys(read(projectId2)).sort();
5111
+ }
5112
+ function setProjectSecret(projectId2, name, value) {
5113
+ validateName(name);
5114
+ if (typeof value !== "string" || !value || value.includes("\x00")) {
5115
+ throw new ProjectSecretError("secret value must be a nonempty string without NUL");
5116
+ }
5117
+ const values = read(projectId2);
5118
+ values[name] = value;
5119
+ write(projectId2, values);
5120
+ }
5121
+ function deleteProjectSecret(projectId2, name) {
5122
+ validateName(name);
5123
+ const values = read(projectId2);
5124
+ delete values[name];
5125
+ write(projectId2, values);
5126
+ }
5127
+ function resolveProjectSecrets(projectId2, names) {
5128
+ const output = Object.create(null);
5129
+ if (!names.length)
5130
+ return output;
5131
+ const values = read(projectId2);
5132
+ for (const name of names) {
5133
+ validateName(name);
5134
+ if (!Object.prototype.hasOwnProperty.call(values, name)) {
5135
+ throw new ProjectSecretError(`Add ${name} in Project → Secrets.`);
5136
+ }
5137
+ output[name] = values[name];
5138
+ }
5139
+ return output;
5140
+ }
5141
+ return { listProjectSecrets, setProjectSecret, deleteProjectSecret, resolveProjectSecrets };
5142
+ }
5143
+ var { listProjectSecrets, setProjectSecret, deleteProjectSecret, resolveProjectSecrets } = createProjectSecretStore();
5144
+
5008
5145
  // ../daemon/src/api.ts
5009
5146
  var MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".json": "application/json", ".png": "image/png", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2" };
5010
5147
  var MAX_BODY_BYTES = 65536;
@@ -5162,7 +5299,7 @@ function localRequestError(req, allowedBrowserOrigins) {
5162
5299
  function createApiServer(deps) {
5163
5300
  const { registry, store, shares } = deps;
5164
5301
  const allowedBrowserOrigins = validatedBrowserOrigins(deps.allowedBrowserOrigins);
5165
- const hooks = { onRequest: (r) => store.request(r) };
5302
+ const hooks = { responseBodies: store.responseBodies, onRequest: (r) => store.request(r) };
5166
5303
  const forwarderIdentity = randomUUID();
5167
5304
  const forwarder = new CleanUrls(() => {
5168
5305
  const address = server.address();
@@ -5216,6 +5353,8 @@ function createApiServer(deps) {
5216
5353
  } catch (error) {
5217
5354
  if (error instanceof HttpError)
5218
5355
  return json(res, error.status, { error: error.message });
5356
+ if (error instanceof ProjectSecretError)
5357
+ return json(res, 422, { error: error.message });
5219
5358
  if (error instanceof ProjectError)
5220
5359
  return json(res, projectStatus(error.code), { error: error.message });
5221
5360
  return json(res, 500, { error: "internal server error" });
@@ -5354,6 +5493,31 @@ async function handleApi(req, res, url, parts, deps) {
5354
5493
  }
5355
5494
  if (parts.length >= 2 && parts[1] !== "remember") {
5356
5495
  const projectId2 = parts[1];
5496
+ if (parts.length === 3 && parts[2] === "secrets" && method === "GET") {
5497
+ const project = requireRememberedProject(projects, projectId2);
5498
+ if (!project.available)
5499
+ throw new ProjectError("invalid", project.error ?? "project configuration is invalid");
5500
+ const list = deps.listProjectSecrets ?? listProjectSecrets;
5501
+ const declared = project.commands.flatMap((command) => command.secrets ?? []);
5502
+ const stored = new Set(list(projectId2));
5503
+ const names = [...new Set([...declared, ...stored])].sort();
5504
+ return json(res, 200, { secrets: names.map((name) => ({ name, configured: stored.has(name) })) });
5505
+ }
5506
+ if (parts.length === 4 && parts[2] === "secrets" && (method === "PUT" || method === "DELETE")) {
5507
+ requireMutationAllowed(deps);
5508
+ const project = requireRememberedProject(projects, projectId2);
5509
+ if (!project.available)
5510
+ throw new ProjectError("invalid", project.error ?? "project configuration is invalid");
5511
+ const name = parts[3];
5512
+ if (method === "PUT") {
5513
+ const body = await readBody(req);
5514
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length !== 1 || typeof body.value !== "string")
5515
+ throw new HttpError(400, 'request body must contain only a string "value"');
5516
+ (deps.setProjectSecret ?? setProjectSecret)(projectId2, name, body.value);
5517
+ } else
5518
+ (deps.deleteProjectSecret ?? deleteProjectSecret)(projectId2, name);
5519
+ return json(res, 200, { ok: true });
5520
+ }
5357
5521
  if (parts.length === 3 && parts[2] === "nickname" && method === "PATCH") {
5358
5522
  requireMutationAllowed(deps);
5359
5523
  const body = await readBody(req);
@@ -5369,7 +5533,9 @@ async function handleApi(req, res, url, parts, deps) {
5369
5533
  const project = projects.details(projectId2);
5370
5534
  const services = registry.list(projectId2);
5371
5535
  const logs = services.flatMap((service) => store.logs(service.key, 100)).sort((a, b) => a.ts - b.ts);
5372
- return json(res, 200, { report: diagnosticReport(project, services, logs, deps.version, [deps.controlToken]) });
5536
+ const names = (deps.listProjectSecrets ?? listProjectSecrets)(projectId2);
5537
+ const secretValues = Object.values((deps.resolveProjectSecrets ?? resolveProjectSecrets)(projectId2, names));
5538
+ return json(res, 200, { report: diagnosticReport(project, services, logs, deps.version, [deps.controlToken, ...secretValues]) });
5373
5539
  }
5374
5540
  if (parts.length === 4 && parts[2] === "profiles" && method === "POST") {
5375
5541
  requireMutationAllowed(deps);
@@ -5466,6 +5632,16 @@ async function handleApi(req, res, url, parts, deps) {
5466
5632
  return json(res, 404, { error: `unknown service "${reference}"` });
5467
5633
  const { key, service: svc } = resolved;
5468
5634
  const sub = parts[2];
5635
+ if (sub === "response" && method === "GET") {
5636
+ const record = store.requests(key, 5000).find((request) => request.id === url.searchParams.get("requestId"));
5637
+ if (!record)
5638
+ throw new HttpError(404, "Recorded request is no longer available");
5639
+ const captured = record.responseBodyId ? store.responseBodies.get(record.responseBodyId, key) : undefined;
5640
+ if (!captured)
5641
+ throw new HttpError(404, "Full response is no longer cached. Make a new request through the LocalDeck URL. The cache holds up to 64 MiB and resets when the daemon restarts.");
5642
+ res.setHeader("cache-control", "no-store");
5643
+ return json(res, 200, captured);
5644
+ }
5469
5645
  if (sub === "replay" && method === "POST") {
5470
5646
  requireMutationAllowed(deps);
5471
5647
  const body = await readBody(req);
@@ -5483,6 +5659,15 @@ async function handleApi(req, res, url, parts, deps) {
5483
5659
  }
5484
5660
  if (!sub && method === "GET")
5485
5661
  return json(res, 200, svc);
5662
+ if ((sub === "logs" || sub === "requests") && method === "DELETE") {
5663
+ requireMutationAllowed(deps);
5664
+ return json(res, 200, await store.clearHistory(key, sub));
5665
+ }
5666
+ if ((sub === "logs" || sub === "requests") && method === "GET") {
5667
+ const boundary = store.historyBoundary(key, sub);
5668
+ if (boundary)
5669
+ res.setHeader("x-localdeck-history-boundary", boundary);
5670
+ }
5486
5671
  if (sub === "logs" && method === "GET")
5487
5672
  return json(res, 200, store.logs(key, readLimit(url)));
5488
5673
  if (sub === "requests" && method === "GET")
@@ -5537,17 +5722,17 @@ function serveStatic(pathname, res, dir) {
5537
5722
  res.end("<h1>LocalDeck daemon</h1><p>Dashboard not built. Run <code>bun run build</code> in the repo. API is at <a href=/api/services>/api/services</a>.</p>");
5538
5723
  return;
5539
5724
  }
5540
- let file = path6.join(dir, path6.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
5725
+ let file = path7.join(dir, path7.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
5541
5726
  if (!file.startsWith(dir))
5542
- file = path6.join(dir, "index.html");
5543
- if (!fs5.existsSync(file) || fs5.statSync(file).isDirectory())
5544
- file = path6.join(dir, "index.html");
5545
- const hashedAsset = path6.dirname(file) === path6.join(dir, "assets") && /-[A-Za-z0-9_-]{8,}\.[a-z0-9]+$/i.test(path6.basename(file));
5727
+ file = path7.join(dir, "index.html");
5728
+ if (!fs6.existsSync(file) || fs6.statSync(file).isDirectory())
5729
+ file = path7.join(dir, "index.html");
5730
+ const hashedAsset = path7.dirname(file) === path7.join(dir, "assets") && /-[A-Za-z0-9_-]{8,}\.[a-z0-9]+$/i.test(path7.basename(file));
5546
5731
  res.writeHead(200, {
5547
- "content-type": MIME[path6.extname(file)] ?? "application/octet-stream",
5732
+ "content-type": MIME[path7.extname(file)] ?? "application/octet-stream",
5548
5733
  "cache-control": hashedAsset ? "public, max-age=31536000, immutable" : "no-cache"
5549
5734
  });
5550
- fs5.createReadStream(file).pipe(res);
5735
+ fs6.createReadStream(file).pipe(res);
5551
5736
  }
5552
5737
  function handleAttach(ws, deps) {
5553
5738
  const { registry, store } = deps;
@@ -5758,7 +5943,7 @@ function buildRegistration(message, deps) {
5758
5943
  if (command.serviceType && command.serviceType !== "http" && (message.upstreamPort !== command.upstream || message.stablePort !== undefined)) {
5759
5944
  throw new Error("database registration must use its configured published upstream port");
5760
5945
  }
5761
- const cwd = command.cwd ? path6.resolve(project.directory, command.cwd) : project.directory;
5946
+ const cwd = command.cwd ? path7.resolve(project.directory, command.cwd) : project.directory;
5762
5947
  requireDirectory(cwd);
5763
5948
  return {
5764
5949
  key: serviceKey(project.id, command.name),
@@ -5778,7 +5963,7 @@ function buildRegistration(message, deps) {
5778
5963
  }
5779
5964
  function requireDirectory(cwd) {
5780
5965
  try {
5781
- if (!fs5.statSync(cwd).isDirectory())
5966
+ if (!fs6.statSync(cwd).isDirectory())
5782
5967
  throw new Error("not a directory");
5783
5968
  } catch {
5784
5969
  throw new Error(`working directory does not exist or is not a directory: ${cwd}`);
@@ -6038,7 +6223,7 @@ class Registry {
6038
6223
  res.writeHead(503).end("LocalDeck: service stopped");
6039
6224
  return;
6040
6225
  }
6041
- proxyRequest(req, res, t, { onRequest: (r) => this.store.request(r) });
6226
+ proxyRequest(req, res, t, { responseBodies: this.store.responseBodies, onRequest: (r) => this.store.request(r) });
6042
6227
  });
6043
6228
  entry.sockets = new Set;
6044
6229
  entry.server.on("connection", (socket) => {
@@ -6049,7 +6234,7 @@ class Registry {
6049
6234
  const t = this.target(key);
6050
6235
  if (!t)
6051
6236
  return socket.destroy();
6052
- proxyUpgrade(req, socket, head, t, { onRequest: (r) => this.store.request(r) });
6237
+ proxyUpgrade(req, socket, head, t, { responseBodies: this.store.responseBodies, onRequest: (r) => this.store.request(r) });
6053
6238
  });
6054
6239
  await new Promise((resolve, reject) => {
6055
6240
  entry.server.once("error", reject);
@@ -6243,8 +6428,86 @@ class Registry {
6243
6428
  }
6244
6429
 
6245
6430
  // ../daemon/src/store.ts
6246
- import fs6 from "node:fs";
6247
- import path7 from "node:path";
6431
+ import fs7 from "node:fs";
6432
+
6433
+ // ../daemon/src/history.ts
6434
+ function isAfterHistoryBoundary(id, boundary) {
6435
+ if (id.startsWith("legacy-"))
6436
+ return false;
6437
+ const [started, sequence] = id.split("-").map((value) => Number.parseInt(value, 36));
6438
+ const [boundaryStarted, boundarySequence] = boundary.split("-").map((value) => Number.parseInt(value, 36));
6439
+ return started > boundaryStarted || started === boundaryStarted && sequence > boundarySequence;
6440
+ }
6441
+
6442
+ // ../daemon/src/response-bodies.ts
6443
+ import { randomUUID as randomUUID2 } from "node:crypto";
6444
+ var RESPONSE_CACHE_BYTES = 64 * 1024 * 1024;
6445
+ var MAX_CAPTURES = 5000;
6446
+
6447
+ class ResponseBodies {
6448
+ capacity;
6449
+ entries = new Map;
6450
+ bytes = 0;
6451
+ constructor(capacity = RESPONSE_CACHE_BYTES) {
6452
+ this.capacity = capacity;
6453
+ }
6454
+ start(service) {
6455
+ const id = randomUUID2();
6456
+ while (this.entries.size >= MAX_CAPTURES)
6457
+ this.remove(this.entries.keys().next().value);
6458
+ this.entries.set(id, { service, body: new BodyCapture(this.capacity), bytes: 0, finished: false, complete: false });
6459
+ return {
6460
+ id,
6461
+ append: (chunk) => {
6462
+ const entry = this.entries.get(id);
6463
+ if (!entry || entry.finished)
6464
+ return;
6465
+ if (entry.bytes + chunk.length > this.capacity) {
6466
+ this.remove(id);
6467
+ return;
6468
+ }
6469
+ while (this.bytes + chunk.length > this.capacity) {
6470
+ const oldest = this.entries.keys().next().value;
6471
+ this.remove(oldest);
6472
+ if (oldest === id)
6473
+ return;
6474
+ }
6475
+ entry.body.append(chunk);
6476
+ entry.bytes += chunk.length;
6477
+ this.bytes += chunk.length;
6478
+ },
6479
+ finish: (complete, encoding) => {
6480
+ const entry = this.entries.get(id);
6481
+ if (entry) {
6482
+ entry.finished = true;
6483
+ entry.complete = complete;
6484
+ entry.encoding = encoding;
6485
+ }
6486
+ }
6487
+ };
6488
+ }
6489
+ get(id, service) {
6490
+ const entry = this.entries.get(id);
6491
+ if (!entry?.finished || entry.service !== service)
6492
+ return;
6493
+ return entry.body.snapshot(entry.complete, entry.encoding);
6494
+ }
6495
+ clear(service) {
6496
+ for (const [id, entry] of this.entries)
6497
+ if (!service || entry.service === service)
6498
+ this.remove(id);
6499
+ }
6500
+ remove(id) {
6501
+ const entry = this.entries.get(id);
6502
+ if (entry) {
6503
+ this.bytes -= entry.bytes;
6504
+ this.entries.delete(id);
6505
+ }
6506
+ }
6507
+ }
6508
+
6509
+ // ../daemon/src/store.ts
6510
+ import path8 from "node:path";
6248
6511
  import { createHash as createHash2 } from "node:crypto";
6249
6512
  import { EventEmitter } from "node:events";
6250
6513
  import { finished } from "node:stream/promises";
@@ -6288,6 +6551,7 @@ function emptyStats() {
6288
6551
  }
6289
6552
 
6290
6553
  class Store extends EventEmitter {
6554
+ responseBodies = new ResponseBodies;
6291
6555
  buckets = new Map;
6292
6556
  logsDir;
6293
6557
  daemonStartedAt;
@@ -6302,12 +6566,12 @@ class Store extends EventEmitter {
6302
6566
  this.logsDir = options.logsDir ?? LOGS_DIR;
6303
6567
  this.daemonStartedAt = options.daemonStartedAt ?? Date.now();
6304
6568
  this.closeAfterMs = options.closeAfterMs ?? DEFAULT_CLOSE_AFTER_MS;
6305
- this.streamFactory = options.streamFactory ?? ((file, streamOptions) => fs6.createWriteStream(file, streamOptions));
6569
+ this.streamFactory = options.streamFactory ?? ((file, streamOptions) => fs7.createWriteStream(file, streamOptions));
6306
6570
  this.maxFileBytes = Math.max(1, options.maxFileBytes ?? 5 * 1024 * 1024);
6307
6571
  if (options.logsDir === undefined)
6308
6572
  ensureDirs();
6309
6573
  else
6310
- fs6.mkdirSync(this.logsDir, { recursive: true });
6574
+ fs7.mkdirSync(this.logsDir, { recursive: true });
6311
6575
  }
6312
6576
  nextId() {
6313
6577
  return `${this.daemonStartedAt.toString(36)}-${(++this.sequence).toString(36)}`;
@@ -6318,19 +6582,21 @@ class Store extends EventEmitter {
6318
6582
  }
6319
6583
  historyFile(service, type) {
6320
6584
  const suffix = type === "log" ? "log.jsonl" : "requests.jsonl";
6321
- return path7.join(this.logsDir, `${encodeURIComponent(service)}.${suffix}`);
6585
+ return path8.join(this.logsDir, `${encodeURIComponent(service)}.${suffix}`);
6322
6586
  }
6323
6587
  legacyHistoryFile(service, type) {
6324
6588
  const safe = service.replace(/[^a-z0-9_-]/gi, "_");
6325
6589
  const suffix = type === "log" ? "log.jsonl" : "requests.jsonl";
6326
- return path7.join(this.logsDir, `${safe}.${suffix}`);
6590
+ return path8.join(this.logsDir, `${safe}.${suffix}`);
6327
6591
  }
6328
6592
  loadHistory(file, type, service, ring, legacyService, budget = this.maxFileBytes) {
6593
+ const marker2 = `${this.historyFile(service, type)}.cleared`;
6594
+ const boundary = fs7.existsSync(marker2) ? fs7.readFileSync(marker2, "utf8") : undefined;
6329
6595
  let fd;
6330
6596
  let size;
6331
6597
  try {
6332
- fd = fs6.openSync(file, "r");
6333
- size = fs6.fstatSync(fd).size;
6598
+ fd = fs7.openSync(file, "r");
6599
+ size = fs7.fstatSync(fd).size;
6334
6600
  } catch (error) {
6335
6601
  if (error.code === "ENOENT")
6336
6602
  return 0;
@@ -6341,9 +6607,9 @@ class Store extends EventEmitter {
6341
6607
  const bytes = Buffer.alloc(count);
6342
6608
  try {
6343
6609
  if (count)
6344
- fs6.readSync(fd, bytes, 0, count, start);
6610
+ fs7.readSync(fd, bytes, 0, count, start);
6345
6611
  } finally {
6346
- fs6.closeSync(fd);
6612
+ fs7.closeSync(fd);
6347
6613
  }
6348
6614
  let lines = bytes.toString("binary").split(`
6349
6615
  `);
@@ -6363,7 +6629,8 @@ class Store extends EventEmitter {
6363
6629
  const input = parsed;
6364
6630
  if (input.service === service || input.service === legacyService) {
6365
6631
  const id = typeof input.id === "string" ? input.id : this.compatibilityId(type, service, offset, serialized);
6366
- ring.push({ ...input, service, id });
6632
+ if (!boundary || isAfterHistoryBoundary(id, boundary))
6633
+ ring.push({ ...input, service, id });
6367
6634
  }
6368
6635
  }
6369
6636
  } catch {}
@@ -6405,14 +6672,14 @@ class Store extends EventEmitter {
6405
6672
  const rotatedReq = `${reqFile}.1`;
6406
6673
  const logCurrentSize = (() => {
6407
6674
  try {
6408
- return fs6.statSync(logFile).size;
6675
+ return fs7.statSync(logFile).size;
6409
6676
  } catch {
6410
6677
  return 0;
6411
6678
  }
6412
6679
  })();
6413
6680
  const reqCurrentSize = (() => {
6414
6681
  try {
6415
- return fs6.statSync(reqFile).size;
6682
+ return fs7.statSync(reqFile).size;
6416
6683
  } catch {
6417
6684
  return 0;
6418
6685
  }
@@ -6479,6 +6746,42 @@ class Store extends EventEmitter {
6479
6746
  requests(service, limit = 200) {
6480
6747
  return this.bucket(service).requests.last(limit);
6481
6748
  }
6749
+ historyBoundary(service, kind) {
6750
+ const marker2 = `${this.historyFile(service, kind === "logs" ? "log" : "request")}.cleared`;
6751
+ return fs7.existsSync(marker2) ? fs7.readFileSync(marker2, "utf8") : undefined;
6752
+ }
6753
+ async clearHistory(service, kind) {
6754
+ if (this.closing)
6755
+ throw new Error("History is unavailable during shutdown");
6756
+ const bucket = this.bucket(service);
6757
+ const state = kind === "logs" ? bucket.logFile : bucket.reqFile;
6758
+ const beforeId = this.nextId();
6759
+ const marker2 = `${state.file}.cleared`;
6760
+ fs7.writeFileSync(`${marker2}.tmp`, beforeId, { mode: 384 });
6761
+ fs7.renameSync(`${marker2}.tmp`, marker2);
6762
+ bucket[kind].clear();
6763
+ if (kind === "requests")
6764
+ this.responseBodies.clear(service);
6765
+ const operation = state.queue.then(async () => {
6766
+ await this.closeStream(state.stream);
6767
+ fs7.writeFileSync(state.file, "", { mode: 384 });
6768
+ fs7.rmSync(`${state.file}.1`, { force: true });
6769
+ state.bytes = 0;
6770
+ state.stream = this.streamFactory(state.file, { flags: "a", mode: 384, highWaterMark: 65536 });
6771
+ state.disabled = false;
6772
+ state.stream.on("error", () => {
6773
+ state.disabled = true;
6774
+ this.reportPersistenceFailure(service, bucket, "history");
6775
+ });
6776
+ });
6777
+ state.queue = operation.catch(() => {
6778
+ state.disabled = true;
6779
+ this.reportPersistenceFailure(service, bucket, "history clear");
6780
+ });
6781
+ const event = this.emitEvent({ type: "history:cleared", service, kind, beforeId });
6782
+ await operation;
6783
+ return event;
6784
+ }
6482
6785
  emitEvent(ev) {
6483
6786
  const event = { ...ev, eventId: this.nextId() };
6484
6787
  this.emit("event", event);
@@ -6487,6 +6790,7 @@ class Store extends EventEmitter {
6487
6790
  close() {
6488
6791
  if (!this.closePromise) {
6489
6792
  this.closing = true;
6793
+ this.responseBodies.clear();
6490
6794
  const buckets = [...this.buckets.values()];
6491
6795
  this.closePromise = (async () => {
6492
6796
  const states = buckets.flatMap((bucket) => [bucket.logFile, bucket.reqFile]);
@@ -6548,12 +6852,12 @@ class Store extends EventEmitter {
6548
6852
  } });
6549
6853
  }
6550
6854
  try {
6551
- if (fs6.existsSync(file))
6552
- fs6.chmodSync(file, 384);
6855
+ if (fs7.existsSync(file))
6856
+ fs7.chmodSync(file, 384);
6553
6857
  } catch {}
6554
6858
  const state = { stream, file, bytes: (() => {
6555
6859
  try {
6556
- return fs6.statSync(file).size;
6860
+ return fs7.statSync(file).size;
6557
6861
  } catch {
6558
6862
  return 0;
6559
6863
  }
@@ -6627,8 +6931,8 @@ class Store extends EventEmitter {
6627
6931
  }
6628
6932
  const rotated = `${state.file}.1`;
6629
6933
  try {
6630
- fs6.rmSync(rotated, { force: true });
6631
- fs6.renameSync(state.file, rotated);
6934
+ fs7.rmSync(rotated, { force: true });
6935
+ fs7.renameSync(state.file, rotated);
6632
6936
  } catch {
6633
6937
  state.disabled = true;
6634
6938
  this.reportPersistenceFailure(service, bucket, "history rotation");
@@ -6636,7 +6940,7 @@ class Store extends EventEmitter {
6636
6940
  }
6637
6941
  state.stream = this.streamFactory(state.file, { flags: "a", mode: 384, highWaterMark: 65536 });
6638
6942
  try {
6639
- fs6.chmodSync(state.file, 384);
6943
+ fs7.chmodSync(state.file, 384);
6640
6944
  } catch {}
6641
6945
  state.bytes = 0;
6642
6946
  state.stream.on("error", () => {
@@ -6682,8 +6986,8 @@ function settlesWithin2(promise, timeoutMs) {
6682
6986
 
6683
6987
  // ../daemon/src/supervisor.ts
6684
6988
  import { spawn as spawn3 } from "node:child_process";
6685
- import path8 from "node:path";
6686
- import fs7 from "node:fs";
6989
+ import path9 from "node:path";
6990
+ import fs8 from "node:fs";
6687
6991
 
6688
6992
  // ../daemon/src/startup-guidance.ts
6689
6993
  function startupGuidance(message) {
@@ -6737,7 +7041,7 @@ async function waitForReadiness(command, getService, options = {}) {
6737
7041
  await delay2(Math.min(command.ready.intervalMs, afterProbe), command.name, options.signal);
6738
7042
  }
6739
7043
  }
6740
- function probeHttp(service, path8, expectedStatus, remainingMs, signal) {
7044
+ function probeHttp(service, path9, expectedStatus, remainingMs, signal) {
6741
7045
  return new Promise((resolve, reject) => {
6742
7046
  if (signal?.aborted)
6743
7047
  return reject(new Error(`${service.name} readiness cancelled`));
@@ -6755,7 +7059,7 @@ function probeHttp(service, path8, expectedStatus, remainingMs, signal) {
6755
7059
  const request = http7.request({
6756
7060
  host: service.upstreamHost,
6757
7061
  port: service.upstreamPort,
6758
- path: path8,
7062
+ path: path9,
6759
7063
  method: "GET",
6760
7064
  timeout: Math.max(1, Math.min(remainingMs, 2000))
6761
7065
  }, (response) => {
@@ -6859,6 +7163,81 @@ function assertNotCancelled(name, signal) {
6859
7163
  throw new Error(`${name} readiness cancelled`);
6860
7164
  }
6861
7165
 
7166
+ // ../daemon/src/secret-output.ts
7167
+ import { Transform } from "node:stream";
7168
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
7169
+ import { stripVTControlCharacters } from "node:util";
7170
+ function secretOutput(values) {
7171
+ const decoder = new StringDecoder2("utf8");
7172
+ let pending = "";
7173
+ let control = "text";
7174
+ function normalize2(text) {
7175
+ if (!values.size)
7176
+ return text;
7177
+ let result = "";
7178
+ for (const character of text) {
7179
+ if (control === "text") {
7180
+ if (character === "\x1B")
7181
+ control = "escape";
7182
+ else if (character === "›")
7183
+ control = "csi";
7184
+ else
7185
+ result += character;
7186
+ } else if (control === "escape") {
7187
+ control = character === "[" ? "csi" : character === "]" ? "osc" : character >= " " && character <= "/" ? "intermediate" : "text";
7188
+ } else if (control === "intermediate") {
7189
+ if (character >= "0" && character <= "~")
7190
+ control = "text";
7191
+ } else if (control === "csi") {
7192
+ if (character >= "@" && character <= "~")
7193
+ control = "text";
7194
+ } else if (control === "osc") {
7195
+ if (character === "\x07")
7196
+ control = "text";
7197
+ else if (character === "\x1B")
7198
+ control = "osc-escape";
7199
+ } else {
7200
+ control = character === "\\" || character === "\x07" ? "text" : character === "\x1B" ? "osc-escape" : "osc";
7201
+ }
7202
+ }
7203
+ return result;
7204
+ }
7205
+ function consume(final) {
7206
+ const secrets = [...values].map(stripVTControlCharacters).filter(Boolean).sort((a, b) => b.length - a.length);
7207
+ if (!secrets.length) {
7208
+ const output2 = pending;
7209
+ pending = "";
7210
+ return output2;
7211
+ }
7212
+ let output = "";
7213
+ let index = 0;
7214
+ while (index < pending.length) {
7215
+ const remaining = pending.slice(index);
7216
+ if (!final && secrets.some((value) => value.length > remaining.length && value.startsWith(remaining)))
7217
+ break;
7218
+ const match = secrets.find((value) => pending.startsWith(value, index));
7219
+ if (match) {
7220
+ output += "[REDACTED]";
7221
+ index += match.length;
7222
+ } else {
7223
+ output += pending[index++];
7224
+ }
7225
+ }
7226
+ pending = pending.slice(index);
7227
+ return output;
7228
+ }
7229
+ return new Transform({
7230
+ transform(chunk, _encoding, callback) {
7231
+ pending += normalize2(decoder.write(chunk));
7232
+ callback(null, consume(false));
7233
+ },
7234
+ flush(callback) {
7235
+ pending += normalize2(decoder.end());
7236
+ callback(null, consume(true));
7237
+ }
7238
+ });
7239
+ }
7240
+
6862
7241
  // ../daemon/src/supervisor.ts
6863
7242
  var DEFAULT_FORCE_AFTER_MS = 5000;
6864
7243
  var DEFAULT_DETECT_AFTER_MS = 300;
@@ -6878,8 +7257,10 @@ class ProcessSupervisor {
6878
7257
  killTree;
6879
7258
  shareLifecycle;
6880
7259
  allocatePort;
7260
+ resolveSecrets;
6881
7261
  upstreamReservations = new Set;
6882
7262
  retries = new Map;
7263
+ redactionValues = new Set;
6883
7264
  retryDelayMs;
6884
7265
  shuttingDown = false;
6885
7266
  shutdownPromise;
@@ -6894,6 +7275,7 @@ class ProcessSupervisor {
6894
7275
  this.killTree = options.killProcessTree ?? killProcessTree;
6895
7276
  this.shareLifecycle = options.shareLifecycle;
6896
7277
  this.allocatePort = options.getFreePort ?? getFreePort;
7278
+ this.resolveSecrets = options.resolveProjectSecrets ?? resolveProjectSecrets;
6897
7279
  }
6898
7280
  start(projectId2, serviceName2) {
6899
7281
  const name = serviceName2.toLowerCase();
@@ -6952,11 +7334,11 @@ class ProcessSupervisor {
6952
7334
  return started;
6953
7335
  const hasDependents = project.commands.some((candidate) => candidate.after.includes(command.name));
6954
7336
  if (command.ready.type === "tcp" && !hasDependents) {
6955
- const cwd2 = command.cwd ? path8.resolve(project.directory, command.cwd) : project.directory;
7337
+ const cwd2 = command.cwd ? path9.resolve(project.directory, command.cwd) : project.directory;
6956
7338
  this.verifyReadiness(command, key, cwd2);
6957
7339
  return started;
6958
7340
  }
6959
- const cwd = command.cwd ? path8.resolve(project.directory, command.cwd) : project.directory;
7341
+ const cwd = command.cwd ? path9.resolve(project.directory, command.cwd) : project.directory;
6960
7342
  return this.verifyReadiness(command, key, cwd, started);
6961
7343
  })();
6962
7344
  attempts.set(command.name, attempt);
@@ -6981,7 +7363,14 @@ class ProcessSupervisor {
6981
7363
  this.setReadiness(key, "checking", command.ready.type === "tcp" ? "Waiting for the service to listen" : "Running configured readiness check");
6982
7364
  this.store.log({ ts: Date.now(), service: key, stream: "system", line: `LocalDeck: checking readiness for ${command.name}` });
6983
7365
  try {
6984
- await waitForReadiness(command, () => isCurrent() ? generation : undefined, { cwd, env: this.expandCommandEnv(command.env, command.after, generation.projectId) });
7366
+ let commandEnv = processGeneration?.commandEnv;
7367
+ if (!commandEnv && generation) {
7368
+ const secretEnv = this.resolveSecrets(generation.projectId, command.secrets ?? []);
7369
+ for (const value of Object.values(secretEnv))
7370
+ this.redactionValues.add(value);
7371
+ commandEnv = { ...this.expandCommandEnv(command.env, command.after, generation.projectId), ...secretEnv };
7372
+ }
7373
+ await waitForReadiness(command, () => isCurrent() ? generation : undefined, { cwd, env: commandEnv });
6985
7374
  if (!isCurrent() || generation.status === "stopped" || generation.status === "stopping")
6986
7375
  return failure(key, command.name, "readiness cancelled");
6987
7376
  const message = command.ready.type === "tcp" ? "Listening on its upstream port" : "Configured readiness check passed";
@@ -7095,13 +7484,21 @@ class ProcessSupervisor {
7095
7484
  const project = this.projects.details(projectId2);
7096
7485
  if (!project.available)
7097
7486
  throw new ProjectError("invalid", project.error);
7098
- const cwd = command.cwd ? path8.resolve(project.directory, command.cwd) : project.directory;
7487
+ const cwd = command.cwd ? path9.resolve(project.directory, command.cwd) : project.directory;
7099
7488
  try {
7100
- if (!fs7.statSync(cwd).isDirectory())
7489
+ if (!fs8.statSync(cwd).isDirectory())
7101
7490
  throw new Error("not a directory");
7102
7491
  } catch {
7103
7492
  throw new ProjectError("invalid", `configured working directory does not exist or is not a directory: ${cwd}`);
7104
7493
  }
7494
+ let secretEnv;
7495
+ try {
7496
+ secretEnv = this.resolveSecrets(projectId2, command.secrets ?? []);
7497
+ for (const value of Object.values(secretEnv))
7498
+ this.redactionValues.add(value);
7499
+ } catch (error) {
7500
+ return failure(key, name, errorMessage2(error));
7501
+ }
7105
7502
  let upstreamPort;
7106
7503
  try {
7107
7504
  upstreamPort = command.upstream ?? await this.reserveAutomaticUpstream();
@@ -7111,7 +7508,7 @@ class ProcessSupervisor {
7111
7508
  const reservedUpstreamPort = command.upstream === undefined ? upstreamPort : undefined;
7112
7509
  let commandEnv;
7113
7510
  try {
7114
- commandEnv = this.expandCommandEnv(command.env, command.after, projectId2);
7511
+ commandEnv = { ...this.expandCommandEnv(command.env, command.after, projectId2), ...secretEnv };
7115
7512
  } catch (error) {
7116
7513
  this.releaseAutomaticUpstream(reservedUpstreamPort);
7117
7514
  return failure(key, name, errorMessage2(error));
@@ -7170,7 +7567,8 @@ class ProcessSupervisor {
7170
7567
  completion: new Promise((resolve) => {
7171
7568
  complete = resolve;
7172
7569
  }),
7173
- reservedUpstreamPort
7570
+ reservedUpstreamPort,
7571
+ commandEnv
7174
7572
  };
7175
7573
  this.active.set(key, managed);
7176
7574
  let finalized = false;
@@ -7209,7 +7607,7 @@ class ProcessSupervisor {
7209
7607
  ts: Date.now(),
7210
7608
  service: key,
7211
7609
  stream: "system",
7212
- line: `process exited (${launchError2 ? launchError2.message : signal ?? `code ${code}`})`
7610
+ line: redactDiagnostic(`process exited (${launchError2 ? launchError2.message : signal ?? `code ${code}`})`, [...this.redactionValues])
7213
7611
  });
7214
7612
  if (retainForCleanup) {
7215
7613
  const current = this.registry.get(key);
@@ -7232,10 +7630,10 @@ class ProcessSupervisor {
7232
7630
  }
7233
7631
  }
7234
7632
  };
7235
- streamLines(child.stdout, (line) => {
7633
+ streamLines(child.stdout.pipe(secretOutput(this.redactionValues)), (line) => {
7236
7634
  this.store.log({ ts: Date.now(), service: key, stream: "stdout", line: stripAnsi(line) });
7237
7635
  });
7238
- streamLines(child.stderr, (line) => {
7636
+ streamLines(child.stderr.pipe(secretOutput(this.redactionValues)), (line) => {
7239
7637
  const clean = stripAnsi(line);
7240
7638
  managed.stderr.push(clean);
7241
7639
  if (managed.stderr.length > STDERR_SUMMARY_LINES)
@@ -7315,7 +7713,7 @@ class ProcessSupervisor {
7315
7713
  else {
7316
7714
  const command = this.projects.resolve(projectId2, name);
7317
7715
  const project = this.projects.details(projectId2);
7318
- const cwd = command.cwd ? path8.resolve(project.directory, command.cwd) : project.directory;
7716
+ const cwd = command.cwd ? path9.resolve(project.directory, command.cwd) : project.directory;
7319
7717
  this.verifyReadiness(command, key, cwd);
7320
7718
  }
7321
7719
  return result;
@@ -7591,7 +7989,7 @@ async function waitForProcessGroupExit(groupId, timeoutMs) {
7591
7989
  return true;
7592
7990
  }
7593
7991
  // ../daemon/package.json
7594
- var version = "1.0.3";
7992
+ var version = "1.1.0";
7595
7993
 
7596
7994
  // ../daemon/src/index.ts
7597
7995
  var VERSION = version;
@@ -7607,8 +8005,8 @@ async function startDaemon(opts = {}) {
7607
8005
  writePrivateState: writePrivateJson,
7608
8006
  ...opts.dependencies
7609
8007
  };
7610
- const stateFile = opts.homeDir ? path9.join(opts.homeDir, "daemon.json") : STATE_FILE;
7611
- const projectsFile = opts.homeDir ? path9.join(opts.homeDir, "projects.json") : PROJECTS_FILE;
8008
+ const stateFile = opts.homeDir ? path10.join(opts.homeDir, "daemon.json") : STATE_FILE;
8009
+ const projectsFile = opts.homeDir ? path10.join(opts.homeDir, "projects.json") : PROJECTS_FILE;
7612
8010
  const apiPort = opts.apiPort ?? DEFAULT_API_PORT;
7613
8011
  const store = dependencies.createStore();
7614
8012
  const registry = dependencies.createRegistry(store, apiPort);
@@ -7668,16 +8066,16 @@ async function startDaemon(opts = {}) {
7668
8066
  if (stateCleanup === "failed-write") {
7669
8067
  if (!stateWriteAttempted)
7670
8068
  return;
7671
- fs8.rmSync(`${stateFile}.${process.pid}.tmp`, { force: true });
8069
+ fs9.rmSync(`${stateFile}.${process.pid}.tmp`, { force: true });
7672
8070
  if (priorState)
7673
8071
  restorePrivateState(stateFile, priorState.contents, priorState.mode);
7674
8072
  else
7675
- fs8.rmSync(stateFile, { force: true });
8073
+ fs9.rmSync(stateFile, { force: true });
7676
8074
  return;
7677
8075
  }
7678
8076
  const state = readJson(stateFile, null);
7679
8077
  if (state?.pid === process.pid)
7680
- fs8.unlinkSync(stateFile);
8078
+ fs9.unlinkSync(stateFile);
7681
8079
  }));
7682
8080
  return errors;
7683
8081
  })();
@@ -7693,15 +8091,15 @@ async function startDaemon(opts = {}) {
7693
8091
  stopPromise.then(resolveStopped, rejectStopped);
7694
8092
  return stopPromise;
7695
8093
  };
7696
- server = dependencies.createApiServer({ registry, store, shares, projects, supervisor, controlToken, isShuttingDown: () => shuttingDown, trackAttachOperation: attachOperations.track, dashboardDir: opts.dashboardDir, allowedBrowserOrigins: opts.allowedBrowserOrigins, version: VERSION, startedAt, shutdown: stop });
8094
+ server = dependencies.createApiServer({ registry, store, shares, projects, supervisor, controlToken, isShuttingDown: () => shuttingDown, trackAttachOperation: attachOperations.track, dashboardDir: opts.dashboardDir, allowedBrowserOrigins: opts.allowedBrowserOrigins, version: VERSION, startedAt, shutdown: stop, listProjectSecrets, setProjectSecret, deleteProjectSecret, resolveProjectSecrets });
7697
8095
  try {
7698
8096
  await new Promise((resolve, reject) => {
7699
8097
  server.once("error", reject);
7700
8098
  server.listen(apiPort, "127.0.0.1", resolve);
7701
8099
  });
7702
- if (fs8.existsSync(stateFile)) {
7703
- const stat = fs8.statSync(stateFile);
7704
- priorState = { contents: fs8.readFileSync(stateFile), mode: stat.mode & 511 };
8100
+ if (fs9.existsSync(stateFile)) {
8101
+ const stat = fs9.statSync(stateFile);
8102
+ priorState = { contents: fs9.readFileSync(stateFile), mode: stat.mode & 511 };
7705
8103
  }
7706
8104
  stateWriteAttempted = true;
7707
8105
  dependencies.writePrivateState(stateFile, {
@@ -7722,9 +8120,9 @@ async function startDaemon(opts = {}) {
7722
8120
  }
7723
8121
  function restorePrivateState(file, contents, mode) {
7724
8122
  const temporary = `${file}.${process.pid}.restore.tmp`;
7725
- fs8.writeFileSync(temporary, contents, { mode });
7726
- fs8.chmodSync(temporary, mode);
7727
- fs8.renameSync(temporary, file);
8123
+ fs9.writeFileSync(temporary, contents, { mode });
8124
+ fs9.chmodSync(temporary, mode);
8125
+ fs9.renameSync(temporary, file);
7728
8126
  }
7729
8127
  function toError3(error) {
7730
8128
  return error instanceof Error ? error : new Error(String(error));
@@ -7759,24 +8157,24 @@ async function findDaemon() {
7759
8157
 
7760
8158
  // src/client.ts
7761
8159
  import { spawn as spawn4, spawnSync } from "node:child_process";
7762
- import fs9 from "node:fs";
7763
- import path10 from "node:path";
8160
+ import fs10 from "node:fs";
8161
+ import path11 from "node:path";
7764
8162
  import { fileURLToPath } from "node:url";
7765
8163
  function dashboardDir() {
7766
- const here = path10.dirname(fileURLToPath(import.meta.url));
8164
+ const here = path11.dirname(fileURLToPath(import.meta.url));
7767
8165
  const candidates = [
7768
- path10.resolve(here, "../dashboard"),
7769
- path10.resolve(here, "../../dashboard/dist")
8166
+ path11.resolve(here, "../dashboard"),
8167
+ path11.resolve(here, "../../dashboard/dist")
7770
8168
  ];
7771
- return candidates.find((d) => fs9.existsSync(path10.join(d, "index.html")));
8169
+ return candidates.find((d) => fs10.existsSync(path11.join(d, "index.html")));
7772
8170
  }
7773
8171
  function daemonCommand() {
7774
8172
  const entry = process.argv[1];
7775
8173
  const runningUnderBun = Boolean(process.versions.bun);
7776
8174
  if (runningUnderBun) {
7777
- const dist = path10.resolve(path10.dirname(fileURLToPath(import.meta.url)), "../dist/index.js");
8175
+ const dist = path11.resolve(path11.dirname(fileURLToPath(import.meta.url)), "../dist/index.js");
7778
8176
  const hasNode = spawnSync("node", ["--version"], { stdio: "ignore" }).status === 0;
7779
- if (fs9.existsSync(dist) && hasNode)
8177
+ if (fs10.existsSync(dist) && hasNode)
7780
8178
  return ["node", [dist, "daemon", "run"]];
7781
8179
  console.warn("LocalDeck: starting daemon under Bun — WebSocket proxying (HMR) will not work. Run `bun run build` and make sure `node` is on PATH.");
7782
8180
  }
@@ -7788,8 +8186,8 @@ async function ensureDaemon() {
7788
8186
  const running = await findDaemon();
7789
8187
  if (running)
7790
8188
  return running;
7791
- fs9.mkdirSync(path10.dirname(DAEMON_LOG), { recursive: true });
7792
- const out = fs9.openSync(DAEMON_LOG, "a");
8189
+ fs10.mkdirSync(path11.dirname(DAEMON_LOG), { recursive: true });
8190
+ const out = fs10.openSync(DAEMON_LOG, "a");
7793
8191
  const [cmd, args] = daemonCommand();
7794
8192
  const child = spawn4(cmd, args, { detached: true, stdio: ["ignore", out, out], env: process.env });
7795
8193
  child.unref();
@@ -7858,4 +8256,4 @@ class Api {
7858
8256
  shutdown = () => this.call("POST", "/api/shutdown");
7859
8257
  }
7860
8258
 
7861
- export { __toESM, __commonJS, __export, __require, wrapper_default, startForwarder, HOME_DIR, STATE_FILE, DEFAULT_API_PORT, findProjectFile, loadProject, loadProjectFile, shellCommand, isPortFree, getFreePort, serviceKey, processTree, listeningPorts, chooseUpstream, connectHost, streamLines, killProcessTree, stripAnsi, redactDiagnostic, environmentValues, startupGuidance, startupLayers, waitForReadiness, VERSION, startDaemon, findDaemon, dashboardDir, ensureDaemon, Api };
8259
+ export { __toESM, __commonJS, __export, __require, wrapper_default, startForwarder, HOME_DIR, STATE_FILE, DEFAULT_API_PORT, findProjectFile, loadProject, loadProjectFile, shellCommand, isPortFree, getFreePort, serviceKey, processTree, listeningPorts, chooseUpstream, connectHost, streamLines, killProcessTree, stripAnsi, redactDiagnostic, environmentValues, listProjectSecrets, resolveProjectSecrets, startupGuidance, startupLayers, waitForReadiness, secretOutput, VERSION, startDaemon, findDaemon, dashboardDir, ensureDaemon, Api };