@devrouter/cli 0.0.36 → 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.36" : "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.36" : "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, {
@@ -6457,7 +6688,8 @@ function probeHttpRoute(host, options = {}) {
6457
6688
  String(options.maxTimeSeconds ?? 5)
6458
6689
  ];
6459
6690
  if (tlsEnabled) {
6460
- args.push("--cacert", getMkcertRootCAPath({ repoPath: options.repoPath }));
6691
+ getMkcertRootCAPath({ repoPath: options.repoPath });
6692
+ args.push("--cacert", CERT_FILE);
6461
6693
  }
6462
6694
  args.push(url);
6463
6695
  const result = (0, import_node_child_process13.spawnSync)("curl", args, { encoding: "utf-8" });
@@ -6781,7 +7013,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6781
7013
  const managedPostStart = resolveManagedPostStartPlan(repoPath);
6782
7014
  const runtime = loadRuntimeConfig(
6783
7015
  repoPath,
6784
- target.kind === "primary" ? "" : target.workspace
7016
+ target.kind === "primary" ? "" : target.workspace,
7017
+ options.profile
6785
7018
  );
6786
7019
  const apps = proxyAppsFromConfig(runtime.config);
6787
7020
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
@@ -6859,7 +7092,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6859
7092
  runManagedPostStart({
6860
7093
  plan: managedPostStart,
6861
7094
  container,
6862
- quiet: options.quiet
7095
+ quiet: options.quiet,
7096
+ profile: runtime.profile
6863
7097
  });
6864
7098
  const publication = await replacePublishedProxyRoutes(
6865
7099
  repoPath,
@@ -6881,7 +7115,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6881
7115
  runManagedPostStart({
6882
7116
  plan: managedPostStart,
6883
7117
  container: recoveredContainer,
6884
- quiet: options.quiet
7118
+ quiet: options.quiet,
7119
+ profile: runtime.profile
6885
7120
  });
6886
7121
  recreated = true;
6887
7122
  replaceHostRoutesForRepo(repoPath, publication.routes);
@@ -6906,6 +7141,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6906
7141
  kind: target.kind,
6907
7142
  repoPath,
6908
7143
  workspace: target.workspace,
7144
+ profile: runtime.profile,
6909
7145
  devpodId,
6910
7146
  urls,
6911
7147
  recreated,
@@ -6977,14 +7213,15 @@ async function runEnsureCommand(options) {
6977
7213
  const repoPath = resolveGitCheckoutPath(options.path);
6978
7214
  const result = await workspaceEnsure(repoPath, {
6979
7215
  open: options.open,
6980
- quiet: Boolean(options.json)
7216
+ quiet: Boolean(options.json),
7217
+ profile: options.profile
6981
7218
  });
6982
7219
  if (options.json) {
6983
7220
  process.stdout.write(`${JSON.stringify(result, null, 2)}
6984
7221
  `);
6985
7222
  return;
6986
7223
  }
6987
- 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}]`;
6988
7225
  const routes = result.urls.map((url) => ` ${url}`).join("\n");
6989
7226
  process.stdout.write(`${label} is ready (${result.devpodId}).
6990
7227
  ${routes}${routes ? "\n" : ""}`);
@@ -11424,7 +11661,7 @@ var init_version = __esm({
11424
11661
 
11425
11662
  // src/cli.ts
11426
11663
  var import_commander = require("commander");
11427
- var CLI_VERSION = true ? "0.0.36" : "0.0.0-dev";
11664
+ var CLI_VERSION = true ? "0.0.38" : "0.0.0-dev";
11428
11665
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
11429
11666
  function withErrorHandling(action2) {
11430
11667
  return async (...args) => {
@@ -11474,12 +11711,13 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
11474
11711
  await runUpCommand2();
11475
11712
  })
11476
11713
  );
11477
- 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(
11478
11715
  withErrorHandling(async (repoPath, _options, command) => {
11479
11716
  const options = command.opts();
11480
11717
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11481
11718
  await runEnsureCommand2({
11482
11719
  path: repoPath,
11720
+ profile: options.profile,
11483
11721
  open: Boolean(options.open),
11484
11722
  json: Boolean(options.json)
11485
11723
  });
@@ -11678,13 +11916,14 @@ workspaceCommand2.command("up").description(
11678
11916
  });
11679
11917
  })
11680
11918
  );
11681
- 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(
11682
11920
  withErrorHandling(
11683
11921
  async (worktreePath, _options, command) => {
11684
11922
  const options = command.opts();
11685
11923
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11686
11924
  await runEnsureCommand2({
11687
11925
  path: worktreePath,
11926
+ profile: options.profile,
11688
11927
  open: Boolean(options.open),
11689
11928
  json: Boolean(options.json)
11690
11929
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.36",
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,30 @@
1
+ # Upgrade to devrouter 0.0.37
2
+
3
+ Devrouter HTTPS readiness probes now pin curl to the exact certificate served
4
+ by the local Traefik router. This avoids a macOS SecureTransport failure while
5
+ retaining hostname verification, SNI, and the existing route status contract.
6
+
7
+ 1. Install `@devrouter/cli@0.0.37` on the host and bump `.devrouter.yml` to
8
+ `devrouter.version: 0.0.37`.
9
+ 2. No schema or data migration is required.
10
+ 3. Re-run `devrouter doctor --repo <checkout> --json` and then use
11
+ `devrouter ensure <checkout> --json` for managed startup. The readiness
12
+ probe still requires the existing mkcert setup and does not use an
13
+ insecure certificate bypass.
14
+
15
+ Verification:
16
+
17
+ - Run `devrouter -V --repo <checkout>` and confirm both the installed CLI and
18
+ local repository report `0.0.37`.
19
+ - Run `devrouter repo devcontainer verify --repo <checkout> --json` and confirm
20
+ the static checks pass.
21
+ - Run `devrouter ensure <checkout> --json` and confirm the HTTPS route reaches
22
+ the existing status contract (`100..499` accepted, `5xx` rejected).
23
+
24
+ Report template:
25
+
26
+ - CLI/config version: `0.0.37`
27
+ - mkcert setup and certificate readiness: `<passed or details>`
28
+ - Static verification: `<passed or details>`
29
+ - Managed HTTPS readiness: `<passed or details>`
30
+ - Remaining blockers: `<none or details>`
@@ -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>`