@devrouter/cli 0.0.37 → 0.0.39

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
@@ -48,6 +48,7 @@ function buildDevrouterSection() {
48
48
  "- `devrouter tls install` (required when repo defines tcp/postgres apps)",
49
49
  "- `devrouter app ls --repo .`",
50
50
  "- Primary or linked devcontainer checkout: `devrouter ensure . --json`",
51
+ "- Managed selective profile: `devrouter ensure . --profile <name> --json`",
51
52
  "- Host/docker runtime app only: `devrouter app run <host-app> --repo . --yes`",
52
53
  "- `devrouter ls`",
53
54
  DEVROUTER_END_SENTINEL
@@ -221,6 +222,78 @@ healthcheck:
221
222
  retries: 20
222
223
  \`\`\`
223
224
 
225
+ ## Profiles
226
+
227
+ Optional named subsets of routed apps in \`.devrouter.yml\` so \`ensure\` can start only what a task needs:
228
+
229
+ \`\`\`yaml
230
+ profiles:
231
+ manage:
232
+ apps: [manage, api, auth]
233
+ readiness: [manage, api]
234
+ pwa:
235
+ apps: [pwa, api, auth]
236
+ readiness: [pwa, api]
237
+ full:
238
+ apps: ['*']
239
+ default: true
240
+ \`\`\`
241
+
242
+ - \`apps\` (required for app-only profiles): routed app names (\`kind=app\`) or \`['*']\` for everything; with \`managedRuntime\`, omit it for a route-free capability profile.
243
+ - \`dependencies\` (optional): \`kind=dependency\` services this profile needs; omitted = every dependency a kept app requires transitively.
244
+ - \`readiness\` (optional): subset of the profile's apps that \`ensure\` HTTP-probes; omitted = all profile apps with an http route.
245
+ - \`default\` (optional): at most one; used when \`--profile\` is omitted. No \`profiles\` key at all = implicit full behavior.
246
+ - 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.
247
+ - 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.
248
+ - Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
249
+
250
+ ### Managed devcontainer resources
251
+
252
+ The optional \`managedRuntime\` registry separates the primary container's base
253
+ Compose services, optional profile services, and repository-owned process
254
+ markers. Profiles select those dimensions independently:
255
+
256
+ \`\`\`yaml
257
+ managedRuntime:
258
+ devcontainer:
259
+ baseServices: [postgres]
260
+ profileServices: [litellm, mcp-server]
261
+ processes: [web, local-mcp]
262
+
263
+ profiles:
264
+ ai:
265
+ apps: [web]
266
+ devcontainerServices: [litellm]
267
+ processes: [web]
268
+ mcp:
269
+ devcontainerServices: [mcp-server]
270
+ processes: [local-mcp]
271
+ full:
272
+ apps: ['*']
273
+ devcontainerServices: ['*']
274
+ processes: ['*']
275
+ default: true
276
+ \`\`\`
277
+
278
+ - \`baseServices\` remain selected with the primary Dev Container service for
279
+ every managed profile; \`profileServices\` and \`processes\` are registries.
280
+ - \`devcontainerServices\` selects registered optional Compose services, and
281
+ \`processes\` selects registered managed process markers.
282
+ - With \`managedRuntime\`, \`apps\` may be omitted for a route-free capability
283
+ profile. An omitted optional dimension selects nothing, so an app-only
284
+ profile does not start LiteLLM, MCP, MailHog, or another optional service.
285
+ - Use \`*\` or the \`full\` profile to select every registered resource. A config
286
+ without \`managedRuntime\` keeps the app-only profile behavior.
287
+ - Native Dev Container clients still use the source configuration's full
288
+ service set. Managed \`ensure\` derives an ignored, marker-owned sibling
289
+ configuration and changes only \`runServices\`.
290
+ - Warm profile changes retain the exact DevPod and volumes, do not rerun
291
+ \`postCreateCommand\`, avoid \`--recreate\` and broad \`down\`, and stop dropped
292
+ services/processes only after exact ownership proof. Routes publish last.
293
+ - Inspect managed desired, active, and drift state with \`devrouter status\` and
294
+ \`devrouter doctor\`; values such as credentials and environment contents are
295
+ never written to managed runtime state.
296
+
224
297
  ## Env var injection
225
298
 
226
299
  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, "_")\`):
@@ -299,7 +372,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
299
372
  - \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
300
373
  - \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
301
374
  - \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
302
- - \`devrouter ensure [path] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts
375
+ - \`devrouter ensure [path] [--profile <name>] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts; a managed profile selects its independent resource dimensions
303
376
  - \`devrouter stop [path] [--delete] [--json]\`: stop the exact DevPod and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
304
377
  - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
305
378
  - \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
@@ -322,7 +395,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
322
395
  - \`devrouter app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
323
396
  - \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
324
397
  - \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
325
- - \`devrouter workspace ensure [path] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
398
+ - \`devrouter workspace ensure [path] [--profile <name>] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
326
399
  - \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
327
400
  - \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]\`: report-only cleanup evidence and exact guarded suggestions; no \`--yes\` or apply mode
328
401
  - \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
@@ -339,7 +412,7 @@ For devcontainer onboarding:
339
412
  4. \`devrouter repo devcontainer write --repo . --dry-run --json\`
340
413
  5. \`devrouter repo devcontainer write --repo . --yes\`
341
414
  6. \`devrouter repo devcontainer verify --repo . --json\`
342
- 7. Start and prove either checkout kind with \`devrouter ensure . --json\`
415
+ 7. Start and prove either checkout kind with \`devrouter ensure . --json\`; for managed selective work, use \`--profile <name>\` and inspect desired/active/drift state
343
416
  8. Run seeds or migrations with \`devrouter exec . -- <command...>\`
344
417
 
345
418
  For host/docker runtime apps only:
@@ -1924,9 +1997,77 @@ function parseApp(value, index) {
1924
1997
  }
1925
1998
  throw new Error(`${pathLabel} has unsupported protocol/runtime combination.`);
1926
1999
  }
2000
+ function parseUniqueStringArray(value, pathLabel) {
2001
+ const values = toStringArray(value, pathLabel);
2002
+ const seen = /* @__PURE__ */ new Set();
2003
+ for (const item of values) {
2004
+ if (seen.has(item)) {
2005
+ throw new Error(`${pathLabel} contains duplicate '${item}'.`);
2006
+ }
2007
+ seen.add(item);
2008
+ }
2009
+ return values;
2010
+ }
2011
+ function parseRequiredUniqueStringArray(value, pathLabel) {
2012
+ if (value === void 0) {
2013
+ throw new Error(`${pathLabel} must be an array of strings.`);
2014
+ }
2015
+ return parseUniqueStringArray(value, pathLabel);
2016
+ }
2017
+ function parseManagedRuntime(value, configPath) {
2018
+ if (value === void 0) {
2019
+ return void 0;
2020
+ }
2021
+ const managedRuntime = ensureObject(value, `${configPath}.managedRuntime`);
2022
+ ensureAllowedKeys(managedRuntime, ["devcontainer", "processes"], `${configPath}.managedRuntime`);
2023
+ const devcontainer = ensureObject(
2024
+ managedRuntime.devcontainer,
2025
+ `${configPath}.managedRuntime.devcontainer`
2026
+ );
2027
+ ensureAllowedKeys(
2028
+ devcontainer,
2029
+ ["baseServices", "profileServices"],
2030
+ `${configPath}.managedRuntime.devcontainer`
2031
+ );
2032
+ const baseServices = parseRequiredUniqueStringArray(
2033
+ devcontainer.baseServices,
2034
+ `${configPath}.managedRuntime.devcontainer.baseServices`
2035
+ );
2036
+ const profileServices = parseRequiredUniqueStringArray(
2037
+ devcontainer.profileServices,
2038
+ `${configPath}.managedRuntime.devcontainer.profileServices`
2039
+ );
2040
+ const baseSet = new Set(baseServices);
2041
+ const overlappingServices = profileServices.filter((service) => baseSet.has(service));
2042
+ if (overlappingServices.length > 0) {
2043
+ throw new Error(
2044
+ `${configPath}.managedRuntime.devcontainer.baseServices and profileServices overlap: ${overlappingServices.join(", ")}.`
2045
+ );
2046
+ }
2047
+ const processes = parseRequiredUniqueStringArray(
2048
+ managedRuntime.processes,
2049
+ `${configPath}.managedRuntime.processes`
2050
+ );
2051
+ const unsafeProcesses = processes.filter(
2052
+ (process2) => !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(process2)
2053
+ );
2054
+ if (unsafeProcesses.length > 0) {
2055
+ throw new Error(
2056
+ `${configPath}.managedRuntime.processes entries must be safe exact identifiers: ${unsafeProcesses.join(", ")}.`
2057
+ );
2058
+ }
2059
+ return {
2060
+ devcontainer: { baseServices, profileServices },
2061
+ processes
2062
+ };
2063
+ }
1927
2064
  function parseConfig(raw, configPath) {
1928
2065
  const root = ensureObject(raw, configPath);
1929
- ensureAllowedKeys(root, ["version", "devrouter", "project", "secretManager", "apps"], configPath);
2066
+ ensureAllowedKeys(
2067
+ root,
2068
+ ["version", "devrouter", "project", "secretManager", "managedRuntime", "profiles", "apps"],
2069
+ configPath
2070
+ );
1930
2071
  const version = toIntegerOrThrow(root.version, `${configPath}.version`);
1931
2072
  if (version !== 1) {
1932
2073
  throw new Error(`${configPath}.version must be 1.`);
@@ -1994,14 +2135,328 @@ function parseConfig(raw, configPath) {
1994
2135
  }
1995
2136
  seenNames.add(app.name);
1996
2137
  }
2138
+ const managedRuntime = parseManagedRuntime(root.managedRuntime, configPath);
2139
+ const profiles = parseProfiles(root.profiles, configPath, apps, managedRuntime);
1997
2140
  return {
1998
2141
  version: 1,
1999
2142
  ...devrouter ? { devrouter } : {},
2000
2143
  project: root.project && typeof root.project === "object" ? { name: root.project.name } : void 0,
2001
2144
  ...secretManager ? { secretManager } : {},
2145
+ ...managedRuntime ? { managedRuntime } : {},
2146
+ ...profiles ? { profiles } : {},
2002
2147
  apps
2003
2148
  };
2004
2149
  }
2150
+ function parseProfiles(value, configPath, apps, managedRuntime) {
2151
+ if (value === void 0) {
2152
+ return void 0;
2153
+ }
2154
+ const raw = ensureObject(value, `${configPath}.profiles`);
2155
+ if (Object.keys(raw).length === 0) {
2156
+ throw new Error(`${configPath}.profiles must define at least one profile.`);
2157
+ }
2158
+ if (Object.keys(raw).length > MAX_PROFILES) {
2159
+ throw new Error(`${configPath}.profiles exceeds the maximum of ${MAX_PROFILES} profiles.`);
2160
+ }
2161
+ const routedNames = new Set(
2162
+ apps.filter((app) => app.kind !== "dependency").map((app) => app.name)
2163
+ );
2164
+ const result = {};
2165
+ let defaultCount = 0;
2166
+ const profileServices = new Set(managedRuntime?.devcontainer.profileServices ?? []);
2167
+ const processMarkers = new Set(managedRuntime?.processes ?? []);
2168
+ for (const [name, profileValue] of Object.entries(raw)) {
2169
+ if (!PROFILE_NAME_RE.test(name)) {
2170
+ throw new Error(
2171
+ `${configPath}.profiles.${name} is not a valid profile name (lowercase alphanumerics and hyphens).`
2172
+ );
2173
+ }
2174
+ const profile = ensureObject(profileValue, `${configPath}.profiles.${name}`);
2175
+ ensureAllowedKeys(
2176
+ profile,
2177
+ ["apps", "dependencies", "readiness", "devcontainerServices", "processes", "default"],
2178
+ `${configPath}.profiles.${name}`
2179
+ );
2180
+ const profileApps = toStringArray(profile.apps, `${configPath}.profiles.${name}.apps`);
2181
+ if (!managedRuntime && profileApps.length === 0) {
2182
+ throw new Error(`${configPath}.profiles.${name}.apps must not be empty.`);
2183
+ }
2184
+ const isWildcard3 = profileApps.length === 1 && profileApps[0] === "*";
2185
+ if (profileApps.includes("*") && !isWildcard3) {
2186
+ throw new Error(
2187
+ `${configPath}.profiles.${name}.apps must use '*' as its only entry when selecting all apps.`
2188
+ );
2189
+ }
2190
+ if (!isWildcard3) {
2191
+ for (const appName of profileApps) {
2192
+ if (!routedNames.has(appName)) {
2193
+ throw new Error(
2194
+ `${configPath}.profiles.${name}.apps references '${appName}', which is not a routed app (kind=app).`
2195
+ );
2196
+ }
2197
+ }
2198
+ }
2199
+ let dependencies;
2200
+ if (profile.dependencies !== void 0) {
2201
+ dependencies = toStringArray(
2202
+ profile.dependencies,
2203
+ `${configPath}.profiles.${name}.dependencies`
2204
+ );
2205
+ for (const depName of dependencies) {
2206
+ const dependency = apps.find((candidate) => candidate.name === depName);
2207
+ if (!dependency) {
2208
+ throw new Error(
2209
+ `${configPath}.profiles.${name}.dependencies references '${depName}', which does not exist in apps.`
2210
+ );
2211
+ }
2212
+ if (dependency.kind !== "dependency") {
2213
+ throw new Error(
2214
+ `${configPath}.profiles.${name}.dependencies references '${depName}', which is not kind=dependency.`
2215
+ );
2216
+ }
2217
+ }
2218
+ }
2219
+ let readiness;
2220
+ if (profile.readiness !== void 0) {
2221
+ readiness = toStringArray(profile.readiness, `${configPath}.profiles.${name}.readiness`);
2222
+ const appSet = new Set(isWildcard3 ? Array.from(routedNames) : profileApps);
2223
+ for (const readyName of readiness) {
2224
+ if (!appSet.has(readyName)) {
2225
+ throw new Error(
2226
+ `${configPath}.profiles.${name}.readiness references '${readyName}', which is not in the profile's apps.`
2227
+ );
2228
+ }
2229
+ }
2230
+ }
2231
+ let devcontainerServices;
2232
+ if (profile.devcontainerServices !== void 0) {
2233
+ if (!managedRuntime) {
2234
+ throw new Error(
2235
+ `${configPath}.profiles.${name}.devcontainerServices requires managedRuntime.`
2236
+ );
2237
+ }
2238
+ devcontainerServices = toStringArray(
2239
+ profile.devcontainerServices,
2240
+ `${configPath}.profiles.${name}.devcontainerServices`
2241
+ );
2242
+ const isServiceWildcard = devcontainerServices.length === 1 && devcontainerServices[0] === "*";
2243
+ if (devcontainerServices.includes("*") && !isServiceWildcard) {
2244
+ throw new Error(
2245
+ `${configPath}.profiles.${name}.devcontainerServices must use '*' as its only entry when selecting all services.`
2246
+ );
2247
+ }
2248
+ if (!isServiceWildcard) {
2249
+ for (const service of devcontainerServices) {
2250
+ if (!profileServices.has(service)) {
2251
+ throw new Error(
2252
+ `${configPath}.profiles.${name}.devcontainerServices references '${service}', which is not a registered profile service.`
2253
+ );
2254
+ }
2255
+ }
2256
+ }
2257
+ }
2258
+ let processes;
2259
+ if (profile.processes !== void 0) {
2260
+ if (!managedRuntime) {
2261
+ throw new Error(`${configPath}.profiles.${name}.processes requires managedRuntime.`);
2262
+ }
2263
+ processes = toStringArray(profile.processes, `${configPath}.profiles.${name}.processes`);
2264
+ const isProcessWildcard = processes.length === 1 && processes[0] === "*";
2265
+ if (processes.includes("*") && !isProcessWildcard) {
2266
+ throw new Error(
2267
+ `${configPath}.profiles.${name}.processes must use '*' as its only entry when selecting all processes.`
2268
+ );
2269
+ }
2270
+ if (!isProcessWildcard) {
2271
+ for (const process2 of processes) {
2272
+ if (!processMarkers.has(process2)) {
2273
+ throw new Error(
2274
+ `${configPath}.profiles.${name}.processes references '${process2}', which is not a registered process marker.`
2275
+ );
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ const selectsDimension = profileApps.length > 0 || (dependencies?.length ?? 0) > 0 || (devcontainerServices?.length ?? 0) > 0 || (processes?.length ?? 0) > 0;
2281
+ if (!selectsDimension) {
2282
+ throw new Error(
2283
+ `${configPath}.profiles.${name}.apps must not be empty; select at least one profile dimension.`
2284
+ );
2285
+ }
2286
+ let isDefault = false;
2287
+ if (profile.default !== void 0) {
2288
+ if (typeof profile.default !== "boolean") {
2289
+ throw new Error(`${configPath}.profiles.${name}.default must be a boolean.`);
2290
+ }
2291
+ isDefault = profile.default;
2292
+ }
2293
+ if (isDefault) {
2294
+ defaultCount += 1;
2295
+ }
2296
+ result[name] = {
2297
+ apps: profileApps,
2298
+ ...dependencies ? { dependencies } : {},
2299
+ ...readiness ? { readiness } : {},
2300
+ ...devcontainerServices ? { devcontainerServices } : {},
2301
+ ...processes ? { processes } : {},
2302
+ ...isDefault ? { default: true } : {}
2303
+ };
2304
+ }
2305
+ if (defaultCount > 1) {
2306
+ throw new Error(`${configPath}.profiles must have at most one default profile.`);
2307
+ }
2308
+ return result;
2309
+ }
2310
+ function canonicalProfileValues(values) {
2311
+ return Array.from(new Set(values)).sort();
2312
+ }
2313
+ function isProfileWildcard(values) {
2314
+ return values?.length === 1 && values[0] === "*";
2315
+ }
2316
+ function normalizeProfile(profile) {
2317
+ return {
2318
+ apps: canonicalProfileValues(profile.apps),
2319
+ ...profile.dependencies ? { dependencies: canonicalProfileValues(profile.dependencies) } : {},
2320
+ ...profile.readiness ? { readiness: canonicalProfileValues(profile.readiness) } : {},
2321
+ ...profile.devcontainerServices ? { devcontainerServices: canonicalProfileValues(profile.devcontainerServices) } : {},
2322
+ ...profile.processes ? { processes: canonicalProfileValues(profile.processes) } : {},
2323
+ ...profile.default ? { default: true } : {}
2324
+ };
2325
+ }
2326
+ function normalizeSelectedProfile(name, profile, managedRuntime) {
2327
+ const normalized = normalizeProfile(profile);
2328
+ if (name !== "full" || !managedRuntime) return normalized;
2329
+ return {
2330
+ ...normalized,
2331
+ apps: ["*"],
2332
+ devcontainerServices: ["*"],
2333
+ processes: ["*"]
2334
+ };
2335
+ }
2336
+ function resolveProfile(config, profileOverride) {
2337
+ const profiles = config.profiles;
2338
+ const mergeSelection = (selection) => {
2339
+ const rawNames = selection.split(",");
2340
+ if (rawNames.some((name) => name.trim().length === 0)) {
2341
+ throw new Error("Profile selection contains an empty token.");
2342
+ }
2343
+ const names = Array.from(new Set(rawNames.map((name) => name.trim())));
2344
+ if (names.length === 0) {
2345
+ throw new Error("Profile selection is empty.");
2346
+ }
2347
+ const missing = names.filter((name) => !profiles?.[name]);
2348
+ if (missing.length > 0) {
2349
+ const available = profiles ? Object.keys(profiles).join(", ") : "(none defined)";
2350
+ throw new Error(
2351
+ `Profile '${missing[0]}' is not defined in .devrouter.yml. Available: ${available}`
2352
+ );
2353
+ }
2354
+ if (names.length === 1) {
2355
+ const profile = profiles?.[names[0]];
2356
+ if (!profile) return { name: names[0] };
2357
+ return {
2358
+ name: names[0],
2359
+ profile: normalizeSelectedProfile(names[0], profile, config.managedRuntime)
2360
+ };
2361
+ }
2362
+ const canonicalName = [...names].sort().join(",");
2363
+ const apps = /* @__PURE__ */ new Set();
2364
+ const dependencies = /* @__PURE__ */ new Set();
2365
+ const readiness = /* @__PURE__ */ new Set();
2366
+ const devcontainerServices = /* @__PURE__ */ new Set();
2367
+ const processes = /* @__PURE__ */ new Set();
2368
+ let hasAppWildcard = false;
2369
+ let hasServiceWildcard = false;
2370
+ let hasProcessWildcard = false;
2371
+ for (const name of names) {
2372
+ const profile = profiles?.[name];
2373
+ if (!profile) continue;
2374
+ if (isProfileWildcard(profile.apps)) {
2375
+ hasAppWildcard = true;
2376
+ } else {
2377
+ for (const appName of profile.apps) apps.add(appName);
2378
+ }
2379
+ for (const depName of profile.dependencies ?? []) dependencies.add(depName);
2380
+ for (const readyName of profile.readiness ?? []) readiness.add(readyName);
2381
+ if (isProfileWildcard(profile.devcontainerServices)) {
2382
+ hasServiceWildcard = true;
2383
+ } else {
2384
+ for (const service of profile.devcontainerServices ?? []) {
2385
+ devcontainerServices.add(service);
2386
+ }
2387
+ }
2388
+ if (isProfileWildcard(profile.processes)) {
2389
+ hasProcessWildcard = true;
2390
+ } else {
2391
+ for (const process2 of profile.processes ?? []) processes.add(process2);
2392
+ }
2393
+ }
2394
+ const merged = {
2395
+ apps: hasAppWildcard ? ["*"] : canonicalProfileValues(Array.from(apps)),
2396
+ ...dependencies.size > 0 ? { dependencies: canonicalProfileValues(Array.from(dependencies)) } : {},
2397
+ ...readiness.size > 0 ? { readiness: canonicalProfileValues(Array.from(readiness)) } : {},
2398
+ ...hasServiceWildcard ? { devcontainerServices: ["*"] } : devcontainerServices.size > 0 ? { devcontainerServices: canonicalProfileValues(Array.from(devcontainerServices)) } : {},
2399
+ ...hasProcessWildcard ? { processes: ["*"] } : processes.size > 0 ? { processes: canonicalProfileValues(Array.from(processes)) } : {}
2400
+ };
2401
+ return { name: canonicalName, profile: merged };
2402
+ };
2403
+ if (profileOverride !== void 0) {
2404
+ return mergeSelection(profileOverride);
2405
+ }
2406
+ if (profiles) {
2407
+ const defaultName = Object.keys(profiles).find((name) => profiles[name].default);
2408
+ if (defaultName) {
2409
+ return {
2410
+ name: defaultName,
2411
+ profile: normalizeSelectedProfile(
2412
+ defaultName,
2413
+ profiles[defaultName],
2414
+ config.managedRuntime
2415
+ )
2416
+ };
2417
+ }
2418
+ return {
2419
+ name: "full",
2420
+ profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
2421
+ };
2422
+ }
2423
+ return {
2424
+ name: "full",
2425
+ profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
2426
+ };
2427
+ }
2428
+ function applyProfile(config, profile) {
2429
+ const wildcardApps = profile?.apps.length === 1 && profile.apps[0] === "*";
2430
+ if (!profile || wildcardApps && profile.dependencies === void 0) {
2431
+ return config;
2432
+ }
2433
+ const appSet = new Set(profile.apps);
2434
+ const next = structuredClone(config);
2435
+ const kept = [];
2436
+ const keptDependencies = /* @__PURE__ */ new Set();
2437
+ for (const app of next.apps) {
2438
+ if (app.kind === "dependency") {
2439
+ continue;
2440
+ }
2441
+ if (!wildcardApps && !appSet.has(app.name)) {
2442
+ continue;
2443
+ }
2444
+ for (const dependency of resolveAppDependencies(config, app)) {
2445
+ keptDependencies.add(dependency.name);
2446
+ }
2447
+ kept.push(app);
2448
+ }
2449
+ for (const dependencyName of profile.dependencies ?? []) {
2450
+ keptDependencies.add(dependencyName);
2451
+ }
2452
+ for (const app of next.apps) {
2453
+ if (app.kind === "dependency" && keptDependencies.has(app.name)) {
2454
+ kept.push(app);
2455
+ }
2456
+ }
2457
+ next.apps = kept;
2458
+ return next;
2459
+ }
2005
2460
  function renderConfig(config) {
2006
2461
  return import_yaml2.default.stringify(config, { lineWidth: 0 });
2007
2462
  }
@@ -2049,7 +2504,7 @@ function loadRepoConfig(repoPath) {
2049
2504
  const config = parseConfig(parsed ?? {}, configPath);
2050
2505
  const requiredVersion = config.devrouter?.version;
2051
2506
  if (requiredVersion && !hasWarnedVersionMismatch) {
2052
- const cliVersion = true ? "0.0.37" : "0.0.0-dev";
2507
+ const cliVersion = true ? "0.0.39" : "0.0.0-dev";
2053
2508
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2054
2509
  hasWarnedVersionMismatch = true;
2055
2510
  process.stderr.write(
@@ -2340,11 +2795,18 @@ function applyWorkspace(config, workspace, repoPath) {
2340
2795
  }
2341
2796
  return next;
2342
2797
  }
2343
- function loadRuntimeConfig(repoPath, workspaceOverride) {
2798
+ function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
2344
2799
  const resolved = resolveRepoPath(repoPath);
2345
2800
  const raw = loadRepoConfig(resolved);
2801
+ const resolvedProfile = resolveProfile(raw, profileOverride);
2346
2802
  const workspace = resolveWorkspace(resolved, workspaceOverride);
2347
- return { config: applyWorkspace(raw, workspace, resolved), workspace };
2803
+ const config = applyProfile(applyWorkspace(raw, workspace, resolved), resolvedProfile.profile);
2804
+ return {
2805
+ config,
2806
+ workspace,
2807
+ profile: resolvedProfile.name,
2808
+ resolvedProfile: resolvedProfile.profile
2809
+ };
2348
2810
  }
2349
2811
  function resolveAppByName(repoPath, name, workspaceOverride) {
2350
2812
  const { config, workspace } = loadRuntimeConfig(repoPath, workspaceOverride);
@@ -2386,7 +2848,7 @@ function resolveAppDependencies(config, app) {
2386
2848
  }
2387
2849
  return results;
2388
2850
  }
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;
2851
+ 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
2852
  var init_repo_config = __esm({
2391
2853
  "src/core/repo-config.ts"() {
2392
2854
  "use strict";
@@ -2410,6 +2872,8 @@ var init_repo_config = __esm({
2410
2872
  denyPorts: [80, 443, 5432],
2411
2873
  allowPortRange: "1024-65535"
2412
2874
  };
2875
+ PROFILE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2876
+ MAX_PROFILES = 32;
2413
2877
  }
2414
2878
  });
2415
2879
 
@@ -2470,6 +2934,7 @@ function buildOnboardingPrompt(options = {}) {
2470
2934
  "- project.name: string (optional)",
2471
2935
  `- secretManager.command: string (optional; SM command including trailing \`--\` boundary; supports \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template placeholder)`,
2472
2936
  `- secretManager.defaultEnv: string (optional; fallback env for \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template; required when command contains \`${SECRET_MANAGER_ENV_PLACEHOLDER}\`)`,
2937
+ "- profiles: map (optional; named subsets of routed apps; see Profile schema below)",
2473
2938
  "- apps: array (required)",
2474
2939
  "",
2475
2940
  "Canonical valid skeleton:",
@@ -2514,6 +2979,18 @@ function buildOnboardingPrompt(options = {}) {
2514
2979
  " - docker.composeFiles: string[]",
2515
2980
  " - do not set host/protocol/tcpProtocol/hostRun/docker.internalPort/docker.router",
2516
2981
  "",
2982
+ "Profile schema (each value in the optional top-level profiles map):",
2983
+ '- apps: array (required for app-only profiles; with managedRuntime it may be omitted for a route-free capability profile; routed app names, or ["*"] for every routed app)',
2984
+ "- dependencies: array (optional; kind=dependency names this profile needs; omitted = every dependency a kept app requires transitively)",
2985
+ "- readiness: array (optional; subset of the profile's apps that `devrouter ensure` HTTP-probes before readiness; omitted = all profile apps with an http route)",
2986
+ "- default: boolean (optional; at most one profile may set true)",
2987
+ "- 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",
2988
+ "- 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",
2989
+ "- managedRuntime (optional) registers devcontainer baseServices, profileServices, and managed process markers",
2990
+ "- managed profiles may add devcontainerServices and processes independently; with managedRuntime, apps may be omitted for a route-free capability profile",
2991
+ "- an omitted optional managed dimension selects nothing; use `*` or a full profile to select every registered service/process; baseServices and the primary service are always retained",
2992
+ "- native Dev Container clients continue to use the source configuration's full service set; managed ensure derives an ignored sibling and changes only runServices",
2993
+ "",
2517
2994
  "Validation rules to enforce:",
2518
2995
  "- kind=app host must end with .localhost",
2519
2996
  `- kind=app runtime=host supports protocol=${formatProtocolRule("host")} only`,
@@ -2549,6 +3026,8 @@ function buildOnboardingPrompt(options = {}) {
2549
3026
  "- For TCP/Postgres, standard app frameworks should use the injected port env vars; only direct-TLS-capable tools should use sslmode=require on :5432.",
2550
3027
  "- `devrouter app exec <name> -- <command>` starts dependencies as needed, resolves env vars, and runs a one-shot command with the resolved env. Exec stops only dependencies started by that invocation (already-running dependencies stay running). If ownership detection fails, exec leaves selected dependencies running to avoid non-owned teardown. Exec preserves argv semantics by default (`shell: false`) and supports explicit shell mode via `--shell` when needed.",
2551
3028
  "- `envMap` on dependency references (config-level) aliases per-dep vars after dependency env resolution. `envMap` fails fast when source var is missing.",
3029
+ "- Managed devcontainer profiles reconcile apps, optional Compose services, and repository-owned processes independently. Warm profile changes retain the exact DevPod and volumes, do not rerun postCreateCommand or use --recreate/down, and publish routes last after exact health/readiness proof.",
3030
+ "- Use `devrouter status --repo <REPO_PATH> --json` and `devrouter doctor --repo <REPO_PATH> --json` to inspect managed desired/active resources, fingerprints, transition state, and values-free drift.",
2552
3031
  "",
2553
3032
  "Workspace isolation (parallel git worktrees / agents):",
2554
3033
  `- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each managed linked worktree has a local Git token plus a durable owner record in the repository's Git common directory spanning the DevPod id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
@@ -2602,6 +3081,7 @@ function buildOnboardingPrompt(options = {}) {
2602
3081
  "- devrouter repo devcontainer write --repo <REPO_PATH> --yes",
2603
3082
  "- devrouter repo devcontainer verify --repo <REPO_PATH> --json",
2604
3083
  "- Normal primary or linked startup: devrouter ensure <REPO_PATH> --json",
3084
+ "- Managed selective startup: devrouter ensure <REPO_PATH> --profile <name> --json",
2605
3085
  "- Container seed/migration: devrouter exec <REPO_PATH> -- <command...>",
2606
3086
  "- Compatibility verification only after ensure: devrouter repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
2607
3087
  "",
@@ -2719,7 +3199,7 @@ var init_ai_prompt = __esm({
2719
3199
  purpose: "Write/update devrouter section in the repo's AGENTS.md and install the devrouter skill."
2720
3200
  },
2721
3201
  {
2722
- command: "devrouter ensure [path] [--open] [--json]",
3202
+ command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
2723
3203
  purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
2724
3204
  },
2725
3205
  {
@@ -2940,6 +3420,9 @@ function printStatus(status) {
2940
3420
  status.repo.valid ? "yes" : `no (${status.repo.error ?? "validation failed"})`
2941
3421
  ]);
2942
3422
  rows.push(["Repo apps", String(status.repo.appCount)]);
3423
+ if (status.repo.managedRuntime) {
3424
+ appendManagedRuntimeRows(rows, status.repo.managedRuntime);
3425
+ }
2943
3426
  }
2944
3427
  process.stdout.write(`${renderTable(["FIELD", "VALUE"], rows)}
2945
3428
  `);
@@ -2951,6 +3434,38 @@ function printStatus(status) {
2951
3434
  }
2952
3435
  }
2953
3436
  }
3437
+ function formatResourceNames(values) {
3438
+ return values.join(", ") || "-";
3439
+ }
3440
+ function formatResourceStatuses(values) {
3441
+ return Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([name, resourceStatus]) => `${name}=${resourceStatus}`).join(", ") || "-";
3442
+ }
3443
+ function appendManagedRuntimeRows(rows, managedRuntime) {
3444
+ if (managedRuntime.mode === "legacy") {
3445
+ rows.push(["Managed runtime", "legacy (app-only compatibility)"]);
3446
+ return;
3447
+ }
3448
+ rows.push(["Runtime mode", managedRuntime.mode]);
3449
+ rows.push(["Runtime status", managedRuntime.status]);
3450
+ rows.push(["Runtime profile", managedRuntime.profile]);
3451
+ rows.push(["Active profile", managedRuntime.activeProfile ?? "-"]);
3452
+ rows.push(["Runtime workspace", managedRuntime.workspace ?? "-"]);
3453
+ rows.push(["Runtime DevPod", managedRuntime.devpodId ?? "-"]);
3454
+ rows.push(["Runtime Compose project", managedRuntime.composeProject ?? "-"]);
3455
+ rows.push(["Desired apps", formatResourceNames(managedRuntime.desired.apps)]);
3456
+ rows.push(["Desired services", formatResourceNames(managedRuntime.desired.services)]);
3457
+ rows.push(["Desired processes", formatResourceNames(managedRuntime.desired.processes)]);
3458
+ rows.push(["Active apps", formatResourceNames(managedRuntime.active.apps)]);
3459
+ rows.push(["Active services", formatResourceNames(managedRuntime.active.services)]);
3460
+ rows.push(["Active processes", formatResourceNames(managedRuntime.active.processes)]);
3461
+ rows.push(["Service statuses", formatResourceStatuses(managedRuntime.serviceStatuses)]);
3462
+ rows.push(["Base service statuses", formatResourceStatuses(managedRuntime.baseServiceStatuses)]);
3463
+ rows.push(["Process statuses", formatResourceStatuses(managedRuntime.processStatuses)]);
3464
+ rows.push(["Source config SHA-256", managedRuntime.sourceConfigSha256 ?? "-"]);
3465
+ rows.push(["Effective config SHA-256", managedRuntime.effectiveConfigSha256 ?? "-"]);
3466
+ rows.push(["Transition phase", managedRuntime.transitionPhase ?? "-"]);
3467
+ rows.push(["Runtime drift", formatResourceNames(managedRuntime.drift)]);
3468
+ }
2954
3469
  function printRoutes(routes, duplicateHosts) {
2955
3470
  if (routes.length === 0) {
2956
3471
  process.stdout.write("No routes found.\n");
@@ -3619,6 +4134,13 @@ function runManagedPostStart(options) {
3619
4134
  `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
3620
4135
  "--env",
3621
4136
  `DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
4137
+ ...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
4138
+ ...options.processes ? [
4139
+ "--env",
4140
+ `DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
4141
+ "--env",
4142
+ "DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
4143
+ ] : [],
3622
4144
  options.container.id,
3623
4145
  "bash",
3624
4146
  "-c",
@@ -3630,8 +4152,51 @@ function runManagedPostStart(options) {
3630
4152
  );
3631
4153
  if (started.status !== 0) {
3632
4154
  const details = commandFailure(started);
3633
- throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
4155
+ throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
4156
+ }
4157
+ }
4158
+ function assertSafeProcessName(name) {
4159
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
4160
+ throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
4161
+ }
4162
+ }
4163
+ function runManagedProcessAction(options) {
4164
+ assertSafeProcessName(options.name);
4165
+ const result = (0, import_node_child_process5.spawnSync)(
4166
+ "docker",
4167
+ [
4168
+ "exec",
4169
+ "--workdir",
4170
+ options.container.workspacePath,
4171
+ "--env",
4172
+ `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
4173
+ options.container.id,
4174
+ "bash",
4175
+ "-c",
4176
+ 'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
4177
+ "devrouter-process",
4178
+ options.action,
4179
+ options.name
4180
+ ],
4181
+ {
4182
+ encoding: "utf-8",
4183
+ stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"]
4184
+ }
4185
+ );
4186
+ if (options.action === "stop") {
4187
+ if (result.status !== 0) {
4188
+ throw new Error(`Could not stop managed process '${options.name}'.`);
4189
+ }
4190
+ return "stopped";
4191
+ }
4192
+ try {
4193
+ const parsed = JSON.parse(result.stdout.trim());
4194
+ if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
4195
+ return parsed.status;
4196
+ }
4197
+ } catch {
3634
4198
  }
4199
+ return "drifted";
3635
4200
  }
3636
4201
  var import_node_child_process5, import_node_crypto3, import_node_fs9, import_node_path8, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
3637
4202
  var init_managed_post_start = __esm({
@@ -4137,61 +4702,1051 @@ var init_routes = __esm({
4137
4702
  }
4138
4703
  });
4139
4704
 
4140
- // src/core/status.ts
4141
- function hasPortBinding(ports, privatePort, publicPort) {
4142
- if (!ports) {
4143
- return false;
4144
- }
4145
- return ports.some((port) => port.PrivatePort === privatePort && port.PublicPort === publicPort);
4705
+ // src/core/managed-runtime-state.ts
4706
+ function stateKey(repoPath, workspace) {
4707
+ return (0, import_node_crypto4.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
4146
4708
  }
4147
- function toRepoStatus(repoPath) {
4148
- const resolvedRepoPath = resolveRepoPath(repoPath);
4149
- const configPath = getRepoConfigPath(resolvedRepoPath);
4150
- const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
4151
- const configExists = import_node_fs12.default.existsSync(configPath);
4152
- if (!explicitRepo && !configExists) {
4153
- return void 0;
4709
+ function managedRuntimeStatePath(repoPath, workspace) {
4710
+ return import_node_path11.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
4711
+ }
4712
+ function isStringArray(value) {
4713
+ return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
4714
+ }
4715
+ function validateState(value, repoPath, workspace) {
4716
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4717
+ throw new Error("Managed runtime state must contain an object.");
4154
4718
  }
4155
- if (!configExists) {
4156
- return {
4157
- path: resolvedRepoPath,
4158
- configPath,
4159
- exists: false,
4160
- valid: false,
4161
- appCount: 0,
4162
- tcpAppCount: 0,
4163
- error: `Missing .devrouter.yml in ${resolvedRepoPath}`
4164
- };
4719
+ const candidate = value;
4720
+ if (candidate.version !== 1) throw new Error("Managed runtime state has an unsupported version.");
4721
+ if (candidate.repoPath !== repoPath || candidate.workspace !== workspace) {
4722
+ throw new Error("Managed runtime state belongs to a different exact workspace.");
4723
+ }
4724
+ if (typeof candidate.devpodId !== "string" || candidate.devpodId.length === 0) {
4725
+ throw new Error("Managed runtime state has no exact DevPod identity.");
4726
+ }
4727
+ if (typeof candidate.composeProject !== "string" || candidate.composeProject.length === 0) {
4728
+ throw new Error("Managed runtime state has no exact Compose project identity.");
4729
+ }
4730
+ if (typeof candidate.profile !== "string" || candidate.profile.length === 0) {
4731
+ throw new Error("Managed runtime state has no canonical profile.");
4732
+ }
4733
+ if (!candidate.desired || typeof candidate.desired !== "object") {
4734
+ throw new Error("Managed runtime state has no desired resource set.");
4735
+ }
4736
+ const desired = candidate.desired;
4737
+ if (!isStringArray(desired.apps) || !isStringArray(desired.services) || !isStringArray(desired.processes)) {
4738
+ throw new Error("Managed runtime state has an invalid desired resource set.");
4739
+ }
4740
+ if (typeof candidate.sourceConfigSha256 !== "string" || !/^[a-f0-9]{64}$/.test(candidate.sourceConfigSha256) || typeof candidate.effectiveConfigSha256 !== "string" || !/^[a-f0-9]{64}$/.test(candidate.effectiveConfigSha256)) {
4741
+ throw new Error("Managed runtime state has invalid config fingerprints.");
4742
+ }
4743
+ if (candidate.status !== "ready" && candidate.status !== "degraded") {
4744
+ throw new Error("Managed runtime state has an invalid transition status.");
4165
4745
  }
4746
+ if (candidate.transitionPhase !== void 0 && typeof candidate.transitionPhase !== "string") {
4747
+ throw new Error("Managed runtime state has an invalid transition phase.");
4748
+ }
4749
+ if (typeof candidate.updatedAt !== "string" || !Number.isFinite(Date.parse(candidate.updatedAt))) {
4750
+ throw new Error("Managed runtime state has an invalid timestamp.");
4751
+ }
4752
+ return {
4753
+ version: 1,
4754
+ repoPath,
4755
+ ...workspace !== void 0 ? { workspace } : {},
4756
+ devpodId: candidate.devpodId,
4757
+ composeProject: candidate.composeProject,
4758
+ profile: candidate.profile,
4759
+ desired: {
4760
+ apps: [...desired.apps],
4761
+ services: [...desired.services],
4762
+ processes: [...desired.processes]
4763
+ },
4764
+ sourceConfigSha256: candidate.sourceConfigSha256,
4765
+ effectiveConfigSha256: candidate.effectiveConfigSha256,
4766
+ status: candidate.status,
4767
+ ...candidate.transitionPhase ? { transitionPhase: candidate.transitionPhase } : {},
4768
+ updatedAt: candidate.updatedAt
4769
+ };
4770
+ }
4771
+ function readManagedRuntimeState(repoPath, workspace) {
4772
+ const statePath = managedRuntimeStatePath(repoPath, workspace);
4773
+ if (!import_node_fs12.default.existsSync(statePath)) return void 0;
4774
+ let parsed;
4166
4775
  try {
4167
- const config = loadRuntimeConfig(resolvedRepoPath).config;
4168
- const tcpAppCount = config.apps.filter(
4169
- (app) => app.kind !== "dependency" && app.protocol === "tcp"
4170
- ).length;
4171
- return {
4172
- path: resolvedRepoPath,
4173
- configPath,
4174
- exists: true,
4175
- valid: true,
4176
- appCount: config.apps.length,
4177
- tcpAppCount
4178
- };
4776
+ parsed = JSON.parse(import_node_fs12.default.readFileSync(statePath, "utf-8"));
4179
4777
  } catch (error) {
4180
- const message = error instanceof Error ? error.message : String(error);
4181
- return {
4182
- path: resolvedRepoPath,
4183
- configPath,
4184
- exists: true,
4185
- valid: false,
4186
- appCount: 0,
4187
- tcpAppCount: 0,
4188
- error: message
4189
- };
4778
+ throw new Error(`Could not parse managed runtime state: ${String(error)}`);
4190
4779
  }
4780
+ return validateState(parsed, repoPath, workspace);
4191
4781
  }
4192
- async function collectRouterStatus(repoPath) {
4193
- const repo = toRepoStatus(repoPath);
4194
- const tlsEnabled = isTLSEnabled();
4782
+ function writeManagedRuntimeState(state) {
4783
+ const validated = validateState(state, state.repoPath, state.workspace);
4784
+ writeFileAtomically(
4785
+ managedRuntimeStatePath(validated.repoPath, validated.workspace),
4786
+ `${JSON.stringify(validated, null, 2)}
4787
+ `
4788
+ );
4789
+ }
4790
+ function markManagedRuntimeDegraded(state, transitionPhase) {
4791
+ writeManagedRuntimeState({
4792
+ ...state,
4793
+ status: "degraded",
4794
+ transitionPhase,
4795
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4796
+ });
4797
+ }
4798
+ var import_node_crypto4, import_node_fs12, import_node_path11;
4799
+ var init_managed_runtime_state = __esm({
4800
+ "src/core/managed-runtime-state.ts"() {
4801
+ "use strict";
4802
+ import_node_crypto4 = require("crypto");
4803
+ import_node_fs12 = __toESM(require("fs"));
4804
+ import_node_path11 = __toESM(require("path"));
4805
+ init_atomic_file();
4806
+ init_router();
4807
+ }
4808
+ });
4809
+
4810
+ // src/core/devcontainer-profile.ts
4811
+ function isObject(value) {
4812
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4813
+ }
4814
+ function nonEmptyString(value, label) {
4815
+ if (typeof value !== "string" || value.trim().length === 0) {
4816
+ throw new Error(`${label} must be a non-empty string.`);
4817
+ }
4818
+ return value.trim();
4819
+ }
4820
+ function stringArray(value, label) {
4821
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
4822
+ throw new Error(`${label} must be an array of strings.`);
4823
+ }
4824
+ const result = value.map((item) => item.trim());
4825
+ if (result.some((item) => item.length === 0)) {
4826
+ throw new Error(`${label} must not contain empty strings.`);
4827
+ }
4828
+ if (new Set(result).size !== result.length) {
4829
+ throw new Error(`${label} must not contain duplicates.`);
4830
+ }
4831
+ return result;
4832
+ }
4833
+ function resolveComposeReference(value, linked) {
4834
+ return value.replace(
4835
+ /\$\{localEnv:([A-Za-z_][A-Za-z0-9_]*)(?::([^}]*))?\}/g,
4836
+ (_match, name, fallback) => {
4837
+ if (name === "DEVCONTAINER_COMPOSE_OVERLAY" && linked) {
4838
+ return "docker-compose.devrouter.yml";
4839
+ }
4840
+ if (fallback !== void 0) return fallback;
4841
+ throw new Error(
4842
+ `Cannot resolve localEnv '${name}' while validating the managed Dev Container config.`
4843
+ );
4844
+ }
4845
+ );
4846
+ }
4847
+ function resolveComposeFiles(source, repoPath, linked) {
4848
+ const composeValue = source.dockerComposeFile;
4849
+ const references = typeof composeValue === "string" ? [composeValue] : Array.isArray(composeValue) && composeValue.every((item) => typeof item === "string") ? composeValue : [];
4850
+ if (references.length === 0) {
4851
+ throw new Error(
4852
+ "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
4853
+ );
4854
+ }
4855
+ const directory = import_node_path12.default.join(repoPath, ".devcontainer");
4856
+ const files = references.map((reference) => {
4857
+ const resolved2 = assertPathWithinRepo(
4858
+ resolveComposeReference(reference, linked),
4859
+ directory,
4860
+ "dockerComposeFile"
4861
+ );
4862
+ if (!import_node_fs13.default.existsSync(resolved2) || !import_node_fs13.default.lstatSync(resolved2).isFile()) {
4863
+ throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
4864
+ }
4865
+ return resolved2;
4866
+ });
4867
+ for (const file of files) {
4868
+ let parsed;
4869
+ try {
4870
+ parsed = import_yaml4.default.parse(import_node_fs13.default.readFileSync(file, "utf-8"));
4871
+ } catch (error) {
4872
+ throw new Error(
4873
+ `Could not parse managed Dev Container compose file '${file}': ${String(error)}`
4874
+ );
4875
+ }
4876
+ if (!isObject(parsed) || !isObject(parsed.services)) {
4877
+ throw new Error(`Managed Dev Container compose file '${file}' has no services map.`);
4878
+ }
4879
+ }
4880
+ const fileArgs = files.flatMap((file) => ["-f", file]);
4881
+ const resolved = (0, import_node_child_process6.spawnSync)(
4882
+ "docker",
4883
+ [
4884
+ "compose",
4885
+ "--profile",
4886
+ "*",
4887
+ "--project-directory",
4888
+ directory,
4889
+ ...fileArgs,
4890
+ "config",
4891
+ "--services",
4892
+ "--no-interpolate",
4893
+ "--no-env-resolution"
4894
+ ],
4895
+ { cwd: directory, encoding: "utf-8" }
4896
+ );
4897
+ if (resolved.status !== 0) {
4898
+ throw new Error("Could not resolve the effective managed Dev Container Compose model.");
4899
+ }
4900
+ const services = String(resolved.stdout ?? "").split(/\r?\n/).map((service) => service.trim()).filter(Boolean);
4901
+ if (services.length === 0 || new Set(services).size !== services.length) {
4902
+ throw new Error("The effective managed Dev Container Compose model has no unique services.");
4903
+ }
4904
+ return { directory, files, services: services.sort() };
4905
+ }
4906
+ function assertIgnoredGeneratedPath(repoPath, generatedPath) {
4907
+ const ignored = (0, import_node_child_process6.spawnSync)(
4908
+ "git",
4909
+ ["-C", repoPath, "check-ignore", "--quiet", "--no-index", "--", generatedPath],
4910
+ { encoding: "utf-8" }
4911
+ );
4912
+ if (ignored.status !== 0) {
4913
+ throw new Error(
4914
+ `Managed Dev Container path '${generatedPath}' must be ignored before devrouter can generate it.`
4915
+ );
4916
+ }
4917
+ }
4918
+ function assertGeneratedPathOwnership(generatedPath) {
4919
+ if (!import_node_fs13.default.existsSync(generatedPath)) return;
4920
+ const stat = import_node_fs13.default.lstatSync(generatedPath);
4921
+ if (!stat.isFile() || stat.isSymbolicLink()) {
4922
+ throw new Error(`Managed Dev Container path '${generatedPath}' is not a regular file.`);
4923
+ }
4924
+ const firstLine2 = import_node_fs13.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
4925
+ if (firstLine2 !== MANAGED_DEVCONTAINER_MARKER) {
4926
+ throw new Error(
4927
+ `Managed Dev Container path '${generatedPath}' exists without the devrouter ownership marker.`
4928
+ );
4929
+ }
4930
+ }
4931
+ function isWildcard(values) {
4932
+ return values?.length === 1 && values[0] === "*";
4933
+ }
4934
+ function sha256(contents) {
4935
+ return (0, import_node_crypto5.createHash)("sha256").update(contents, "utf-8").digest("hex");
4936
+ }
4937
+ function safeComposeProject(value) {
4938
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
4939
+ throw new Error(`Docker Compose project '${value}' is not a safe exact identifier.`);
4940
+ }
4941
+ return value;
4942
+ }
4943
+ function inspectManagedDevcontainerConfig(options) {
4944
+ const managedRuntime = options.config.managedRuntime;
4945
+ if (!managedRuntime) {
4946
+ throw new Error("Cannot prepare a managed Dev Container without managedRuntime.");
4947
+ }
4948
+ const sourcePath = import_node_path12.default.join(options.repoPath, ".devcontainer/devcontainer.json");
4949
+ if (!import_node_fs13.default.existsSync(sourcePath) || !import_node_fs13.default.lstatSync(sourcePath).isFile()) {
4950
+ throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
4951
+ }
4952
+ const sourceContents = import_node_fs13.default.readFileSync(sourcePath, "utf-8");
4953
+ let parsed;
4954
+ try {
4955
+ parsed = import_yaml4.default.parse(sourceContents);
4956
+ } catch (error) {
4957
+ throw new Error(`Could not parse managed Dev Container source config: ${String(error)}`);
4958
+ }
4959
+ if (!isObject(parsed)) {
4960
+ throw new Error("Managed Dev Container source config must contain an object.");
4961
+ }
4962
+ const source = parsed;
4963
+ const primaryService = nonEmptyString(source.service, "devcontainer.service");
4964
+ const compose = resolveComposeFiles(source, options.repoPath, options.linked);
4965
+ const composeServiceSet = new Set(compose.services);
4966
+ if (!composeServiceSet.has(primaryService)) {
4967
+ throw new Error(`devcontainer.service '${primaryService}' is not in the Compose model.`);
4968
+ }
4969
+ const nativeRunServices = source.runServices === void 0 ? compose.services : stringArray(source.runServices, "devcontainer.runServices");
4970
+ for (const service of nativeRunServices) {
4971
+ if (!composeServiceSet.has(service)) {
4972
+ throw new Error(`devcontainer.runServices references unknown service '${service}'.`);
4973
+ }
4974
+ }
4975
+ if (!nativeRunServices.includes(primaryService)) {
4976
+ throw new Error(`devcontainer.runServices must include primary service '${primaryService}'.`);
4977
+ }
4978
+ const baseServices = managedRuntime.devcontainer.baseServices;
4979
+ const profileServices = managedRuntime.devcontainer.profileServices;
4980
+ for (const service of [...baseServices, ...profileServices]) {
4981
+ if (!composeServiceSet.has(service)) {
4982
+ throw new Error(`managedRuntime references unknown Compose service '${service}'.`);
4983
+ }
4984
+ if (!nativeRunServices.includes(service)) {
4985
+ throw new Error(`managedRuntime service '${service}' is missing from native runServices.`);
4986
+ }
4987
+ }
4988
+ if (baseServices.includes(primaryService) || profileServices.includes(primaryService)) {
4989
+ throw new Error(`managedRuntime must not classify primary service '${primaryService}'.`);
4990
+ }
4991
+ const classified = /* @__PURE__ */ new Set([primaryService, ...baseServices, ...profileServices]);
4992
+ const unclassified = nativeRunServices.filter((service) => !classified.has(service));
4993
+ if (unclassified.length > 0) {
4994
+ throw new Error(
4995
+ `managedRuntime does not classify native runServices: ${unclassified.join(", ")}.`
4996
+ );
4997
+ }
4998
+ const desiredProfileServices = !options.profile || isWildcard(options.profile.devcontainerServices) ? [...profileServices] : [...options.profile.devcontainerServices ?? []];
4999
+ const unregisteredDesiredServices = desiredProfileServices.filter(
5000
+ (service) => !profileServices.includes(service)
5001
+ );
5002
+ if (unregisteredDesiredServices.length > 0) {
5003
+ throw new Error(
5004
+ `Profile selects unregistered managed services: ${unregisteredDesiredServices.join(", ")}.`
5005
+ );
5006
+ }
5007
+ const desiredSet = /* @__PURE__ */ new Set([primaryService, ...baseServices, ...desiredProfileServices]);
5008
+ const desiredServices = nativeRunServices.filter((service) => desiredSet.has(service));
5009
+ const effective = JSON.parse(JSON.stringify(source));
5010
+ effective.runServices = desiredServices;
5011
+ const contents = `${MANAGED_DEVCONTAINER_MARKER}
5012
+ ${JSON.stringify(effective, null, 2)}
5013
+ `;
5014
+ const generatedPath = import_node_path12.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5015
+ assertIgnoredGeneratedPath(options.repoPath, generatedPath);
5016
+ assertGeneratedPathOwnership(generatedPath);
5017
+ return {
5018
+ sourcePath,
5019
+ generatedPath,
5020
+ generatedRelativePath: MANAGED_DEVCONTAINER_PATH,
5021
+ sourceConfigSha256: sha256(sourceContents),
5022
+ effectiveConfigSha256: sha256(contents),
5023
+ primaryService,
5024
+ composeDirectory: compose.directory,
5025
+ composeFiles: compose.files,
5026
+ composeServices: compose.services,
5027
+ nativeRunServices,
5028
+ baseServices: [...baseServices],
5029
+ profileServices: [...profileServices],
5030
+ desiredProfileServices,
5031
+ desiredServices,
5032
+ contents
5033
+ };
5034
+ }
5035
+ function writeManagedDevcontainerConfig(plan) {
5036
+ writeFileAtomically(plan.generatedPath, plan.contents);
5037
+ }
5038
+ function inspectManagedDevcontainerGeneratedConfig(plan) {
5039
+ let stat;
5040
+ try {
5041
+ stat = import_node_fs13.default.lstatSync(plan.generatedPath);
5042
+ } catch (error) {
5043
+ if (error.code === "ENOENT") return { status: "missing" };
5044
+ throw error;
5045
+ }
5046
+ if (!stat.isFile() || stat.isSymbolicLink()) return { status: "foreign" };
5047
+ const contents = import_node_fs13.default.readFileSync(plan.generatedPath, "utf-8");
5048
+ if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5049
+ `)) {
5050
+ return { status: "foreign" };
5051
+ }
5052
+ const fingerprint = sha256(contents);
5053
+ return {
5054
+ status: fingerprint === plan.effectiveConfigSha256 ? "valid" : "drifted",
5055
+ sha256: fingerprint
5056
+ };
5057
+ }
5058
+ function removeManagedDevcontainerConfig(plan) {
5059
+ if (!import_node_fs13.default.existsSync(plan.generatedPath)) return;
5060
+ const stat = import_node_fs13.default.lstatSync(plan.generatedPath);
5061
+ if (!stat.isFile()) {
5062
+ throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not a regular file.`);
5063
+ }
5064
+ const contents = import_node_fs13.default.readFileSync(plan.generatedPath, "utf-8");
5065
+ if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5066
+ `)) {
5067
+ throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not devrouter-owned.`);
5068
+ }
5069
+ import_node_fs13.default.unlinkSync(plan.generatedPath);
5070
+ }
5071
+ function assertSafeContainerId(containerId) {
5072
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
5073
+ throw new Error(`Container '${containerId}' is not a safe exact identifier.`);
5074
+ }
5075
+ }
5076
+ function startExactManagedServices(options) {
5077
+ if (options.services.length === 0) return;
5078
+ const project = safeComposeProject(options.composeProject);
5079
+ const services = Array.from(new Set(options.services));
5080
+ if (services.some((service) => !options.plan.composeServices.includes(service))) {
5081
+ throw new Error("Exact Compose startup contains a service outside the validated model.");
5082
+ }
5083
+ const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
5084
+ const env = { ...process.env };
5085
+ if (options.workspace) {
5086
+ env.WORKSPACE = options.workspace.token;
5087
+ env.DEVROUTER_WORKSPACE = options.workspace.token;
5088
+ env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
5089
+ env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
5090
+ } else {
5091
+ delete env.WORKSPACE;
5092
+ delete env.DEVROUTER_WORKSPACE;
5093
+ delete env.DEVROUTER_GIT_COMMON_DIR;
5094
+ delete env.DEVCONTAINER_COMPOSE_OVERLAY;
5095
+ }
5096
+ const result = (0, import_node_child_process6.spawnSync)(
5097
+ "docker",
5098
+ [
5099
+ "compose",
5100
+ "--project-name",
5101
+ project,
5102
+ "--project-directory",
5103
+ options.plan.composeDirectory,
5104
+ ...fileArgs,
5105
+ "up",
5106
+ "-d",
5107
+ "--no-recreate",
5108
+ "--no-deps",
5109
+ ...services
5110
+ ],
5111
+ {
5112
+ cwd: options.plan.composeDirectory,
5113
+ encoding: "utf-8",
5114
+ env,
5115
+ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit"
5116
+ }
5117
+ );
5118
+ if (result.status !== 0) {
5119
+ throw new Error(
5120
+ `Exact Compose startup failed for project '${project}' and service set '${services.join(",")}'.`
5121
+ );
5122
+ }
5123
+ }
5124
+ function stopExactManagedService(containerId, service) {
5125
+ assertSafeContainerId(containerId);
5126
+ const result = (0, import_node_child_process6.spawnSync)("docker", ["stop", containerId], {
5127
+ encoding: "utf-8",
5128
+ stdio: "inherit"
5129
+ });
5130
+ if (result.status !== 0) {
5131
+ throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5132
+ }
5133
+ }
5134
+ var import_node_child_process6, import_node_crypto5, import_node_fs13, import_node_path12, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
5135
+ var init_devcontainer_profile = __esm({
5136
+ "src/core/devcontainer-profile.ts"() {
5137
+ "use strict";
5138
+ import_node_child_process6 = require("child_process");
5139
+ import_node_crypto5 = require("crypto");
5140
+ import_node_fs13 = __toESM(require("fs"));
5141
+ import_node_path12 = __toESM(require("path"));
5142
+ import_yaml4 = __toESM(require("yaml"));
5143
+ init_atomic_file();
5144
+ init_paths();
5145
+ MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
5146
+ MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
5147
+ }
5148
+ });
5149
+
5150
+ // src/core/devpod-environment.ts
5151
+ function inspectWorkspaceContainers(options) {
5152
+ let ids = options?.ids;
5153
+ if (!ids) {
5154
+ const listed = (0, import_node_child_process7.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
5155
+ encoding: "utf-8"
5156
+ });
5157
+ if (listed.status !== 0) {
5158
+ throw new Error(
5159
+ `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
5160
+ );
5161
+ }
5162
+ ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
5163
+ }
5164
+ if (ids.length === 0) return [];
5165
+ const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
5166
+ const inspected = (0, import_node_child_process7.spawnSync)(
5167
+ "docker",
5168
+ ["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
5169
+ { encoding: "utf-8" }
5170
+ );
5171
+ if (inspected.status !== 0) {
5172
+ throw new Error(
5173
+ `docker inspect failed: ${(inspected.stderr || inspected.stdout || "unknown error").trim()}`
5174
+ );
5175
+ }
5176
+ return inspected.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
5177
+ }
5178
+ function workspaceAppContainers(containers, repoPath) {
5179
+ return containers.filter((container) => {
5180
+ const workingDir = container.labels["com.docker.compose.project.working_dir"];
5181
+ return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path13.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5182
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5183
+ );
5184
+ });
5185
+ }
5186
+ function hasExactComposeIdentity(container, options) {
5187
+ if (options.composeProject !== void 0 && container.labels["com.docker.compose.project"] !== options.composeProject || container.labels["com.docker.compose.service"] !== options.service || !sameWorkspacePath(
5188
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
5189
+ import_node_path13.default.join(options.repoPath, ".devcontainer")
5190
+ )) {
5191
+ return false;
5192
+ }
5193
+ const expectedFiles = options.composeFiles;
5194
+ if (!expectedFiles) return true;
5195
+ const actualFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim()).filter(Boolean);
5196
+ const providerGeneratedFiles = actualFiles.filter((file) => {
5197
+ const normalized = file.replaceAll("\\", "/");
5198
+ return /\/.devpod\/agent\/contexts\/[^/]+\/workspaces\/[^/]+\/.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
5199
+ normalized
5200
+ );
5201
+ });
5202
+ return actualFiles.length === expectedFiles.length + providerGeneratedFiles.length && expectedFiles.every(
5203
+ (expected) => actualFiles.some((actual) => sameWorkspacePath(actual, expected))
5204
+ ) && actualFiles.every(
5205
+ (actual) => providerGeneratedFiles.includes(actual) || expectedFiles.some((expected) => sameWorkspacePath(actual, expected))
5206
+ );
5207
+ }
5208
+ function resolveRunningWorkspaceContainer(repoPath) {
5209
+ const matches = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
5210
+ (container2) => container2.state.Running
5211
+ );
5212
+ if (matches.length !== 1) {
5213
+ throw new Error(
5214
+ `Expected one running workspace app container for '${repoPath}', found ${matches.length}.`
5215
+ );
5216
+ }
5217
+ const container = matches[0];
5218
+ const repoMount = container.mounts.find(
5219
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5220
+ );
5221
+ if (!repoMount) {
5222
+ throw new Error(`Workspace app container no longer mounts '${repoPath}'.`);
5223
+ }
5224
+ return { id: container.id, workspacePath: repoMount.Destination };
5225
+ }
5226
+ var import_node_child_process7, import_node_path13, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5227
+ var init_devpod_environment = __esm({
5228
+ "src/core/devpod-environment.ts"() {
5229
+ "use strict";
5230
+ import_node_child_process7 = require("child_process");
5231
+ import_node_path13 = __toESM(require("path"));
5232
+ init_workspace();
5233
+ SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
5234
+ SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
5235
+ /}$/,
5236
+ ',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
5237
+ );
5238
+ }
5239
+ });
5240
+
5241
+ // src/core/managed-runtime-status.ts
5242
+ function sortedUnique(values) {
5243
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
5244
+ }
5245
+ function selectedResources(values, allValues, hasResolvedProfile) {
5246
+ if (values === void 0) return hasResolvedProfile ? [] : sortedUnique(allValues);
5247
+ return values.length === 1 && values[0] === "*" ? sortedUnique(allValues) : sortedUnique(values);
5248
+ }
5249
+ function containerResourceStatus(container) {
5250
+ if (!container) return "missing";
5251
+ if (!container.state.Running) return "stopped";
5252
+ const health = container.state.Health?.Status;
5253
+ if (health === "starting") return "starting";
5254
+ if (health === "unhealthy") return "unhealthy";
5255
+ return "healthy";
5256
+ }
5257
+ function isActiveResourceStatus(status) {
5258
+ return status === "running" || status === "healthy" || status === "starting" || status === "unhealthy";
5259
+ }
5260
+ function isReadyResourceStatus(status) {
5261
+ return status === "running" || status === "healthy";
5262
+ }
5263
+ function exactRoutes(repoPath, workspace) {
5264
+ try {
5265
+ const routes = listHostRouteState().filter(
5266
+ (route) => sameWorkspacePath(route.repoPath, repoPath) && route.workspace === workspace
5267
+ );
5268
+ return { apps: sortedUnique(routes.map((route) => route.name)), drift: [] };
5269
+ } catch {
5270
+ return { apps: [], drift: ["managed route state could not be inspected"] };
5271
+ }
5272
+ }
5273
+ function compareResourceSet(label, expected, actual) {
5274
+ if (expected.length !== actual.length || expected.some((value, index) => value !== actual[index])) {
5275
+ return `active ${label} differ from the selected profile`;
5276
+ }
5277
+ return void 0;
5278
+ }
5279
+ function resourceStatusesForMissing(names, status) {
5280
+ return Object.fromEntries(names.map((name) => [name, status]));
5281
+ }
5282
+ function inspectManagedRuntime(options) {
5283
+ const {
5284
+ repoPath,
5285
+ config,
5286
+ registeredServices,
5287
+ registeredProcesses,
5288
+ desiredServices,
5289
+ desiredProcesses,
5290
+ workspace,
5291
+ state
5292
+ } = options;
5293
+ const drift = [];
5294
+ const baseServices = sortedUnique(config.managedRuntime?.devcontainer.baseServices ?? []);
5295
+ const serviceStatuses = resourceStatusesForMissing(registeredServices, "missing");
5296
+ const baseStatuses = resourceStatusesForMissing(baseServices, "missing");
5297
+ const processStatuses = resourceStatusesForMissing(registeredProcesses, "missing");
5298
+ const activeServices = [];
5299
+ const activeProcesses = [];
5300
+ let plan;
5301
+ try {
5302
+ plan = inspectManagedDevcontainerConfig({
5303
+ repoPath,
5304
+ config,
5305
+ profile: {
5306
+ apps: ["*"],
5307
+ devcontainerServices: desiredServices,
5308
+ processes: ["*"]
5309
+ },
5310
+ linked: workspace !== void 0
5311
+ });
5312
+ } catch {
5313
+ drift.push("managed Dev Container configuration could not be inspected");
5314
+ return {
5315
+ plan,
5316
+ primaryReady: false,
5317
+ primaryActive: false,
5318
+ baseStatuses,
5319
+ serviceStatuses,
5320
+ processStatuses,
5321
+ activeServices,
5322
+ activeProcesses,
5323
+ drift
5324
+ };
5325
+ }
5326
+ let containers;
5327
+ try {
5328
+ containers = inspectWorkspaceContainers();
5329
+ } catch {
5330
+ drift.push("managed workspace containers could not be inspected");
5331
+ return {
5332
+ plan,
5333
+ primaryReady: false,
5334
+ primaryActive: false,
5335
+ baseStatuses,
5336
+ serviceStatuses,
5337
+ processStatuses,
5338
+ activeServices,
5339
+ activeProcesses,
5340
+ drift
5341
+ };
5342
+ }
5343
+ if (!plan) {
5344
+ drift.push("managed Dev Container configuration could not be inspected");
5345
+ return {
5346
+ plan,
5347
+ primaryReady: false,
5348
+ primaryActive: false,
5349
+ baseStatuses,
5350
+ serviceStatuses,
5351
+ processStatuses,
5352
+ activeServices,
5353
+ activeProcesses,
5354
+ drift
5355
+ };
5356
+ }
5357
+ const resolvedPlan = plan;
5358
+ let generatedConfigSha256;
5359
+ let generatedConfigMissing = false;
5360
+ try {
5361
+ const generated = inspectManagedDevcontainerGeneratedConfig(resolvedPlan);
5362
+ generatedConfigSha256 = generated.sha256;
5363
+ if (generated.status === "missing") {
5364
+ generatedConfigMissing = true;
5365
+ } else if (generated.status === "foreign") {
5366
+ drift.push("managed generated Dev Container configuration is not devrouter-owned");
5367
+ } else if (generated.status === "drifted") {
5368
+ drift.push("managed generated Dev Container configuration changed");
5369
+ }
5370
+ } catch {
5371
+ drift.push("managed generated Dev Container configuration could not be inspected");
5372
+ }
5373
+ const appContainers = workspaceAppContainers(containers, repoPath).filter(
5374
+ (container) => hasExactComposeIdentity(container, {
5375
+ repoPath,
5376
+ service: resolvedPlan.primaryService,
5377
+ composeFiles: resolvedPlan.composeFiles
5378
+ })
5379
+ );
5380
+ const projectScopedPrimary = state?.composeProject ? appContainers.filter(
5381
+ (container) => container.labels["com.docker.compose.project"] === state.composeProject
5382
+ ) : appContainers;
5383
+ let primary;
5384
+ let composeProject = state?.composeProject;
5385
+ if (projectScopedPrimary.length === 1) {
5386
+ primary = projectScopedPrimary[0];
5387
+ composeProject = primary.labels["com.docker.compose.project"] ?? composeProject;
5388
+ } else if (projectScopedPrimary.length > 1) {
5389
+ drift.push("managed primary container identity is ambiguous");
5390
+ } else if (appContainers.length > 0) {
5391
+ drift.push(
5392
+ state?.composeProject ? "managed primary container is foreign" : "managed primary container identity is ambiguous"
5393
+ );
5394
+ }
5395
+ if (!composeProject && appContainers.length === 1) {
5396
+ composeProject = appContainers[0].labels["com.docker.compose.project"];
5397
+ }
5398
+ if (primary && !composeProject) {
5399
+ drift.push("managed primary container has no Compose project identity");
5400
+ }
5401
+ const exactService = (service) => containers.filter(
5402
+ (container) => hasExactComposeIdentity(container, {
5403
+ repoPath,
5404
+ service,
5405
+ composeProject,
5406
+ composeFiles: resolvedPlan.composeFiles
5407
+ })
5408
+ );
5409
+ const unscopedService = (service) => containers.filter(
5410
+ (container) => hasExactComposeIdentity(container, {
5411
+ repoPath,
5412
+ service,
5413
+ composeFiles: resolvedPlan.composeFiles
5414
+ })
5415
+ );
5416
+ const inspectService = (service, statuses) => {
5417
+ const exact = exactService(service);
5418
+ if (exact.length > 1) {
5419
+ statuses[service] = "drifted";
5420
+ drift.push(`managed service '${service}' identity is ambiguous`);
5421
+ return;
5422
+ }
5423
+ if (exact.length === 0) {
5424
+ const foreign = unscopedService(service);
5425
+ statuses[service] = foreign.length > 0 ? "foreign" : "missing";
5426
+ if (foreign.length > 0) drift.push(`managed service '${service}' is foreign`);
5427
+ return;
5428
+ }
5429
+ const status = containerResourceStatus(exact[0]);
5430
+ statuses[service] = status;
5431
+ if (isActiveResourceStatus(status)) {
5432
+ if (statuses === serviceStatuses) activeServices.push(service);
5433
+ }
5434
+ };
5435
+ for (const service of registeredServices) inspectService(service, serviceStatuses);
5436
+ for (const service of baseServices) inspectService(service, baseStatuses);
5437
+ const managedServicesActive = activeServices.length > 0 || Object.values(baseStatuses).some(isActiveResourceStatus);
5438
+ const primaryWorkspacePath = primary?.mounts.find(
5439
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5440
+ )?.Destination;
5441
+ const primaryActive = Boolean(primary?.state.Running);
5442
+ if (generatedConfigMissing && (state || primaryActive || managedServicesActive)) {
5443
+ drift.push("managed generated Dev Container configuration is missing");
5444
+ }
5445
+ const primaryHealth = primary?.state.Health?.Status;
5446
+ const primaryReady = primaryActive && primaryHealth !== "starting" && primaryHealth !== "unhealthy";
5447
+ if (!primary) {
5448
+ if (managedServicesActive) {
5449
+ drift.push("managed primary container is missing");
5450
+ }
5451
+ } else if (!primaryActive) {
5452
+ if (managedServicesActive) {
5453
+ drift.push("managed primary container is stopped");
5454
+ }
5455
+ } else if (primaryHealth === "unhealthy") {
5456
+ drift.push("managed primary container is unhealthy");
5457
+ }
5458
+ if (primary && primaryActive && primaryWorkspacePath) {
5459
+ for (const process2 of registeredProcesses) {
5460
+ let status;
5461
+ try {
5462
+ status = runManagedProcessAction({
5463
+ container: { id: primary.id, workspacePath: primaryWorkspacePath },
5464
+ name: process2,
5465
+ action: "status",
5466
+ quiet: true
5467
+ });
5468
+ } catch {
5469
+ status = "drifted";
5470
+ }
5471
+ processStatuses[process2] = status;
5472
+ if (status === "running") activeProcesses.push(process2);
5473
+ if (desiredProcesses.includes(process2) && status !== "running") {
5474
+ drift.push(`managed process '${process2}' is not running`);
5475
+ }
5476
+ }
5477
+ } else {
5478
+ if (primary && !primaryActive) {
5479
+ for (const process2 of registeredProcesses) processStatuses[process2] = "stopped";
5480
+ }
5481
+ if (desiredProcesses.length > 0 && (primaryActive || managedServicesActive)) {
5482
+ drift.push("managed processes cannot be proven without the primary container");
5483
+ }
5484
+ }
5485
+ for (const service of desiredServices) {
5486
+ const status = serviceStatuses[service] ?? "missing";
5487
+ if (!isReadyResourceStatus(status) && status !== "starting" && (primaryActive || managedServicesActive)) {
5488
+ drift.push(`managed service '${service}' is not ready`);
5489
+ }
5490
+ }
5491
+ for (const service of baseServices) {
5492
+ const status = baseStatuses[service] ?? "missing";
5493
+ if (!isReadyResourceStatus(status) && status !== "starting" && (primaryActive || managedServicesActive)) {
5494
+ drift.push(`managed base service '${service}' is not ready`);
5495
+ }
5496
+ }
5497
+ return {
5498
+ plan,
5499
+ generatedConfigSha256,
5500
+ composeProject,
5501
+ primaryReady,
5502
+ primaryActive,
5503
+ baseStatuses,
5504
+ serviceStatuses,
5505
+ processStatuses,
5506
+ activeServices: sortedUnique(activeServices),
5507
+ activeProcesses: sortedUnique(activeProcesses),
5508
+ drift
5509
+ };
5510
+ }
5511
+ function collectManagedRuntimeStatus(options) {
5512
+ if (!options.config.managedRuntime) {
5513
+ return {
5514
+ mode: "legacy",
5515
+ status: "legacy",
5516
+ profile: options.profile,
5517
+ desired: { ...EMPTY_RESOURCES },
5518
+ active: { ...EMPTY_RESOURCES },
5519
+ serviceStatuses: {},
5520
+ baseServiceStatuses: {},
5521
+ processStatuses: {},
5522
+ drift: []
5523
+ };
5524
+ }
5525
+ const managedRuntime = options.config.managedRuntime;
5526
+ const hasResolvedProfile = options.resolvedProfile !== void 0;
5527
+ const routedApps = options.config.apps.filter((app) => app.kind !== "dependency").map((app) => app.name);
5528
+ const registeredServices = sortedUnique(managedRuntime.devcontainer.profileServices);
5529
+ const registeredProcesses = sortedUnique(managedRuntime.processes);
5530
+ const desiredApps = selectedResources(
5531
+ options.resolvedProfile?.apps,
5532
+ routedApps,
5533
+ hasResolvedProfile
5534
+ );
5535
+ const desiredServices = selectedResources(
5536
+ options.resolvedProfile?.devcontainerServices,
5537
+ registeredServices,
5538
+ hasResolvedProfile
5539
+ );
5540
+ const desiredProcesses = selectedResources(
5541
+ options.resolvedProfile?.processes,
5542
+ registeredProcesses,
5543
+ hasResolvedProfile
5544
+ );
5545
+ let state;
5546
+ let stateReadFailed = false;
5547
+ try {
5548
+ state = readManagedRuntimeState(options.repoPath, options.workspace);
5549
+ } catch {
5550
+ stateReadFailed = true;
5551
+ }
5552
+ const drift = [];
5553
+ if (stateReadFailed) drift.push("managed runtime state could not be read");
5554
+ if (state?.status === "degraded") drift.push("the last managed transition is degraded");
5555
+ if (state) {
5556
+ const desiredState = {
5557
+ apps: sortedUnique(state.desired.apps),
5558
+ services: sortedUnique(state.desired.services),
5559
+ processes: sortedUnique(state.desired.processes)
5560
+ };
5561
+ const desired = {
5562
+ apps: desiredApps,
5563
+ services: desiredServices,
5564
+ processes: desiredProcesses
5565
+ };
5566
+ for (const [label, expected, actual] of [
5567
+ ["apps", desired.apps, desiredState.apps],
5568
+ ["services", desired.services, desiredState.services],
5569
+ ["processes", desired.processes, desiredState.processes]
5570
+ ]) {
5571
+ const mismatch = compareResourceSet(label, expected, actual);
5572
+ if (mismatch) drift.push(mismatch);
5573
+ }
5574
+ if (state.profile !== options.profile)
5575
+ drift.push("active profile differs from the selected profile");
5576
+ }
5577
+ const inspection = inspectManagedRuntime({
5578
+ repoPath: options.repoPath,
5579
+ config: options.config,
5580
+ registeredServices,
5581
+ registeredProcesses,
5582
+ desiredServices,
5583
+ desiredProcesses,
5584
+ workspace: options.workspace,
5585
+ state
5586
+ });
5587
+ drift.push(...inspection.drift);
5588
+ if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
5589
+ drift.push("managed Dev Container source configuration changed");
5590
+ }
5591
+ if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
5592
+ drift.push("managed Dev Container effective configuration changed");
5593
+ }
5594
+ if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
5595
+ drift.push("managed generated Dev Container configuration differs from last successful state");
5596
+ }
5597
+ const routes = exactRoutes(options.repoPath, options.workspace);
5598
+ drift.push(...routes.drift);
5599
+ const activeApps = routes.apps;
5600
+ const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
5601
+ if (activeApps.length > 0 && !inspection.primaryActive) {
5602
+ drift.push("active app routes exist without a running primary container");
5603
+ }
5604
+ if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
5605
+ drift.push("one or more selected app routes are missing");
5606
+ }
5607
+ if (activeApps.some((app) => !desiredApps.includes(app))) {
5608
+ drift.push("one or more active app routes are outside the selected profile");
5609
+ }
5610
+ if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
5611
+ drift.push("one or more active services are outside the selected profile");
5612
+ }
5613
+ if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
5614
+ drift.push("one or more active processes are outside the selected profile");
5615
+ }
5616
+ const requiredServicesReady = desiredServices.every(
5617
+ (service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
5618
+ );
5619
+ const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
5620
+ const processesReady = desiredProcesses.every(
5621
+ (process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
5622
+ );
5623
+ const routesReady = desiredApps.every((app) => activeApps.includes(app));
5624
+ const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
5625
+ const status = state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
5626
+ const active = {
5627
+ apps: activeApps,
5628
+ services: inspection.activeServices,
5629
+ processes: inspection.activeProcesses
5630
+ };
5631
+ return {
5632
+ mode: "managed",
5633
+ status,
5634
+ profile: options.profile,
5635
+ ...state?.profile ? { activeProfile: state.profile } : {},
5636
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
5637
+ ...state?.devpodId ? { devpodId: state.devpodId } : {},
5638
+ ...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
5639
+ desired: {
5640
+ apps: desiredApps,
5641
+ services: desiredServices,
5642
+ processes: desiredProcesses
5643
+ },
5644
+ active,
5645
+ serviceStatuses: inspection.serviceStatuses,
5646
+ baseServiceStatuses: inspection.baseStatuses,
5647
+ processStatuses: inspection.processStatuses,
5648
+ drift: sortedUnique(drift),
5649
+ ...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
5650
+ ...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
5651
+ effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
5652
+ } : {},
5653
+ ...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
5654
+ };
5655
+ }
5656
+ var EMPTY_RESOURCES;
5657
+ var init_managed_runtime_status = __esm({
5658
+ "src/core/managed-runtime-status.ts"() {
5659
+ "use strict";
5660
+ init_devcontainer_profile();
5661
+ init_devpod_environment();
5662
+ init_host_routes();
5663
+ init_managed_post_start();
5664
+ init_managed_runtime_state();
5665
+ init_workspace();
5666
+ EMPTY_RESOURCES = {
5667
+ apps: [],
5668
+ services: [],
5669
+ processes: []
5670
+ };
5671
+ }
5672
+ });
5673
+
5674
+ // src/core/status.ts
5675
+ function hasPortBinding(ports, privatePort, publicPort) {
5676
+ if (!ports) {
5677
+ return false;
5678
+ }
5679
+ return ports.some((port) => port.PrivatePort === privatePort && port.PublicPort === publicPort);
5680
+ }
5681
+ function toRepoStatus(repoPath) {
5682
+ const resolvedRepoPath = resolveRepoPath(repoPath);
5683
+ const configPath = getRepoConfigPath(resolvedRepoPath);
5684
+ const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
5685
+ const configExists = import_node_fs14.default.existsSync(configPath);
5686
+ if (!explicitRepo && !configExists) {
5687
+ return void 0;
5688
+ }
5689
+ if (!configExists) {
5690
+ return {
5691
+ path: resolvedRepoPath,
5692
+ configPath,
5693
+ exists: false,
5694
+ valid: false,
5695
+ appCount: 0,
5696
+ tcpAppCount: 0,
5697
+ error: `Missing .devrouter.yml in ${resolvedRepoPath}`
5698
+ };
5699
+ }
5700
+ try {
5701
+ let runtime = loadRuntimeConfig(resolvedRepoPath);
5702
+ if (runtime.config.managedRuntime) {
5703
+ let state;
5704
+ try {
5705
+ state = readManagedRuntimeState(resolvedRepoPath, runtime.workspace);
5706
+ } catch {
5707
+ }
5708
+ if (state && state.profile !== runtime.profile) {
5709
+ try {
5710
+ runtime = loadRuntimeConfig(resolvedRepoPath, runtime.workspace, state.profile);
5711
+ } catch {
5712
+ }
5713
+ }
5714
+ }
5715
+ const config = runtime.config;
5716
+ const tcpAppCount = config.apps.filter(
5717
+ (app) => app.kind !== "dependency" && app.protocol === "tcp"
5718
+ ).length;
5719
+ return {
5720
+ path: resolvedRepoPath,
5721
+ configPath,
5722
+ exists: true,
5723
+ valid: true,
5724
+ appCount: config.apps.length,
5725
+ tcpAppCount,
5726
+ managedRuntime: collectManagedRuntimeStatus({
5727
+ repoPath: resolvedRepoPath,
5728
+ workspace: runtime.workspace,
5729
+ config,
5730
+ profile: runtime.profile,
5731
+ resolvedProfile: runtime.resolvedProfile
5732
+ })
5733
+ };
5734
+ } catch (error) {
5735
+ const message = error instanceof Error ? error.message : String(error);
5736
+ return {
5737
+ path: resolvedRepoPath,
5738
+ configPath,
5739
+ exists: true,
5740
+ valid: false,
5741
+ appCount: 0,
5742
+ tcpAppCount: 0,
5743
+ error: message
5744
+ };
5745
+ }
5746
+ }
5747
+ async function collectRouterStatus(repoPath) {
5748
+ const repo = toRepoStatus(repoPath);
5749
+ const tlsEnabled = isTLSEnabled();
4195
5750
  let container;
4196
5751
  let networkIsPresent = false;
4197
5752
  let dockerContext = "unknown";
@@ -4275,12 +5830,14 @@ async function collectRouterStatus(repoPath) {
4275
5830
  }
4276
5831
  };
