@devrouter/cli 0.0.37 → 0.0.38

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/devrouter.js CHANGED
@@ -221,6 +221,31 @@ healthcheck:
221
221
  retries: 20
222
222
  \`\`\`
223
223
 
224
+ ## Profiles
225
+
226
+ Optional named subsets of routed apps in \`.devrouter.yml\` so \`ensure\` can start only what a task needs:
227
+
228
+ \`\`\`yaml
229
+ profiles:
230
+ manage:
231
+ apps: [manage, api, auth]
232
+ readiness: [manage, api]
233
+ pwa:
234
+ apps: [pwa, api, auth]
235
+ readiness: [pwa, api]
236
+ full:
237
+ apps: ['*']
238
+ default: true
239
+ \`\`\`
240
+
241
+ - \`apps\` (required): routed app names (\`kind=app\`) or \`['*']\` for everything.
242
+ - \`dependencies\` (optional): \`kind=dependency\` services this profile needs; omitted = every dependency a kept app requires transitively.
243
+ - \`readiness\` (optional): subset of the profile's apps that \`ensure\` HTTP-probes; omitted = all profile apps with an http route.
244
+ - \`default\` (optional): at most one; used when \`--profile\` is omitted. No \`profiles\` key at all = implicit full behavior.
245
+ - Validation is strict at config load: unknown keys, non-routed \`apps\`, non-dependency \`dependencies\`, \`readiness\` outside the profile's apps, and multiple defaults are rejected.
246
+ - Selection: \`devrouter ensure <path> --profile <name>\`. Comma-separated selections (\`--profile manage,pwa\`) merge with deduplication; the canonical name is sorted-unique so order never affects identity or fingerprints. A wildcard member collapses to everything.
247
+ - Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
248
+
224
249
  ## Env var injection
225
250
 
226
251
  When a host app depends on a TCP Docker service, \`devrouter app run\` and \`devrouter app exec\` inject per-dep deterministic vars (where \`{PREFIX} = dep.name.toUpperCase().replace(/-/g, "_")\`):
@@ -1926,7 +1951,11 @@ function parseApp(value, index) {
1926
1951
  }
1927
1952
  function parseConfig(raw, configPath) {
1928
1953
  const root = ensureObject(raw, configPath);
1929
- ensureAllowedKeys(root, ["version", "devrouter", "project", "secretManager", "apps"], configPath);
1954
+ ensureAllowedKeys(
1955
+ root,
1956
+ ["version", "devrouter", "project", "secretManager", "profiles", "apps"],
1957
+ configPath
1958
+ );
1930
1959
  const version = toIntegerOrThrow(root.version, `${configPath}.version`);
1931
1960
  if (version !== 1) {
1932
1961
  throw new Error(`${configPath}.version must be 1.`);
@@ -1994,14 +2023,202 @@ function parseConfig(raw, configPath) {
1994
2023
  }
1995
2024
  seenNames.add(app.name);
1996
2025
  }
2026
+ const profiles = parseProfiles(root.profiles, configPath, apps);
1997
2027
  return {
1998
2028
  version: 1,
1999
2029
  ...devrouter ? { devrouter } : {},
2000
2030
  project: root.project && typeof root.project === "object" ? { name: root.project.name } : void 0,
2001
2031
  ...secretManager ? { secretManager } : {},
2032
+ ...profiles ? { profiles } : {},
2002
2033
  apps
2003
2034
  };
2004
2035
  }
2036
+ function parseProfiles(value, configPath, apps) {
2037
+ if (value === void 0) {
2038
+ return void 0;
2039
+ }
2040
+ const raw = ensureObject(value, `${configPath}.profiles`);
2041
+ if (Object.keys(raw).length === 0) {
2042
+ throw new Error(`${configPath}.profiles must define at least one profile.`);
2043
+ }
2044
+ if (Object.keys(raw).length > MAX_PROFILES) {
2045
+ throw new Error(`${configPath}.profiles exceeds the maximum of ${MAX_PROFILES} profiles.`);
2046
+ }
2047
+ const routedNames = new Set(
2048
+ apps.filter((app) => app.kind !== "dependency").map((app) => app.name)
2049
+ );
2050
+ const result = {};
2051
+ let defaultCount = 0;
2052
+ for (const [name, profileValue] of Object.entries(raw)) {
2053
+ if (!PROFILE_NAME_RE.test(name)) {
2054
+ throw new Error(
2055
+ `${configPath}.profiles.${name} is not a valid profile name (lowercase alphanumerics and hyphens).`
2056
+ );
2057
+ }
2058
+ const profile = ensureObject(profileValue, `${configPath}.profiles.${name}`);
2059
+ ensureAllowedKeys(
2060
+ profile,
2061
+ ["apps", "dependencies", "readiness", "default"],
2062
+ `${configPath}.profiles.${name}`
2063
+ );
2064
+ const profileApps = toStringArray(profile.apps, `${configPath}.profiles.${name}.apps`);
2065
+ if (profileApps.length === 0) {
2066
+ throw new Error(`${configPath}.profiles.${name}.apps must not be empty.`);
2067
+ }
2068
+ const isWildcard = profileApps.length === 1 && profileApps[0] === "*";
2069
+ if (!isWildcard) {
2070
+ for (const appName of profileApps) {
2071
+ if (!routedNames.has(appName)) {
2072
+ throw new Error(
2073
+ `${configPath}.profiles.${name}.apps references '${appName}', which is not a routed app (kind=app).`
2074
+ );
2075
+ }
2076
+ }
2077
+ }
2078
+ let dependencies;
2079
+ if (profile.dependencies !== void 0) {
2080
+ dependencies = toStringArray(
2081
+ profile.dependencies,
2082
+ `${configPath}.profiles.${name}.dependencies`
2083
+ );
2084
+ for (const depName of dependencies) {
2085
+ const dependency = apps.find((candidate) => candidate.name === depName);
2086
+ if (!dependency) {
2087
+ throw new Error(
2088
+ `${configPath}.profiles.${name}.dependencies references '${depName}', which does not exist in apps.`
2089
+ );
2090
+ }
2091
+ if (dependency.kind !== "dependency") {
2092
+ throw new Error(
2093
+ `${configPath}.profiles.${name}.dependencies references '${depName}', which is not kind=dependency.`
2094
+ );
2095
+ }
2096
+ }
2097
+ }
2098
+ let readiness;
2099
+ if (profile.readiness !== void 0) {
2100
+ readiness = toStringArray(profile.readiness, `${configPath}.profiles.${name}.readiness`);
2101
+ const appSet = new Set(isWildcard ? Array.from(routedNames) : profileApps);
2102
+ for (const readyName of readiness) {
2103
+ if (!appSet.has(readyName)) {
2104
+ throw new Error(
2105
+ `${configPath}.profiles.${name}.readiness references '${readyName}', which is not in the profile's apps.`
2106
+ );
2107
+ }
2108
+ }
2109
+ }
2110
+ let isDefault = false;
2111
+ if (profile.default !== void 0) {
2112
+ if (typeof profile.default !== "boolean") {
2113
+ throw new Error(`${configPath}.profiles.${name}.default must be a boolean.`);
2114
+ }
2115
+ isDefault = profile.default;
2116
+ }
2117
+ if (isDefault) {
2118
+ defaultCount += 1;
2119
+ }
2120
+ result[name] = {
2121
+ apps: profileApps,
2122
+ ...dependencies ? { dependencies } : {},
2123
+ ...readiness ? { readiness } : {},
2124
+ ...isDefault ? { default: true } : {}
2125
+ };
2126
+ }
2127
+ if (defaultCount > 1) {
2128
+ throw new Error(`${configPath}.profiles must have at most one default profile.`);
2129
+ }
2130
+ return result;
2131
+ }
2132
+ function resolveProfile(config, profileOverride) {
2133
+ const profiles = config.profiles;
2134
+ const mergeSelection = (selection) => {
2135
+ const names = Array.from(
2136
+ new Set(
2137
+ selection.split(",").map((name) => name.trim()).filter(Boolean)
2138
+ )
2139
+ );
2140
+ if (names.length === 0) {
2141
+ throw new Error("Profile selection is empty.");
2142
+ }
2143
+ const missing = names.filter((name) => !profiles?.[name]);
2144
+ if (missing.length > 0) {
2145
+ const available = profiles ? Object.keys(profiles).join(", ") : "(none defined)";
2146
+ throw new Error(
2147
+ `Profile '${missing[0]}' is not defined in .devrouter.yml. Available: ${available}`
2148
+ );
2149
+ }
2150
+ if (names.length === 1) {
2151
+ return { name: names[0], profile: profiles?.[names[0]] };
2152
+ }
2153
+ const canonicalName = [...names].sort().join(",");
2154
+ const apps = /* @__PURE__ */ new Set();
2155
+ const dependencies = /* @__PURE__ */ new Set();
2156
+ const readiness = /* @__PURE__ */ new Set();
2157
+ let hasWildcard = false;
2158
+ for (const name of names) {
2159
+ const profile = profiles?.[name];
2160
+ if (!profile) continue;
2161
+ if (profile.apps.length === 1 && profile.apps[0] === "*") {
2162
+ hasWildcard = true;
2163
+ continue;
2164
+ }
2165
+ for (const appName of profile.apps) apps.add(appName);
2166
+ for (const depName of profile.dependencies ?? []) dependencies.add(depName);
2167
+ for (const readyName of profile.readiness ?? []) readiness.add(readyName);
2168
+ }
2169
+ if (hasWildcard) {
2170
+ return { name: canonicalName, profile: { apps: ["*"] } };
2171
+ }
2172
+ const merged = {
2173
+ apps: Array.from(apps),
2174
+ ...dependencies.size > 0 ? { dependencies: Array.from(dependencies) } : {},
2175
+ ...readiness.size > 0 ? { readiness: Array.from(readiness) } : {}
2176
+ };
2177
+ return { name: canonicalName, profile: merged };
2178
+ };
2179
+ if (profileOverride !== void 0) {
2180
+ return mergeSelection(profileOverride);
2181
+ }
2182
+ if (profiles) {
2183
+ const defaultName = Object.keys(profiles).find((name) => profiles[name].default);
2184
+ if (defaultName) {
2185
+ return { name: defaultName, profile: profiles[defaultName] };
2186
+ }
2187
+ return { name: "full", profile: void 0 };
2188
+ }
2189
+ return { name: "full", profile: void 0 };
2190
+ }
2191
+ function applyProfile(config, profile) {
2192
+ if (!profile || profile.apps.length === 1 && profile.apps[0] === "*") {
2193
+ return config;
2194
+ }
2195
+ const appSet = new Set(profile.apps);
2196
+ const next = structuredClone(config);
2197
+ const kept = [];
2198
+ const keptDependencies = /* @__PURE__ */ new Set();
2199
+ for (const app of next.apps) {
2200
+ if (app.kind === "dependency") {
2201
+ continue;
2202
+ }
2203
+ if (!appSet.has(app.name)) {
2204
+ continue;
2205
+ }
2206
+ for (const dependency of resolveAppDependencies(config, app)) {
2207
+ keptDependencies.add(dependency.name);
2208
+ }
2209
+ kept.push(app);
2210
+ }
2211
+ for (const dependencyName of profile.dependencies ?? []) {
2212
+ keptDependencies.add(dependencyName);
2213
+ }
2214
+ for (const app of next.apps) {
2215
+ if (app.kind === "dependency" && keptDependencies.has(app.name)) {
2216
+ kept.push(app);
2217
+ }
2218
+ }
2219
+ next.apps = kept;
2220
+ return next;
2221
+ }
2005
2222
  function renderConfig(config) {
2006
2223
  return import_yaml2.default.stringify(config, { lineWidth: 0 });
2007
2224
  }
