localdeck 1.0.3 → 1.2.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/CHANGELOG.md +36 -0
- package/README.md +12 -0
- package/dashboard/assets/index-B2HB87hg.js +20 -0
- package/dashboard/assets/index-Tp7Rr3xY.css +1 -0
- package/dashboard/index.html +2 -2
- package/dist/{index-3qpxpxwq.js → index-zmpw8b4a.js} +486 -88
- package/dist/index.js +28 -10
- package/dist/{mcp-tgg2n05x.js → mcp-dwx4ja4k.js} +6 -2
- package/dist/{update-2f1pgdst.js → update-sfxqqdjp.js} +1 -1
- package/docs/configuration.md +6 -1
- package/docs/dashboard.md +12 -2
- package/package.json +1 -1
- package/dashboard/assets/index-qPKQBj-D.js +0 -20
- package/dashboard/assets/index-sKTai3O1.css +0 -1
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-
|
|
32
|
+
} from "./index-zmpw8b4a.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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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-
|
|
900
|
+
return (await import("./update-sfxqqdjp.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-
|
|
906
|
+
return (await import("./mcp-dwx4ja4k.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
|
-
|
|
10
|
-
|
|
9
|
+
listProjectSecrets,
|
|
10
|
+
redactDiagnostic,
|
|
11
|
+
resolveProjectSecrets
|
|
12
|
+
} from "./index-zmpw8b4a.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))
|
package/docs/configuration.md
CHANGED
|
@@ -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.
|
|
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
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Open the dashboard with `localdeck open`. It runs on `http://localhost:7777` by default and updates service status and logs live.
|
|
6
6
|
|
|
7
|
+
## Command palette
|
|
8
|
+
|
|
9
|
+
Press **⌘K** on macOS or **Ctrl+K** on Windows/Linux, or click **Search commands** in the sidebar. Search by action, project, or service name; words can appear in any order, such as `logs web my-app`. Use **↑/↓** to select, **Enter** to run, and **Escape** to close. Closing returns focus to the previous control.
|
|
10
|
+
|
|
11
|
+
Commands open projects, Settings, running app URLs, logs, and requests, and start, stop, or restart individual services through the existing dashboard controls. Start is available for configured stopped services; restart is available for configured daemon-owned services. Controls are unavailable while disconnected or while that service is starting, stopping, or restarting. Failed actions stay visible in the palette. Project loading failures show a Retry control.
|
|
12
|
+
|
|
7
13
|
## Pages and projects
|
|
8
14
|
|
|
9
15
|
- **Projects** (`/projects`) lists projects remembered through the CLI. Selecting one opens `/projects/<id>`.
|
|
@@ -26,9 +32,11 @@ A nickname such as `platform` produces `http://platform.web.localhost:7777`. Wit
|
|
|
26
32
|
|
|
27
33
|
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
34
|
|
|
35
|
+
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.
|
|
36
|
+
|
|
29
37
|
## Logs
|
|
30
38
|
|
|
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.
|
|
39
|
+
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
40
|
|
|
33
41
|
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
42
|
|
|
@@ -70,7 +78,9 @@ Project diagnostics include configuration, readiness errors, and recent logs. En
|
|
|
70
78
|
|
|
71
79
|
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
80
|
|
|
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.
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
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
84
|
|
|
75
85
|
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
86
|
|