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/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-tv699dzb.js";
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 up --profile <name> run a saved profile and its prerequisites
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-a6z3akjx.js")).updateLocalDeck();
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-c138pjc5.js")).runMcp(rest);
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 profiles = project.profiles;
1037
- if (profile && (!profiles || !Object.prototype.hasOwnProperty.call(profiles, profile)))
1038
- fail(`unknown profile "${profile}". Available profiles: ${Object.keys(profiles ?? {}).join(", ") || "(none)"}`);
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(profiles[profile]);
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
- await dependencies.runForeground(selected.map((p, i) => toRunOptions(p, project, remembered, { prefix: selected.length > 1, color: PALETTE[i % PALETTE.length] })));
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-tv699dzb.js";
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
- items.set(serviceKey, { key: serviceKey, projectId: summary.id, projectName: summary.name, name: c.name, status: "stopped", managedBy: "daemon", configured: true, ...items.get(serviceKey) });
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
- return { data: { service: target.runtime ?? { key: args.serviceKey, projectId: target.projectId, name: target.name, status: "stopped", configured: true, managedBy: "daemon" }, notice } };
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);
@@ -3,7 +3,7 @@ import {
3
3
  STATE_FILE,
4
4
  assertUserRuntime,
5
5
  findDaemon
6
- } from "./index-tv699dzb.js";
6
+ } from "./index-xsyz4cc9.js";
7
7
 
8
8
  // src/update.ts
9
9
  import { spawn } from "node:child_process";
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 the names in the saved `app` profile and their prerequisites. Additional name/tag selectors must stay within the profile. |
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#profiles-and-environment-wiring).
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
 
@@ -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
- ### Profiles and environment wiring
50
+ ### Environment wiring
51
51
 
52
- Use the object format when defining startup profiles:
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
- { "name": "docs", "run": "npm run docs" }
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": { "app": ["web"], "everything": ["web", "docs"] }
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
- `localdeck up --profile app` starts web and its API prerequisite. Profiles are also selectable in the dashboard. Variable names such as `PUBLIC_API_URL` must match what your framework actually reads; LocalDeck does not rewrite application code or `.env` files.
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 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.
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, 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.
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
- ## Pages and projects
13
+ ## Layout and navigation
14
14
 
15
- - **Projects** (`/projects`) lists projects remembered through the CLI. Selecting one opens `/projects/<id>`.
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
- Routes support refresh, direct links, and browser Back/Forward. LocalDeck remembers registered project files; it does not scan your disk. Missing or invalid projects remain visible with recovery actions until fixed or forgotten.
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
- Project pages expose individual Start/Stop, View logs, View requests, and Share controls, plus Start all and Stop all. Start includes prerequisites and waits for readiness; Stop all reverses dependency order. Local and shared URLs appear below each service. Failed services keep their own errors so other services remain usable.
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
- Dashboard-started services run in the background under the daemon. Terminal services stay attached to their original CLI session; stopping them is supported, but restart them from their terminal. Unexpected process exits trigger up to three automatic restarts, each after a three-second delay. Logs show the attempt number; after the third failed retry, start the service manually to try again. The retry budget lasts until the next manual start. Stop, Stop all, Ctrl-C, and daemon shutdown cancel pending retries. Shutting down the daemon stops its services and shares.
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
- Use **Settings** beside the project name to edit its nickname and service configuration. The nickname is saved to `.localdeck`; an unchanged nickname disables Save, and a blank nickname removes it.
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
- 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.
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 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.
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
- 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.
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 available when navigating between pages.
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**, enter an optional password, and submit. 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.
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 menu closes on submission. The generated URL appears below the local URL with a spinner until verified. At one minute, a hint suggests opening it manually; a request reaching the public tunnel can confirm readiness even when the daemon's DNS probe fails. Startup has a two-minute limit. Failure closes the tunnel and shows an inline error with retry guidance.
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. The Share menu also provides Copy link and Stop sharing.
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 prompt above Local runtime. LocalDeck verifies its own response through port 80 before displaying links without `:7777`. Keep the dashboard itself on its original port.
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
- | `start_service` | Start a configured service and prerequisites in the background, using the daemon supervisor. |
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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localdeck",
3
- "version": "1.5.0",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "description": "Stable ports, shareable HTTPS links and request logs for your local dev servers",
6
6
  "bin": {