@@ -2049,7 +2266,7 @@ function loadRepoConfig(repoPath) {
2049
2266
  const config = parseConfig(parsed ?? {}, configPath);
2050
2267
  const requiredVersion = config.devrouter?.version;
2051
2268
  if (requiredVersion && !hasWarnedVersionMismatch) {
2052
- const cliVersion = true ? "0.0.37" : "0.0.0-dev";
2269
+ const cliVersion = true ? "0.0.38" : "0.0.0-dev";
2053
2270
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2054
2271
  hasWarnedVersionMismatch = true;
2055
2272
  process.stderr.write(
@@ -2340,11 +2557,13 @@ function applyWorkspace(config, workspace, repoPath) {
2340
2557
  }
2341
2558
  return next;
2342
2559
  }
2343
- function loadRuntimeConfig(repoPath, workspaceOverride) {
2560
+ function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
2344
2561
  const resolved = resolveRepoPath(repoPath);
2345
2562
  const raw = loadRepoConfig(resolved);
2563
+ const resolvedProfile = resolveProfile(raw, profileOverride);
2346
2564
  const workspace = resolveWorkspace(resolved, workspaceOverride);
2347
- return { config: applyWorkspace(raw, workspace, resolved), workspace };
2565
+ const config = applyProfile(applyWorkspace(raw, workspace, resolved), resolvedProfile.profile);
2566
+ return { config, workspace, profile: resolvedProfile.name };
2348
2567
  }
2349
2568
  function resolveAppByName(repoPath, name, workspaceOverride) {
2350
2569
  const { config, workspace } = loadRuntimeConfig(repoPath, workspaceOverride);
@@ -2386,7 +2605,7 @@ function resolveAppDependencies(config, app) {
2386
2605
  }
2387
2606
  return results;
2388
2607
  }
2389
- var import_node_fs7, import_node_path6, import_yaml2, hasWarnedVersionMismatch, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY;
2608
+ var import_node_fs7, import_node_path6, import_yaml2, hasWarnedVersionMismatch, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY, PROFILE_NAME_RE, MAX_PROFILES;
2390
2609
  var init_repo_config = __esm({
2391
2610
  "src/core/repo-config.ts"() {
2392
2611
  "use strict";
@@ -2410,6 +2629,8 @@ var init_repo_config = __esm({
2410
2629
  denyPorts: [80, 443, 5432],
2411
2630
  allowPortRange: "1024-65535"
2412
2631
  };
2632
+ PROFILE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2633
+ MAX_PROFILES = 32;
2413
2634
  }
2414
2635
  });
2415
2636
 
@@ -2470,6 +2691,7 @@ function buildOnboardingPrompt(options = {}) {
2470
2691
  "- project.name: string (optional)",
2471
2692
  `- secretManager.command: string (optional; SM command including trailing \`--\` boundary; supports \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template placeholder)`,
2472
2693
  `- secretManager.defaultEnv: string (optional; fallback env for \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template; required when command contains \`${SECRET_MANAGER_ENV_PLACEHOLDER}\`)`,
2694
+ "- profiles: map (optional; named subsets of routed apps; see Profile schema below)",
2473
2695
  "- apps: array (required)",
2474
2696
  "",
2475
2697
  "Canonical valid skeleton:",
@@ -2514,6 +2736,14 @@ function buildOnboardingPrompt(options = {}) {
2514
2736
  " - docker.composeFiles: string[]",
2515
2737
  " - do not set host/protocol/tcpProtocol/hostRun/docker.internalPort/docker.router",
2516
2738
  "",
2739
+ "Profile schema (each value in the optional top-level profiles map):",
2740
+ '- apps: array (required; routed app names, or ["*"] for every routed app)',
2741
+ "- dependencies: array (optional; kind=dependency names this profile needs; omitted = every dependency a kept app requires transitively)",
2742
+ "- readiness: array (optional; subset of the profile's apps that `devrouter ensure` HTTP-probes before readiness; omitted = all profile apps with an http route)",
2743
+ "- default: boolean (optional; at most one profile may set true)",
2744
+ "- validation: unknown profile keys are rejected; apps must reference routed apps (kind=app); dependencies must reference kind=dependency apps; readiness entries must be in the profile's apps; at most one default",
2745
+ "- selection: `devrouter ensure <path> --profile <name>` scopes routes/readiness to the profile; comma-separated selections (e.g. --profile a,b) merge with deduplication and a canonical sorted name; managed adapters receive DEVROUTER_PROFILE in the post-start env",
2746
+ "",
2517
2747
  "Validation rules to enforce:",
2518
2748
  "- kind=app host must end with .localhost",
2519
2749
  `- kind=app runtime=host supports protocol=${formatProtocolRule("host")} only`,
@@ -3619,6 +3849,7 @@ function runManagedPostStart(options) {
3619
3849
  `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
3620
3850
  "--env",
3621
3851
  `DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
3852
+ ...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
3622
3853
  options.container.id,
3623
3854
  "bash",
3624
3855
  "-c",
@@ -3630,7 +3861,7 @@ function runManagedPostStart(options) {
3630
3861
  );
3631
3862
  if (started.status !== 0) {
3632
3863
  const details = commandFailure(started);
3633
- throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
3864
+ throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
3634
3865
  }
3635
3866
  }
3636
3867
  var import_node_child_process5, import_node_crypto3, import_node_fs9, import_node_path8, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
@@ -5794,7 +6025,7 @@ async function buildDoctorReport(options = {}) {
5794
6025
  const config = runtimeConfig.config;
5795
6026
  loadedConfig = config;
5796
6027
  loadedWorkspace = runtimeConfig.workspace;
5797
- const cliVersion = true ? "0.0.37" : "0.0.0-dev";
6028
+ const cliVersion = true ? "0.0.38" : "0.0.0-dev";
5798
6029
  const configVersion = config.devrouter?.version;
5799
6030
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
5800
6031
  addCheck(checks, {
@@ -6782,7 +7013,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6782
7013
  const managedPostStart = resolveManagedPostStartPlan(repoPath);
6783
7014
  const runtime = loadRuntimeConfig(
6784
7015
  repoPath,
6785
- target.kind === "primary" ? "" : target.workspace
7016
+ target.kind === "primary" ? "" : target.workspace,
7017
+ options.profile
6786
7018
  );
6787
7019
  const apps = proxyAppsFromConfig(runtime.config);
6788
7020
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
@@ -6860,7 +7092,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6860
7092
  runManagedPostStart({
6861
7093
  plan: managedPostStart,
6862
7094
  container,
6863
- quiet: options.quiet
7095
+ quiet: options.quiet,
7096
+ profile: runtime.profile
6864
7097
  });
6865
7098
  const publication = await replacePublishedProxyRoutes(
6866
7099
  repoPath,
@@ -6882,7 +7115,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6882
7115
  runManagedPostStart({
6883
7116
  plan: managedPostStart,
6884
7117
  container: recoveredContainer,
6885
- quiet: options.quiet
7118
+ quiet: options.quiet,
7119
+ profile: runtime.profile
6886
7120
  });
6887
7121
  recreated = true;
6888
7122
  replaceHostRoutesForRepo(repoPath, publication.routes);
@@ -6907,6 +7141,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6907
7141
  kind: target.kind,
6908
7142
  repoPath,
6909
7143
  workspace: target.workspace,
7144
+ profile: runtime.profile,
6910
7145
  devpodId,
6911
7146
  urls,
6912
7147
  recreated,
@@ -6978,14 +7213,15 @@ async function runEnsureCommand(options) {
6978
7213
  const repoPath = resolveGitCheckoutPath(options.path);
6979
7214
  const result = await workspaceEnsure(repoPath, {
6980
7215
  open: options.open,
6981
- quiet: Boolean(options.json)
7216
+ quiet: Boolean(options.json),
7217
+ profile: options.profile
6982
7218
  });
6983
7219
  if (options.json) {
6984
7220
  process.stdout.write(`${JSON.stringify(result, null, 2)}
6985
7221
  `);
6986
7222
  return;
6987
7223
  }
6988
- const label = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
7224
+ const label = result.kind === "primary" ? `Primary checkout [profile: ${result.profile}]` : `Workspace '${result.workspace}' [profile: ${result.profile}]`;
6989
7225
  const routes = result.urls.map((url) => ` ${url}`).join("\n");
6990
7226
  process.stdout.write(`${label} is ready (${result.devpodId}).
6991
7227
  ${routes}${routes ? "\n" : ""}`);
@@ -11425,7 +11661,7 @@ var init_version = __esm({
11425
11661
 
11426
11662
  // src/cli.ts
11427
11663
  var import_commander = require("commander");
11428
- var CLI_VERSION = true ? "0.0.37" : "0.0.0-dev";
11664
+ var CLI_VERSION = true ? "0.0.38" : "0.0.0-dev";
11429
11665
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
11430
11666
  function withErrorHandling(action2) {
11431
11667
  return async (...args) => {
@@ -11475,12 +11711,13 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
11475
11711
  await runUpCommand2();
11476
11712
  })
11477
11713
  );
11478
- program.command("ensure").description("Start and prove a primary or linked checkout's environment and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
11714
+ program.command("ensure").description("Start and prove a primary or linked checkout's environment and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
11479
11715
  withErrorHandling(async (repoPath, _options, command) => {
11480
11716
  const options = command.opts();
11481
11717
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11482
11718
  await runEnsureCommand2({
11483
11719
  path: repoPath,
11720
+ profile: options.profile,
11484
11721
  open: Boolean(options.open),
11485
11722
  json: Boolean(options.json)
11486
11723
  });
@@ -11679,13 +11916,14 @@ workspaceCommand2.command("up").description(
11679
11916
  });
11680
11917
  })
11681
11918
  );
11682
- workspaceCommand2.command("ensure").description("Start and prove a primary or linked checkout's DevPod, upstreams, and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
11919
+ workspaceCommand2.command("ensure").description("Start and prove a primary or linked checkout's DevPod, upstreams, and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
11683
11920
  withErrorHandling(
11684
11921
  async (worktreePath, _options, command) => {
11685
11922
  const options = command.opts();
11686
11923
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11687
11924
  await runEnsureCommand2({
11688
11925
  path: worktreePath,
11926
+ profile: options.profile,
11689
11927
  open: Boolean(options.open),
11690
11928
  json: Boolean(options.json)
11691
11929
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,45 @@
1
+ # Upgrade to devrouter 0.0.38
2
+
3
+ Devrouter now supports profile-scoped environments: a `profiles` map in
4
+ `.devrouter.yml` plus `devrouter ensure --profile <name>` starts, routes, and
5
+ readiness-probes only the apps a task needs. Comma-separated selections
6
+ (`--profile manage,pwa`) merge profiles with deduplication, and managed
7
+ adapters receive `DEVROUTER_PROFILE` so they can scope their dev process tree
8
+ to the same selection.
9
+
10
+ 1. Install `@devrouter/cli@0.0.38` on the host and bump `.devrouter.yml` to
11
+ `devrouter.version: 0.0.38`.
12
+ 2. No schema or data migration is required. Repositories without a `profiles`
13
+ key keep the implicit full behavior; adding `profiles` is optional.
14
+ Validation is strict: unknown keys, references to apps that are not routed
15
+ (`kind=app`), references to apps that are not `kind=dependency` in
16
+ `dependencies`, `readiness` entries outside the profile's apps, and more
17
+ than one `default: true` are rejected at config-load time.
18
+ 3. Optional: declare profiles and select one per task, e.g.
19
+ `devrouter ensure <checkout> --profile <name>` or
20
+ `devrouter ensure <checkout> --profile <name-a>,<name-b>`. The merged name
21
+ is canonicalized (sorted unique), so order does not affect identity.
22
+ Repository post-start adapters can read `DEVROUTER_PROFILE` and must treat
23
+ an empty/unset value as the default profile.
24
+ 4. Consumers that parse `ensure --json` output must accept the new `profile`
25
+ field in the result object.
26
+
27
+ Verification:
28
+
29
+ - Run `devrouter -V --repo <checkout>` and confirm both the installed CLI and
30
+ the local repository report `0.0.38`.
31
+ - Run `devrouter doctor --repo <checkout> --json` and confirm the config still
32
+ validates.
33
+ - With profiles declared, run `devrouter ensure <checkout> --profile <name>
34
+ --json` and confirm the result reports `"profile": "<name>"` and that only
35
+ the profile's routes are published (`devrouter ls`).
36
+ - Re-run `devrouter ensure <checkout> --json` without `--profile` and confirm
37
+ the default/full route set is restored.
38
+
39
+ Report template:
40
+
41
+ - CLI/config version: `0.0.38`
42
+ - Config validation: `<passed or details>`
43
+ - Profile-scoped ensure: `<passed or details>`
44
+ - Default (full) ensure after profile use: `<passed or details>`
45
+ - Remaining blockers: `<none or details>`