4277
5832
  }
4278
- var import_node_fs12;
5833
+ var import_node_fs14;
4279
5834
  var init_status = __esm({
4280
5835
  "src/core/status.ts"() {
4281
5836
  "use strict";
4282
- import_node_fs12 = __toESM(require("fs"));
5837
+ import_node_fs14 = __toESM(require("fs"));
4283
5838
  init_docker();
5839
+ init_managed_runtime_state();
5840
+ init_managed_runtime_status();
4284
5841
  init_repo_config();
4285
5842
  init_router();
4286
5843
  }
@@ -4288,7 +5845,7 @@ var init_status = __esm({
4288
5845
 
4289
5846
  // src/core/tls.ts
4290
5847
  function runOrThrow(command, args) {
4291
- const result = (0, import_node_child_process6.spawnSync)(command, args, {
5848
+ const result = (0, import_node_child_process8.spawnSync)(command, args, {
4292
5849
  encoding: "utf-8"
4293
5850
  });
4294
5851
  if (result.status !== 0) {
@@ -4298,7 +5855,7 @@ function runOrThrow(command, args) {
4298
5855
  return result.stdout.trim();
4299
5856
  }
4300
5857
  function commandExists(command) {
4301
- const result = (0, import_node_child_process6.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
5858
+ const result = (0, import_node_child_process8.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
4302
5859
  return result.status === 0;
4303
5860
  }
4304
5861
  function ensureMkcert() {
@@ -4316,8 +5873,8 @@ function tlsSetupCommand(repoPath) {
4316
5873
  }
4317
5874
  function getMkcertRootCAPath(options = {}) {
4318
5875
  ensureMkcert();
4319
- const rootCAPath = import_node_path11.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
4320
- if (!import_node_fs13.default.existsSync(rootCAPath)) {
5876
+ const rootCAPath = import_node_path14.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
5877
+ if (!import_node_fs15.default.existsSync(rootCAPath)) {
4321
5878
  throw new Error(
4322
5879
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
4323
5880
  );
@@ -4350,7 +5907,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
4350
5907
  return normalizeUniqueHosts(names);
4351
5908
  }
4352
5909
  function parseCertificateDnsHosts(pem) {
4353
- const certificate = new import_node_crypto4.X509Certificate(pem);
5910
+ const certificate = new import_node_crypto6.X509Certificate(pem);
4354
5911
  const subjectAltName = certificate.subjectAltName ?? "";
4355
5912
  if (subjectAltName.length === 0) {
4356
5913
  return [];
@@ -4383,10 +5940,10 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
4383
5940
  );
4384
5941
  }
4385
5942
  function readCurrentCertificateHosts() {
4386
- if (!import_node_fs13.default.existsSync(CERT_FILE)) {
5943
+ if (!import_node_fs15.default.existsSync(CERT_FILE)) {
4387
5944
  return [];
4388
5945
  }
4389
- const pem = import_node_fs13.default.readFileSync(CERT_FILE, "utf-8");
5946
+ const pem = import_node_fs15.default.readFileSync(CERT_FILE, "utf-8");
4390
5947
  return parseCertificateDnsHosts(pem);
4391
5948
  }
4392
5949
  function currentCertificateHostsOrEmpty() {
@@ -4475,14 +6032,14 @@ Run: ${tlsSetupCommand(options.repoPath)}`
4475
6032
  );
4476
6033
  }
4477
6034
  }
4478
- var import_node_child_process6, import_node_crypto4, import_node_fs13, import_node_path11, DEFAULT_TLS_CERT_HOSTS;
6035
+ var import_node_child_process8, import_node_crypto6, import_node_fs15, import_node_path14, DEFAULT_TLS_CERT_HOSTS;
4479
6036
  var init_tls = __esm({
4480
6037
  "src/core/tls.ts"() {
4481
6038
  "use strict";
4482
- import_node_child_process6 = require("child_process");
4483
- import_node_crypto4 = require("crypto");
4484
- import_node_fs13 = __toESM(require("fs"));
4485
- import_node_path11 = __toESM(require("path"));
6039
+ import_node_child_process8 = require("child_process");
6040
+ import_node_crypto6 = require("crypto");
6041
+ import_node_fs15 = __toESM(require("fs"));
6042
+ import_node_path14 = __toESM(require("path"));
4486
6043
  init_docker();
4487
6044
  init_host_routes();
4488
6045
  init_router();
@@ -4498,7 +6055,7 @@ function outputFromResult(result) {
4498
6055
  return output2.length > 0 ? output2 : void 0;
4499
6056
  }
4500
6057
  function runTool(command, args = []) {
4501
- const result = (0, import_node_child_process7.spawnSync)(command, args, {
6058
+ const result = (0, import_node_child_process9.spawnSync)(command, args, {
4502
6059
  encoding: "utf-8"
4503
6060
  });
4504
6061
  if (result.error) {
@@ -4555,12 +6112,12 @@ function parseMinimumNodeMajor(value) {
4555
6112
  return Number(match[1]);
4556
6113
  }
4557
6114
  function readPackageJson(repoPath) {
4558
- const packagePath = import_node_path12.default.join(repoPath, "package.json");
4559
- if (!import_node_fs14.default.existsSync(packagePath)) {
6115
+ const packagePath = import_node_path15.default.join(repoPath, "package.json");
6116
+ if (!import_node_fs16.default.existsSync(packagePath)) {
4560
6117
  return void 0;
4561
6118
  }
4562
6119
  try {
4563
- return JSON.parse(import_node_fs14.default.readFileSync(packagePath, "utf-8"));
6120
+ return JSON.parse(import_node_fs16.default.readFileSync(packagePath, "utf-8"));
4564
6121
  } catch {
4565
6122
  return void 0;
4566
6123
  }
@@ -4655,19 +6212,19 @@ function buildGlobalToolChecks(repoPath) {
4655
6212
  checks.push(nodeToolchainCheck(repoPath));
4656
6213
  return checks;
4657
6214
  }
4658
- var import_node_child_process7, import_node_fs14, import_node_path12;
6215
+ var import_node_child_process9, import_node_fs16, import_node_path15;
4659
6216
  var init_tool_diagnostics = __esm({
4660
6217
  "src/core/tool-diagnostics.ts"() {
4661
6218
  "use strict";
4662
- import_node_child_process7 = require("child_process");
4663
- import_node_fs14 = __toESM(require("fs"));
4664
- import_node_path12 = __toESM(require("path"));
6219
+ import_node_child_process9 = require("child_process");
6220
+ import_node_fs16 = __toESM(require("fs"));
6221
+ import_node_path15 = __toESM(require("path"));
4665
6222
  }
4666
6223
  });
4667
6224
 
4668
6225
  // src/core/devpod-workspaces.ts
4669
6226
  function listDevpodWorkspaces() {
4670
- const result = (0, import_node_child_process8.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
6227
+ const result = (0, import_node_child_process10.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
4671
6228
  encoding: "utf-8"
4672
6229
  });
4673
6230
  if (result.status !== 0) {
@@ -4726,7 +6283,7 @@ function parseDevpodRuntimeStatus(output2, expectedId) {
4726
6283
  }
4727
6284
  }
4728
6285
  function inspectDevpodRuntimeStatus(devpodId) {
4729
- const result = (0, import_node_child_process8.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
6286
+ const result = (0, import_node_child_process10.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
4730
6287
  encoding: "utf-8"
4731
6288
  });
4732
6289
  if (result.status !== 0 || result.error) return "unknown";
@@ -4759,18 +6316,18 @@ function selectDevpodWorkspace(workspaces, repoPath) {
4759
6316
  }
4760
6317
  return matches[0];
4761
6318
  }
4762
- var import_node_child_process8;
6319
+ var import_node_child_process10;
4763
6320
  var init_devpod_workspaces = __esm({
4764
6321
  "src/core/devpod-workspaces.ts"() {
4765
6322
  "use strict";
4766
- import_node_child_process8 = require("child_process");
6323
+ import_node_child_process10 = require("child_process");
4767
6324
  init_workspace();
4768
6325
  }
4769
6326
  });
4770
6327
 
4771
6328
  // src/core/devpod-mutation.ts
4772
6329
  function withMutationLock(activity, target, operation) {
4773
- import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6330
+ import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
4774
6331
  return withFileLockSync(
4775
6332
  DEVPOD_MUTATION_LOCK_FILE,
4776
6333
  { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
@@ -4782,7 +6339,7 @@ function commandFailure2(result) {
4782
6339
  }
4783
6340
  function runDevpodAction(action2, devpodId, force = false) {
4784
6341
  const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
4785
- const result = (0, import_node_child_process9.spawnSync)("devpod", args, { encoding: "utf-8" });
6342
+ const result = (0, import_node_child_process11.spawnSync)("devpod", args, { encoding: "utf-8" });
4786
6343
  if (result.status !== 0) {
4787
6344
  throw new Error(
4788
6345
  `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
@@ -4846,6 +6403,9 @@ function startDevpodWorkspace(options) {
4846
6403
  }
4847
6404
  const args = ["up", options.repoPath];
4848
6405
  if (devpodId) args.push("--id", devpodId);
6406
+ if (options.devcontainerPath) {
6407
+ args.push("--devcontainer-path", options.devcontainerPath);
6408
+ }
4849
6409
  args.push("--open-ide=false");
4850
6410
  if (options.workspace) {
4851
6411
  args.push(
@@ -4868,7 +6428,7 @@ function startDevpodWorkspace(options) {
4868
6428
  delete env.DEVROUTER_GIT_COMMON_DIR;
4869
6429
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
4870
6430
  }
4871
- const result = (0, import_node_child_process9.spawnSync)("devpod", args, {
6431
+ const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
4872
6432
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
4873
6433
  env
4874
6434
  });
@@ -4895,17 +6455,17 @@ function startDevpodWorkspace(options) {
4895
6455
  }
4896
6456
  });
4897
6457
  }
4898
- var import_node_child_process9, import_node_fs15, import_node_path13, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
6458
+ var import_node_child_process11, import_node_fs17, import_node_path16, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
4899
6459
  var init_devpod_mutation = __esm({
4900
6460
  "src/core/devpod-mutation.ts"() {
4901
6461
  "use strict";
4902
- import_node_child_process9 = require("child_process");
4903
- import_node_fs15 = __toESM(require("fs"));
4904
- import_node_path13 = __toESM(require("path"));
6462
+ import_node_child_process11 = require("child_process");
6463
+ import_node_fs17 = __toESM(require("fs"));
6464
+ import_node_path16 = __toESM(require("path"));
4905
6465
  init_devpod_workspaces();
4906
6466
  init_file_lock();
4907
6467
  init_router();
4908
- DEVPOD_MUTATION_LOCK_FILE = import_node_path13.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
6468
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path16.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
4909
6469
  DEVPOD_MUTATION_WAIT_MS = 6e4;
4910
6470
  DevpodStartPostconditionError = class extends Error {
4911
6471
  };
@@ -4919,7 +6479,7 @@ function commandError(command, repoPath, stderr) {
4919
6479
  );
4920
6480
  }
4921
6481
  function resolveGitCommonDir(repoPath) {
4922
- const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
6482
+ const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
4923
6483
  encoding: "utf-8",
4924
6484
  env: READ_ONLY_GIT_ENV
4925
6485
  });
@@ -4927,10 +6487,10 @@ function resolveGitCommonDir(repoPath) {
4927
6487
  if (result.status !== 0 || !output2) {
4928
6488
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
4929
6489
  }
4930
- return comparableWorkspacePath(import_node_path14.default.isAbsolute(output2) ? output2 : import_node_path14.default.resolve(repoPath, output2));
6490
+ return comparableWorkspacePath(import_node_path17.default.isAbsolute(output2) ? output2 : import_node_path17.default.resolve(repoPath, output2));
4931
6491
  }
4932
6492
  function resolveGitTopLevel(repoPath) {
4933
- const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
6493
+ const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
4934
6494
  encoding: "utf-8",
4935
6495
  env: READ_ONLY_GIT_ENV
4936
6496
  });
@@ -4941,7 +6501,7 @@ function resolveGitTopLevel(repoPath) {
4941
6501
  return comparableWorkspacePath(output2);
4942
6502
  }
4943
6503
  function listGitWorktrees(repoPath) {
4944
- const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
6504
+ const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
4945
6505
  encoding: "utf-8",
4946
6506
  env: READ_ONLY_GIT_ENV
4947
6507
  });
@@ -4978,7 +6538,7 @@ function listGitWorktrees(repoPath) {
4978
6538
  return worktrees;
4979
6539
  }
4980
6540
  function ownershipDirectory(repoPath) {
4981
- return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
6541
+ return import_node_path17.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
4982
6542
  }
4983
6543
  function validateWorkspace(value, label) {
4984
6544
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -5006,7 +6566,7 @@ function validateRecord(value, expectedWorkspace) {
5006
6566
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
5007
6567
  );
5008
6568
  }
5009
- if (typeof candidate.worktreePath !== "string" || !import_node_path14.default.isAbsolute(candidate.worktreePath)) {
6569
+ if (typeof candidate.worktreePath !== "string" || !import_node_path17.default.isAbsolute(candidate.worktreePath)) {
5010
6570
  throw new Error("invalid workspace ownership worktreePath");
5011
6571
  }
5012
6572
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -5026,7 +6586,7 @@ function validateRecord(value, expectedWorkspace) {
5026
6586
  function readRecordFile(filePath, expectedWorkspace) {
5027
6587
  let parsed;
5028
6588
  try {
5029
- parsed = JSON.parse(import_node_fs16.default.readFileSync(filePath, "utf-8"));
6589
+ parsed = JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
5030
6590
  } catch (error) {
5031
6591
  if (error instanceof SyntaxError) {
5032
6592
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -5042,7 +6602,7 @@ function listWorkspaceOwnership(repoPath) {
5042
6602
  function listWorkspaceOwnershipInDirectory(directory) {
5043
6603
  let entries;
5044
6604
  try {
5045
- entries = import_node_fs16.default.readdirSync(directory, { withFileTypes: true });
6605
+ entries = import_node_fs18.default.readdirSync(directory, { withFileTypes: true });
5046
6606
  } catch (error) {
5047
6607
  if (error.code === "ENOENT") return [];
5048
6608
  throw error;
@@ -5050,14 +6610,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
5050
6610
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
5051
6611
  const workspace = entry.name.slice(0, -".json".length);
5052
6612
  validateWorkspace(workspace, "filename");
5053
- return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
6613
+ return readRecordFile(import_node_path17.default.join(directory, entry.name), workspace);
5054
6614
  });
5055
6615
  }
5056
6616
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
5057
6617
  const workspace = validateWorkspace(input2.workspace, "workspace");
5058
6618
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
5059
6619
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
5060
- const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
6620
+ const filePath = import_node_path17.default.join(directory, `${workspace}.json`);
5061
6621
  const now = (/* @__PURE__ */ new Date()).toISOString();
5062
6622
  const records = listWorkspaceOwnershipInDirectory(directory);
5063
6623
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -5093,9 +6653,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
5093
6653
  return record;
5094
6654
  }
5095
6655
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
5096
- const filePath = import_node_path14.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
6656
+ const filePath = import_node_path17.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
5097
6657
  try {
5098
- import_node_fs16.default.rmSync(filePath);
6658
+ import_node_fs18.default.rmSync(filePath);
5099
6659
  return true;
5100
6660
  } catch (error) {
5101
6661
  if (error.code === "ENOENT") return false;
@@ -5106,7 +6666,7 @@ function sameOwnershipRecord(left, right) {
5106
6666
  return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
5107
6667
  }
5108
6668
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
5109
- const filePath = import_node_path14.default.join(
6669
+ const filePath = import_node_path17.default.join(
5110
6670
  directory,
5111
6671
  `${validateWorkspace(expected.workspace, "workspace")}.json`
5112
6672
  );
@@ -5118,14 +6678,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
5118
6678
  throw error;
5119
6679
  }
5120
6680
  if (!sameOwnershipRecord(current, expected)) return "changed";
5121
- import_node_fs16.default.rmSync(filePath);
6681
+ import_node_fs18.default.rmSync(filePath);
5122
6682
  return "removed";
5123
6683
  }
5124
6684
  function withWorkspaceOwnershipTransaction(repoPath, operation) {
5125
6685
  const directory = ownershipDirectory(repoPath);
5126
- import_node_fs16.default.mkdirSync(directory, { recursive: true });
6686
+ import_node_fs18.default.mkdirSync(directory, { recursive: true });
5127
6687
  return withFileLockSync(
5128
- import_node_path14.default.join(directory, ".lock"),
6688
+ import_node_path17.default.join(directory, ".lock"),
5129
6689
  { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
5130
6690
  () => operation({
5131
6691
  list: () => listWorkspaceOwnershipInDirectory(directory),
@@ -5156,7 +6716,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
5156
6716
  if (worktree?.locked) {
5157
6717
  return { ownerStatus: "locked", devpodStatus, worktree };
5158
6718
  }
5159
- if ((!worktree || worktree.prunable) && import_node_fs16.default.existsSync(record.worktreePath)) {
6719
+ if ((!worktree || worktree.prunable) && import_node_fs18.default.existsSync(record.worktreePath)) {
5160
6720
  return { ownerStatus: "conflict", devpodStatus, worktree };
5161
6721
  }
5162
6722
  if (!worktree || worktree.prunable) {
@@ -5180,20 +6740,20 @@ function listMissingWorkspaceOwnership(repoPath) {
5180
6740
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
5181
6741
  );
5182
6742
  }
5183
- var import_node_child_process10, import_node_fs16, import_node_path14, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
6743
+ var import_node_child_process12, import_node_fs18, import_node_path17, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
5184
6744
  var init_workspace_ownership = __esm({
5185
6745
  "src/core/workspace-ownership.ts"() {
5186
6746
  "use strict";
5187
- import_node_child_process10 = require("child_process");
5188
- import_node_fs16 = __toESM(require("fs"));
5189
- import_node_path14 = __toESM(require("path"));
6747
+ import_node_child_process12 = require("child_process");
6748
+ import_node_fs18 = __toESM(require("fs"));
6749
+ import_node_path17 = __toESM(require("path"));
5190
6750
  init_atomic_file();
5191
6751
  init_devpod_workspaces();
5192
6752
  init_file_lock();
5193
6753
  init_workspace();
5194
6754
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
5195
6755
  OWNERSHIP_VERSION = 1;
5196
- OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
6756
+ OWNERSHIP_DIR = import_node_path17.default.join("devrouter", "workspaces");
5197
6757
  }
5198
6758
  });
5199
6759
 
@@ -5207,10 +6767,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
5207
6767
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
5208
6768
  const comparableRepo = comparableWorkspacePath(repoPath);
5209
6769
  const comparableWorktree = comparableWorkspacePath(worktreePath);
5210
- const localRoot = import_node_path15.default.join(comparableRepo, "trees") + import_node_path15.default.sep;
6770
+ const localRoot = import_node_path18.default.join(comparableRepo, "trees") + import_node_path18.default.sep;
5211
6771
  if (comparableWorktree.startsWith(localRoot)) return true;
5212
- const legacyPrefix = `${import_node_path15.default.basename(comparableRepo)}-`;
5213
- return import_node_path15.default.dirname(comparableWorktree) === import_node_path15.default.dirname(comparableRepo) && import_node_path15.default.basename(comparableWorktree).startsWith(legacyPrefix);
6772
+ const legacyPrefix = `${import_node_path18.default.basename(comparableRepo)}-`;
6773
+ return import_node_path18.default.dirname(comparableWorktree) === import_node_path18.default.dirname(comparableRepo) && import_node_path18.default.basename(comparableWorktree).startsWith(legacyPrefix);
5214
6774
  }
5215
6775
  function previewActions(devpodStatus, routeCount, includeRecord) {
5216
6776
  const actions = [
@@ -5453,11 +7013,11 @@ function applyWorkspaceGc(plan) {
5453
7013
  candidates
5454
7014
  };
5455
7015
  }
5456
- var import_node_path15;
7016
+ var import_node_path18;
5457
7017
  var init_workspace_gc = __esm({
5458
7018
  "src/core/workspace-gc.ts"() {
5459
7019
  "use strict";
5460
- import_node_path15 = __toESM(require("path"));
7020
+ import_node_path18 = __toESM(require("path"));
5461
7021
  init_devpod_mutation();
5462
7022
  init_devpod_workspaces();
5463
7023
  init_host_routes();
@@ -5527,12 +7087,12 @@ function inspectPostgresCredentials(repoPath, config) {
5527
7087
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
5528
7088
  continue;
5529
7089
  }
5530
- if (!import_node_fs17.default.existsSync(absolutePath)) {
7090
+ if (!import_node_fs19.default.existsSync(absolutePath)) {
5531
7091
  continue;
5532
7092
  }
5533
7093
  try {
5534
- const raw = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
5535
- const parsed = import_yaml4.default.parse(raw);
7094
+ const raw = import_node_fs19.default.readFileSync(absolutePath, "utf-8");
7095
+ const parsed = import_yaml5.default.parse(raw);
5536
7096
  const root = asRecord2(parsed);
5537
7097
  const services = asRecord2(root?.services);
5538
7098
  const service = asRecord2(services?.[app.docker.service]);
@@ -5782,6 +7342,22 @@ async function buildDoctorReport(options = {}) {
5782
7342
  level: "ok",
5783
7343
  summary: `.devrouter.yml is valid (${repo.appCount} app(s)).`
5784
7344
  });
7345
+ if (repo.managedRuntime) {
7346
+ const managedRuntime = repo.managedRuntime;
7347
+ const level = managedRuntime.mode === "legacy" || managedRuntime.status === "ready" ? "ok" : managedRuntime.status === "starting" || managedRuntime.status === "stopped" ? "warn" : "error";
7348
+ const summary2 = managedRuntime.mode === "legacy" ? "Legacy runtime contract is active." : managedRuntime.status === "ready" ? `Managed runtime profile '${managedRuntime.profile}' is ready.` : managedRuntime.status === "starting" ? `Managed runtime profile '${managedRuntime.profile}' is starting.` : managedRuntime.status === "stopped" ? `Managed runtime profile '${managedRuntime.profile}' is stopped.` : managedRuntime.status === "failed-transition" ? `Managed runtime profile '${managedRuntime.profile}' has a failed transition.` : `Managed runtime profile '${managedRuntime.profile}' has runtime drift.`;
7349
+ const details = [
7350
+ managedRuntime.transitionPhase ? `transitionPhase=${managedRuntime.transitionPhase}` : void 0,
7351
+ ...managedRuntime.drift
7352
+ ].filter((detail) => Boolean(detail)).join("; ");
7353
+ addCheck(checks, {
7354
+ id: "repo.managed-runtime",
7355
+ level,
7356
+ summary: summary2,
7357
+ ...details ? { details } : {},
7358
+ suggestion: managedRuntime.mode === "legacy" ? void 0 : managedRuntime.status === "failed-transition" ? `Inspect: dev status --repo ${repo.path}; resolve the reported drift before retrying ensure.` : managedRuntime.status === "ready" ? void 0 : `Run: dev ensure ${repo.path}`
7359
+ });
7360
+ }
5785
7361
  if (repo.appCount === 0) {
5786
7362
  addCheck(checks, {
5787
7363
  id: "repo.apps",
@@ -5794,7 +7370,7 @@ async function buildDoctorReport(options = {}) {
5794
7370
  const config = runtimeConfig.config;
5795
7371
  loadedConfig = config;
5796
7372
  loadedWorkspace = runtimeConfig.workspace;
5797
- const cliVersion = true ? "0.0.37" : "0.0.0-dev";
7373
+ const cliVersion = true ? "0.0.39" : "0.0.0-dev";
5798
7374
  const configVersion = config.devrouter?.version;
5799
7375
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
5800
7376
  addCheck(checks, {
@@ -5824,9 +7400,9 @@ async function buildDoctorReport(options = {}) {
5824
7400
  (app) => app.docker.composeFiles.map((filePath) => ({
5825
7401
  app: app.name,
5826
7402
  filePath,
5827
- absolutePath: import_node_path16.default.resolve(repo.path, filePath)
7403
+ absolutePath: import_node_path19.default.resolve(repo.path, filePath)
5828
7404
  }))
5829
- ).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
7405
+ ).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
5830
7406
  addCheck(checks, {
5831
7407
  id: "repo.compose-files",
5832
7408
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -5869,8 +7445,8 @@ async function buildDoctorReport(options = {}) {
5869
7445
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
5870
7446
  app: app.name,
5871
7447
  cwd: app.hostRun.cwd,
5872
- absolutePath: import_node_path16.default.resolve(repo.path, app.hostRun.cwd)
5873
- })).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
7448
+ absolutePath: import_node_path19.default.resolve(repo.path, app.hostRun.cwd)
7449
+ })).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
5874
7450
  addCheck(checks, {
5875
7451
  id: "repo.host-cwd",
5876
7452
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -6014,13 +7590,13 @@ async function buildDoctorReport(options = {}) {
6014
7590
  nextSteps
6015
7591
  };
6016
7592
  }
6017
- var import_node_fs17, import_node_path16, import_yaml4, POSTGRES_DEFAULTS;
7593
+ var import_node_fs19, import_node_path19, import_yaml5, POSTGRES_DEFAULTS;
6018
7594
  var init_doctor = __esm({
6019
7595
  "src/core/doctor.ts"() {
6020
7596
  "use strict";
6021
- import_node_fs17 = __toESM(require("fs"));
6022
- import_node_path16 = __toESM(require("path"));
6023
- import_yaml4 = __toESM(require("yaml"));
7597
+ import_node_fs19 = __toESM(require("fs"));
7598
+ import_node_path19 = __toESM(require("path"));
7599
+ import_yaml5 = __toESM(require("yaml"));
6024
7600
  init_docker();
6025
7601
  init_host_routes();
6026
7602
  init_repo_config();
@@ -6277,12 +7853,12 @@ function parseSsPortListeners(stdout, port) {
6277
7853
  return listeners;
6278
7854
  }
6279
7855
  function findPortListeners(port) {
6280
- const result = (0, import_node_child_process11.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
7856
+ const result = (0, import_node_child_process13.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
6281
7857
  encoding: "utf-8"
6282
7858
  });
6283
7859
  if (result.error && result.error.code === "ENOENT") {
6284
7860
  if (process.platform === "linux") {
6285
- const ssResult = (0, import_node_child_process11.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
7861
+ const ssResult = (0, import_node_child_process13.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
6286
7862
  if (ssResult.status === 0 && ssResult.stdout) {
6287
7863
  return parseSsPortListeners(ssResult.stdout, port);
6288
7864
  }
@@ -6311,11 +7887,11 @@ function findPortListeners(port) {
6311
7887
  };
6312
7888
  });
6313
7889
  }
6314
- var import_node_child_process11;
7890
+ var import_node_child_process13;
6315
7891
  var init_ports = __esm({
6316
7892
  "src/util/ports.ts"() {
6317
7893
  "use strict";
6318
- import_node_child_process11 = require("child_process");
7894
+ import_node_child_process13 = require("child_process");
6319
7895
  }
6320
7896
  });
6321
7897
 
@@ -6341,101 +7917,32 @@ ${details}
6341
7917
 
6342
7918
  Mitigation:
6343
7919
  1) Stop the conflicting process/container
6344
- 2) Re-run: dev up
6345
-
6346
- Debug commands:
6347
- - lsof -nP -iTCP:80 -sTCP:LISTEN
6348
- - lsof -nP -iTCP:443 -sTCP:LISTEN
6349
- - lsof -nP -iTCP:5432 -sTCP:LISTEN`;
6350
- }
6351
- async function runUpCommand() {
6352
- await ensureNetwork(DEVNET_NAME);
6353
- ensureRouterFiles();
6354
- const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
6355
- if (!routerRunning) {
6356
- const message = buildPortConflictMessage();
6357
- if (message) {
6358
- throw new Error(message);
6359
- }
6360
- }
6361
- startRouterStack();
6362
- process.stdout.write("devrouter is up.\n");
6363
- }
6364
- var init_up = __esm({
6365
- "src/commands/up.ts"() {
6366
- "use strict";
6367
- init_docker();
6368
- init_router();
6369
- init_ports();
6370
- }
6371
- });
6372
-
6373
- // src/core/devpod-environment.ts
6374
- function inspectWorkspaceContainers(options) {
6375
- let ids = options?.ids;
6376
- if (!ids) {
6377
- const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
6378
- encoding: "utf-8"
6379
- });
6380
- if (listed.status !== 0) {
6381
- throw new Error(
6382
- `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
6383
- );
6384
- }
6385
- ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
6386
- }
6387
- if (ids.length === 0) return [];
6388
- const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
6389
- const inspected = (0, import_node_child_process12.spawnSync)(
6390
- "docker",
6391
- ["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
6392
- { encoding: "utf-8" }
6393
- );
6394
- if (inspected.status !== 0) {
6395
- throw new Error(
6396
- `docker inspect failed: ${(inspected.stderr || inspected.stdout || "unknown error").trim()}`
6397
- );
6398
- }
6399
- return inspected.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
6400
- }
6401
- function workspaceAppContainers(containers, repoPath) {
6402
- return containers.filter((container) => {
6403
- const workingDir = container.labels["com.docker.compose.project.working_dir"];
6404
- return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
6405
- (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
6406
- );
6407
- });
6408
- }
6409
- function resolveRunningWorkspaceContainer(repoPath) {
6410
- const matches = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
6411
- (container2) => container2.state.Running
6412
- );
6413
- if (matches.length !== 1) {
6414
- throw new Error(
6415
- `Expected one running workspace app container for '${repoPath}', found ${matches.length}.`
6416
- );
6417
- }
6418
- const container = matches[0];
6419
- const repoMount = container.mounts.find(
6420
- (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
6421
- );
6422
- if (!repoMount) {
6423
- throw new Error(`Workspace app container no longer mounts '${repoPath}'.`);
7920
+ 2) Re-run: dev up
7921
+
7922
+ Debug commands:
7923
+ - lsof -nP -iTCP:80 -sTCP:LISTEN
7924
+ - lsof -nP -iTCP:443 -sTCP:LISTEN
7925
+ - lsof -nP -iTCP:5432 -sTCP:LISTEN`;
7926
+ }
7927
+ async function runUpCommand() {
7928
+ await ensureNetwork(DEVNET_NAME);
7929
+ ensureRouterFiles();
7930
+ const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
7931
+ if (!routerRunning) {
7932
+ const message = buildPortConflictMessage();
7933
+ if (message) {
7934
+ throw new Error(message);
7935
+ }
6424
7936
  }
6425
- return { id: container.id, workspacePath: repoMount.Destination };
7937
+ startRouterStack();
7938
+ process.stdout.write("devrouter is up.\n");
6426
7939
  }
6427
- var import_node_child_process12, import_node_path17, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
6428
- var init_devpod_environment = __esm({
6429
- "src/core/devpod-environment.ts"() {
7940
+ var init_up = __esm({
7941
+ "src/commands/up.ts"() {
6430
7942
  "use strict";
6431
- import_node_child_process12 = require("child_process");
6432
- import_node_path17 = __toESM(require("path"));
6433
- init_workspace();
6434
- SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
6435
- SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
6436
- /}$/,
6437
- ',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
6438
- );
7943
+ init_docker();
7944
+ init_router();
7945
+ init_ports();
6439
7946
  }
6440
7947
  });
6441
7948
 
@@ -6461,18 +7968,18 @@ function probeHttpRoute(host, options = {}) {
6461
7968
  args.push("--cacert", CERT_FILE);
6462
7969
  }
6463
7970
  args.push(url);
6464
- const result = (0, import_node_child_process13.spawnSync)("curl", args, { encoding: "utf-8" });
7971
+ const result = (0, import_node_child_process14.spawnSync)("curl", args, { encoding: "utf-8" });
6465
7972
  const stdout = result.stdout?.trim() ?? "";
6466
7973
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
6467
7974
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
6468
7975
  const details = ok ? `HTTP ${status}` : [status === void 0 ? void 0 : `HTTP ${status}`, result.stderr?.trim()].filter(Boolean).join(": ") || `curl exited with status ${result.status ?? "unknown"}`;
6469
7976
  return { ok, status, details };
6470
7977
  }
6471
- var import_node_child_process13;
7978
+ var import_node_child_process14;
6472
7979
  var init_http_route_probe = __esm({
6473
7980
  "src/core/http-route-probe.ts"() {
6474
7981
  "use strict";
6475
- import_node_child_process13 = require("child_process");
7982
+ import_node_child_process14 = require("child_process");
6476
7983
  init_router();
6477
7984
  init_tls();
6478
7985
  }
@@ -6551,11 +8058,11 @@ var init_route_publication = __esm({
6551
8058
  // src/core/workspace-ensure.ts
6552
8059
  function assertOverlay(container, repoPath) {
6553
8060
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
6554
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path18.default.join(repoPath, ".devcontainer"))) {
8061
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path20.default.join(repoPath, ".devcontainer"))) {
6555
8062
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
6556
8063
  }
6557
8064
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
6558
- const expectedOverlay = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8065
+ const expectedOverlay = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
6559
8066
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
6560
8067
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
6561
8068
  }
@@ -6569,6 +8076,197 @@ function assertReady(container, label) {
6569
8076
  throw new Error(`${label} container '${container.id}' is not healthy (${health}).`);
6570
8077
  }
6571
8078
  }
8079
+ function exactWorkspaceServiceContainers(containers, repoPath, composeProject, service, composeFiles2) {
8080
+ return containers.filter(
8081
+ (container) => hasExactComposeIdentity(container, {
8082
+ repoPath,
8083
+ composeProject,
8084
+ service,
8085
+ composeFiles: composeFiles2
8086
+ })
8087
+ );
8088
+ }
8089
+ function resolveComposeProject(containers, repoPath, appContainerId, primaryService, composeFiles2) {
8090
+ const app = containers.find((candidate) => candidate.id === appContainerId);
8091
+ if (!app) {
8092
+ throw new Error(
8093
+ `Workspace app container '${appContainerId}' disappeared during reconciliation.`
8094
+ );
8095
+ }
8096
+ if (!hasExactComposeIdentity(app, {
8097
+ repoPath,
8098
+ service: primaryService,
8099
+ composeFiles: composeFiles2
8100
+ })) {
8101
+ throw new Error(`Workspace app container '${appContainerId}' has an unexpected Compose model.`);
8102
+ }
8103
+ const project = app.labels["com.docker.compose.project"];
8104
+ if (!project) {
8105
+ throw new Error(`Workspace app container '${appContainerId}' has no Compose project label.`);
8106
+ }
8107
+ return project;
8108
+ }
8109
+ async function waitForManagedServices(plan, repoPath, composeProject, expectedServices, expectedAppId, timeoutMs) {
8110
+ const deadline = Date.now() + timeoutMs;
8111
+ let lastError;
8112
+ do {
8113
+ try {
8114
+ const containers = inspectWorkspaceContainers();
8115
+ for (const service of expectedServices) {
8116
+ const matches = exactWorkspaceServiceContainers(
8117
+ containers,
8118
+ repoPath,
8119
+ composeProject,
8120
+ service,
8121
+ plan.composeFiles
8122
+ );
8123
+ if (matches.length !== 1) {
8124
+ throw new Error(
8125
+ `Managed service '${service}' must have exactly one exact workspace container; found ${matches.length}.`
8126
+ );
8127
+ }
8128
+ if (service === plan.primaryService && matches[0].id !== expectedAppId) {
8129
+ throw new Error(
8130
+ `Managed primary service '${service}' changed container identity during profile transition.`
8131
+ );
8132
+ }
8133
+ assertReady(matches[0], `Managed service '${service}'`);
8134
+ }
8135
+ return;
8136
+ } catch (error) {
8137
+ lastError = error;
8138
+ }
8139
+ if (Date.now() < deadline) await sleep(POLL_INTERVAL_MS);
8140
+ } while (Date.now() < deadline);
8141
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
8142
+ }
8143
+ function stopDroppedManagedServices(plan, repoPath, composeProject) {
8144
+ const desired = new Set(plan.desiredProfileServices);
8145
+ const containers = inspectWorkspaceContainers();
8146
+ for (const service of plan.profileServices) {
8147
+ if (desired.has(service)) continue;
8148
+ const matches = exactWorkspaceServiceContainers(
8149
+ containers,
8150
+ repoPath,
8151
+ composeProject,
8152
+ service,
8153
+ plan.composeFiles
8154
+ );
8155
+ if (matches.length > 1) {
8156
+ throw new Error(
8157
+ `Managed service '${service}' has multiple exact workspace containers; refusing to stop any.`
8158
+ );
8159
+ }
8160
+ const match = matches[0];
8161
+ if (match?.state.Running) stopExactManagedService(match.id, service);
8162
+ }
8163
+ }
8164
+ function assertDroppedManagedServicesStopped(plan, repoPath, composeProject) {
8165
+ const desired = new Set(plan.desiredProfileServices);
8166
+ const containers = inspectWorkspaceContainers();
8167
+ for (const service of plan.profileServices) {
8168
+ if (desired.has(service)) continue;
8169
+ const matches = exactWorkspaceServiceContainers(
8170
+ containers,
8171
+ repoPath,
8172
+ composeProject,
8173
+ service,
8174
+ plan.composeFiles
8175
+ );
8176
+ if (matches.some((match) => match.state.Running)) {
8177
+ throw new Error(`Managed service '${service}' remains running after exact stop.`);
8178
+ }
8179
+ }
8180
+ }
8181
+ function routeInputFromState(route) {
8182
+ return {
8183
+ name: route.name,
8184
+ host: route.host,
8185
+ protocol: route.protocol,
8186
+ tcpProtocol: route.tcpProtocol,
8187
+ repoPath: route.repoPath,
8188
+ port: route.port,
8189
+ mode: route.mode,
8190
+ upstreamHost: route.upstreamHost,
8191
+ pid: route.pid,
8192
+ command: route.command,
8193
+ workspace: route.workspace
8194
+ };
8195
+ }
8196
+ function isWildcard2(values) {
8197
+ return values?.length === 1 && values[0] === "*";
8198
+ }
8199
+ function desiredManagedProcesses(managedRuntime, profile) {
8200
+ if (!profile || isWildcard2(profile.processes)) return [...managedRuntime.processes];
8201
+ return [...profile.processes ?? []];
8202
+ }
8203
+ function restorePreviousManagedConfig(options) {
8204
+ const restoredPlan = inspectManagedDevcontainerConfig({
8205
+ repoPath: options.repoPath,
8206
+ config: options.config,
8207
+ profile: {
8208
+ apps: [],
8209
+ devcontainerServices: [...options.previousState.desired.services]
8210
+ },
8211
+ linked: options.linked
8212
+ });
8213
+ if (restoredPlan.effectiveConfigSha256 !== options.previousState.effectiveConfigSha256) {
8214
+ throw new Error(
8215
+ "The previous managed Dev Container configuration fingerprint no longer matches the current source."
8216
+ );
8217
+ }
8218
+ writeManagedDevcontainerConfig(restoredPlan);
8219
+ }
8220
+ function isWarmWorkspaceActive(repoPath) {
8221
+ try {
8222
+ return workspaceAppContainers(inspectWorkspaceContainers(), repoPath).some(
8223
+ (container) => container.state.Running
8224
+ );
8225
+ } catch {
8226
+ return true;
8227
+ }
8228
+ }
8229
+ function captureFirstTransitionBaseline(options) {
8230
+ const containers = inspectWorkspaceContainers();
8231
+ const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
8232
+ (container) => container.state.Running
8233
+ );
8234
+ if (runningPrimary.length === 0) return void 0;
8235
+ if (runningPrimary.length > 1) {
8236
+ throw new Error(
8237
+ "Cannot start the first managed transition while multiple workspace app containers are running."
8238
+ );
8239
+ }
8240
+ const primary = runningPrimary[0];
8241
+ const composeProject = primary.labels["com.docker.compose.project"];
8242
+ const workspacePath = primary.mounts.find(
8243
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, options.repoPath)
8244
+ )?.Destination;
8245
+ if (!composeProject || !workspacePath) {
8246
+ throw new Error(
8247
+ "Cannot prove the exact pre-transition Compose identity for the first managed transition."
8248
+ );
8249
+ }
8250
+ const services = options.plan.profileServices.filter(
8251
+ (service) => containers.some(
8252
+ (container) => container.state.Running && hasExactComposeIdentity(container, { repoPath: options.repoPath, service, composeProject })
8253
+ )
8254
+ );
8255
+ const activeProcesses = [];
8256
+ for (const process2 of options.processes) {
8257
+ const status = runManagedProcessAction({
8258
+ container: { id: primary.id, workspacePath },
8259
+ name: process2,
8260
+ action: "status",
8261
+ quiet: true
8262
+ });
8263
+ if (status === "running") activeProcesses.push(process2);
8264
+ }
8265
+ return {
8266
+ services,
8267
+ processes: activeProcesses
8268
+ };
8269
+ }
6572
8270
  function validateWorkspaceContainers(containers, options) {
6573
8271
  const appContainers = workspaceAppContainers(containers, options.repoPath);
6574
8272
  if (appContainers.length !== 1) {
@@ -6629,7 +8327,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6629
8327
  target
6630
8328
  });
6631
8329
  if (target.kind === "linked") {
6632
- const workspaceEnv = (0, import_node_child_process14.spawnSync)(
8330
+ const workspaceEnv = (0, import_node_child_process15.spawnSync)(
6633
8331
  "docker",
6634
8332
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
6635
8333
  { encoding: "utf-8" }
@@ -6639,7 +8337,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6639
8337
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
6640
8338
  );
6641
8339
  }
6642
- const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
8340
+ const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
6643
8341
  "docker",
6644
8342
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
6645
8343
  { encoding: "utf-8" }
@@ -6650,7 +8348,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6650
8348
  );
6651
8349
  }
6652
8350
  }
6653
- const gitCheck = (0, import_node_child_process14.spawnSync)(
8351
+ const gitCheck = (0, import_node_child_process15.spawnSync)(
6654
8352
  "docker",
6655
8353
  [
6656
8354
  "exec",
@@ -6737,14 +8435,14 @@ function resolvePrimaryTarget(repoPath) {
6737
8435
  }
6738
8436
  function isPrimaryCheckout(repoPath) {
6739
8437
  try {
6740
- return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
8438
+ return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
6741
8439
  } catch {
6742
8440
  return false;
6743
8441
  }
6744
8442
  }
6745
8443
  function openUrls(urls) {
6746
8444
  for (const url of urls) {
6747
- const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
8445
+ const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
6748
8446
  if (opened.status !== 0) {
6749
8447
  process.stderr.write(`Warning: could not open '${url}'.
6750
8448
  `);
@@ -6770,23 +8468,92 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6770
8468
  }
6771
8469
  return withWorkspaceLifecycleLock(repoPath, async () => {
6772
8470
  let environmentStarted = false;
8471
+ let managedPlan;
8472
+ let previousManagedState;
8473
+ let managedComposeProject;
8474
+ let managedContainer;
8475
+ let previousRoutes = [];
8476
+ let candidateRoutesPublished = false;
8477
+ let transitionPhase = "validation";
8478
+ let managedProcessRegistry = [];
8479
+ let managedPostStartPlan;
8480
+ let managedRuntimeConfig;
8481
+ let managedConfigWritten = false;
8482
+ let managedWorkspaceEnv;
8483
+ let firstTransitionBaseline;
6773
8484
  try {
6774
8485
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
6775
8486
  let devpodId = target.devpodId;
6776
8487
  if (target.kind === "linked") {
6777
- const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
6778
- if (!import_node_fs18.default.existsSync(overlayPath)) {
8488
+ const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8489
+ if (!import_node_fs20.default.existsSync(overlayPath)) {
6779
8490
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
6780
8491
  }
6781
8492
  }
6782
8493
  const managedPostStart = resolveManagedPostStartPlan(repoPath);
8494
+ managedPostStartPlan = managedPostStart;
6783
8495
  const runtime = loadRuntimeConfig(
6784
8496
  repoPath,
6785
- target.kind === "primary" ? "" : target.workspace
8497
+ target.kind === "primary" ? "" : target.workspace,
8498
+ options.profile
6786
8499
  );
8500
+ const managedRuntime = runtime.config.managedRuntime;
8501
+ managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
8502
+ managedProcessRegistry = managedRuntime?.processes ?? [];
8503
+ const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
8504
+ if (managedRuntime) {
8505
+ previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
8506
+ if (previousManagedState?.status === "degraded") {
8507
+ throw new Error(
8508
+ "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
8509
+ );
8510
+ }
8511
+ if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
8512
+ throw new Error(
8513
+ `Managed runtime state names DevPod '${previousManagedState.devpodId}', not the exact target '${target.devpodId ?? "(absent)"}'.`
8514
+ );
8515
+ }
8516
+ if (previousManagedState?.desired.services.some(
8517
+ (service) => !managedRuntime.devcontainer.profileServices.includes(service)
8518
+ )) {
8519
+ throw new Error("Managed runtime state contains an unregistered profile service.");
8520
+ }
8521
+ if (previousManagedState?.desired.processes.some(
8522
+ (process2) => !managedRuntime.processes.includes(process2)
8523
+ )) {
8524
+ throw new Error("Managed runtime state contains an unregistered process marker.");
8525
+ }
8526
+ previousRoutes = listHostRouteState().filter(
8527
+ (route) => sameWorkspacePath(route.repoPath, repoPath)
8528
+ );
8529
+ if (managedPostStart.kind !== "runtime") {
8530
+ throw new Error(
8531
+ "managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
8532
+ );
8533
+ }
8534
+ managedPlan = inspectManagedDevcontainerConfig({
8535
+ repoPath,
8536
+ config: runtime.config,
8537
+ profile: runtime.resolvedProfile,
8538
+ linked: target.kind === "linked"
8539
+ });
8540
+ managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
8541
+ if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
8542
+ throw new Error(
8543
+ `Managed Dev Container source configuration changed since the last successful transition; a warm ensure cannot apply it to the existing runtime for '${repoPath}'. Stop and delete the exact runtime, then run ensure again to recreate it from the current source configuration.`
8544
+ );
8545
+ }
8546
+ if (!previousManagedState) {
8547
+ firstTransitionBaseline = captureFirstTransitionBaseline({
8548
+ repoPath,
8549
+ plan: managedPlan,
8550
+ processes: managedRuntime.processes
8551
+ });
8552
+ }
8553
+ }
6787
8554
  const apps = proxyAppsFromConfig(runtime.config);
6788
8555
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
6789
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path18.default.basename(repoPath)) ?? "app";
8556
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
6790
8557
  for (const [index, app] of apps.entries()) {
6791
8558
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
6792
8559
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -6795,6 +8562,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6795
8562
  );
6796
8563
  }
6797
8564
  }
8565
+ if (managedPlan) {
8566
+ writeManagedDevcontainerConfig(managedPlan);
8567
+ managedConfigWritten = true;
8568
+ }
6798
8569
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
6799
8570
  const ownership = target.kind === "linked" ? {
6800
8571
  workspace: target.workspace,
@@ -6812,6 +8583,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6812
8583
  devpodId = startDevpodWorkspace({
6813
8584
  repoPath,
6814
8585
  devpodId: requestedTarget.devpodId,
8586
+ devcontainerPath: managedPlan?.generatedRelativePath,
6815
8587
  recreate,
6816
8588
  quiet: options.quiet,
6817
8589
  ...requestedTarget.kind === "linked" ? {
@@ -6840,7 +8612,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6840
8612
  try {
6841
8613
  startAndProveAttachment();
6842
8614
  } catch (error) {
6843
- if (!target.hadExactDevpod) {
8615
+ if (managedPlan || !target.hadExactDevpod) {
6844
8616
  throw error;
6845
8617
  }
6846
8618
  container = await recreateAndPreflight();
@@ -6850,23 +8622,113 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6850
8622
  try {
6851
8623
  container = await preflight(0);
6852
8624
  } catch (error) {
6853
- if (!target.hadExactDevpod) {
8625
+ if (managedPlan || !target.hadExactDevpod) {
6854
8626
  throw error;
6855
8627
  }
6856
8628
  container = await recreateAndPreflight();
6857
8629
  recreated = true;
6858
8630
  }
6859
8631
  }
8632
+ managedContainer = container;
8633
+ if (managedPlan) {
8634
+ transitionPhase = "service-start";
8635
+ const initialContainers = inspectWorkspaceContainers();
8636
+ managedComposeProject = resolveComposeProject(
8637
+ initialContainers,
8638
+ repoPath,
8639
+ container.id,
8640
+ managedPlan.primaryService,
8641
+ managedPlan.composeFiles
8642
+ );
8643
+ if (previousManagedState && previousManagedState.composeProject !== managedComposeProject) {
8644
+ throw new Error(
8645
+ `Managed runtime state names Compose project '${previousManagedState.composeProject}', not '${managedComposeProject}'.`
8646
+ );
8647
+ }
8648
+ const missingServices = [];
8649
+ for (const service of managedPlan.desiredServices) {
8650
+ const matches = exactWorkspaceServiceContainers(
8651
+ initialContainers,
8652
+ repoPath,
8653
+ managedComposeProject,
8654
+ service,
8655
+ managedPlan.composeFiles
8656
+ );
8657
+ if (matches.length > 1) {
8658
+ throw new Error(
8659
+ `Managed service '${service}' has multiple exact workspace containers; refusing to reconcile it.`
8660
+ );
8661
+ }
8662
+ if (!matches[0]?.state.Running) missingServices.push(service);
8663
+ }
8664
+ startExactManagedServices({
8665
+ plan: managedPlan,
8666
+ composeProject: managedComposeProject,
8667
+ services: missingServices,
8668
+ quiet: options.quiet,
8669
+ workspace: managedWorkspaceEnv
8670
+ });
8671
+ await waitForManagedServices(
8672
+ managedPlan,
8673
+ repoPath,
8674
+ managedComposeProject,
8675
+ managedPlan.desiredServices,
8676
+ container.id,
8677
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8678
+ );
8679
+ container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
8680
+ managedContainer = container;
8681
+ }
8682
+ transitionPhase = "process-start";
6860
8683
  runManagedPostStart({
6861
8684
  plan: managedPostStart,
6862
8685
  container,
6863
- quiet: options.quiet
8686
+ quiet: options.quiet,
8687
+ profile: runtime.profile,
8688
+ ...managedPlan ? { processes: desiredProcesses } : {}
6864
8689
  });
8690
+ if (managedPlan && managedComposeProject) {
8691
+ const processRegistry = runtime.config.managedRuntime?.processes ?? [];
8692
+ const desiredProcessSet = new Set(desiredProcesses);
8693
+ for (const process2 of processRegistry) {
8694
+ if (desiredProcessSet.has(process2)) continue;
8695
+ runManagedProcessAction({
8696
+ container,
8697
+ name: process2,
8698
+ action: "stop",
8699
+ quiet: options.quiet
8700
+ });
8701
+ }
8702
+ for (const process2 of desiredProcesses) {
8703
+ const status = runManagedProcessAction({
8704
+ container,
8705
+ name: process2,
8706
+ action: "status",
8707
+ quiet: options.quiet
8708
+ });
8709
+ if (status !== "running") {
8710
+ throw new Error(`Managed process '${process2}' is not running (${status}).`);
8711
+ }
8712
+ }
8713
+ transitionPhase = "service-stop";
8714
+ stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
8715
+ assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
8716
+ await waitForManagedServices(
8717
+ managedPlan,
8718
+ repoPath,
8719
+ managedComposeProject,
8720
+ managedPlan.desiredServices,
8721
+ container.id,
8722
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8723
+ );
8724
+ }
8725
+ transitionPhase = "route-publication";
6865
8726
  const publication = await replacePublishedProxyRoutes(
6866
8727
  repoPath,
6867
8728
  runtime.config,
6868
8729
  target.workspace
6869
8730
  );
8731
+ candidateRoutesPublished = true;
6870
8732
  try {
6871
8733
  await waitForHttpRoutes(
6872
8734
  repoPath,
@@ -6874,6 +8736,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6874
8736
  options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
6875
8737
  );
6876
8738
  } catch (error) {
8739
+ if (managedPlan) {
8740
+ throw error;
8741
+ }
6877
8742
  replaceHostRoutesForRepo(repoPath, []);
6878
8743
  if (!target.hadExactDevpod || recreated) {
6879
8744
  throw error;
@@ -6882,7 +8747,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6882
8747
  runManagedPostStart({
6883
8748
  plan: managedPostStart,
6884
8749
  container: recoveredContainer,
6885
- quiet: options.quiet
8750
+ quiet: options.quiet,
8751
+ profile: runtime.profile
6886
8752
  });
6887
8753
  recreated = true;
6888
8754
  replaceHostRoutesForRepo(repoPath, publication.routes);
@@ -6903,16 +8769,216 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6903
8769
  if (!devpodId) {
6904
8770
  throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
6905
8771
  }
8772
+ if (managedPlan && managedComposeProject) {
8773
+ writeManagedRuntimeState({
8774
+ version: 1,
8775
+ repoPath,
8776
+ ...target.workspace !== void 0 ? { workspace: target.workspace } : {},
8777
+ devpodId,
8778
+ composeProject: managedComposeProject,
8779
+ profile: runtime.profile,
8780
+ desired: {
8781
+ apps: apps.map((app) => app.name).sort(),
8782
+ services: [...managedPlan.desiredProfileServices].sort(),
8783
+ processes: [...desiredProcesses].sort()
8784
+ },
8785
+ sourceConfigSha256: managedPlan.sourceConfigSha256,
8786
+ effectiveConfigSha256: managedPlan.effectiveConfigSha256,
8787
+ status: "ready",
8788
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8789
+ });
8790
+ }
8791
+ const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
8792
+ repoPath,
8793
+ workspace: target.workspace,
8794
+ config: runtime.config,
8795
+ profile: runtime.profile,
8796
+ resolvedProfile: runtime.resolvedProfile
8797
+ }) : void 0;
6906
8798
  return {
6907
8799
  kind: target.kind,
6908
8800
  repoPath,
6909
8801
  workspace: target.workspace,
8802
+ profile: runtime.profile,
6910
8803
  devpodId,
6911
8804
  urls,
6912
8805
  recreated,
6913
- tlsRefreshed: publication.tlsRefreshed
8806
+ tlsRefreshed: publication.tlsRefreshed,
8807
+ ...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
6914
8808
  };
6915
8809
  } catch (error) {
8810
+ if (managedPlan && managedContainer && managedComposeProject) {
8811
+ const rollbackErrors = [];
8812
+ const failedPhase = transitionPhase;
8813
+ transitionPhase = "rollback";
8814
+ const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
8815
+ const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
8816
+ const previousServiceSet = new Set(previousServices);
8817
+ const previousProcessSet = new Set(previousProcesses);
8818
+ try {
8819
+ if (managedConfigWritten) {
8820
+ if (previousManagedState && managedRuntimeConfig) {
8821
+ restorePreviousManagedConfig({
8822
+ repoPath,
8823
+ config: managedRuntimeConfig,
8824
+ linked,
8825
+ previousState: previousManagedState
8826
+ });
8827
+ } else {
8828
+ removeManagedDevcontainerConfig(managedPlan);
8829
+ }
8830
+ }
8831
+ } catch (rollbackError) {
8832
+ rollbackErrors.push(
8833
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8834
+ );
8835
+ }
8836
+ try {
8837
+ const previousAllServices = [
8838
+ managedPlan.primaryService,
8839
+ ...managedPlan.baseServices,
8840
+ ...previousServices
8841
+ ];
8842
+ startExactManagedServices({
8843
+ plan: managedPlan,
8844
+ composeProject: managedComposeProject,
8845
+ services: previousAllServices,
8846
+ quiet: options.quiet,
8847
+ workspace: managedWorkspaceEnv
8848
+ });
8849
+ await waitForManagedServices(
8850
+ managedPlan,
8851
+ repoPath,
8852
+ managedComposeProject,
8853
+ previousAllServices,
8854
+ managedContainer.id,
8855
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8856
+ );
8857
+ } catch (rollbackError) {
8858
+ rollbackErrors.push(
8859
+ `services: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8860
+ );
8861
+ }
8862
+ try {
8863
+ if (!managedPostStartPlan) {
8864
+ throw new Error("Managed post-start plan disappeared during rollback.");
8865
+ }
8866
+ runManagedPostStart({
8867
+ plan: managedPostStartPlan,
8868
+ container: managedContainer,
8869
+ quiet: options.quiet,
8870
+ profile: previousManagedState?.profile ?? "full",
8871
+ processes: previousProcesses
8872
+ });
8873
+ for (const process2 of managedProcessRegistry) {
8874
+ if (previousProcessSet.has(process2)) continue;
8875
+ runManagedProcessAction({
8876
+ container: managedContainer,
8877
+ name: process2,
8878
+ action: "stop",
8879
+ quiet: options.quiet
8880
+ });
8881
+ }
8882
+ } catch (rollbackError) {
8883
+ rollbackErrors.push(
8884
+ `processes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8885
+ );
8886
+ }
8887
+ try {
8888
+ const containers = inspectWorkspaceContainers();
8889
+ for (const service of managedPlan.profileServices) {
8890
+ if (previousServiceSet.has(service)) continue;
8891
+ const matches = exactWorkspaceServiceContainers(
8892
+ containers,
8893
+ repoPath,
8894
+ managedComposeProject,
8895
+ service,
8896
+ managedPlan.composeFiles
8897
+ );
8898
+ if (matches.length > 1) {
8899
+ throw new Error(`service '${service}' has multiple exact containers`);
8900
+ }
8901
+ if (matches[0]?.state.Running) stopExactManagedService(matches[0].id, service);
8902
+ }
8903
+ assertDroppedManagedServicesStopped(
8904
+ {
8905
+ ...managedPlan,
8906
+ desiredProfileServices: previousServices,
8907
+ desiredServices: [
8908
+ managedPlan.primaryService,
8909
+ ...managedPlan.baseServices,
8910
+ ...previousServices
8911
+ ]
8912
+ },
8913
+ repoPath,
8914
+ managedComposeProject
8915
+ );
8916
+ } catch (rollbackError) {
8917
+ rollbackErrors.push(
8918
+ `service cleanup: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8919
+ );
8920
+ }
8921
+ if (candidateRoutesPublished) {
8922
+ try {
8923
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8924
+ } catch (rollbackError) {
8925
+ rollbackErrors.push(
8926
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8927
+ );
8928
+ }
8929
+ }
8930
+ if (rollbackErrors.length > 0 && previousManagedState) {
8931
+ try {
8932
+ markManagedRuntimeDegraded(previousManagedState, failedPhase);
8933
+ } catch (stateError) {
8934
+ rollbackErrors.push(
8935
+ `state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
8936
+ );
8937
+ }
8938
+ }
8939
+ const original = error instanceof Error ? error.message : String(error);
8940
+ const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : " Candidate runtime was rolled back.";
8941
+ throw new Error(`${original}${suffix}`);
8942
+ }
8943
+ if (managedPlan) {
8944
+ const original = error instanceof Error ? error.message : String(error);
8945
+ const rollbackErrors = [];
8946
+ if (managedConfigWritten) {
8947
+ try {
8948
+ if (previousManagedState && managedRuntimeConfig) {
8949
+ restorePreviousManagedConfig({
8950
+ repoPath,
8951
+ config: managedRuntimeConfig,
8952
+ linked,
8953
+ previousState: previousManagedState
8954
+ });
8955
+ } else {
8956
+ removeManagedDevcontainerConfig(managedPlan);
8957
+ }
8958
+ } catch (rollbackError) {
8959
+ rollbackErrors.push(
8960
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8961
+ );
8962
+ }
8963
+ }
8964
+ if (candidateRoutesPublished) {
8965
+ try {
8966
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8967
+ } catch (rollbackError) {
8968
+ rollbackErrors.push(
8969
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8970
+ );
8971
+ }
8972
+ }
8973
+ if (rollbackErrors.length > 0) {
8974
+ throw new Error(
8975
+ `${original} Rollback left degraded drift: ${rollbackErrors.join("; ")}.`
8976
+ );
8977
+ }
8978
+ throw new Error(
8979
+ `${original} Managed runtime transition did not reach a rollback boundary.`
8980
+ );
8981
+ }
6916
8982
  if (environmentStarted) {
6917
8983
  try {
6918
8984
  replaceHostRoutesForRepo(repoPath, []);
@@ -6926,19 +8992,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6926
8992
  }
6927
8993
  });
6928
8994
  }
6929
- var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
8995
+ var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
6930
8996
  var init_workspace_ensure = __esm({
6931
8997
  "src/core/workspace-ensure.ts"() {
6932
8998
  "use strict";
6933
- import_node_child_process14 = require("child_process");
6934
- import_node_fs18 = __toESM(require("fs"));
6935
- import_node_path18 = __toESM(require("path"));
8999
+ import_node_child_process15 = require("child_process");
9000
+ import_node_fs20 = __toESM(require("fs"));
9001
+ import_node_path20 = __toESM(require("path"));
9002
+ init_devcontainer_profile();
6936
9003
  init_devpod_environment();
6937
9004
  init_devpod_mutation();
6938
9005
  init_devpod_workspaces();
6939
9006
  init_host_routes();
6940
9007
  init_http_route_probe();
6941
9008
  init_managed_post_start();
9009
+ init_managed_runtime_state();
9010
+ init_managed_runtime_status();
6942
9011
  init_repo_config();
6943
9012
  init_route_publication();
6944
9013
  init_router();
@@ -6978,14 +9047,15 @@ async function runEnsureCommand(options) {
6978
9047
  const repoPath = resolveGitCheckoutPath(options.path);
6979
9048
  const result = await workspaceEnsure(repoPath, {
6980
9049
  open: options.open,
6981
- quiet: Boolean(options.json)
9050
+ quiet: Boolean(options.json),
9051
+ profile: options.profile
6982
9052
  });
6983
9053
  if (options.json) {
6984
9054
  process.stdout.write(`${JSON.stringify(result, null, 2)}
6985
9055
  `);
6986
9056
  return;
6987
9057
  }
6988
- const label = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
9058
+ const label = result.kind === "primary" ? `Primary checkout [profile: ${result.profile}]` : `Workspace '${result.workspace}' [profile: ${result.profile}]`;
6989
9059
  const routes = result.urls.map((url) => ` ${url}`).join("\n");
6990
9060
  process.stdout.write(`${label} is ready (${result.devpodId}).
6991
9061
  ${routes}${routes ? "\n" : ""}`);
@@ -7008,20 +9078,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
7008
9078
  );
7009
9079
  }
7010
9080
  function defaultWorktreePath(mainRepo, ws) {
7011
- return import_node_path19.default.join(mainRepo, "trees", ws);
9081
+ return import_node_path21.default.join(mainRepo, "trees", ws);
7012
9082
  }
7013
9083
  function assertDefaultWorktreeRootIgnored(mainRepo) {
7014
- const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9084
+ const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
7015
9085
  encoding: "utf-8"
7016
9086
  });
7017
9087
  if (ignored.status !== 0) {
7018
9088
  throw new Error(
7019
- `Default worktree root '${import_node_path19.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path19.default.join(mainRepo, ".gitignore")}' or use --path.`
9089
+ `Default worktree root '${import_node_path21.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path21.default.join(mainRepo, ".gitignore")}' or use --path.`
7020
9090
  );
7021
9091
  }
7022
9092
  }
7023
9093
  function legacyDefaultWorktreePath(mainRepo, ws) {
7024
- return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
9094
+ return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
7025
9095
  }
7026
9096
  function teardownFallbackPath(mainRepo, workspace) {
7027
9097
  const candidates = [
@@ -7133,13 +9203,13 @@ function assertFullDownPreflight(mainRepo, target) {
7133
9203
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
7134
9204
  throw new Error("Refusing to remove the primary Git checkout.");
7135
9205
  }
7136
- if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
9206
+ if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
7137
9207
  if (target.worktree.locked) {
7138
9208
  throw new Error(
7139
9209
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
7140
9210
  );
7141
9211
  }
7142
- const status = (0, import_node_child_process15.spawnSync)(
9212
+ const status = (0, import_node_child_process16.spawnSync)(
7143
9213
  "git",
7144
9214
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
7145
9215
  { encoding: "utf-8" }
@@ -7160,8 +9230,8 @@ async function workspaceUp(branch, opts = {}) {
7160
9230
  if (!ws) {
7161
9231
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
7162
9232
  }
7163
- const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
7164
- if (import_node_fs19.default.existsSync(worktreePath)) {
9233
+ const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9234
+ if (import_node_fs21.default.existsSync(worktreePath)) {
7165
9235
  const registered = listGitWorktrees(mainRepo).find(
7166
9236
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
7167
9237
  );
@@ -7179,11 +9249,11 @@ async function workspaceUp(branch, opts = {}) {
7179
9249
  if (!opts.path) {
7180
9250
  assertDefaultWorktreeRootIgnored(mainRepo);
7181
9251
  }
7182
- const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9252
+ const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
7183
9253
  encoding: "utf-8"
7184
9254
  });
7185
9255
  if (add.status !== 0) {
7186
- const addNew = (0, import_node_child_process15.spawnSync)(
9256
+ const addNew = (0, import_node_child_process16.spawnSync)(
7187
9257
  "git",
7188
9258
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
7189
9259
  {
@@ -7288,8 +9358,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7288
9358
  opts.quiet
7289
9359
  );
7290
9360
  if (removeWorktree) {
7291
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath)) {
7292
- const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
9361
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
9362
+ const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
7293
9363
  encoding: "utf-8"
7294
9364
  });
7295
9365
  if (rm.status !== 0) {
@@ -7307,7 +9377,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7307
9377
  }
7308
9378
  return result;
7309
9379
  };
7310
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9380
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
7311
9381
  }
7312
9382
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
7313
9383
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -7342,13 +9412,13 @@ async function workspaceStop(target, opts = {}) {
7342
9412
  async function workspaceDown(target, opts = {}) {
7343
9413
  return runWorkspaceLifecycle("down", target, opts);
7344
9414
  }
7345
- var import_node_child_process15, import_node_fs19, import_node_path19;
9415
+ var import_node_child_process16, import_node_fs21, import_node_path21;
7346
9416
  var init_workspace_lifecycle = __esm({
7347
9417
  "src/core/workspace-lifecycle.ts"() {
7348
9418
  "use strict";
7349
- import_node_child_process15 = require("child_process");
7350
- import_node_fs19 = __toESM(require("fs"));
7351
- import_node_path19 = __toESM(require("path"));
9419
+ import_node_child_process16 = require("child_process");
9420
+ import_node_fs21 = __toESM(require("fs"));
9421
+ import_node_path21 = __toESM(require("path"));
7352
9422
  init_devpod_mutation();
7353
9423
  init_devpod_workspaces();
7354
9424
  init_repo_config();
@@ -7475,7 +9545,7 @@ function ensureGuidance(repoPath) {
7475
9545
  return `Run 'devrouter ensure ${repoPath}' first.`;
7476
9546
  }
7477
9547
  function assertRunningDevpod(devpodId, repoPath) {
7478
- const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
9548
+ const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
7479
9549
  encoding: "utf-8"
7480
9550
  });
7481
9551
  if (result.status !== 0) {
@@ -7513,7 +9583,7 @@ async function devpodExec(repoPath, command) {
7513
9583
  }
7514
9584
  assertRunningDevpod(devpod.id, repoPath);
7515
9585
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
7516
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
9586
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto7.randomUUID)()}__:`;
7517
9587
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
7518
9588
  const literalCommand = command.map(quotePosixArg).join(" ");
7519
9589
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -7531,7 +9601,7 @@ async function devpodExec(repoPath, command) {
7531
9601
  wrappedCommand
7532
9602
  ];
7533
9603
  return new Promise((resolve, reject) => {
7534
- const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9604
+ const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
7535
9605
  let pending = Buffer.alloc(0);
7536
9606
  let statusBytes = Buffer.alloc(0);
7537
9607
  let readingStatus = false;
@@ -7590,12 +9660,12 @@ async function devpodExec(repoPath, command) {
7590
9660
  });
7591
9661
  });
7592
9662
  }
7593
- var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
9663
+ var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
7594
9664
  var init_devpod_exec = __esm({
7595
9665
  "src/core/devpod-exec.ts"() {
7596
9666
  "use strict";
7597
- import_node_child_process16 = require("child_process");
7598
- import_node_crypto5 = require("crypto");
9667
+ import_node_child_process17 = require("child_process");
9668
+ import_node_crypto7 = require("crypto");
7599
9669
  init_devpod_environment();
7600
9670
  init_devpod_workspaces();
7601
9671
  init_workspace();
@@ -7779,7 +9849,7 @@ async function runOpenCommand(name) {
7779
9849
  );
7780
9850
  return;
7781
9851
  }
7782
- const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
9852
+ const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
7783
9853
  if (result.status !== 0) {
7784
9854
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7785
9855
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -7787,11 +9857,11 @@ async function runOpenCommand(name) {
7787
9857
  process.stdout.write(`Opened ${url}
7788
9858
  `);
7789
9859
  }
7790
- var import_node_child_process17;
9860
+ var import_node_child_process18;
7791
9861
  var init_open = __esm({
7792
9862
  "src/commands/open.ts"() {
7793
9863
  "use strict";
7794
- import_node_child_process17 = require("child_process");
9864
+ import_node_child_process18 = require("child_process");
7795
9865
  init_docker();
7796
9866
  init_host_routes();
7797
9867
  init_repo_config();
@@ -7814,7 +9884,7 @@ async function runLogsCommand(options) {
7814
9884
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
7815
9885
  if (options.follow) {
7816
9886
  args.splice(1, 0, "-f");
7817
- const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
9887
+ const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
7818
9888
  const onSignal = () => {
7819
9889
  child.kill("SIGTERM");
7820
9890
  };
@@ -7828,17 +9898,17 @@ async function runLogsCommand(options) {
7828
9898
  });
7829
9899
  });
7830
9900
  } else {
7831
- const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
9901
+ const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
7832
9902
  if (result.status !== 0) {
7833
9903
  throw new Error("Failed to retrieve router logs.");
7834
9904
  }
7835
9905
  }
7836
9906
  }
7837
- var import_node_child_process18;
9907
+ var import_node_child_process19;
7838
9908
  var init_logs = __esm({
7839
9909
  "src/commands/logs.ts"() {
7840
9910
  "use strict";
7841
- import_node_child_process18 = require("child_process");
9911
+ import_node_child_process19 = require("child_process");
7842
9912
  init_docker();
7843
9913
  init_router();
7844
9914
  }
@@ -7878,17 +9948,17 @@ function asRecord3(value) {
7878
9948
  return value;
7879
9949
  }
7880
9950
  function readJson(filePath) {
7881
- if (!import_node_fs20.default.existsSync(filePath)) {
9951
+ if (!import_node_fs22.default.existsSync(filePath)) {
7882
9952
  return void 0;
7883
9953
  }
7884
9954
  try {
7885
- return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
9955
+ return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
7886
9956
  } catch {
7887
9957
  return void 0;
7888
9958
  }
7889
9959
  }
7890
9960
  function relative(repoPath, filePath) {
7891
- return import_node_path20.default.relative(repoPath, filePath) || ".";
9961
+ return import_node_path22.default.relative(repoPath, filePath) || ".";
7892
9962
  }
7893
9963
  function redactEnvAssignments(value) {
7894
9964
  return value.replace(
@@ -7929,7 +9999,7 @@ function inspectPackageManager(repoPath, pkg) {
7929
9999
  ["bun.lock", "bun"]
7930
10000
  ];
7931
10001
  for (const [fileName, name] of lockfiles) {
7932
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10002
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
7933
10003
  return { name, source: fileName };
7934
10004
  }
7935
10005
  }
@@ -7944,9 +10014,9 @@ function inspectNode(repoPath, pkg) {
7944
10014
  if (typeof engines?.node === "string") {
7945
10015
  return { version: engines.node, source: "package.json:engines.node" };
7946
10016
  }
7947
- const nvmrc = import_node_path20.default.join(repoPath, ".nvmrc");
7948
- if (import_node_fs20.default.existsSync(nvmrc)) {
7949
- const version = import_node_fs20.default.readFileSync(nvmrc, "utf-8").trim();
10017
+ const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
10018
+ if (import_node_fs22.default.existsSync(nvmrc)) {
10019
+ const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
7950
10020
  return { version, source: ".nvmrc" };
7951
10021
  }
7952
10022
  return void 0;
@@ -8007,7 +10077,7 @@ function configuredComposeFiles(repoPath) {
8007
10077
  const files = config.apps.filter(
8008
10078
  (app) => app.runtime === "docker"
8009
10079
  ).flatMap((app) => app.docker.composeFiles).filter(
8010
- (fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
10080
+ (fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
8011
10081
  );
8012
10082
  return Array.from(new Set(files));
8013
10083
  } catch {
@@ -8025,10 +10095,10 @@ function composeFiles(repoPath) {
8025
10095
  ...configuredComposeFiles(repoPath)
8026
10096
  ];
8027
10097
  return Array.from(new Set(candidates)).filter(
8028
- (fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
10098
+ (fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
8029
10099
  );
8030
10100
  }
8031
- function stringArray(value) {
10101
+ function stringArray2(value) {
8032
10102
  if (!Array.isArray(value)) {
8033
10103
  return [];
8034
10104
  }
@@ -8063,7 +10133,7 @@ function inspectServices(repoPath) {
8063
10133
  const services = [];
8064
10134
  for (const fileName of composeFiles(repoPath)) {
8065
10135
  try {
8066
- const parsed = import_yaml5.default.parse(import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8"));
10136
+ const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
8067
10137
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
8068
10138
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
8069
10139
  const service = asRecord3(value);
@@ -8077,7 +10147,7 @@ function inspectServices(repoPath) {
8077
10147
  kind: kind.kind,
8078
10148
  source: fileName,
8079
10149
  image,
8080
- ports: stringArray(service.ports),
10150
+ ports: stringArray2(service.ports),
8081
10151
  hasHealthcheck: service.healthcheck !== void 0,
8082
10152
  envNames: envNames(service.environment),
8083
10153
  confidence: kind.confidence
@@ -8098,8 +10168,8 @@ function inspectServices(repoPath) {
8098
10168
  return services;
8099
10169
  }
8100
10170
  function inspectEnvFiles(repoPath) {
8101
- const files = import_node_fs20.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
8102
- const content = import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8");
10171
+ const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10172
+ const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
8103
10173
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
8104
10174
  return { path: fileName, names };
8105
10175
  });
@@ -8113,18 +10183,18 @@ function inspectEnvFiles(repoPath) {
8113
10183
  };
8114
10184
  }
8115
10185
  function inspectDevcontainer(repoPath) {
8116
- const dir = import_node_path20.default.join(repoPath, ".devcontainer");
8117
- if (!import_node_fs20.default.existsSync(dir)) {
10186
+ const dir = import_node_path22.default.join(repoPath, ".devcontainer");
10187
+ if (!import_node_fs22.default.existsSync(dir)) {
8118
10188
  return { exists: false, files: [] };
8119
10189
  }
8120
10190
  return {
8121
10191
  exists: true,
8122
- files: import_node_fs20.default.readdirSync(dir).filter((fileName) => import_node_fs20.default.statSync(import_node_path20.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
10192
+ files: import_node_fs22.default.readdirSync(dir).filter((fileName) => import_node_fs22.default.statSync(import_node_path22.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
8123
10193
  };
8124
10194
  }
8125
10195
  function inspectDevrouter(repoPath) {
8126
10196
  const configPath = getRepoConfigPath(repoPath);
8127
- if (!import_node_fs20.default.existsSync(configPath)) {
10197
+ if (!import_node_fs22.default.existsSync(configPath)) {
8128
10198
  return {
8129
10199
  exists: false,
8130
10200
  configPath,
@@ -8168,15 +10238,15 @@ function inspectAgentGuidance(repoPath) {
8168
10238
  ["AGENTS.md", "agents"],
8169
10239
  ["CLAUDE.md", "claude"]
8170
10240
  ]) {
8171
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10241
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
8172
10242
  results.push({ path: fileName, kind });
8173
10243
  }
8174
10244
  }
8175
- const skillsDir = import_node_path20.default.join(repoPath, ".agents", "skills");
8176
- if (import_node_fs20.default.existsSync(skillsDir)) {
8177
- for (const name of import_node_fs20.default.readdirSync(skillsDir).sort()) {
8178
- const skillPath = import_node_path20.default.join(skillsDir, name, "SKILL.md");
8179
- if (import_node_fs20.default.existsSync(skillPath)) {
10245
+ const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
10246
+ if (import_node_fs22.default.existsSync(skillsDir)) {
10247
+ for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
10248
+ const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
10249
+ if (import_node_fs22.default.existsSync(skillPath)) {
8180
10250
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
8181
10251
  }
8182
10252
  }
@@ -8219,7 +10289,7 @@ function buildIssues(report) {
8219
10289
  }
8220
10290
  function inspectRepo(options = {}) {
8221
10291
  const repoPath = resolveRepoPath(options.repo);
8222
- const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
10292
+ const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
8223
10293
  const scripts = inspectScripts(pkg);
8224
10294
  const reportWithoutIssues = {
8225
10295
  repoPath,
@@ -8238,13 +10308,13 @@ function inspectRepo(options = {}) {
8238
10308
  issues: buildIssues(reportWithoutIssues)
8239
10309
  };
8240
10310
  }
8241
- var import_node_fs20, import_node_path20, import_yaml5;
10311
+ var import_node_fs22, import_node_path22, import_yaml6;
8242
10312
  var init_repo_inspect = __esm({
8243
10313
  "src/core/repo-inspect.ts"() {
8244
10314
  "use strict";
8245
- import_node_fs20 = __toESM(require("fs"));
8246
- import_node_path20 = __toESM(require("path"));
8247
- import_yaml5 = __toESM(require("yaml"));
10315
+ import_node_fs22 = __toESM(require("fs"));
10316
+ import_node_path22 = __toESM(require("path"));
10317
+ import_yaml6 = __toESM(require("yaml"));
8248
10318
  init_repo_config();
8249
10319
  }
8250
10320
  });
@@ -8348,7 +10418,7 @@ function requiredFileChecks(repoPath) {
8348
10418
  ".devcontainer/docker-compose.yml",
8349
10419
  ".devrouter.yml"
8350
10420
  ];
8351
- const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
10421
+ const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
8352
10422
  return {
8353
10423
  id: "repo.devcontainer.verify-files",
8354
10424
  level: missing.length === 0 ? "ok" : "error",
@@ -8567,12 +10637,12 @@ async function verifyDevcontainer(options = {}) {
8567
10637
  nextSteps: collectNextSteps3(checks)
8568
10638
  };
8569
10639
  }
8570
- var import_node_fs21, import_node_path21;
10640
+ var import_node_fs23, import_node_path23;
8571
10641
  var init_devcontainer_verify = __esm({
8572
10642
  "src/core/devcontainer-verify.ts"() {
8573
10643
  "use strict";
8574
- import_node_fs21 = __toESM(require("fs"));
8575
- import_node_path21 = __toESM(require("path"));
10644
+ import_node_fs23 = __toESM(require("fs"));
10645
+ import_node_path23 = __toESM(require("path"));
8576
10646
  init_capabilities();
8577
10647
  init_doctor();
8578
10648
  init_host_routes();
@@ -8868,7 +10938,7 @@ function packageManagerIssues(repo) {
8868
10938
  }
8869
10939
  function plannedFiles(repoPath, version) {
8870
10940
  const repo = inspectRepo({ repo: repoPath });
8871
- const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
10941
+ const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
8872
10942
  const nodeMajor = majorVersion(repo.node?.version, "24");
8873
10943
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
8874
10944
  const port = inferPort2(repo);
@@ -8913,8 +10983,8 @@ function plannedFiles(repoPath, version) {
8913
10983
  };
8914
10984
  }
8915
10985
  function classifyFile(repoPath, file) {
8916
- const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
8917
- if (!import_node_fs22.default.existsSync(absolutePath)) {
10986
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
10987
+ if (!import_node_fs24.default.existsSync(absolutePath)) {
8918
10988
  return {
8919
10989
  path: file.relativePath,
8920
10990
  action: "create",
@@ -8922,7 +10992,7 @@ function classifyFile(repoPath, file) {
8922
10992
  bytes: Buffer.byteLength(file.content)
8923
10993
  };
8924
10994
  }
8925
- const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
10995
+ const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
8926
10996
  if (!current.includes(MANAGED_MARKER2)) {
8927
10997
  return {
8928
10998
  path: file.relativePath,
@@ -8979,11 +11049,11 @@ function buildPlan(repoPath, dryRun, version) {
8979
11049
  };
8980
11050
  }
8981
11051
  function writeFile(repoPath, file) {
8982
- const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
8983
- import_node_fs22.default.mkdirSync(import_node_path22.default.dirname(absolutePath), { recursive: true });
8984
- import_node_fs22.default.writeFileSync(absolutePath, file.content, "utf-8");
11052
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11053
+ import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
11054
+ import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
8985
11055
  if (file.executable) {
8986
- import_node_fs22.default.chmodSync(absolutePath, 493);
11056
+ import_node_fs24.default.chmodSync(absolutePath, 493);
8987
11057
  }
8988
11058
  }
8989
11059
  function writeDevcontainer(options = {}) {
@@ -9021,12 +11091,12 @@ function writeDevcontainer(options = {}) {
9021
11091
  nextSteps: postWriteNextSteps(repoPath)
9022
11092
  };
9023
11093
  }
9024
- var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11094
+ var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
9025
11095
  var init_devcontainer_write = __esm({
9026
11096
  "src/core/devcontainer-write.ts"() {
9027
11097
  "use strict";
9028
- import_node_fs22 = __toESM(require("fs"));
9029
- import_node_path22 = __toESM(require("path"));
11098
+ import_node_fs24 = __toESM(require("fs"));
11099
+ import_node_path24 = __toESM(require("path"));
9030
11100
  init_repo_config();
9031
11101
  init_repo_inspect();
9032
11102
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -9417,7 +11487,7 @@ function sanitizeRouterId(value) {
9417
11487
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
9418
11488
  }
9419
11489
  function repoHash(repoPath) {
9420
- return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
11490
+ return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
9421
11491
  }
9422
11492
  function asDockerApp(app) {
9423
11493
  return app.runtime === "docker";
@@ -9496,11 +11566,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
9496
11566
  if (dockerApps.length === 0) {
9497
11567
  throw new Error("No docker apps selected to prepare compose overlay.");
9498
11568
  }
9499
- const cachePath = import_node_path23.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
9500
- import_node_fs23.default.mkdirSync(cachePath, { recursive: true });
9501
- const overlayPath = import_node_path23.default.join(cachePath, "compose.devrouter.yml");
11569
+ const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
11570
+ import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
11571
+ const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
9502
11572
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
9503
- import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
11573
+ import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
9504
11574
  return {
9505
11575
  overlayPath,
9506
11576
  composeFiles: ensureComposeFiles(dockerApps),
@@ -9514,7 +11584,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
9514
11584
  fileArgs.push("-f", resolved);
9515
11585
  }
9516
11586
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
9517
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11587
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9518
11588
  encoding: "utf-8",
9519
11589
  cwd: repoPath
9520
11590
  });
@@ -9542,7 +11612,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
9542
11612
  "--services",
9543
11613
  ...services
9544
11614
  ];
9545
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11615
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9546
11616
  encoding: "utf-8",
9547
11617
  cwd: repoPath
9548
11618
  });
@@ -9568,7 +11638,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
9568
11638
  fileArgs.push("-f", resolved);
9569
11639
  }
9570
11640
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
9571
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11641
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9572
11642
  encoding: "utf-8",
9573
11643
  cwd: repoPath
9574
11644
  });
@@ -9594,7 +11664,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
9594
11664
  String(tail),
9595
11665
  ...services
9596
11666
  ];
9597
- (0, import_node_child_process19.spawnSync)("docker", args, {
11667
+ (0, import_node_child_process20.spawnSync)("docker", args, {
9598
11668
  stdio: "inherit",
9599
11669
  cwd: repoPath
9600
11670
  });
@@ -9606,7 +11676,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9606
11676
  fileArgs.push("-f", resolved);
9607
11677
  }
9608
11678
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
9609
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11679
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9610
11680
  encoding: "utf-8",
9611
11681
  cwd: repoPath
9612
11682
  });
@@ -9620,15 +11690,15 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9620
11690
  const port = Number(match[1]);
9621
11691
  return Number.isInteger(port) && port > 0 ? port : void 0;
9622
11692
  }
9623
- var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
11693
+ var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
9624
11694
  var init_docker_run = __esm({
9625
11695
  "src/core/docker-run.ts"() {
9626
11696
  "use strict";
9627
- import_node_child_process19 = require("child_process");
9628
- import_node_crypto6 = require("crypto");
9629
- import_node_fs23 = __toESM(require("fs"));
9630
- import_node_path23 = __toESM(require("path"));
9631
- import_yaml6 = __toESM(require("yaml"));
11697
+ import_node_child_process20 = require("child_process");
11698
+ import_node_crypto8 = require("crypto");
11699
+ import_node_fs25 = __toESM(require("fs"));
11700
+ import_node_path25 = __toESM(require("path"));
11701
+ import_yaml7 = __toESM(require("yaml"));
9632
11702
  init_docker_error_guidance();
9633
11703
  init_paths();
9634
11704
  init_router();
@@ -9717,7 +11787,7 @@ function isProcessRunning(pid) {
9717
11787
  }
9718
11788
  }
9719
11789
  function readProcessTree(rootPid) {
9720
- const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
11790
+ const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
9721
11791
  if (result.status !== 0) {
9722
11792
  return [rootPid];
9723
11793
  }
@@ -9828,12 +11898,12 @@ function detectListeningPorts(pids) {
9828
11898
  if (pids.length === 0) {
9829
11899
  return [];
9830
11900
  }
9831
- const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
11901
+ const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
9832
11902
  encoding: "utf-8"
9833
11903
  });
9834
11904
  if (result.error && result.error.code === "ENOENT") {
9835
11905
  if (process.platform === "linux") {
9836
- const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
11906
+ const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
9837
11907
  if (ssResult.status === 0 && ssResult.stdout) {
9838
11908
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
9839
11909
  }
@@ -9892,7 +11962,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
9892
11962
  app.hostRun.command,
9893
11963
  true
9894
11964
  ) : app.hostRun.command;
9895
- const child = (0, import_node_child_process20.spawn)(spawnCommand, {
11965
+ const child = (0, import_node_child_process21.spawn)(spawnCommand, {
9896
11966
  cwd: commandCwd,
9897
11967
  stdio: "inherit",
9898
11968
  shell: true,
@@ -10260,7 +12330,7 @@ async function execWithAppEnv(options) {
10260
12330
  options.command[0],
10261
12331
  true
10262
12332
  );
10263
- child = (0, import_node_child_process20.spawn)(wrapped, {
12333
+ child = (0, import_node_child_process21.spawn)(wrapped, {
10264
12334
  cwd: deps.repoPath,
10265
12335
  stdio: "inherit",
10266
12336
  shell: true,
@@ -10274,7 +12344,7 @@ async function execWithAppEnv(options) {
10274
12344
  false
10275
12345
  );
10276
12346
  const [cmd, ...wrappedArgs] = wrapped;
10277
- child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
12347
+ child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
10278
12348
  cwd: deps.repoPath,
10279
12349
  stdio: "inherit",
10280
12350
  shell: false,
@@ -10282,7 +12352,7 @@ async function execWithAppEnv(options) {
10282
12352
  });
10283
12353
  }
10284
12354
  } else if (options.shell) {
10285
- child = (0, import_node_child_process20.spawn)(options.command[0], {
12355
+ child = (0, import_node_child_process21.spawn)(options.command[0], {
10286
12356
  cwd: deps.repoPath,
10287
12357
  stdio: "inherit",
10288
12358
  shell: true,
@@ -10290,7 +12360,7 @@ async function execWithAppEnv(options) {
10290
12360
  });
10291
12361
  } else {
10292
12362
  const [command, ...args] = options.command;
10293
- child = (0, import_node_child_process20.spawn)(command, args, {
12363
+ child = (0, import_node_child_process21.spawn)(command, args, {
10294
12364
  cwd: deps.repoPath,
10295
12365
  stdio: "inherit",
10296
12366
  shell: false,
@@ -10311,11 +12381,11 @@ async function execWithAppEnv(options) {
10311
12381
  deps.stopDeps();
10312
12382
  }
10313
12383
  }
10314
- var import_node_child_process20, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
12384
+ var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
10315
12385
  var init_app_run = __esm({
10316
12386
  "src/core/app-run.ts"() {
10317
12387
  "use strict";
10318
- import_node_child_process20 = require("child_process");
12388
+ import_node_child_process21 = require("child_process");
10319
12389
  import_node_net = __toESM(require("net"));
10320
12390
  import_node_process = require("process");
10321
12391
  import_promises = require("readline/promises");
@@ -10449,13 +12519,13 @@ function measureWorktreeConsumption(worktreePath, options) {
10449
12519
  const startedAt = Date.now();
10450
12520
  let rootStat;
10451
12521
  try {
10452
- rootStat = import_node_fs24.default.lstatSync(worktreePath);
12522
+ rootStat = import_node_fs26.default.lstatSync(worktreePath);
10453
12523
  } catch (error) {
10454
12524
  return { status: "unknown", reason: describeError(error, worktreePath) };
10455
12525
  }
10456
12526
  let rootEntries;
10457
12527
  try {
10458
- rootEntries = import_node_fs24.default.readdirSync(worktreePath, { withFileTypes: true });
12528
+ rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
10459
12529
  } catch (error) {
10460
12530
  return { status: "unknown", reason: describeError(error, worktreePath) };
10461
12531
  }
@@ -10481,10 +12551,10 @@ function measureWorktreeConsumption(worktreePath, options) {
10481
12551
  timedOut = true;
10482
12552
  break;
10483
12553
  }
10484
- const entryPath = import_node_path24.default.join(dirPath, entry.name);
12554
+ const entryPath = import_node_path26.default.join(dirPath, entry.name);
10485
12555
  let entryStat;
10486
12556
  try {
10487
- entryStat = import_node_fs24.default.lstatSync(entryPath);
12557
+ entryStat = import_node_fs26.default.lstatSync(entryPath);
10488
12558
  } catch (error) {
10489
12559
  if (error?.code === "ENOENT") continue;
10490
12560
  unreadableReason = describeIncompleteWalk(error);
@@ -10494,7 +12564,7 @@ function measureWorktreeConsumption(worktreePath, options) {
10494
12564
  if (!entryStat.isDirectory()) continue;
10495
12565
  let childEntries;
10496
12566
  try {
10497
- childEntries = import_node_fs24.default.readdirSync(entryPath, { withFileTypes: true });
12567
+ childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
10498
12568
  } catch (error) {
10499
12569
  unreadableReason = describeIncompleteWalk(error);
10500
12570
  break;
@@ -10558,12 +12628,12 @@ function describeError(error, worktreePath) {
10558
12628
  }
10559
12629
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
10560
12630
  }
10561
- var import_node_fs24, import_node_path24, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
12631
+ var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
10562
12632
  var init_workspace_consumption = __esm({
10563
12633
  "src/core/workspace-consumption.ts"() {
10564
12634
  "use strict";
10565
- import_node_fs24 = __toESM(require("fs"));
10566
- import_node_path24 = __toESM(require("path"));
12635
+ import_node_fs26 = __toESM(require("fs"));
12636
+ import_node_path26 = __toESM(require("path"));
10567
12637
  init_devpod_environment();
10568
12638
  DEFAULT_DEADLINE_MS = 1e4;
10569
12639
  BLOCK_SIZE_BYTES = 512;
@@ -10664,7 +12734,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
10664
12734
  }
10665
12735
  function readGitSnapshot(worktree, commandRunner) {
10666
12736
  const comparablePath = comparableWorkspacePath(worktree.path);
10667
- if (worktree.prunable || !import_node_fs25.default.existsSync(comparablePath)) {
12737
+ if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
10668
12738
  return { worktree, checkout: "missing", head: null, committerDate: null };
10669
12739
  }
10670
12740
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -11195,7 +13265,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11195
13265
  containers = measureContainersFn(worktreePaths);
11196
13266
  } catch (error) {
11197
13267
  const reason = `container measurement failed: ${describeCause(error)}`;
11198
- containers = new Map(worktreePaths.map((path24) => [path24, unknownContainers(reason)]));
13268
+ containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
11199
13269
  }
11200
13270
  }
11201
13271
  const rows = records.map((record) => {
@@ -11243,12 +13313,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11243
13313
  workspaces: rows
11244
13314
  };
11245
13315
  }
11246
- var import_node_child_process21, import_node_fs25, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
13316
+ var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
11247
13317
  var init_workspace_cleanup = __esm({
11248
13318
  "src/core/workspace-cleanup.ts"() {
11249
13319
  "use strict";
11250
- import_node_child_process21 = require("child_process");
11251
- import_node_fs25 = __toESM(require("fs"));
13320
+ import_node_child_process22 = require("child_process");
13321
+ import_node_fs27 = __toESM(require("fs"));
11252
13322
  init_devpod_workspaces();
11253
13323
  init_host_routes();
11254
13324
  init_repo_config();
@@ -11265,7 +13335,7 @@ var init_workspace_cleanup = __esm({
11265
13335
  ];
11266
13336
  defaultCommandRunner = (command, args, input2) => {
11267
13337
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
11268
- const result = (0, import_node_child_process21.spawnSync)(command, args, {
13338
+ const result = (0, import_node_child_process22.spawnSync)(command, args, {
11269
13339
  encoding: "utf-8",
11270
13340
  env: environment,
11271
13341
  ...input2 === void 0 ? {} : { input: input2 }
@@ -11425,7 +13495,7 @@ var init_version = __esm({
11425
13495
 
11426
13496
  // src/cli.ts
11427
13497
  var import_commander = require("commander");
11428
- var CLI_VERSION = true ? "0.0.37" : "0.0.0-dev";
13498
+ var CLI_VERSION = true ? "0.0.39" : "0.0.0-dev";
11429
13499
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
11430
13500
  function withErrorHandling(action2) {
11431
13501
  return async (...args) => {
@@ -11475,12 +13545,13 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
11475
13545
  await runUpCommand2();
11476
13546
  })
11477
13547
  );
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(
13548
+ 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
13549
  withErrorHandling(async (repoPath, _options, command) => {
11480
13550
  const options = command.opts();
11481
13551
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11482
13552
  await runEnsureCommand2({
11483
13553
  path: repoPath,
13554
+ profile: options.profile,
11484
13555
  open: Boolean(options.open),
11485
13556
  json: Boolean(options.json)
11486
13557
  });
@@ -11679,13 +13750,14 @@ workspaceCommand2.command("up").description(
11679
13750
  });
11680
13751
  })
11681
13752
  );
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(
13753
+ 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
13754
  withErrorHandling(
11684
13755
  async (worktreePath, _options, command) => {
11685
13756
  const options = command.opts();
11686
13757
  const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
11687
13758
  await runEnsureCommand2({
11688
13759
  path: worktreePath,
13760
+ profile: options.profile,
11689
13761
  open: Boolean(options.open),
11690
13762
  json: Boolean(options.json)
11691
13763
  });