localdeck 1.5.0 → 1.6.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/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +69 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dashboard/assets/index-CcN_iA3K.js +21 -0
- package/dashboard/assets/index-DDw1Wq-H.css +1 -0
- package/dashboard/index.html +2 -2
- package/dist/{index-tv699dzb.js → index-xsyz4cc9.js} +284 -69
- package/dist/index.js +98 -13
- package/dist/{mcp-c138pjc5.js → mcp-210n1fxv.js} +31 -3
- package/dist/{update-a6z3akjx.js → update-hvkw7mkw.js} +1 -1
- package/docs/commands.md +15 -2
- package/docs/configuration.md +42 -6
- package/docs/dashboard.md +40 -20
- package/docs/mcp.md +5 -3
- package/package.json +1 -1
- package/dashboard/assets/index-CCtEO4zm.css +0 -1
- package/dashboard/assets/index-DGUiiFV-.js +0 -21
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
HOME_DIR,
|
|
5
5
|
VERSION,
|
|
6
6
|
__require,
|
|
7
|
+
applyProfiles,
|
|
7
8
|
chooseUpstream,
|
|
8
9
|
connectHost,
|
|
9
10
|
dashboardDir,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
unreachableListenerMessage,
|
|
32
33
|
waitForReadiness,
|
|
33
34
|
wrapper_default
|
|
34
|
-
} from "./index-
|
|
35
|
+
} from "./index-xsyz4cc9.js";
|
|
35
36
|
|
|
36
37
|
// src/index.ts
|
|
37
38
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -155,7 +156,8 @@ async function startService(opts, overrides = {}) {
|
|
|
155
156
|
upstreamPort,
|
|
156
157
|
upstreamSource: opts.upstreamPort === undefined ? "assigned" : "manual",
|
|
157
158
|
cwd,
|
|
158
|
-
command: opts.displayCommand ?? opts.command.join(" ")
|
|
159
|
+
command: opts.displayCommand ?? opts.command.join(" "),
|
|
160
|
+
...opts.project ? { profiles: opts.profiles ?? [] } : {}
|
|
159
161
|
});
|
|
160
162
|
});
|
|
161
163
|
ws.on("message", (raw) => {
|
|
@@ -849,7 +851,7 @@ ${c.bold("Usage")}
|
|
|
849
851
|
localdeck run <name> [--port 4000] [--upstream 5173] [--] <command...>
|
|
850
852
|
localdeck <name> run a command from the project's .localdeck file
|
|
851
853
|
localdeck up [tag|name...] run every .localdeck command (or just the tagged/named ones) in one terminal
|
|
852
|
-
localdeck
|
|
854
|
+
localdeck profile [on|off <name...>|clear] list or switch this project's profiles
|
|
853
855
|
localdeck init [--dry-run] [--force] preview/create/replace the project configuration
|
|
854
856
|
localdeck init --name web --command 'your command'
|
|
855
857
|
localdeck commands list configured services (alias: cmds)
|
|
@@ -915,13 +917,16 @@ async function main(argv, overrides = {}) {
|
|
|
915
917
|
case "--update":
|
|
916
918
|
if (rest.length)
|
|
917
919
|
throw new Error("Usage: localdeck --update");
|
|
918
|
-
return (await import("./update-
|
|
920
|
+
return (await import("./update-hvkw7mkw.js")).updateLocalDeck();
|
|
919
921
|
case "up":
|
|
920
922
|
return cmdUp(rest, dependencies);
|
|
923
|
+
case "profile":
|
|
924
|
+
case "profiles":
|
|
925
|
+
return cmdProfile(rest, dependencies);
|
|
921
926
|
case "init":
|
|
922
927
|
return cmdInit(rest, dependencies);
|
|
923
928
|
case "mcp":
|
|
924
|
-
return (await import("./mcp-
|
|
929
|
+
return (await import("./mcp-210n1fxv.js")).runMcp(rest);
|
|
925
930
|
case "doctor":
|
|
926
931
|
return cmdDoctor(rest);
|
|
927
932
|
case "forward":
|
|
@@ -961,10 +966,12 @@ async function main(argv, overrides = {}) {
|
|
|
961
966
|
const found = project && findProjectCommand(project, cmd);
|
|
962
967
|
if (found) {
|
|
963
968
|
const remembered = await rememberProject(project, dependencies);
|
|
969
|
+
const launch = await resolveLaunch(project, remembered, dependencies);
|
|
964
970
|
const selected = startupLayers(project.commands, [found.name]).flat();
|
|
965
|
-
return dependencies.runForeground(selected.map((command, index) => toRunOptions(command, project, remembered, {
|
|
971
|
+
return dependencies.runForeground(selected.map((command, index) => toRunOptions(launch.effective(command), project, remembered, {
|
|
966
972
|
prefix: selected.length > 1,
|
|
967
|
-
color: PALETTE[index % PALETTE.length]
|
|
973
|
+
color: PALETTE[index % PALETTE.length],
|
|
974
|
+
...launch.options
|
|
968
975
|
})));
|
|
969
976
|
}
|
|
970
977
|
const hint = project ? ` Commands in ${path3.basename(project.file)}: ${project.commands.map((p) => p.name).join(", ") || "(none)"}` : "";
|
|
@@ -1006,6 +1013,32 @@ async function rememberProject(project, dependencies) {
|
|
|
1006
1013
|
return;
|
|
1007
1014
|
}
|
|
1008
1015
|
}
|
|
1016
|
+
async function resolveLaunch(project, remembered, dependencies) {
|
|
1017
|
+
let active = [];
|
|
1018
|
+
if (project.profiles?.length && remembered) {
|
|
1019
|
+
try {
|
|
1020
|
+
const daemon = await dependencies.ensureDaemon();
|
|
1021
|
+
active = (await dependencies.createApi(daemon.apiPort).profiles(remembered.id)).active;
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
dependencies.warn(`LocalDeck: could not read active profiles, running without them: ${error instanceof Error ? error.message : String(error)}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
let resolved;
|
|
1027
|
+
try {
|
|
1028
|
+
resolved = applyProfiles(project, active);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1031
|
+
}
|
|
1032
|
+
if (resolved.active.length)
|
|
1033
|
+
dependencies.log(c.dim(`Profiles: ${resolved.active.join(" + ")}`));
|
|
1034
|
+
for (const clash of resolved.clashes)
|
|
1035
|
+
dependencies.warn(`LocalDeck: ${clash.profiles.join(" and ")} both set ${clash.service} ${clash.field}; ${clash.winner} wins because it is listed later.`);
|
|
1036
|
+
const byName = new Map(resolved.commands.map((command) => [command.name, command]));
|
|
1037
|
+
return {
|
|
1038
|
+
effective: (command) => byName.get(command.name) ?? command,
|
|
1039
|
+
options: remembered ? { profiles: resolved.active } : {}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1009
1042
|
async function printProjectCommands(failIfNone, dependencies) {
|
|
1010
1043
|
const project = tryLoadProject(dependencies);
|
|
1011
1044
|
if (!project) {
|
|
@@ -1017,6 +1050,9 @@ async function printProjectCommands(failIfNone, dependencies) {
|
|
|
1017
1050
|
console.log(`${c.bold("Project commands")} ${c.dim(`(${path3.relative(process.cwd(), project.file) || path3.basename(project.file)})`)}`);
|
|
1018
1051
|
const rows = project.commands.map((p) => [c.cyan(p.name), p.port ? String(p.port) : c.dim("auto"), p.after.join(",") || "", p.tags.join(",") || "", c.dim(p.run)]);
|
|
1019
1052
|
console.log(table(rows, ["NAME", "PORT", "AFTER", "TAGS", "RUN"]));
|
|
1053
|
+
if (project.profiles?.length)
|
|
1054
|
+
console.log(`
|
|
1055
|
+
${c.bold("Profiles")} ${project.profiles.map((profile) => profile.name).join(", ")} ${c.dim("· localdeck profile")}`);
|
|
1020
1056
|
console.log(c.dim(`
|
|
1021
1057
|
run one: localdeck <name> · run all: localdeck up · by tag: localdeck up <tag>`));
|
|
1022
1058
|
}
|
|
@@ -1033,12 +1069,16 @@ async function cmdUp(args, dependencies) {
|
|
|
1033
1069
|
fail("--profile needs a profile name");
|
|
1034
1070
|
args = args.filter((_, i) => i !== profileIndex && i !== profileIndex + 1);
|
|
1035
1071
|
}
|
|
1036
|
-
const
|
|
1037
|
-
if (profile && (
|
|
1038
|
-
fail(`
|
|
1072
|
+
const legacy = project.legacyProfiles;
|
|
1073
|
+
if (profile && project.profiles?.some((candidate) => candidate.name === profile))
|
|
1074
|
+
fail(`"${profile}" is a profile you turn on, not a group to start: localdeck profile on ${profile}`);
|
|
1075
|
+
if (profile && (!legacy || !Object.prototype.hasOwnProperty.call(legacy, profile)))
|
|
1076
|
+
fail(`unknown profile "${profile}". Group services with tags and run: localdeck up <tag>`);
|
|
1077
|
+
if (profile)
|
|
1078
|
+
dependencies.warn(`LocalDeck: --profile is deprecated. "${profile}" is now a tag; run: localdeck up ${profile}`);
|
|
1039
1079
|
let selected = project.commands;
|
|
1040
1080
|
if (profile) {
|
|
1041
|
-
const names = new Set(
|
|
1081
|
+
const names = new Set(legacy[profile]);
|
|
1042
1082
|
selected = project.commands.filter((command) => names.has(command.name));
|
|
1043
1083
|
if (!selected.length)
|
|
1044
1084
|
fail(`profile "${profile}" selects no configured services`);
|
|
@@ -1058,7 +1098,50 @@ async function cmdUp(args, dependencies) {
|
|
|
1058
1098
|
fail("nothing to run");
|
|
1059
1099
|
selected = startupLayers(project.commands, selected.map((command) => command.name)).flat();
|
|
1060
1100
|
const remembered = await rememberProject(project, dependencies);
|
|
1061
|
-
|
|
1101
|
+
const launch = await resolveLaunch(project, remembered, dependencies);
|
|
1102
|
+
await dependencies.runForeground(selected.map((p, i) => toRunOptions(launch.effective(p), project, remembered, { prefix: selected.length > 1, color: PALETTE[i % PALETTE.length], ...launch.options })));
|
|
1103
|
+
}
|
|
1104
|
+
async function cmdProfile(args, dependencies) {
|
|
1105
|
+
const project = tryLoadProject(dependencies);
|
|
1106
|
+
if (!project)
|
|
1107
|
+
fail("no .localdeck file found here or in a parent folder. Create one with: localdeck init");
|
|
1108
|
+
const [action = "list", ...names] = args;
|
|
1109
|
+
if (!["list", "ls", "on", "off", "clear"].includes(action))
|
|
1110
|
+
fail(`unknown profile action "${action}". Usage: localdeck profile [on|off <name...>|clear]`);
|
|
1111
|
+
if ((action === "on" || action === "off") && !names.length)
|
|
1112
|
+
fail(`localdeck profile ${action} needs at least one profile name`);
|
|
1113
|
+
if (!project.profiles?.length) {
|
|
1114
|
+
if (action === "list" || action === "ls") {
|
|
1115
|
+
dependencies.log(`No profiles in ${path3.basename(project.file)}. Add a "profiles" object to run services differently, for example against staging.`);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
fail(`${path3.basename(project.file)} defines no profiles`);
|
|
1119
|
+
}
|
|
1120
|
+
const remembered = await rememberProject(project, dependencies);
|
|
1121
|
+
if (!remembered)
|
|
1122
|
+
fail("the LocalDeck daemon is needed to switch profiles");
|
|
1123
|
+
const daemon = await dependencies.ensureDaemon();
|
|
1124
|
+
const api = dependencies.createApi(daemon.apiPort);
|
|
1125
|
+
let current = await api.profiles(remembered.id);
|
|
1126
|
+
if (action === "on" || action === "off" || action === "clear") {
|
|
1127
|
+
const wanted = names.map((name) => name.toLowerCase());
|
|
1128
|
+
const unknown = wanted.filter((name) => !current.profiles.some((candidate) => candidate.name === name));
|
|
1129
|
+
if (unknown.length)
|
|
1130
|
+
fail(`unknown profile ${unknown.map((name) => `"${name}"`).join(", ")}. Available profiles: ${current.profiles.map((candidate) => candidate.name).join(", ")}`);
|
|
1131
|
+
const next = action === "clear" ? [] : action === "on" ? [...new Set([...current.active, ...wanted])] : current.active.filter((name) => !wanted.includes(name));
|
|
1132
|
+
current = await api.setActiveProfiles(remembered.id, next);
|
|
1133
|
+
dependencies.log(current.active.length ? `Profiles on: ${c.cyan(current.active.join(" + "))}` : "No profiles on · running base services");
|
|
1134
|
+
dependencies.log(c.dim("Running services keep their settings until restarted."));
|
|
1135
|
+
} else {
|
|
1136
|
+
const rows = current.profiles.map((candidate) => [candidate.active ? c.green("on") : c.dim("off"), c.cyan(candidate.name), candidate.services.join(","), candidate.description ?? ""]);
|
|
1137
|
+
dependencies.log(table(rows, ["", "PROFILE", "CHANGES", "DESCRIPTION"]));
|
|
1138
|
+
dependencies.log(c.dim(`
|
|
1139
|
+
turn on: localdeck profile on <name> · off: localdeck profile off <name> · all off: localdeck profile clear`));
|
|
1140
|
+
}
|
|
1141
|
+
for (const clash of current.clashes)
|
|
1142
|
+
dependencies.warn(`LocalDeck: ${clash.profiles.join(" and ")} both set ${clash.service} ${clash.field}; ${clash.winner} wins because it is listed later.`);
|
|
1143
|
+
if (current.error)
|
|
1144
|
+
dependencies.warn(`LocalDeck: ${current.error}`);
|
|
1062
1145
|
}
|
|
1063
1146
|
async function cmdInit(args, dependencies) {
|
|
1064
1147
|
const root = findPackageRoot() ?? process.cwd();
|
|
@@ -1185,10 +1268,12 @@ async function cmdRun(args, dependencies) {
|
|
|
1185
1268
|
const found = project && name ? findProjectCommand(project, name) : undefined;
|
|
1186
1269
|
if (found) {
|
|
1187
1270
|
const remembered = await rememberProject(project, dependencies);
|
|
1271
|
+
const launch = await resolveLaunch(project, remembered, dependencies);
|
|
1188
1272
|
const selected = startupLayers(project.commands, [found.name]).flat();
|
|
1189
|
-
return dependencies.runForeground(selected.map((command2, index) => toRunOptions(command2, project, remembered, {
|
|
1273
|
+
return dependencies.runForeground(selected.map((command2, index) => toRunOptions(launch.effective(command2), project, remembered, {
|
|
1190
1274
|
prefix: selected.length > 1,
|
|
1191
1275
|
color: PALETTE[index % PALETTE.length],
|
|
1276
|
+
...launch.options,
|
|
1192
1277
|
...command2.name === found.name ? { stablePort: opts.port ?? found.port, upstreamPort: opts.upstream ?? found.upstream } : {}
|
|
1193
1278
|
})));
|
|
1194
1279
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
listProjectSecrets,
|
|
10
10
|
redactDiagnostic,
|
|
11
11
|
resolveProjectSecrets
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-xsyz4cc9.js";
|
|
13
13
|
|
|
14
14
|
// ../../node_modules/.bun/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
|
|
15
15
|
var require_code = __commonJS((exports) => {
|
|
@@ -34043,6 +34043,14 @@ function createMcpServer(options) {
|
|
|
34043
34043
|
throw new Error("Unknown service key; use list_services");
|
|
34044
34044
|
return { projectId, name, details, runtime };
|
|
34045
34045
|
}
|
|
34046
|
+
function profileState(details, name, runtime) {
|
|
34047
|
+
const current = details?.fingerprints?.[name];
|
|
34048
|
+
const running = runtime && runtime.status !== "stopped";
|
|
34049
|
+
return {
|
|
34050
|
+
...running ? { profiles: runtime.profiles ?? [] } : {},
|
|
34051
|
+
...running && runtime.configFingerprint && current ? { restartNeeded: runtime.configFingerprint !== current } : {}
|
|
34052
|
+
};
|
|
34053
|
+
}
|
|
34046
34054
|
tool("list_projects", "List remembered projects, their exact IDs and configuration availability.", { limit }, false, async (args, api2) => {
|
|
34047
34055
|
const all = (await api2.projects()).filter((p) => !options.projectId || p.id === options.projectId);
|
|
34048
34056
|
return { data: { items: all.slice(0, args.limit), truncated: all.length > args.limit } };
|
|
@@ -34063,14 +34071,34 @@ function createMcpServer(options) {
|
|
|
34063
34071
|
const details = await project(api2, summary.id, secrets);
|
|
34064
34072
|
for (const c of details.commands) {
|
|
34065
34073
|
const serviceKey = `${summary.id}:${c.name}`;
|
|
34066
|
-
|
|
34074
|
+
const existing = items.get(serviceKey);
|
|
34075
|
+
items.set(serviceKey, { key: serviceKey, projectId: summary.id, projectName: summary.name, name: c.name, status: "stopped", managedBy: "daemon", configured: true, ...existing, ...profileState(details, c.name, existing && "upstreamPort" in existing ? existing : undefined) });
|
|
34067
34076
|
}
|
|
34068
34077
|
}
|
|
34069
34078
|
return { data: { items: [...items.values()].slice(0, args.limit), truncated: items.size > args.limit, unavailable } };
|
|
34070
34079
|
});
|
|
34071
34080
|
tool("get_service", "Inspect a registered or configured service. Ports and counters appear after its first start.", serviceInput, false, async (args, api2, secrets) => {
|
|
34072
34081
|
const target = await service(api2, args.serviceKey, secrets);
|
|
34073
|
-
|
|
34082
|
+
const command = target.details?.commands.find((c) => c.name === target.name);
|
|
34083
|
+
const origin = target.details?.provenance?.[target.name];
|
|
34084
|
+
return { data: {
|
|
34085
|
+
service: { ...target.runtime ?? { key: args.serviceKey, projectId: target.projectId, name: target.name, status: "stopped", configured: true, managedBy: "daemon" }, ...profileState(target.details, target.name, target.runtime) },
|
|
34086
|
+
...command ? { configuration: {
|
|
34087
|
+
run: command.run,
|
|
34088
|
+
cwd: command.cwd ?? ".",
|
|
34089
|
+
activeProfiles: target.details?.activeProfiles ?? [],
|
|
34090
|
+
...origin ? { sources: { run: origin.run, cwd: origin.cwd, envNames: origin.env, secretNames: origin.secrets } } : {}
|
|
34091
|
+
} } : {},
|
|
34092
|
+
notice
|
|
34093
|
+
} };
|
|
34094
|
+
});
|
|
34095
|
+
tool("list_profiles", "List a project's profiles: overlays that change how services run (command, env, secrets, cwd) on this computer. Shows which are on and any clashes. Does not start anything.", { projectId: id }, false, async (args, api2, secrets) => {
|
|
34096
|
+
await project(api2, args.projectId, secrets);
|
|
34097
|
+
return { data: { ...await api2.profiles(args.projectId) } };
|
|
34098
|
+
});
|
|
34099
|
+
tool("set_profiles", "Replace the list of profiles turned on for a project on this computer. Never starts, stops or restarts services; running services keep their settings until restarted (list_services shows restartNeeded).", { projectId: id, profiles: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,40}$/i)).max(64) }, true, async (args, api2, secrets) => {
|
|
34100
|
+
await project(api2, args.projectId, secrets);
|
|
34101
|
+
return { data: { ...await api2.setActiveProfiles(args.projectId, args.profiles.map((name) => name.toLowerCase())) } };
|
|
34074
34102
|
});
|
|
34075
34103
|
tool("get_logs", "Read recent logs. Filters apply to the last 200 retained records. No live follow.", { ...serviceInput, limit, stream: exports_external.enum(["stdout", "stderr", "system"]).optional() }, false, async (args, api2, secrets) => {
|
|
34076
34104
|
await service(api2, args.serviceKey, secrets);
|
package/docs/commands.md
CHANGED
|
@@ -29,11 +29,24 @@ Initialization uses the nearest `package.json` directory, or the current folder
|
|
|
29
29
|
| `localdeck web` | Runs the configured `web` service and its transitive prerequisites in the terminal. |
|
|
30
30
|
| `localdeck up` | Runs all configured services, starting each after its own prerequisites become ready. |
|
|
31
31
|
| `localdeck up frontend api` | Runs services matching any supplied name or tag, plus their prerequisites. |
|
|
32
|
-
| `localdeck up --profile app` | Runs
|
|
32
|
+
| `localdeck up --profile app` | Deprecated. Runs a pre-1.6 list-style profile, now converted to the tag `app`; use `localdeck up app`. |
|
|
33
33
|
| `localdeck run web` | Runs the configured `web` service; an alternative to `localdeck web`. |
|
|
34
34
|
| `localdeck run api -- node server.js` | Runs a one-off command without requiring a configuration. |
|
|
35
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
36
|
|
|
37
|
+
Terminal runs use the profiles turned on for the project and print them first, for example `Profiles: staging`.
|
|
38
|
+
|
|
39
|
+
## Profiles
|
|
40
|
+
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `localdeck profile` | Lists the project's profiles, which services each changes, and which are on. Alias: `profiles`. |
|
|
44
|
+
| `localdeck profile on staging debug` | Turns on one or more profiles for this project on this computer. |
|
|
45
|
+
| `localdeck profile off debug` | Turns profiles off. |
|
|
46
|
+
| `localdeck profile clear` | Turns every profile off, so services run as defined in the base configuration. |
|
|
47
|
+
|
|
48
|
+
Profile commands never start, stop or restart services. Running services keep their settings until restarted. See [profiles](configuration.md#profiles).
|
|
49
|
+
|
|
37
50
|
`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
51
|
|
|
39
52
|
`{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.
|
|
@@ -87,7 +100,7 @@ Normal startup handles clean URLs automatically where supported. Manual `forward
|
|
|
87
100
|
| `NO_COLOR` | Disables CLI colors. |
|
|
88
101
|
| `CI` | Suppresses interactive clean-URL preparation when set. |
|
|
89
102
|
|
|
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#
|
|
103
|
+
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#environment-wiring).
|
|
91
104
|
|
|
92
105
|
## MCP client connection
|
|
93
106
|
|
package/docs/configuration.md
CHANGED
|
@@ -47,22 +47,58 @@ For Docker Compose databases, set `upstream` to the host-published database port
|
|
|
47
47
|
|
|
48
48
|
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.
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Environment wiring
|
|
51
51
|
|
|
52
|
-
Use the
|
|
52
|
+
Use `env` with `{service:NAME:url}` or `{service:NAME:port}` to hand one service the address of another it starts after:
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
|
56
56
|
"services": [
|
|
57
57
|
{ "name": "api", "run": "npm run api", "ready": "/health" },
|
|
58
|
-
{ "name": "web", "run": "npm run web", "after": "api", "env": { "PUBLIC_API_URL": "{service:api:url}" } }
|
|
59
|
-
|
|
58
|
+
{ "name": "web", "run": "npm run web", "after": "api", "env": { "PUBLIC_API_URL": "{service:api:url}" } }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Variable names such as `PUBLIC_API_URL` must match what your framework actually reads; LocalDeck does not rewrite application code or `.env` files.
|
|
64
|
+
|
|
65
|
+
### Profiles
|
|
66
|
+
|
|
67
|
+
A profile changes how services run without renaming them. `web` stays `web`, with the same port, link and startup order, while a `staging` profile swaps its command and environment. With no profile on, services run exactly as defined above; that is your local setup.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"services": [
|
|
72
|
+
{ "name": "api", "run": "npm run api", "env": { "LOG_LEVEL": "info" } },
|
|
73
|
+
{ "name": "web", "run": "npm run dev", "after": "api", "env": { "API_URL": "{service:api:url}" } }
|
|
60
74
|
],
|
|
61
|
-
"profiles": {
|
|
75
|
+
"profiles": {
|
|
76
|
+
"staging": {
|
|
77
|
+
"description": "Frontend talks to the staging API",
|
|
78
|
+
"services": {
|
|
79
|
+
"web": {
|
|
80
|
+
"run": "npm run dev:staging",
|
|
81
|
+
"env": { "API_URL": "https://api.staging.example.com" },
|
|
82
|
+
"secrets": ["STAGING_TOKEN"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"debug": {
|
|
87
|
+
"description": "Attach the Node inspector to the API",
|
|
88
|
+
"services": { "api": { "env": { "NODE_OPTIONS": "--inspect", "LOG_LEVEL": "debug" } } }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
62
91
|
}
|
|
63
92
|
```
|
|
64
93
|
|
|
65
|
-
|
|
94
|
+
- A profile may change a service's `run`, `cwd`, `env` and `secrets`. Name, ports, `after`, readiness and service type always come from the base service, so links and startup order never change.
|
|
95
|
+
- `env` merges key by key: `debug` above keeps `api`'s other variables and replaces `LOG_LEVEL`. `secrets` are added to the base list. The same reserved-name rules apply as for base services.
|
|
96
|
+
- Several profiles can be on at once. They apply in the order they are listed in the file. When two of them set the same command, directory or variable, the one listed later wins, and the dashboard and CLI say so.
|
|
97
|
+
- Which profiles are on is stored **on this computer only**, in `~/.localdeck/projects.json` (or `$LOCALDECK_HOME`). It is never written to `.localdeck`, so turning on `staging` does not change the file for your teammates.
|
|
98
|
+
- Turn profiles on with `localdeck profile on staging`, the Profiles menu at the top of the project page, or the MCP `set_profiles` tool. Switching never starts, stops or restarts anything. A running service keeps its settings and is marked **Restart to apply** until you restart it.
|
|
99
|
+
- Profile names use lowercase letters, numbers, `-` and `_`. An unknown service, or a field a profile cannot change, is rejected when the file loads.
|
|
100
|
+
|
|
101
|
+
**Older list-style profiles.** Before 1.6, `"profiles": { "app": ["web"] }` named a group of services to start. That form still loads: each listed service is tagged `app`, so run the group with `localdeck up app`. `localdeck up --profile app` keeps working for now with a notice. A file cannot mix service lists and profile objects under `profiles`.
|
|
66
102
|
|
|
67
103
|
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.
|
|
68
104
|
|
package/docs/dashboard.md
CHANGED
|
@@ -6,39 +6,59 @@ Open the dashboard with `localdeck open`. It runs on `http://localhost:7777` by
|
|
|
6
6
|
|
|
7
7
|
## Command palette
|
|
8
8
|
|
|
9
|
-
Press **⌘K** on macOS or **Ctrl+K** on Windows/Linux, or click **Search
|
|
9
|
+
Press **⌘K** on macOS or **Ctrl+K** on Windows/Linux, or click **Search** 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
10
|
|
|
11
|
-
Commands open projects, Settings, running app URLs, logs,
|
|
11
|
+
Commands open projects, project and global Settings, running app URLs, and each service's logs, requests or configuration page, and start, stop, or restart individual services. 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
12
|
|
|
13
|
-
##
|
|
13
|
+
## Layout and navigation
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
- **All Services** (`/services`) lists configured and running services across projects, with search, Start/Stop, Share, View logs, and View requests controls.
|
|
17
|
-
- **Settings** (`/settings`) contains Clean URLs and the log display limit.
|
|
18
|
-
- **About** (`/about`) contains setup and command information.
|
|
19
|
-
- **Changelog** (`/changelog`) lists release features and changes.
|
|
15
|
+
The sidebar holds **Search**, **Projects**, **All services**, and your projects with a status dot and running count. Its footer shows the daemon status; the ⚙ menu opens **Settings**, **About**, and **Changelog**. Collapse the sidebar to icons with the button beside the logo, or drag its edge (arrow keys and Home/End also work) to resize it. On phones it opens as an overlay and closes after you navigate.
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
Every view has its own address, so refresh, direct links, middle-click and browser Back/Forward all work. Back and Forward return to where you were on the page. Search boxes keep their text in the address (`?q=`).
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
| Address | Page |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `/projects` | Projects you've remembered through the CLI |
|
|
22
|
+
| `/projects/<id>` | A project's **Overview**; tabs for **Activity**, **Checks**, **Secrets** and **Settings** follow the same address |
|
|
23
|
+
| `/projects/<id>/services/<name>` | A service's page: **Logs**, **Requests** (`…/requests`) and **Config** (`…/config`) tabs |
|
|
24
|
+
| `/services` | All services across projects |
|
|
25
|
+
| `/settings`, `/about`, `/changelog` | App settings, setup and command reference, release notes |
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
LocalDeck remembers registered project files; it does not scan your disk. Missing or invalid projects remain visible with Retry and Forget until fixed or forgotten.
|
|
28
|
+
|
|
29
|
+
## Projects and services
|
|
30
|
+
|
|
31
|
+
The project header shows the project name and status, the **Environment** (Git worktree) and **Profiles** menus, the tabs, and **Start project** / **Start remaining**, **Stop project** and **Open app**. Start includes prerequisites and waits for readiness; Stop reverses dependency order.
|
|
32
|
+
|
|
33
|
+
Each service row shows its address, ports, share link and status, with Start, Stop (and Restart when its configuration changed) and an options menu: **View logs**, **View requests**, **Configure**, **Watch in panel**, **Copy address**, and **Share…** or **Stop sharing**. Select a service's name to open its page. Failed services keep their own errors so other services remain usable.
|
|
34
|
+
|
|
35
|
+
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 (the service page says so). 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 project, Ctrl-C, and daemon shutdown cancel pending retries. Shutting down the daemon stops its services and shares.
|
|
36
|
+
|
|
37
|
+
## Feedback and confirmations
|
|
38
|
+
|
|
39
|
+
Saving shows a short confirmation in the bottom-right corner and closes whatever opened the form. Errors stay on screen until you dismiss them, and the field that caused one is marked. Actions that delete something ask first: forgetting a project, removing a secret, deleting a check, clearing logs or requests, and removing the Clean URLs forwarder. Press **Escape** to close the topmost menu, dialog or inspector.
|
|
40
|
+
|
|
41
|
+
## Profiles
|
|
42
|
+
|
|
43
|
+
When `.localdeck` defines [profiles](configuration.md#profiles), a **Profiles** menu appears beside Environment at the top of the project page. Tick profiles to turn them on or off for this project on this computer; **Turn all off** clears them. Each entry shows its description and which services it changes. If two active profiles change the same setting, the menu says which one wins.
|
|
44
|
+
|
|
45
|
+
The active profiles appear next to the project status, and each service they change gets a small profile tag. Switching never starts, stops or restarts services. A running service whose settings changed shows **Restart to apply profile changes** with a Restart button. Terminal-owned services say to restart them in their terminal instead. The same notice appears after hand-editing a running service's command, directory, environment or secret names in `.localdeck`.
|
|
26
46
|
|
|
27
47
|
## Project settings
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
Open the **Settings** tab (`/projects/<project-id>/settings`) for the project's nickname, service configuration, profiles, diagnostics and **Forget project** (under Danger zone). The nickname is saved to `.localdeck`; an unchanged nickname disables Save, and a blank nickname removes it.
|
|
30
50
|
|
|
31
51
|
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.
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
Each service links to its **Config** tab, which shows **What it will run**: its command, working directory, environment variables and secret names with active profiles applied, and whether each came from the base service or a profile. Secret values are never shown. 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. The **Profiles** section lists each profile, whether it is on, and what it changes; profiles are also edited in `.localdeck`.
|
|
34
54
|
|
|
35
|
-
The Secrets
|
|
55
|
+
The **Secrets** tab (`/projects/<project-id>/secrets`) lists declared names and whether each value is configured. **Add secret**, **Replace** and **Set value** open a dialog; saving closes it. **Remove** asks before deleting 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
56
|
|
|
37
57
|
## Logs
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
Each service page has **Logs** and **Requests** tabs for that service. To watch several services at once, open the panel at the bottom: select **Logs** or **Requests** in the bar at the bottom of the window, press **⌘J** (**Ctrl+J**), or choose **Watch in panel** from a service's menu. Pick sources from the **Services** menu; **All selected** combines their output chronologically. Filter retained output or follow new lines. Use the trash icon beside a source to clear its logs or requests after confirming. Clearing keeps the service running and new output continues to appear.
|
|
40
60
|
|
|
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
|
|
61
|
+
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 open while you move between pages.
|
|
42
62
|
|
|
43
63
|
**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.
|
|
44
64
|
|
|
@@ -46,17 +66,17 @@ Tunnel requests and sharing events have a **SHARE** label and distinct color. Re
|
|
|
46
66
|
|
|
47
67
|
## Public sharing
|
|
48
68
|
|
|
49
|
-
Install [cloudflared](https://developers.cloudflare.com/tunnel/downloads/) separately. Start an HTTP service, choose **Share
|
|
69
|
+
Install [cloudflared](https://developers.cloudflare.com/tunnel/downloads/) separately. Start an HTTP service, choose **Share…** from its options menu, enter an optional password, and select **Create link**. Leaving the password blank generates one; reveal it with **Show** next to the link. To share without a password, tick **Public link** explicitly. Database services cannot be shared over HTTP.
|
|
50
70
|
|
|
51
|
-
The
|
|
71
|
+
The dialog 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.
|
|
52
72
|
|
|
53
|
-
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.
|
|
73
|
+
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. Copy the link with the button beside it; **Stop sharing** is in the service's options menu.
|
|
54
74
|
|
|
55
75
|
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.
|
|
56
76
|
|
|
57
77
|
## Clean URLs
|
|
58
78
|
|
|
59
|
-
Enable **Clean URLs** in Settings or through the
|
|
79
|
+
Enable **Clean URLs** in Settings or through the notice at the bottom of the sidebar. LocalDeck verifies its own response through port 80 before displaying links without `:7777`. Keep the dashboard itself on its original port.
|
|
60
80
|
|
|
61
81
|
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.
|
|
62
82
|
|
package/docs/mcp.md
CHANGED
|
@@ -51,8 +51,8 @@ A protocol connection alone starts no apps. The first tool call starts the daemo
|
|
|
51
51
|
| Tool | What it does |
|
|
52
52
|
| --- | --- |
|
|
53
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. |
|
|
54
|
+
| `list_services` | List registered and configured services, including ones that have never run. Optional `projectId` filter. Running services include the `profiles` they started with and `restartNeeded`. |
|
|
55
|
+
| `get_service` | Read a configured or registered service's state; ports, URLs and counters appear after its first start. `configuration` shows the resolved command and where each setting came from (base or a profile); environment and secret values are not returned. |
|
|
56
56
|
| `get_logs` | Read recent logs, optionally filtered by `stream`: stdout, stderr or system. |
|
|
57
57
|
| `get_requests` | Read HTTP request records, captured headers, and body previews when available, optionally filtered by exact `status`, `method`, or a `path` substring. |
|
|
58
58
|
| `get_response` | Read a cached response body using the exact service key and request ID from `get_requests`; does not replay the request. |
|
|
@@ -66,7 +66,9 @@ A protocol connection alone starts no apps. The first tool call starts the daemo
|
|
|
66
66
|
| `save_check` | Save a recorded GET/HEAD using `projectId`, `serviceKey`, `requestId`, `name`, and `expectedStatus` (100–599), without running it. |
|
|
67
67
|
| `list_checks` | Read a project's saved checks and retained results. |
|
|
68
68
|
| `run_check` | Explicitly execute one saved `checkId` against its current local service. Even GET can have side effects. |
|
|
69
|
-
| `
|
|
69
|
+
| `list_profiles` | Read a project's profiles, which services each changes, which are on, and any clashes. |
|
|
70
|
+
| `set_profiles` | Replace the list of profiles turned on for `projectId` on this computer. Never starts, stops or restarts services; running services report `restartNeeded` until restarted. |
|
|
71
|
+
| `start_service` | Start a configured service and prerequisites in the background, using the daemon supervisor. Uses the project's active profiles. |
|
|
70
72
|
| `stop_service` | Stop a registered service and its share. Terminal services receive the existing stop request. |
|
|
71
73
|
| `restart_service` | Restart a configured daemon-owned service. Active terminal services must be restarted in their original terminal. |
|
|
72
74
|
| `replay_request` | Resend a recorded GET/HEAD, selected by `requestId`, to that service's current local upstream. |
|