@devrouter/cli 0.0.38 → 0.0.40

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
@@ -238,7 +239,7 @@ profiles:
238
239
  default: true
239
240
  \`\`\`
240
241
 
241
- - \`apps\` (required): routed app names (\`kind=app\`) or \`['*']\` for everything.
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.
242
243
  - \`dependencies\` (optional): \`kind=dependency\` services this profile needs; omitted = every dependency a kept app requires transitively.
243
244
  - \`readiness\` (optional): subset of the profile's apps that \`ensure\` HTTP-probes; omitted = all profile apps with an http route.
244
245
  - \`default\` (optional): at most one; used when \`--profile\` is omitted. No \`profiles\` key at all = implicit full behavior.
@@ -246,6 +247,53 @@ profiles:
246
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.
247
248
  - Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
248
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
+
249
297
  ## Env var injection
250
298
 
251
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, "_")\`):
@@ -324,7 +372,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
324
372
  - \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
325
373
  - \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
326
374
  - \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
327
- - \`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
328
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
329
377
  - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
330
378
  - \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
@@ -347,7 +395,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
347
395
  - \`devrouter app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
348
396
  - \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
349
397
  - \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
350
- - \`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\`
351
399
  - \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
352
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
353
401
  - \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
@@ -364,7 +412,7 @@ For devcontainer onboarding:
364
412
  4. \`devrouter repo devcontainer write --repo . --dry-run --json\`
365
413
  5. \`devrouter repo devcontainer write --repo . --yes\`
366
414
  6. \`devrouter repo devcontainer verify --repo . --json\`
367
- 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
368
416
  8. Run seeds or migrations with \`devrouter exec . -- <command...>\`
369
417
 
370
418
  For host/docker runtime apps only:
@@ -1949,11 +1997,75 @@ function parseApp(value, index) {
1949
1997
  }
1950
1998
  throw new Error(`${pathLabel} has unsupported protocol/runtime combination.`);
1951
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
+ }
1952
2064
  function parseConfig(raw, configPath) {
1953
2065
  const root = ensureObject(raw, configPath);
1954
2066
  ensureAllowedKeys(
1955
2067
  root,
1956
- ["version", "devrouter", "project", "secretManager", "profiles", "apps"],
2068
+ ["version", "devrouter", "project", "secretManager", "managedRuntime", "profiles", "apps"],
1957
2069
  configPath
1958
2070
  );
1959
2071
  const version = toIntegerOrThrow(root.version, `${configPath}.version`);
@@ -2023,17 +2135,19 @@ function parseConfig(raw, configPath) {
2023
2135
  }
2024
2136
  seenNames.add(app.name);
2025
2137
  }
2026
- const profiles = parseProfiles(root.profiles, configPath, apps);
2138
+ const managedRuntime = parseManagedRuntime(root.managedRuntime, configPath);
2139
+ const profiles = parseProfiles(root.profiles, configPath, apps, managedRuntime);
2027
2140
  return {
2028
2141
  version: 1,
2029
2142
  ...devrouter ? { devrouter } : {},
2030
2143
  project: root.project && typeof root.project === "object" ? { name: root.project.name } : void 0,
2031
2144
  ...secretManager ? { secretManager } : {},
2145
+ ...managedRuntime ? { managedRuntime } : {},
2032
2146
  ...profiles ? { profiles } : {},
2033
2147
  apps
2034
2148
  };
2035
2149
  }
2036
- function parseProfiles(value, configPath, apps) {
2150
+ function parseProfiles(value, configPath, apps, managedRuntime) {
2037
2151
  if (value === void 0) {
2038
2152
  return void 0;
2039
2153
  }
@@ -2049,6 +2163,8 @@ function parseProfiles(value, configPath, apps) {
2049
2163
  );
2050
2164
  const result = {};
2051
2165
  let defaultCount = 0;
2166
+ const profileServices = new Set(managedRuntime?.devcontainer.profileServices ?? []);
2167
+ const processMarkers = new Set(managedRuntime?.processes ?? []);
2052
2168
  for (const [name, profileValue] of Object.entries(raw)) {
2053
2169
  if (!PROFILE_NAME_RE.test(name)) {
2054
2170
  throw new Error(
@@ -2058,15 +2174,20 @@ function parseProfiles(value, configPath, apps) {
2058
2174
  const profile = ensureObject(profileValue, `${configPath}.profiles.${name}`);
2059
2175
  ensureAllowedKeys(
2060
2176
  profile,
2061
- ["apps", "dependencies", "readiness", "default"],
2177
+ ["apps", "dependencies", "readiness", "devcontainerServices", "processes", "default"],
2062
2178
  `${configPath}.profiles.${name}`
2063
2179
  );
2064
2180
  const profileApps = toStringArray(profile.apps, `${configPath}.profiles.${name}.apps`);
2065
- if (profileApps.length === 0) {
2181
+ if (!managedRuntime && profileApps.length === 0) {
2066
2182
  throw new Error(`${configPath}.profiles.${name}.apps must not be empty.`);
2067
2183
  }
2068
- const isWildcard = profileApps.length === 1 && profileApps[0] === "*";
2069
- if (!isWildcard) {
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) {
2070
2191
  for (const appName of profileApps) {
2071
2192
  if (!routedNames.has(appName)) {
2072
2193
  throw new Error(
@@ -2098,7 +2219,7 @@ function parseProfiles(value, configPath, apps) {
2098
2219
  let readiness;
2099
2220
  if (profile.readiness !== void 0) {
2100
2221
  readiness = toStringArray(profile.readiness, `${configPath}.profiles.${name}.readiness`);
2101
- const appSet = new Set(isWildcard ? Array.from(routedNames) : profileApps);
2222
+ const appSet = new Set(isWildcard3 ? Array.from(routedNames) : profileApps);
2102
2223
  for (const readyName of readiness) {
2103
2224
  if (!appSet.has(readyName)) {
2104
2225
  throw new Error(
@@ -2107,6 +2228,61 @@ function parseProfiles(value, configPath, apps) {
2107
2228
  }
2108
2229
  }
2109
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
+ }
2110
2286
  let isDefault = false;
2111
2287
  if (profile.default !== void 0) {
2112
2288
  if (typeof profile.default !== "boolean") {
@@ -2121,6 +2297,8 @@ function parseProfiles(value, configPath, apps) {
2121
2297
  apps: profileApps,
2122
2298
  ...dependencies ? { dependencies } : {},
2123
2299
  ...readiness ? { readiness } : {},
2300
+ ...devcontainerServices ? { devcontainerServices } : {},
2301
+ ...processes ? { processes } : {},
2124
2302
  ...isDefault ? { default: true } : {}
2125
2303
  };
2126
2304
  }
@@ -2129,14 +2307,40 @@ function parseProfiles(value, configPath, apps) {
2129
2307
  }
2130
2308
  return result;
2131
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
+ }
2132
2336
  function resolveProfile(config, profileOverride) {
2133
2337
  const profiles = config.profiles;
2134
2338
  const mergeSelection = (selection) => {
2135
- const names = Array.from(
2136
- new Set(
2137
- selection.split(",").map((name) => name.trim()).filter(Boolean)
2138
- )
2139
- );
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())));
2140
2344
  if (names.length === 0) {
2141
2345
  throw new Error("Profile selection is empty.");
2142
2346
  }
@@ -2148,31 +2352,51 @@ function resolveProfile(config, profileOverride) {
2148
2352
  );
2149
2353
  }
2150
2354
  if (names.length === 1) {
2151
- return { name: names[0], profile: profiles?.[names[0]] };
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
+ };
2152
2361
  }
2153
2362
  const canonicalName = [...names].sort().join(",");
2154
2363
  const apps = /* @__PURE__ */ new Set();
2155
2364
  const dependencies = /* @__PURE__ */ new Set();
2156
2365
  const readiness = /* @__PURE__ */ new Set();
2157
- let hasWildcard = false;
2366
+ const devcontainerServices = /* @__PURE__ */ new Set();
2367
+ const processes = /* @__PURE__ */ new Set();
2368
+ let hasAppWildcard = false;
2369
+ let hasServiceWildcard = false;
2370
+ let hasProcessWildcard = false;
2158
2371
  for (const name of names) {
2159
2372
  const profile = profiles?.[name];
2160
2373
  if (!profile) continue;
2161
- if (profile.apps.length === 1 && profile.apps[0] === "*") {
2162
- hasWildcard = true;
2163
- continue;
2374
+ if (isProfileWildcard(profile.apps)) {
2375
+ hasAppWildcard = true;
2376
+ } else {
2377
+ for (const appName of profile.apps) apps.add(appName);
2164
2378
  }
2165
- for (const appName of profile.apps) apps.add(appName);
2166
2379
  for (const depName of profile.dependencies ?? []) dependencies.add(depName);
2167
2380
  for (const readyName of profile.readiness ?? []) readiness.add(readyName);
2168
- }
2169
- if (hasWildcard) {
2170
- return { name: canonicalName, profile: { apps: ["*"] } };
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
+ }
2171
2393
  }
2172
2394
  const merged = {
2173
- apps: Array.from(apps),
2174
- ...dependencies.size > 0 ? { dependencies: Array.from(dependencies) } : {},
2175
- ...readiness.size > 0 ? { readiness: Array.from(readiness) } : {}
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)) } : {}
2176
2400
  };
2177
2401
  return { name: canonicalName, profile: merged };
2178
2402
  };
@@ -2182,14 +2406,28 @@ function resolveProfile(config, profileOverride) {
2182
2406
  if (profiles) {
2183
2407
  const defaultName = Object.keys(profiles).find((name) => profiles[name].default);
2184
2408
  if (defaultName) {
2185
- return { name: defaultName, profile: profiles[defaultName] };
2409
+ return {
2410
+ name: defaultName,
2411
+ profile: normalizeSelectedProfile(
2412
+ defaultName,
2413
+ profiles[defaultName],
2414
+ config.managedRuntime
2415
+ )
2416
+ };
2186
2417
  }
2187
- return { name: "full", profile: void 0 };
2418
+ return {
2419
+ name: "full",
2420
+ profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
2421
+ };
2188
2422
  }
2189
- return { name: "full", profile: void 0 };
2423
+ return {
2424
+ name: "full",
2425
+ profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
2426
+ };
2190
2427
  }
2191
2428
  function applyProfile(config, profile) {
2192
- if (!profile || profile.apps.length === 1 && profile.apps[0] === "*") {
2429
+ const wildcardApps = profile?.apps.length === 1 && profile.apps[0] === "*";
2430
+ if (!profile || wildcardApps && profile.dependencies === void 0) {
2193
2431
  return config;
2194
2432
  }
2195
2433
  const appSet = new Set(profile.apps);
@@ -2200,7 +2438,7 @@ function applyProfile(config, profile) {
2200
2438
  if (app.kind === "dependency") {
2201
2439
  continue;
2202
2440
  }
2203
- if (!appSet.has(app.name)) {
2441
+ if (!wildcardApps && !appSet.has(app.name)) {
2204
2442
  continue;
2205
2443
  }
2206
2444
  for (const dependency of resolveAppDependencies(config, app)) {
@@ -2266,7 +2504,7 @@ function loadRepoConfig(repoPath) {
2266
2504
  const config = parseConfig(parsed ?? {}, configPath);
2267
2505
  const requiredVersion = config.devrouter?.version;
2268
2506
  if (requiredVersion && !hasWarnedVersionMismatch) {
2269
- const cliVersion = true ? "0.0.38" : "0.0.0-dev";
2507
+ const cliVersion = true ? "0.0.40" : "0.0.0-dev";
2270
2508
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2271
2509
  hasWarnedVersionMismatch = true;
2272
2510
  process.stderr.write(
@@ -2563,7 +2801,12 @@ function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
2563
2801
  const resolvedProfile = resolveProfile(raw, profileOverride);
2564
2802
  const workspace = resolveWorkspace(resolved, workspaceOverride);
2565
2803
  const config = applyProfile(applyWorkspace(raw, workspace, resolved), resolvedProfile.profile);
2566
- return { config, workspace, profile: resolvedProfile.name };
2804
+ return {
2805
+ config,
2806
+ workspace,
2807
+ profile: resolvedProfile.name,
2808
+ resolvedProfile: resolvedProfile.profile
2809
+ };
2567
2810
  }
2568
2811
  function resolveAppByName(repoPath, name, workspaceOverride) {
2569
2812
  const { config, workspace } = loadRuntimeConfig(repoPath, workspaceOverride);
@@ -2737,12 +2980,16 @@ function buildOnboardingPrompt(options = {}) {
2737
2980
  " - do not set host/protocol/tcpProtocol/hostRun/docker.internalPort/docker.router",
2738
2981
  "",
2739
2982
  "Profile schema (each value in the optional top-level profiles map):",
2740
- '- apps: array (required; routed app names, or ["*"] for every routed app)',
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)',
2741
2984
  "- dependencies: array (optional; kind=dependency names this profile needs; omitted = every dependency a kept app requires transitively)",
2742
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)",
2743
2986
  "- default: boolean (optional; at most one profile may set true)",
2744
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",
2745
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",
2746
2993
  "",
2747
2994
  "Validation rules to enforce:",
2748
2995
  "- kind=app host must end with .localhost",
@@ -2779,6 +3026,8 @@ function buildOnboardingPrompt(options = {}) {
2779
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.",
2780
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.",
2781
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.",
2782
3031
  "",
2783
3032
  "Workspace isolation (parallel git worktrees / agents):",
2784
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.`,
@@ -2832,6 +3081,7 @@ function buildOnboardingPrompt(options = {}) {
2832
3081
  "- devrouter repo devcontainer write --repo <REPO_PATH> --yes",
2833
3082
  "- devrouter repo devcontainer verify --repo <REPO_PATH> --json",
2834
3083
  "- Normal primary or linked startup: devrouter ensure <REPO_PATH> --json",
3084
+ "- Managed selective startup: devrouter ensure <REPO_PATH> --profile <name> --json",
2835
3085
  "- Container seed/migration: devrouter exec <REPO_PATH> -- <command...>",
2836
3086
  "- Compatibility verification only after ensure: devrouter repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
2837
3087
  "",
@@ -2949,7 +3199,7 @@ var init_ai_prompt = __esm({
2949
3199
  purpose: "Write/update devrouter section in the repo's AGENTS.md and install the devrouter skill."
2950
3200
  },
2951
3201
  {
2952
- command: "devrouter ensure [path] [--open] [--json]",
3202
+ command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
2953
3203
  purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
2954
3204
  },
2955
3205
  {
@@ -3170,6 +3420,9 @@ function printStatus(status) {
3170
3420
  status.repo.valid ? "yes" : `no (${status.repo.error ?? "validation failed"})`
3171
3421
  ]);
3172
3422
  rows.push(["Repo apps", String(status.repo.appCount)]);
3423
+ if (status.repo.managedRuntime) {
3424
+ appendManagedRuntimeRows(rows, status.repo.managedRuntime);
3425
+ }
3173
3426
  }
3174
3427
  process.stdout.write(`${renderTable(["FIELD", "VALUE"], rows)}
3175
3428
  `);
@@ -3181,6 +3434,38 @@ function printStatus(status) {
3181
3434
  }
3182
3435
  }
3183
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
+ }
3184
3469
  function printRoutes(routes, duplicateHosts) {
3185
3470
  if (routes.length === 0) {
3186
3471
  process.stdout.write("No routes found.\n");
@@ -3850,6 +4135,12 @@ function runManagedPostStart(options) {
3850
4135
  "--env",
3851
4136
  `DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
3852
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
+ ] : [],
3853
4144
  options.container.id,
3854
4145
  "bash",
3855
4146
  "-c",
@@ -3864,6 +4155,49 @@ function runManagedPostStart(options) {
3864
4155
  throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
3865
4156
  }
3866
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 {
4198
+ }
4199
+ return "drifted";
4200
+ }
3867
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;
3868
4202
  var init_managed_post_start = __esm({
3869
4203
  "src/core/managed-post-start.ts"() {
@@ -4368,150 +4702,1142 @@ var init_routes = __esm({
4368
4702
  }
4369
4703
  });
4370
4704
 
4371
- // src/core/status.ts
4372
- function hasPortBinding(ports, privatePort, publicPort) {
4373
- if (!ports) {
4374
- return false;
4375
- }
4376
- 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");
4377
4708
  }
4378
- function toRepoStatus(repoPath) {
4379
- const resolvedRepoPath = resolveRepoPath(repoPath);
4380
- const configPath = getRepoConfigPath(resolvedRepoPath);
4381
- const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
4382
- const configExists = import_node_fs12.default.existsSync(configPath);
4383
- if (!explicitRepo && !configExists) {
4384
- return void 0;
4385
- }
4386
- if (!configExists) {
4387
- return {
4388
- path: resolvedRepoPath,
4389
- configPath,
4390
- exists: false,
4391
- valid: false,
4392
- appCount: 0,
4393
- tcpAppCount: 0,
4394
- error: `Missing .devrouter.yml in ${resolvedRepoPath}`
4395
- };
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.");
4396
4718
  }
4397
- try {
4398
- const config = loadRuntimeConfig(resolvedRepoPath).config;
4399
- const tcpAppCount = config.apps.filter(
4400
- (app) => app.kind !== "dependency" && app.protocol === "tcp"
4401
- ).length;
4402
- return {
4403
- path: resolvedRepoPath,
4404
- configPath,
4405
- exists: true,
4406
- valid: true,
4407
- appCount: config.apps.length,
4408
- tcpAppCount
4409
- };
4410
- } catch (error) {
4411
- const message = error instanceof Error ? error.message : String(error);
4412
- return {
4413
- path: resolvedRepoPath,
4414
- configPath,
4415
- exists: true,
4416
- valid: false,
4417
- appCount: 0,
4418
- tcpAppCount: 0,
4419
- error: message
4420
- };
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.");
4421
4723
  }
4422
- }
4423
- async function collectRouterStatus(repoPath) {
4424
- const repo = toRepoStatus(repoPath);
4425
- const tlsEnabled = isTLSEnabled();
4426
- let container;
4427
- let networkIsPresent = false;
4428
- let dockerContext = "unknown";
4429
- let dockerUnavailableMessage;
4430
- try {
4431
- dockerContext = getCurrentDockerContext();
4432
- } catch (error) {
4433
- const message = error instanceof Error ? error.message : String(error);
4434
- dockerContext = `unavailable (${message})`;
4435
- dockerUnavailableMessage = message;
4724
+ if (typeof candidate.devpodId !== "string" || candidate.devpodId.length === 0) {
4725
+ throw new Error("Managed runtime state has no exact DevPod identity.");
4436
4726
  }
4437
- try {
4438
- container = await findContainerByName(ROUTER_CONTAINER_NAME);
4439
- } catch (error) {
4440
- const message = error instanceof Error ? error.message : String(error);
4441
- dockerUnavailableMessage = dockerUnavailableMessage ?? message;
4727
+ if (typeof candidate.composeProject !== "string" || candidate.composeProject.length === 0) {
4728
+ throw new Error("Managed runtime state has no exact Compose project identity.");
4442
4729
  }
4443
- try {
4444
- networkIsPresent = await networkExists(DEVNET_NAME);
4445
- } catch (error) {
4446
- const message = error instanceof Error ? error.message : String(error);
4447
- dockerUnavailableMessage = dockerUnavailableMessage ?? message;
4730
+ if (typeof candidate.profile !== "string" || candidate.profile.length === 0) {
4731
+ throw new Error("Managed runtime state has no canonical profile.");
4448
4732
  }
4449
- const tcpBoundPorts = {};
4450
- for (const protocol of getActiveTcpProtocols()) {
4451
- const entry = TCP_PROTOCOL_REGISTRY[protocol];
4452
- if (entry) {
4453
- tcpBoundPorts[protocol] = hasPortBinding(container?.Ports, entry.port, entry.port);
4454
- }
4733
+ if (!candidate.desired || typeof candidate.desired !== "object") {
4734
+ throw new Error("Managed runtime state has no desired resource set.");
4455
4735
  }
4456
- const boundPorts = {
4457
- web80: hasPortBinding(container?.Ports, 80, 80),
4458
- web443: hasPortBinding(container?.Ports, 443, 443),
4459
- dashboard8080: hasPortBinding(container?.Ports, 8080, 8080),
4460
- tcp: tcpBoundPorts
4461
- };
4462
- const nextSteps = [];
4463
- const httpRoutingReady = container?.State === "running" && boundPorts.web80;
4464
- const activeTcpProtocols = getActiveTcpProtocols();
4465
- const tcpRoutingReady = container?.State === "running" && tlsEnabled && activeTcpProtocols.length > 0 && activeTcpProtocols.every((p) => boundPorts.tcp[p]);
4466
- if (container?.State !== "running") {
4467
- nextSteps.push("Run: dev up");
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.");
4468
4739
  }
4469
- if (dockerUnavailableMessage) {
4470
- nextSteps.push("Ensure Docker is running and reachable from the active Docker context");
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.");
4471
4742
  }
4472
- if (!tlsEnabled) {
4473
- nextSteps.push("Run: dev tls install (required for TCP routing, recommended for HTTP)");
4743
+ if (candidate.status !== "ready" && candidate.status !== "degraded") {
4744
+ throw new Error("Managed runtime state has an invalid transition status.");
4474
4745
  }
4475
- if (repo && !repo.exists) {
4476
- nextSteps.push(`Run: dev repo init --repo ${repo.path}`);
4477
- } else if (repo && !repo.valid) {
4478
- nextSteps.push("Fix .devrouter.yml validation errors and re-run `dev doctor --repo <path>`");
4479
- } else if (repo?.valid && repo.appCount === 0) {
4480
- nextSteps.push(
4481
- `Run: dev app add --name <name> --host <name>.localhost --protocol http --runtime host --repo ${repo.path}`
4482
- );
4483
- } else if (repo?.valid) {
4484
- nextSteps.push(`Run: dev app ls --repo ${repo.path}`);
4485
- nextSteps.push("Run: dev app run <name> --repo <path> --yes");
4486
- nextSteps.push("Run: dev ls");
4746
+ if (candidate.transitionPhase !== void 0 && typeof candidate.transitionPhase !== "string") {
4747
+ throw new Error("Managed runtime state has an invalid transition phase.");
4487
4748
  }
4488
- const files = getRouterFileLayout();
4489
- if (files.missing.length > 0) {
4490
- nextSteps.push("Run: dev up to re-create missing global router files");
4749
+ if (typeof candidate.updatedAt !== "string" || !Number.isFinite(Date.parse(candidate.updatedAt))) {
4750
+ throw new Error("Managed runtime state has an invalid timestamp.");
4491
4751
  }
4492
4752
  return {
4493
- dockerContext,
4494
- routerRunning: container?.State === "running",
4495
- routerContainerName: ROUTER_CONTAINER_NAME,
4496
- boundPorts,
4497
- tlsConfigured: isTLSConfigured(),
4498
- certPresent: areTLSCertsPresent(),
4499
- tlsEnabled,
4500
- networkExists: networkIsPresent,
4501
- repo,
4502
- insights: {
4503
- httpRoutingReady,
4504
- tcpRoutingReady,
4505
- nextSteps: Array.from(new Set(nextSteps))
4506
- }
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
4507
4769
  };
4508
4770
  }
4509
- var import_node_fs12;
4510
- var init_status = __esm({
4511
- "src/core/status.ts"() {
4512
- "use strict";
4513
- import_node_fs12 = __toESM(require("fs"));
4514
- init_docker();
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;
4775
+ try {
4776
+ parsed = JSON.parse(import_node_fs12.default.readFileSync(statePath, "utf-8"));
4777
+ } catch (error) {
4778
+ throw new Error(`Could not parse managed runtime state: ${String(error)}`);
4779
+ }
4780
+ return validateState(parsed, repoPath, workspace);
4781
+ }
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();
5750
+ let container;
5751
+ let networkIsPresent = false;
5752
+ let dockerContext = "unknown";
5753
+ let dockerUnavailableMessage;
5754
+ try {
5755
+ dockerContext = getCurrentDockerContext();
5756
+ } catch (error) {
5757
+ const message = error instanceof Error ? error.message : String(error);
5758
+ dockerContext = `unavailable (${message})`;
5759
+ dockerUnavailableMessage = message;
5760
+ }
5761
+ try {
5762
+ container = await findContainerByName(ROUTER_CONTAINER_NAME);
5763
+ } catch (error) {
5764
+ const message = error instanceof Error ? error.message : String(error);
5765
+ dockerUnavailableMessage = dockerUnavailableMessage ?? message;
5766
+ }
5767
+ try {
5768
+ networkIsPresent = await networkExists(DEVNET_NAME);
5769
+ } catch (error) {
5770
+ const message = error instanceof Error ? error.message : String(error);
5771
+ dockerUnavailableMessage = dockerUnavailableMessage ?? message;
5772
+ }
5773
+ const tcpBoundPorts = {};
5774
+ for (const protocol of getActiveTcpProtocols()) {
5775
+ const entry = TCP_PROTOCOL_REGISTRY[protocol];
5776
+ if (entry) {
5777
+ tcpBoundPorts[protocol] = hasPortBinding(container?.Ports, entry.port, entry.port);
5778
+ }
5779
+ }
5780
+ const boundPorts = {
5781
+ web80: hasPortBinding(container?.Ports, 80, 80),
5782
+ web443: hasPortBinding(container?.Ports, 443, 443),
5783
+ dashboard8080: hasPortBinding(container?.Ports, 8080, 8080),
5784
+ tcp: tcpBoundPorts
5785
+ };
5786
+ const nextSteps = [];
5787
+ const httpRoutingReady = container?.State === "running" && boundPorts.web80;
5788
+ const activeTcpProtocols = getActiveTcpProtocols();
5789
+ const tcpRoutingReady = container?.State === "running" && tlsEnabled && activeTcpProtocols.length > 0 && activeTcpProtocols.every((p) => boundPorts.tcp[p]);
5790
+ if (container?.State !== "running") {
5791
+ nextSteps.push("Run: dev up");
5792
+ }
5793
+ if (dockerUnavailableMessage) {
5794
+ nextSteps.push("Ensure Docker is running and reachable from the active Docker context");
5795
+ }
5796
+ if (!tlsEnabled) {
5797
+ nextSteps.push("Run: dev tls install (required for TCP routing, recommended for HTTP)");
5798
+ }
5799
+ if (repo && !repo.exists) {
5800
+ nextSteps.push(`Run: dev repo init --repo ${repo.path}`);
5801
+ } else if (repo && !repo.valid) {
5802
+ nextSteps.push("Fix .devrouter.yml validation errors and re-run `dev doctor --repo <path>`");
5803
+ } else if (repo?.valid && repo.appCount === 0) {
5804
+ nextSteps.push(
5805
+ `Run: dev app add --name <name> --host <name>.localhost --protocol http --runtime host --repo ${repo.path}`
5806
+ );
5807
+ } else if (repo?.valid) {
5808
+ nextSteps.push(`Run: dev app ls --repo ${repo.path}`);
5809
+ nextSteps.push("Run: dev app run <name> --repo <path> --yes");
5810
+ nextSteps.push("Run: dev ls");
5811
+ }
5812
+ const files = getRouterFileLayout();
5813
+ if (files.missing.length > 0) {
5814
+ nextSteps.push("Run: dev up to re-create missing global router files");
5815
+ }
5816
+ return {
5817
+ dockerContext,
5818
+ routerRunning: container?.State === "running",
5819
+ routerContainerName: ROUTER_CONTAINER_NAME,
5820
+ boundPorts,
5821
+ tlsConfigured: isTLSConfigured(),
5822
+ certPresent: areTLSCertsPresent(),
5823
+ tlsEnabled,
5824
+ networkExists: networkIsPresent,
5825
+ repo,
5826
+ insights: {
5827
+ httpRoutingReady,
5828
+ tcpRoutingReady,
5829
+ nextSteps: Array.from(new Set(nextSteps))
5830
+ }
5831
+ };
5832
+ }
5833
+ var import_node_fs14;
5834
+ var init_status = __esm({
5835
+ "src/core/status.ts"() {
5836
+ "use strict";
5837
+ import_node_fs14 = __toESM(require("fs"));
5838
+ init_docker();
5839
+ init_managed_runtime_state();
5840
+ init_managed_runtime_status();
4515
5841
  init_repo_config();
4516
5842
  init_router();
4517
5843
  }
@@ -4519,7 +5845,7 @@ var init_status = __esm({
4519
5845
 
4520
5846
  // src/core/tls.ts
4521
5847
  function runOrThrow(command, args) {
4522
- const result = (0, import_node_child_process6.spawnSync)(command, args, {
5848
+ const result = (0, import_node_child_process8.spawnSync)(command, args, {
4523
5849
  encoding: "utf-8"
4524
5850
  });
4525
5851
  if (result.status !== 0) {
@@ -4529,7 +5855,7 @@ function runOrThrow(command, args) {
4529
5855
  return result.stdout.trim();
4530
5856
  }
4531
5857
  function commandExists(command) {
4532
- 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" });
4533
5859
  return result.status === 0;
4534
5860
  }
4535
5861
  function ensureMkcert() {
@@ -4547,8 +5873,8 @@ function tlsSetupCommand(repoPath) {
4547
5873
  }
4548
5874
  function getMkcertRootCAPath(options = {}) {
4549
5875
  ensureMkcert();
4550
- const rootCAPath = import_node_path11.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
4551
- 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)) {
4552
5878
  throw new Error(
4553
5879
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
4554
5880
  );
@@ -4581,7 +5907,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
4581
5907
  return normalizeUniqueHosts(names);
4582
5908
  }
4583
5909
  function parseCertificateDnsHosts(pem) {
4584
- const certificate = new import_node_crypto4.X509Certificate(pem);
5910
+ const certificate = new import_node_crypto6.X509Certificate(pem);
4585
5911
  const subjectAltName = certificate.subjectAltName ?? "";
4586
5912
  if (subjectAltName.length === 0) {
4587
5913
  return [];
@@ -4614,10 +5940,10 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
4614
5940
  );
4615
5941
  }
4616
5942
  function readCurrentCertificateHosts() {
4617
- if (!import_node_fs13.default.existsSync(CERT_FILE)) {
5943
+ if (!import_node_fs15.default.existsSync(CERT_FILE)) {
4618
5944
  return [];
4619
5945
  }
4620
- const pem = import_node_fs13.default.readFileSync(CERT_FILE, "utf-8");
5946
+ const pem = import_node_fs15.default.readFileSync(CERT_FILE, "utf-8");
4621
5947
  return parseCertificateDnsHosts(pem);
4622
5948
  }
4623
5949
  function currentCertificateHostsOrEmpty() {
@@ -4706,14 +6032,14 @@ Run: ${tlsSetupCommand(options.repoPath)}`
4706
6032
  );
4707
6033
  }
4708
6034
  }
4709
- 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;
4710
6036
  var init_tls = __esm({
4711
6037
  "src/core/tls.ts"() {
4712
6038
  "use strict";
4713
- import_node_child_process6 = require("child_process");
4714
- import_node_crypto4 = require("crypto");
4715
- import_node_fs13 = __toESM(require("fs"));
4716
- 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"));
4717
6043
  init_docker();
4718
6044
  init_host_routes();
4719
6045
  init_router();
@@ -4729,7 +6055,7 @@ function outputFromResult(result) {
4729
6055
  return output2.length > 0 ? output2 : void 0;
4730
6056
  }
4731
6057
  function runTool(command, args = []) {
4732
- const result = (0, import_node_child_process7.spawnSync)(command, args, {
6058
+ const result = (0, import_node_child_process9.spawnSync)(command, args, {
4733
6059
  encoding: "utf-8"
4734
6060
  });
4735
6061
  if (result.error) {
@@ -4786,12 +6112,12 @@ function parseMinimumNodeMajor(value) {
4786
6112
  return Number(match[1]);
4787
6113
  }
4788
6114
  function readPackageJson(repoPath) {
4789
- const packagePath = import_node_path12.default.join(repoPath, "package.json");
4790
- 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)) {
4791
6117
  return void 0;
4792
6118
  }
4793
6119
  try {
4794
- return JSON.parse(import_node_fs14.default.readFileSync(packagePath, "utf-8"));
6120
+ return JSON.parse(import_node_fs16.default.readFileSync(packagePath, "utf-8"));
4795
6121
  } catch {
4796
6122
  return void 0;
4797
6123
  }
@@ -4886,19 +6212,19 @@ function buildGlobalToolChecks(repoPath) {
4886
6212
  checks.push(nodeToolchainCheck(repoPath));
4887
6213
  return checks;
4888
6214
  }
4889
- var import_node_child_process7, import_node_fs14, import_node_path12;
6215
+ var import_node_child_process9, import_node_fs16, import_node_path15;
4890
6216
  var init_tool_diagnostics = __esm({
4891
6217
  "src/core/tool-diagnostics.ts"() {
4892
6218
  "use strict";
4893
- import_node_child_process7 = require("child_process");
4894
- import_node_fs14 = __toESM(require("fs"));
4895
- 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"));
4896
6222
  }
4897
6223
  });
4898
6224
 
4899
6225
  // src/core/devpod-workspaces.ts
4900
6226
  function listDevpodWorkspaces() {
4901
- 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"], {
4902
6228
  encoding: "utf-8"
4903
6229
  });
4904
6230
  if (result.status !== 0) {
@@ -4957,7 +6283,7 @@ function parseDevpodRuntimeStatus(output2, expectedId) {
4957
6283
  }
4958
6284
  }
4959
6285
  function inspectDevpodRuntimeStatus(devpodId) {
4960
- 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"], {
4961
6287
  encoding: "utf-8"
4962
6288
  });
4963
6289
  if (result.status !== 0 || result.error) return "unknown";
@@ -4990,18 +6316,18 @@ function selectDevpodWorkspace(workspaces, repoPath) {
4990
6316
  }
4991
6317
  return matches[0];
4992
6318
  }
4993
- var import_node_child_process8;
6319
+ var import_node_child_process10;
4994
6320
  var init_devpod_workspaces = __esm({
4995
6321
  "src/core/devpod-workspaces.ts"() {
4996
6322
  "use strict";
4997
- import_node_child_process8 = require("child_process");
6323
+ import_node_child_process10 = require("child_process");
4998
6324
  init_workspace();
4999
6325
  }
5000
6326
  });
5001
6327
 
5002
6328
  // src/core/devpod-mutation.ts
5003
6329
  function withMutationLock(activity, target, operation) {
5004
- import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6330
+ import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
5005
6331
  return withFileLockSync(
5006
6332
  DEVPOD_MUTATION_LOCK_FILE,
5007
6333
  { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
@@ -5013,7 +6339,7 @@ function commandFailure2(result) {
5013
6339
  }
5014
6340
  function runDevpodAction(action2, devpodId, force = false) {
5015
6341
  const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
5016
- 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" });
5017
6343
  if (result.status !== 0) {
5018
6344
  throw new Error(
5019
6345
  `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
@@ -5077,6 +6403,9 @@ function startDevpodWorkspace(options) {
5077
6403
  }
5078
6404
  const args = ["up", options.repoPath];
5079
6405
  if (devpodId) args.push("--id", devpodId);
6406
+ if (options.devcontainerPath) {
6407
+ args.push("--devcontainer-path", options.devcontainerPath);
6408
+ }
5080
6409
  args.push("--open-ide=false");
5081
6410
  if (options.workspace) {
5082
6411
  args.push(
@@ -5099,7 +6428,7 @@ function startDevpodWorkspace(options) {
5099
6428
  delete env.DEVROUTER_GIT_COMMON_DIR;
5100
6429
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
5101
6430
  }
5102
- const result = (0, import_node_child_process9.spawnSync)("devpod", args, {
6431
+ const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
5103
6432
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
5104
6433
  env
5105
6434
  });
@@ -5126,17 +6455,17 @@ function startDevpodWorkspace(options) {
5126
6455
  }
5127
6456
  });
5128
6457
  }
5129
- 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;
5130
6459
  var init_devpod_mutation = __esm({
5131
6460
  "src/core/devpod-mutation.ts"() {
5132
6461
  "use strict";
5133
- import_node_child_process9 = require("child_process");
5134
- import_node_fs15 = __toESM(require("fs"));
5135
- 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"));
5136
6465
  init_devpod_workspaces();
5137
6466
  init_file_lock();
5138
6467
  init_router();
5139
- 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");
5140
6469
  DEVPOD_MUTATION_WAIT_MS = 6e4;
5141
6470
  DevpodStartPostconditionError = class extends Error {
5142
6471
  };
@@ -5150,7 +6479,7 @@ function commandError(command, repoPath, stderr) {
5150
6479
  );
5151
6480
  }
5152
6481
  function resolveGitCommonDir(repoPath) {
5153
- 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"], {
5154
6483
  encoding: "utf-8",
5155
6484
  env: READ_ONLY_GIT_ENV
5156
6485
  });
@@ -5158,10 +6487,10 @@ function resolveGitCommonDir(repoPath) {
5158
6487
  if (result.status !== 0 || !output2) {
5159
6488
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
5160
6489
  }
5161
- 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));
5162
6491
  }
5163
6492
  function resolveGitTopLevel(repoPath) {
5164
- 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"], {
5165
6494
  encoding: "utf-8",
5166
6495
  env: READ_ONLY_GIT_ENV
5167
6496
  });
@@ -5172,7 +6501,7 @@ function resolveGitTopLevel(repoPath) {
5172
6501
  return comparableWorkspacePath(output2);
5173
6502
  }
5174
6503
  function listGitWorktrees(repoPath) {
5175
- 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"], {
5176
6505
  encoding: "utf-8",
5177
6506
  env: READ_ONLY_GIT_ENV
5178
6507
  });
@@ -5209,7 +6538,7 @@ function listGitWorktrees(repoPath) {
5209
6538
  return worktrees;
5210
6539
  }
5211
6540
  function ownershipDirectory(repoPath) {
5212
- return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
6541
+ return import_node_path17.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
5213
6542
  }
5214
6543
  function validateWorkspace(value, label) {
5215
6544
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -5237,7 +6566,7 @@ function validateRecord(value, expectedWorkspace) {
5237
6566
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
5238
6567
  );
5239
6568
  }
5240
- 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)) {
5241
6570
  throw new Error("invalid workspace ownership worktreePath");
5242
6571
  }
5243
6572
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -5257,7 +6586,7 @@ function validateRecord(value, expectedWorkspace) {
5257
6586
  function readRecordFile(filePath, expectedWorkspace) {
5258
6587
  let parsed;
5259
6588
  try {
5260
- parsed = JSON.parse(import_node_fs16.default.readFileSync(filePath, "utf-8"));
6589
+ parsed = JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
5261
6590
  } catch (error) {
5262
6591
  if (error instanceof SyntaxError) {
5263
6592
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -5273,7 +6602,7 @@ function listWorkspaceOwnership(repoPath) {
5273
6602
  function listWorkspaceOwnershipInDirectory(directory) {
5274
6603
  let entries;
5275
6604
  try {
5276
- entries = import_node_fs16.default.readdirSync(directory, { withFileTypes: true });
6605
+ entries = import_node_fs18.default.readdirSync(directory, { withFileTypes: true });
5277
6606
  } catch (error) {
5278
6607
  if (error.code === "ENOENT") return [];
5279
6608
  throw error;
@@ -5281,14 +6610,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
5281
6610
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
5282
6611
  const workspace = entry.name.slice(0, -".json".length);
5283
6612
  validateWorkspace(workspace, "filename");
5284
- return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
6613
+ return readRecordFile(import_node_path17.default.join(directory, entry.name), workspace);
5285
6614
  });
5286
6615
  }
5287
6616
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
5288
6617
  const workspace = validateWorkspace(input2.workspace, "workspace");
5289
6618
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
5290
6619
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
5291
- const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
6620
+ const filePath = import_node_path17.default.join(directory, `${workspace}.json`);
5292
6621
  const now = (/* @__PURE__ */ new Date()).toISOString();
5293
6622
  const records = listWorkspaceOwnershipInDirectory(directory);
5294
6623
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -5324,9 +6653,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
5324
6653
  return record;
5325
6654
  }
5326
6655
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
5327
- 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`);
5328
6657
  try {
5329
- import_node_fs16.default.rmSync(filePath);
6658
+ import_node_fs18.default.rmSync(filePath);
5330
6659
  return true;
5331
6660
  } catch (error) {
5332
6661
  if (error.code === "ENOENT") return false;
@@ -5337,7 +6666,7 @@ function sameOwnershipRecord(left, right) {
5337
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;
5338
6667
  }
5339
6668
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
5340
- const filePath = import_node_path14.default.join(
6669
+ const filePath = import_node_path17.default.join(
5341
6670
  directory,
5342
6671
  `${validateWorkspace(expected.workspace, "workspace")}.json`
5343
6672
  );
@@ -5349,14 +6678,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
5349
6678
  throw error;
5350
6679
  }
5351
6680
  if (!sameOwnershipRecord(current, expected)) return "changed";
5352
- import_node_fs16.default.rmSync(filePath);
6681
+ import_node_fs18.default.rmSync(filePath);
5353
6682
  return "removed";
5354
6683
  }
5355
6684
  function withWorkspaceOwnershipTransaction(repoPath, operation) {
5356
6685
  const directory = ownershipDirectory(repoPath);
5357
- import_node_fs16.default.mkdirSync(directory, { recursive: true });
6686
+ import_node_fs18.default.mkdirSync(directory, { recursive: true });
5358
6687
  return withFileLockSync(
5359
- import_node_path14.default.join(directory, ".lock"),
6688
+ import_node_path17.default.join(directory, ".lock"),
5360
6689
  { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
5361
6690
  () => operation({
5362
6691
  list: () => listWorkspaceOwnershipInDirectory(directory),
@@ -5387,7 +6716,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
5387
6716
  if (worktree?.locked) {
5388
6717
  return { ownerStatus: "locked", devpodStatus, worktree };
5389
6718
  }
5390
- if ((!worktree || worktree.prunable) && import_node_fs16.default.existsSync(record.worktreePath)) {
6719
+ if ((!worktree || worktree.prunable) && import_node_fs18.default.existsSync(record.worktreePath)) {
5391
6720
  return { ownerStatus: "conflict", devpodStatus, worktree };
5392
6721
  }
5393
6722
  if (!worktree || worktree.prunable) {
@@ -5411,20 +6740,20 @@ function listMissingWorkspaceOwnership(repoPath) {
5411
6740
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
5412
6741
  );
5413
6742
  }
5414
- 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;
5415
6744
  var init_workspace_ownership = __esm({
5416
6745
  "src/core/workspace-ownership.ts"() {
5417
6746
  "use strict";
5418
- import_node_child_process10 = require("child_process");
5419
- import_node_fs16 = __toESM(require("fs"));
5420
- 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"));
5421
6750
  init_atomic_file();
5422
6751
  init_devpod_workspaces();
5423
6752
  init_file_lock();
5424
6753
  init_workspace();
5425
6754
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
5426
6755
  OWNERSHIP_VERSION = 1;
5427
- OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
6756
+ OWNERSHIP_DIR = import_node_path17.default.join("devrouter", "workspaces");
5428
6757
  }
5429
6758
  });
5430
6759
 
@@ -5438,10 +6767,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
5438
6767
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
5439
6768
  const comparableRepo = comparableWorkspacePath(repoPath);
5440
6769
  const comparableWorktree = comparableWorkspacePath(worktreePath);
5441
- 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;
5442
6771
  if (comparableWorktree.startsWith(localRoot)) return true;
5443
- const legacyPrefix = `${import_node_path15.default.basename(comparableRepo)}-`;
5444
- 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);
5445
6774
  }
5446
6775
  function previewActions(devpodStatus, routeCount, includeRecord) {
5447
6776
  const actions = [
@@ -5684,11 +7013,11 @@ function applyWorkspaceGc(plan) {
5684
7013
  candidates
5685
7014
  };
5686
7015
  }
5687
- var import_node_path15;
7016
+ var import_node_path18;
5688
7017
  var init_workspace_gc = __esm({
5689
7018
  "src/core/workspace-gc.ts"() {
5690
7019
  "use strict";
5691
- import_node_path15 = __toESM(require("path"));
7020
+ import_node_path18 = __toESM(require("path"));
5692
7021
  init_devpod_mutation();
5693
7022
  init_devpod_workspaces();
5694
7023
  init_host_routes();
@@ -5758,12 +7087,12 @@ function inspectPostgresCredentials(repoPath, config) {
5758
7087
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
5759
7088
  continue;
5760
7089
  }
5761
- if (!import_node_fs17.default.existsSync(absolutePath)) {
7090
+ if (!import_node_fs19.default.existsSync(absolutePath)) {
5762
7091
  continue;
5763
7092
  }
5764
7093
  try {
5765
- const raw = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
5766
- 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);
5767
7096
  const root = asRecord2(parsed);
5768
7097
  const services = asRecord2(root?.services);
5769
7098
  const service = asRecord2(services?.[app.docker.service]);
@@ -6013,6 +7342,22 @@ async function buildDoctorReport(options = {}) {
6013
7342
  level: "ok",
6014
7343
  summary: `.devrouter.yml is valid (${repo.appCount} app(s)).`
6015
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
+ }
6016
7361
  if (repo.appCount === 0) {
6017
7362
  addCheck(checks, {
6018
7363
  id: "repo.apps",
@@ -6025,7 +7370,7 @@ async function buildDoctorReport(options = {}) {
6025
7370
  const config = runtimeConfig.config;
6026
7371
  loadedConfig = config;
6027
7372
  loadedWorkspace = runtimeConfig.workspace;
6028
- const cliVersion = true ? "0.0.38" : "0.0.0-dev";
7373
+ const cliVersion = true ? "0.0.40" : "0.0.0-dev";
6029
7374
  const configVersion = config.devrouter?.version;
6030
7375
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
6031
7376
  addCheck(checks, {
@@ -6055,9 +7400,9 @@ async function buildDoctorReport(options = {}) {
6055
7400
  (app) => app.docker.composeFiles.map((filePath) => ({
6056
7401
  app: app.name,
6057
7402
  filePath,
6058
- absolutePath: import_node_path16.default.resolve(repo.path, filePath)
7403
+ absolutePath: import_node_path19.default.resolve(repo.path, filePath)
6059
7404
  }))
6060
- ).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
7405
+ ).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
6061
7406
  addCheck(checks, {
6062
7407
  id: "repo.compose-files",
6063
7408
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -6100,8 +7445,8 @@ async function buildDoctorReport(options = {}) {
6100
7445
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
6101
7446
  app: app.name,
6102
7447
  cwd: app.hostRun.cwd,
6103
- absolutePath: import_node_path16.default.resolve(repo.path, app.hostRun.cwd)
6104
- })).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));
6105
7450
  addCheck(checks, {
6106
7451
  id: "repo.host-cwd",
6107
7452
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -6245,13 +7590,13 @@ async function buildDoctorReport(options = {}) {
6245
7590
  nextSteps
6246
7591
  };
6247
7592
  }
6248
- var import_node_fs17, import_node_path16, import_yaml4, POSTGRES_DEFAULTS;
7593
+ var import_node_fs19, import_node_path19, import_yaml5, POSTGRES_DEFAULTS;
6249
7594
  var init_doctor = __esm({
6250
7595
  "src/core/doctor.ts"() {
6251
7596
  "use strict";
6252
- import_node_fs17 = __toESM(require("fs"));
6253
- import_node_path16 = __toESM(require("path"));
6254
- 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"));
6255
7600
  init_docker();
6256
7601
  init_host_routes();
6257
7602
  init_repo_config();
@@ -6508,12 +7853,12 @@ function parseSsPortListeners(stdout, port) {
6508
7853
  return listeners;
6509
7854
  }
6510
7855
  function findPortListeners(port) {
6511
- 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"], {
6512
7857
  encoding: "utf-8"
6513
7858
  });
6514
7859
  if (result.error && result.error.code === "ENOENT") {
6515
7860
  if (process.platform === "linux") {
6516
- 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" });
6517
7862
  if (ssResult.status === 0 && ssResult.stdout) {
6518
7863
  return parseSsPortListeners(ssResult.stdout, port);
6519
7864
  }
@@ -6542,11 +7887,11 @@ function findPortListeners(port) {
6542
7887
  };
6543
7888
  });
6544
7889
  }
6545
- var import_node_child_process11;
7890
+ var import_node_child_process13;
6546
7891
  var init_ports = __esm({
6547
7892
  "src/util/ports.ts"() {
6548
7893
  "use strict";
6549
- import_node_child_process11 = require("child_process");
7894
+ import_node_child_process13 = require("child_process");
6550
7895
  }
6551
7896
  });
6552
7897
 
@@ -6578,95 +7923,26 @@ Debug commands:
6578
7923
  - lsof -nP -iTCP:80 -sTCP:LISTEN
6579
7924
  - lsof -nP -iTCP:443 -sTCP:LISTEN
6580
7925
  - lsof -nP -iTCP:5432 -sTCP:LISTEN`;
6581
- }
6582
- async function runUpCommand() {
6583
- await ensureNetwork(DEVNET_NAME);
6584
- ensureRouterFiles();
6585
- const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
6586
- if (!routerRunning) {
6587
- const message = buildPortConflictMessage();
6588
- if (message) {
6589
- throw new Error(message);
6590
- }
6591
- }
6592
- startRouterStack();
6593
- process.stdout.write("devrouter is up.\n");
6594
- }
6595
- var init_up = __esm({
6596
- "src/commands/up.ts"() {
6597
- "use strict";
6598
- init_docker();
6599
- init_router();
6600
- init_ports();
6601
- }
6602
- });
6603
-
6604
- // src/core/devpod-environment.ts
6605
- function inspectWorkspaceContainers(options) {
6606
- let ids = options?.ids;
6607
- if (!ids) {
6608
- const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
6609
- encoding: "utf-8"
6610
- });
6611
- if (listed.status !== 0) {
6612
- throw new Error(
6613
- `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
6614
- );
6615
- }
6616
- ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
6617
- }
6618
- if (ids.length === 0) return [];
6619
- const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
6620
- const inspected = (0, import_node_child_process12.spawnSync)(
6621
- "docker",
6622
- ["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
6623
- { encoding: "utf-8" }
6624
- );
6625
- if (inspected.status !== 0) {
6626
- throw new Error(
6627
- `docker inspect failed: ${(inspected.stderr || inspected.stdout || "unknown error").trim()}`
6628
- );
6629
- }
6630
- return inspected.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
6631
- }
6632
- function workspaceAppContainers(containers, repoPath) {
6633
- return containers.filter((container) => {
6634
- const workingDir = container.labels["com.docker.compose.project.working_dir"];
6635
- return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
6636
- (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
6637
- );
6638
- });
6639
- }
6640
- function resolveRunningWorkspaceContainer(repoPath) {
6641
- const matches = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
6642
- (container2) => container2.state.Running
6643
- );
6644
- if (matches.length !== 1) {
6645
- throw new Error(
6646
- `Expected one running workspace app container for '${repoPath}', found ${matches.length}.`
6647
- );
6648
- }
6649
- const container = matches[0];
6650
- const repoMount = container.mounts.find(
6651
- (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
6652
- );
6653
- if (!repoMount) {
6654
- throw new Error(`Workspace app container no longer mounts '${repoPath}'.`);
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
+ }
6655
7936
  }
6656
- return { id: container.id, workspacePath: repoMount.Destination };
7937
+ startRouterStack();
7938
+ process.stdout.write("devrouter is up.\n");
6657
7939
  }
6658
- var import_node_child_process12, import_node_path17, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
6659
- var init_devpod_environment = __esm({
6660
- "src/core/devpod-environment.ts"() {
7940
+ var init_up = __esm({
7941
+ "src/commands/up.ts"() {
6661
7942
  "use strict";
6662
- import_node_child_process12 = require("child_process");
6663
- import_node_path17 = __toESM(require("path"));
6664
- init_workspace();
6665
- 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}}}';
6666
- SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
6667
- /}$/,
6668
- ',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
6669
- );
7943
+ init_docker();
7944
+ init_router();
7945
+ init_ports();
6670
7946
  }
6671
7947
  });
6672
7948
 
@@ -6692,18 +7968,18 @@ function probeHttpRoute(host, options = {}) {
6692
7968
  args.push("--cacert", CERT_FILE);
6693
7969
  }
6694
7970
  args.push(url);
6695
- 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" });
6696
7972
  const stdout = result.stdout?.trim() ?? "";
6697
7973
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
6698
7974
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
6699
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"}`;
6700
7976
  return { ok, status, details };
6701
7977
  }
6702
- var import_node_child_process13;
7978
+ var import_node_child_process14;
6703
7979
  var init_http_route_probe = __esm({
6704
7980
  "src/core/http-route-probe.ts"() {
6705
7981
  "use strict";
6706
- import_node_child_process13 = require("child_process");
7982
+ import_node_child_process14 = require("child_process");
6707
7983
  init_router();
6708
7984
  init_tls();
6709
7985
  }
@@ -6782,11 +8058,11 @@ var init_route_publication = __esm({
6782
8058
  // src/core/workspace-ensure.ts
6783
8059
  function assertOverlay(container, repoPath) {
6784
8060
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
6785
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path18.default.join(repoPath, ".devcontainer"))) {
8061
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path20.default.join(repoPath, ".devcontainer"))) {
6786
8062
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
6787
8063
  }
6788
8064
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
6789
- const expectedOverlay = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8065
+ const expectedOverlay = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
6790
8066
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
6791
8067
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
6792
8068
  }
@@ -6800,6 +8076,209 @@ function assertReady(container, label) {
6800
8076
  throw new Error(`${label} container '${container.id}' is not healthy (${health}).`);
6801
8077
  }
6802
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 restoreFirstTransitionManagedConfig(options) {
8221
+ const restoredPlan = inspectManagedDevcontainerConfig({
8222
+ repoPath: options.repoPath,
8223
+ config: options.config,
8224
+ profile: {
8225
+ apps: [],
8226
+ devcontainerServices: [...options.services]
8227
+ },
8228
+ linked: options.linked
8229
+ });
8230
+ writeManagedDevcontainerConfig(restoredPlan);
8231
+ }
8232
+ function isWarmWorkspaceActive(repoPath) {
8233
+ try {
8234
+ return workspaceAppContainers(inspectWorkspaceContainers(), repoPath).some(
8235
+ (container) => container.state.Running
8236
+ );
8237
+ } catch {
8238
+ return true;
8239
+ }
8240
+ }
8241
+ function captureFirstTransitionBaseline(options) {
8242
+ const containers = inspectWorkspaceContainers();
8243
+ const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
8244
+ (container) => container.state.Running
8245
+ );
8246
+ if (runningPrimary.length === 0) return void 0;
8247
+ if (runningPrimary.length > 1) {
8248
+ throw new Error(
8249
+ "Cannot start the first managed transition while multiple workspace app containers are running."
8250
+ );
8251
+ }
8252
+ const primary = runningPrimary[0];
8253
+ const composeProject = primary.labels["com.docker.compose.project"];
8254
+ const workspacePath = primary.mounts.find(
8255
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, options.repoPath)
8256
+ )?.Destination;
8257
+ if (!composeProject || !workspacePath) {
8258
+ throw new Error(
8259
+ "Cannot prove the exact pre-transition Compose identity for the first managed transition."
8260
+ );
8261
+ }
8262
+ const services = options.plan.profileServices.filter(
8263
+ (service) => containers.some(
8264
+ (container) => container.state.Running && hasExactComposeIdentity(container, { repoPath: options.repoPath, service, composeProject })
8265
+ )
8266
+ );
8267
+ const activeProcesses = [];
8268
+ for (const process2 of options.processes) {
8269
+ const status = runManagedProcessAction({
8270
+ container: { id: primary.id, workspacePath },
8271
+ name: process2,
8272
+ action: "status",
8273
+ quiet: true
8274
+ });
8275
+ if (status === "running") activeProcesses.push(process2);
8276
+ }
8277
+ return {
8278
+ services,
8279
+ processes: activeProcesses
8280
+ };
8281
+ }
6803
8282
  function validateWorkspaceContainers(containers, options) {
6804
8283
  const appContainers = workspaceAppContainers(containers, options.repoPath);
6805
8284
  if (appContainers.length !== 1) {
@@ -6860,7 +8339,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6860
8339
  target
6861
8340
  });
6862
8341
  if (target.kind === "linked") {
6863
- const workspaceEnv = (0, import_node_child_process14.spawnSync)(
8342
+ const workspaceEnv = (0, import_node_child_process15.spawnSync)(
6864
8343
  "docker",
6865
8344
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
6866
8345
  { encoding: "utf-8" }
@@ -6870,7 +8349,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6870
8349
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
6871
8350
  );
6872
8351
  }
6873
- const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
8352
+ const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
6874
8353
  "docker",
6875
8354
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
6876
8355
  { encoding: "utf-8" }
@@ -6881,7 +8360,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6881
8360
  );
6882
8361
  }
6883
8362
  }
6884
- const gitCheck = (0, import_node_child_process14.spawnSync)(
8363
+ const gitCheck = (0, import_node_child_process15.spawnSync)(
6885
8364
  "docker",
6886
8365
  [
6887
8366
  "exec",
@@ -6968,14 +8447,14 @@ function resolvePrimaryTarget(repoPath) {
6968
8447
  }
6969
8448
  function isPrimaryCheckout(repoPath) {
6970
8449
  try {
6971
- return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
8450
+ return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
6972
8451
  } catch {
6973
8452
  return false;
6974
8453
  }
6975
8454
  }
6976
8455
  function openUrls(urls) {
6977
8456
  for (const url of urls) {
6978
- const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
8457
+ const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
6979
8458
  if (opened.status !== 0) {
6980
8459
  process.stderr.write(`Warning: could not open '${url}'.
6981
8460
  `);
@@ -7001,24 +8480,92 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7001
8480
  }
7002
8481
  return withWorkspaceLifecycleLock(repoPath, async () => {
7003
8482
  let environmentStarted = false;
8483
+ let managedPlan;
8484
+ let previousManagedState;
8485
+ let managedComposeProject;
8486
+ let managedContainer;
8487
+ let previousRoutes = [];
8488
+ let candidateRoutesPublished = false;
8489
+ let transitionPhase = "validation";
8490
+ let managedProcessRegistry = [];
8491
+ let managedPostStartPlan;
8492
+ let managedRuntimeConfig;
8493
+ let managedConfigWritten = false;
8494
+ let managedWorkspaceEnv;
8495
+ let firstTransitionBaseline;
7004
8496
  try {
7005
8497
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
7006
8498
  let devpodId = target.devpodId;
7007
8499
  if (target.kind === "linked") {
7008
- const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7009
- if (!import_node_fs18.default.existsSync(overlayPath)) {
8500
+ const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8501
+ if (!import_node_fs20.default.existsSync(overlayPath)) {
7010
8502
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
7011
8503
  }
7012
8504
  }
7013
8505
  const managedPostStart = resolveManagedPostStartPlan(repoPath);
8506
+ managedPostStartPlan = managedPostStart;
7014
8507
  const runtime = loadRuntimeConfig(
7015
8508
  repoPath,
7016
8509
  target.kind === "primary" ? "" : target.workspace,
7017
8510
  options.profile
7018
8511
  );
8512
+ const managedRuntime = runtime.config.managedRuntime;
8513
+ managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
8514
+ managedProcessRegistry = managedRuntime?.processes ?? [];
8515
+ const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
8516
+ if (managedRuntime) {
8517
+ previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
8518
+ if (previousManagedState?.status === "degraded") {
8519
+ throw new Error(
8520
+ "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
8521
+ );
8522
+ }
8523
+ if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
8524
+ throw new Error(
8525
+ `Managed runtime state names DevPod '${previousManagedState.devpodId}', not the exact target '${target.devpodId ?? "(absent)"}'.`
8526
+ );
8527
+ }
8528
+ if (previousManagedState?.desired.services.some(
8529
+ (service) => !managedRuntime.devcontainer.profileServices.includes(service)
8530
+ )) {
8531
+ throw new Error("Managed runtime state contains an unregistered profile service.");
8532
+ }
8533
+ if (previousManagedState?.desired.processes.some(
8534
+ (process2) => !managedRuntime.processes.includes(process2)
8535
+ )) {
8536
+ throw new Error("Managed runtime state contains an unregistered process marker.");
8537
+ }
8538
+ previousRoutes = listHostRouteState().filter(
8539
+ (route) => sameWorkspacePath(route.repoPath, repoPath)
8540
+ );
8541
+ if (managedPostStart.kind !== "runtime") {
8542
+ throw new Error(
8543
+ "managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
8544
+ );
8545
+ }
8546
+ managedPlan = inspectManagedDevcontainerConfig({
8547
+ repoPath,
8548
+ config: runtime.config,
8549
+ profile: runtime.resolvedProfile,
8550
+ linked: target.kind === "linked"
8551
+ });
8552
+ managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
8553
+ if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
8554
+ throw new Error(
8555
+ `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.`
8556
+ );
8557
+ }
8558
+ if (!previousManagedState) {
8559
+ firstTransitionBaseline = captureFirstTransitionBaseline({
8560
+ repoPath,
8561
+ plan: managedPlan,
8562
+ processes: managedRuntime.processes
8563
+ });
8564
+ }
8565
+ }
7019
8566
  const apps = proxyAppsFromConfig(runtime.config);
7020
8567
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
7021
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path18.default.basename(repoPath)) ?? "app";
8568
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
7022
8569
  for (const [index, app] of apps.entries()) {
7023
8570
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
7024
8571
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -7027,6 +8574,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7027
8574
  );
7028
8575
  }
7029
8576
  }
8577
+ if (managedPlan) {
8578
+ writeManagedDevcontainerConfig(managedPlan);
8579
+ managedConfigWritten = true;
8580
+ }
7030
8581
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
7031
8582
  const ownership = target.kind === "linked" ? {
7032
8583
  workspace: target.workspace,
@@ -7044,6 +8595,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7044
8595
  devpodId = startDevpodWorkspace({
7045
8596
  repoPath,
7046
8597
  devpodId: requestedTarget.devpodId,
8598
+ devcontainerPath: managedPlan?.generatedRelativePath,
7047
8599
  recreate,
7048
8600
  quiet: options.quiet,
7049
8601
  ...requestedTarget.kind === "linked" ? {
@@ -7072,7 +8624,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7072
8624
  try {
7073
8625
  startAndProveAttachment();
7074
8626
  } catch (error) {
7075
- if (!target.hadExactDevpod) {
8627
+ if (managedPlan || !target.hadExactDevpod) {
7076
8628
  throw error;
7077
8629
  }
7078
8630
  container = await recreateAndPreflight();
@@ -7082,24 +8634,113 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7082
8634
  try {
7083
8635
  container = await preflight(0);
7084
8636
  } catch (error) {
7085
- if (!target.hadExactDevpod) {
8637
+ if (managedPlan || !target.hadExactDevpod) {
7086
8638
  throw error;
7087
8639
  }
7088
8640
  container = await recreateAndPreflight();
7089
8641
  recreated = true;
7090
8642
  }
7091
8643
  }
8644
+ managedContainer = container;
8645
+ if (managedPlan) {
8646
+ transitionPhase = "service-start";
8647
+ const initialContainers = inspectWorkspaceContainers();
8648
+ managedComposeProject = resolveComposeProject(
8649
+ initialContainers,
8650
+ repoPath,
8651
+ container.id,
8652
+ managedPlan.primaryService,
8653
+ managedPlan.composeFiles
8654
+ );
8655
+ if (previousManagedState && previousManagedState.composeProject !== managedComposeProject) {
8656
+ throw new Error(
8657
+ `Managed runtime state names Compose project '${previousManagedState.composeProject}', not '${managedComposeProject}'.`
8658
+ );
8659
+ }
8660
+ const missingServices = [];
8661
+ for (const service of managedPlan.desiredServices) {
8662
+ const matches = exactWorkspaceServiceContainers(
8663
+ initialContainers,
8664
+ repoPath,
8665
+ managedComposeProject,
8666
+ service,
8667
+ managedPlan.composeFiles
8668
+ );
8669
+ if (matches.length > 1) {
8670
+ throw new Error(
8671
+ `Managed service '${service}' has multiple exact workspace containers; refusing to reconcile it.`
8672
+ );
8673
+ }
8674
+ if (!matches[0]?.state.Running) missingServices.push(service);
8675
+ }
8676
+ startExactManagedServices({
8677
+ plan: managedPlan,
8678
+ composeProject: managedComposeProject,
8679
+ services: missingServices,
8680
+ quiet: options.quiet,
8681
+ workspace: managedWorkspaceEnv
8682
+ });
8683
+ await waitForManagedServices(
8684
+ managedPlan,
8685
+ repoPath,
8686
+ managedComposeProject,
8687
+ managedPlan.desiredServices,
8688
+ container.id,
8689
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8690
+ );
8691
+ container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
8692
+ managedContainer = container;
8693
+ }
8694
+ transitionPhase = "process-start";
7092
8695
  runManagedPostStart({
7093
8696
  plan: managedPostStart,
7094
8697
  container,
7095
8698
  quiet: options.quiet,
7096
- profile: runtime.profile
8699
+ profile: runtime.profile,
8700
+ ...managedPlan ? { processes: desiredProcesses } : {}
7097
8701
  });
8702
+ if (managedPlan && managedComposeProject) {
8703
+ const processRegistry = runtime.config.managedRuntime?.processes ?? [];
8704
+ const desiredProcessSet = new Set(desiredProcesses);
8705
+ for (const process2 of processRegistry) {
8706
+ if (desiredProcessSet.has(process2)) continue;
8707
+ runManagedProcessAction({
8708
+ container,
8709
+ name: process2,
8710
+ action: "stop",
8711
+ quiet: options.quiet
8712
+ });
8713
+ }
8714
+ for (const process2 of desiredProcesses) {
8715
+ const status = runManagedProcessAction({
8716
+ container,
8717
+ name: process2,
8718
+ action: "status",
8719
+ quiet: options.quiet
8720
+ });
8721
+ if (status !== "running") {
8722
+ throw new Error(`Managed process '${process2}' is not running (${status}).`);
8723
+ }
8724
+ }
8725
+ transitionPhase = "service-stop";
8726
+ stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
8727
+ assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
8728
+ await waitForManagedServices(
8729
+ managedPlan,
8730
+ repoPath,
8731
+ managedComposeProject,
8732
+ managedPlan.desiredServices,
8733
+ container.id,
8734
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8735
+ );
8736
+ }
8737
+ transitionPhase = "route-publication";
7098
8738
  const publication = await replacePublishedProxyRoutes(
7099
8739
  repoPath,
7100
8740
  runtime.config,
7101
8741
  target.workspace
7102
8742
  );
8743
+ candidateRoutesPublished = true;
7103
8744
  try {
7104
8745
  await waitForHttpRoutes(
7105
8746
  repoPath,
@@ -7107,6 +8748,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7107
8748
  options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
7108
8749
  );
7109
8750
  } catch (error) {
8751
+ if (managedPlan) {
8752
+ throw error;
8753
+ }
7110
8754
  replaceHostRoutesForRepo(repoPath, []);
7111
8755
  if (!target.hadExactDevpod || recreated) {
7112
8756
  throw error;
@@ -7137,6 +8781,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7137
8781
  if (!devpodId) {
7138
8782
  throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
7139
8783
  }
8784
+ if (managedPlan && managedComposeProject) {
8785
+ writeManagedRuntimeState({
8786
+ version: 1,
8787
+ repoPath,
8788
+ ...target.workspace !== void 0 ? { workspace: target.workspace } : {},
8789
+ devpodId,
8790
+ composeProject: managedComposeProject,
8791
+ profile: runtime.profile,
8792
+ desired: {
8793
+ apps: apps.map((app) => app.name).sort(),
8794
+ services: [...managedPlan.desiredProfileServices].sort(),
8795
+ processes: [...desiredProcesses].sort()
8796
+ },
8797
+ sourceConfigSha256: managedPlan.sourceConfigSha256,
8798
+ effectiveConfigSha256: managedPlan.effectiveConfigSha256,
8799
+ status: "ready",
8800
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8801
+ });
8802
+ }
8803
+ const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
8804
+ repoPath,
8805
+ workspace: target.workspace,
8806
+ config: runtime.config,
8807
+ profile: runtime.profile,
8808
+ resolvedProfile: runtime.resolvedProfile
8809
+ }) : void 0;
7140
8810
  return {
7141
8811
  kind: target.kind,
7142
8812
  repoPath,
@@ -7145,9 +8815,187 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7145
8815
  devpodId,
7146
8816
  urls,
7147
8817
  recreated,
7148
- tlsRefreshed: publication.tlsRefreshed
8818
+ tlsRefreshed: publication.tlsRefreshed,
8819
+ ...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
7149
8820
  };
7150
8821
  } catch (error) {
8822
+ if (managedPlan && managedContainer && managedComposeProject) {
8823
+ const rollbackErrors = [];
8824
+ const failedPhase = transitionPhase;
8825
+ transitionPhase = "rollback";
8826
+ const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
8827
+ const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
8828
+ const previousServiceSet = new Set(previousServices);
8829
+ const previousProcessSet = new Set(previousProcesses);
8830
+ try {
8831
+ if (managedConfigWritten) {
8832
+ if (previousManagedState && managedRuntimeConfig) {
8833
+ restorePreviousManagedConfig({
8834
+ repoPath,
8835
+ config: managedRuntimeConfig,
8836
+ linked,
8837
+ previousState: previousManagedState
8838
+ });
8839
+ } else if (managedRuntimeConfig) {
8840
+ restoreFirstTransitionManagedConfig({
8841
+ repoPath,
8842
+ config: managedRuntimeConfig,
8843
+ linked,
8844
+ services: previousServices
8845
+ });
8846
+ }
8847
+ }
8848
+ } catch (rollbackError) {
8849
+ rollbackErrors.push(
8850
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8851
+ );
8852
+ }
8853
+ try {
8854
+ const previousAllServices = [
8855
+ managedPlan.primaryService,
8856
+ ...managedPlan.baseServices,
8857
+ ...previousServices
8858
+ ];
8859
+ startExactManagedServices({
8860
+ plan: managedPlan,
8861
+ composeProject: managedComposeProject,
8862
+ services: previousAllServices,
8863
+ quiet: options.quiet,
8864
+ workspace: managedWorkspaceEnv
8865
+ });
8866
+ await waitForManagedServices(
8867
+ managedPlan,
8868
+ repoPath,
8869
+ managedComposeProject,
8870
+ previousAllServices,
8871
+ managedContainer.id,
8872
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8873
+ );
8874
+ } catch (rollbackError) {
8875
+ rollbackErrors.push(
8876
+ `services: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8877
+ );
8878
+ }
8879
+ try {
8880
+ if (!managedPostStartPlan) {
8881
+ throw new Error("Managed post-start plan disappeared during rollback.");
8882
+ }
8883
+ runManagedPostStart({
8884
+ plan: managedPostStartPlan,
8885
+ container: managedContainer,
8886
+ quiet: options.quiet,
8887
+ profile: previousManagedState?.profile ?? "full",
8888
+ processes: previousProcesses
8889
+ });
8890
+ for (const process2 of managedProcessRegistry) {
8891
+ if (previousProcessSet.has(process2)) continue;
8892
+ runManagedProcessAction({
8893
+ container: managedContainer,
8894
+ name: process2,
8895
+ action: "stop",
8896
+ quiet: options.quiet
8897
+ });
8898
+ }
8899
+ } catch (rollbackError) {
8900
+ rollbackErrors.push(
8901
+ `processes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8902
+ );
8903
+ }
8904
+ try {
8905
+ const containers = inspectWorkspaceContainers();
8906
+ for (const service of managedPlan.profileServices) {
8907
+ if (previousServiceSet.has(service)) continue;
8908
+ const matches = exactWorkspaceServiceContainers(
8909
+ containers,
8910
+ repoPath,
8911
+ managedComposeProject,
8912
+ service,
8913
+ managedPlan.composeFiles
8914
+ );
8915
+ if (matches.length > 1) {
8916
+ throw new Error(`service '${service}' has multiple exact containers`);
8917
+ }
8918
+ if (matches[0]?.state.Running) stopExactManagedService(matches[0].id, service);
8919
+ }
8920
+ assertDroppedManagedServicesStopped(
8921
+ {
8922
+ ...managedPlan,
8923
+ desiredProfileServices: previousServices,
8924
+ desiredServices: [
8925
+ managedPlan.primaryService,
8926
+ ...managedPlan.baseServices,
8927
+ ...previousServices
8928
+ ]
8929
+ },
8930
+ repoPath,
8931
+ managedComposeProject
8932
+ );
8933
+ } catch (rollbackError) {
8934
+ rollbackErrors.push(
8935
+ `service cleanup: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8936
+ );
8937
+ }
8938
+ if (candidateRoutesPublished) {
8939
+ try {
8940
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8941
+ } catch (rollbackError) {
8942
+ rollbackErrors.push(
8943
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8944
+ );
8945
+ }
8946
+ }
8947
+ if (rollbackErrors.length > 0 && previousManagedState) {
8948
+ try {
8949
+ markManagedRuntimeDegraded(previousManagedState, failedPhase);
8950
+ } catch (stateError) {
8951
+ rollbackErrors.push(
8952
+ `state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
8953
+ );
8954
+ }
8955
+ }
8956
+ const original = error instanceof Error ? error.message : String(error);
8957
+ const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : " Candidate runtime was rolled back.";
8958
+ throw new Error(`${original}${suffix}`);
8959
+ }
8960
+ if (managedPlan) {
8961
+ const original = error instanceof Error ? error.message : String(error);
8962
+ const rollbackErrors = [];
8963
+ if (managedConfigWritten) {
8964
+ try {
8965
+ if (previousManagedState && managedRuntimeConfig) {
8966
+ restorePreviousManagedConfig({
8967
+ repoPath,
8968
+ config: managedRuntimeConfig,
8969
+ linked,
8970
+ previousState: previousManagedState
8971
+ });
8972
+ } else if (!environmentStarted) {
8973
+ removeManagedDevcontainerConfig(managedPlan);
8974
+ }
8975
+ } catch (rollbackError) {
8976
+ rollbackErrors.push(
8977
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8978
+ );
8979
+ }
8980
+ }
8981
+ if (candidateRoutesPublished) {
8982
+ try {
8983
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8984
+ } catch (rollbackError) {
8985
+ rollbackErrors.push(
8986
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8987
+ );
8988
+ }
8989
+ }
8990
+ if (rollbackErrors.length > 0) {
8991
+ throw new Error(
8992
+ `${original} Rollback left degraded drift: ${rollbackErrors.join("; ")}.`
8993
+ );
8994
+ }
8995
+ throw new Error(
8996
+ `${original} Managed runtime transition did not reach a rollback boundary.`
8997
+ );
8998
+ }
7151
8999
  if (environmentStarted) {
7152
9000
  try {
7153
9001
  replaceHostRoutesForRepo(repoPath, []);
@@ -7161,19 +9009,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7161
9009
  }
7162
9010
  });
7163
9011
  }
7164
- var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
9012
+ var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
7165
9013
  var init_workspace_ensure = __esm({
7166
9014
  "src/core/workspace-ensure.ts"() {
7167
9015
  "use strict";
7168
- import_node_child_process14 = require("child_process");
7169
- import_node_fs18 = __toESM(require("fs"));
7170
- import_node_path18 = __toESM(require("path"));
9016
+ import_node_child_process15 = require("child_process");
9017
+ import_node_fs20 = __toESM(require("fs"));
9018
+ import_node_path20 = __toESM(require("path"));
9019
+ init_devcontainer_profile();
7171
9020
  init_devpod_environment();
7172
9021
  init_devpod_mutation();
7173
9022
  init_devpod_workspaces();
7174
9023
  init_host_routes();
7175
9024
  init_http_route_probe();
7176
9025
  init_managed_post_start();
9026
+ init_managed_runtime_state();
9027
+ init_managed_runtime_status();
7177
9028
  init_repo_config();
7178
9029
  init_route_publication();
7179
9030
  init_router();
@@ -7244,20 +9095,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
7244
9095
  );
7245
9096
  }
7246
9097
  function defaultWorktreePath(mainRepo, ws) {
7247
- return import_node_path19.default.join(mainRepo, "trees", ws);
9098
+ return import_node_path21.default.join(mainRepo, "trees", ws);
7248
9099
  }
7249
9100
  function assertDefaultWorktreeRootIgnored(mainRepo) {
7250
- const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9101
+ const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
7251
9102
  encoding: "utf-8"
7252
9103
  });
7253
9104
  if (ignored.status !== 0) {
7254
9105
  throw new Error(
7255
- `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.`
9106
+ `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.`
7256
9107
  );
7257
9108
  }
7258
9109
  }
7259
9110
  function legacyDefaultWorktreePath(mainRepo, ws) {
7260
- return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
9111
+ return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
7261
9112
  }
7262
9113
  function teardownFallbackPath(mainRepo, workspace) {
7263
9114
  const candidates = [
@@ -7369,13 +9220,13 @@ function assertFullDownPreflight(mainRepo, target) {
7369
9220
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
7370
9221
  throw new Error("Refusing to remove the primary Git checkout.");
7371
9222
  }
7372
- if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
9223
+ if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
7373
9224
  if (target.worktree.locked) {
7374
9225
  throw new Error(
7375
9226
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
7376
9227
  );
7377
9228
  }
7378
- const status = (0, import_node_child_process15.spawnSync)(
9229
+ const status = (0, import_node_child_process16.spawnSync)(
7379
9230
  "git",
7380
9231
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
7381
9232
  { encoding: "utf-8" }
@@ -7396,8 +9247,8 @@ async function workspaceUp(branch, opts = {}) {
7396
9247
  if (!ws) {
7397
9248
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
7398
9249
  }
7399
- const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
7400
- if (import_node_fs19.default.existsSync(worktreePath)) {
9250
+ const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9251
+ if (import_node_fs21.default.existsSync(worktreePath)) {
7401
9252
  const registered = listGitWorktrees(mainRepo).find(
7402
9253
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
7403
9254
  );
@@ -7415,11 +9266,11 @@ async function workspaceUp(branch, opts = {}) {
7415
9266
  if (!opts.path) {
7416
9267
  assertDefaultWorktreeRootIgnored(mainRepo);
7417
9268
  }
7418
- const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9269
+ const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
7419
9270
  encoding: "utf-8"
7420
9271
  });
7421
9272
  if (add.status !== 0) {
7422
- const addNew = (0, import_node_child_process15.spawnSync)(
9273
+ const addNew = (0, import_node_child_process16.spawnSync)(
7423
9274
  "git",
7424
9275
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
7425
9276
  {
@@ -7524,8 +9375,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7524
9375
  opts.quiet
7525
9376
  );
7526
9377
  if (removeWorktree) {
7527
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath)) {
7528
- const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
9378
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
9379
+ const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
7529
9380
  encoding: "utf-8"
7530
9381
  });
7531
9382
  if (rm.status !== 0) {
@@ -7543,7 +9394,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7543
9394
  }
7544
9395
  return result;
7545
9396
  };
7546
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9397
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
7547
9398
  }
7548
9399
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
7549
9400
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -7578,13 +9429,13 @@ async function workspaceStop(target, opts = {}) {
7578
9429
  async function workspaceDown(target, opts = {}) {
7579
9430
  return runWorkspaceLifecycle("down", target, opts);
7580
9431
  }
7581
- var import_node_child_process15, import_node_fs19, import_node_path19;
9432
+ var import_node_child_process16, import_node_fs21, import_node_path21;
7582
9433
  var init_workspace_lifecycle = __esm({
7583
9434
  "src/core/workspace-lifecycle.ts"() {
7584
9435
  "use strict";
7585
- import_node_child_process15 = require("child_process");
7586
- import_node_fs19 = __toESM(require("fs"));
7587
- import_node_path19 = __toESM(require("path"));
9436
+ import_node_child_process16 = require("child_process");
9437
+ import_node_fs21 = __toESM(require("fs"));
9438
+ import_node_path21 = __toESM(require("path"));
7588
9439
  init_devpod_mutation();
7589
9440
  init_devpod_workspaces();
7590
9441
  init_repo_config();
@@ -7711,7 +9562,7 @@ function ensureGuidance(repoPath) {
7711
9562
  return `Run 'devrouter ensure ${repoPath}' first.`;
7712
9563
  }
7713
9564
  function assertRunningDevpod(devpodId, repoPath) {
7714
- const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
9565
+ const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
7715
9566
  encoding: "utf-8"
7716
9567
  });
7717
9568
  if (result.status !== 0) {
@@ -7749,7 +9600,7 @@ async function devpodExec(repoPath, command) {
7749
9600
  }
7750
9601
  assertRunningDevpod(devpod.id, repoPath);
7751
9602
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
7752
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
9603
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto7.randomUUID)()}__:`;
7753
9604
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
7754
9605
  const literalCommand = command.map(quotePosixArg).join(" ");
7755
9606
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -7767,7 +9618,7 @@ async function devpodExec(repoPath, command) {
7767
9618
  wrappedCommand
7768
9619
  ];
7769
9620
  return new Promise((resolve, reject) => {
7770
- const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9621
+ const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
7771
9622
  let pending = Buffer.alloc(0);
7772
9623
  let statusBytes = Buffer.alloc(0);
7773
9624
  let readingStatus = false;
@@ -7826,12 +9677,12 @@ async function devpodExec(repoPath, command) {
7826
9677
  });
7827
9678
  });
7828
9679
  }
7829
- var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
9680
+ var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
7830
9681
  var init_devpod_exec = __esm({
7831
9682
  "src/core/devpod-exec.ts"() {
7832
9683
  "use strict";
7833
- import_node_child_process16 = require("child_process");
7834
- import_node_crypto5 = require("crypto");
9684
+ import_node_child_process17 = require("child_process");
9685
+ import_node_crypto7 = require("crypto");
7835
9686
  init_devpod_environment();
7836
9687
  init_devpod_workspaces();
7837
9688
  init_workspace();
@@ -8015,7 +9866,7 @@ async function runOpenCommand(name) {
8015
9866
  );
8016
9867
  return;
8017
9868
  }
8018
- const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
9869
+ const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
8019
9870
  if (result.status !== 0) {
8020
9871
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
8021
9872
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -8023,11 +9874,11 @@ async function runOpenCommand(name) {
8023
9874
  process.stdout.write(`Opened ${url}
8024
9875
  `);
8025
9876
  }
8026
- var import_node_child_process17;
9877
+ var import_node_child_process18;
8027
9878
  var init_open = __esm({
8028
9879
  "src/commands/open.ts"() {
8029
9880
  "use strict";
8030
- import_node_child_process17 = require("child_process");
9881
+ import_node_child_process18 = require("child_process");
8031
9882
  init_docker();
8032
9883
  init_host_routes();
8033
9884
  init_repo_config();
@@ -8050,7 +9901,7 @@ async function runLogsCommand(options) {
8050
9901
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
8051
9902
  if (options.follow) {
8052
9903
  args.splice(1, 0, "-f");
8053
- const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
9904
+ const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
8054
9905
  const onSignal = () => {
8055
9906
  child.kill("SIGTERM");
8056
9907
  };
@@ -8064,17 +9915,17 @@ async function runLogsCommand(options) {
8064
9915
  });
8065
9916
  });
8066
9917
  } else {
8067
- const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
9918
+ const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
8068
9919
  if (result.status !== 0) {
8069
9920
  throw new Error("Failed to retrieve router logs.");
8070
9921
  }
8071
9922
  }
8072
9923
  }
8073
- var import_node_child_process18;
9924
+ var import_node_child_process19;
8074
9925
  var init_logs = __esm({
8075
9926
  "src/commands/logs.ts"() {
8076
9927
  "use strict";
8077
- import_node_child_process18 = require("child_process");
9928
+ import_node_child_process19 = require("child_process");
8078
9929
  init_docker();
8079
9930
  init_router();
8080
9931
  }
@@ -8114,17 +9965,17 @@ function asRecord3(value) {
8114
9965
  return value;
8115
9966
  }
8116
9967
  function readJson(filePath) {
8117
- if (!import_node_fs20.default.existsSync(filePath)) {
9968
+ if (!import_node_fs22.default.existsSync(filePath)) {
8118
9969
  return void 0;
8119
9970
  }
8120
9971
  try {
8121
- return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
9972
+ return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
8122
9973
  } catch {
8123
9974
  return void 0;
8124
9975
  }
8125
9976
  }
8126
9977
  function relative(repoPath, filePath) {
8127
- return import_node_path20.default.relative(repoPath, filePath) || ".";
9978
+ return import_node_path22.default.relative(repoPath, filePath) || ".";
8128
9979
  }
8129
9980
  function redactEnvAssignments(value) {
8130
9981
  return value.replace(
@@ -8165,7 +10016,7 @@ function inspectPackageManager(repoPath, pkg) {
8165
10016
  ["bun.lock", "bun"]
8166
10017
  ];
8167
10018
  for (const [fileName, name] of lockfiles) {
8168
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10019
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
8169
10020
  return { name, source: fileName };
8170
10021
  }
8171
10022
  }
@@ -8180,9 +10031,9 @@ function inspectNode(repoPath, pkg) {
8180
10031
  if (typeof engines?.node === "string") {
8181
10032
  return { version: engines.node, source: "package.json:engines.node" };
8182
10033
  }
8183
- const nvmrc = import_node_path20.default.join(repoPath, ".nvmrc");
8184
- if (import_node_fs20.default.existsSync(nvmrc)) {
8185
- const version = import_node_fs20.default.readFileSync(nvmrc, "utf-8").trim();
10034
+ const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
10035
+ if (import_node_fs22.default.existsSync(nvmrc)) {
10036
+ const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
8186
10037
  return { version, source: ".nvmrc" };
8187
10038
  }
8188
10039
  return void 0;
@@ -8243,7 +10094,7 @@ function configuredComposeFiles(repoPath) {
8243
10094
  const files = config.apps.filter(
8244
10095
  (app) => app.runtime === "docker"
8245
10096
  ).flatMap((app) => app.docker.composeFiles).filter(
8246
- (fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
10097
+ (fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
8247
10098
  );
8248
10099
  return Array.from(new Set(files));
8249
10100
  } catch {
@@ -8261,10 +10112,10 @@ function composeFiles(repoPath) {
8261
10112
  ...configuredComposeFiles(repoPath)
8262
10113
  ];
8263
10114
  return Array.from(new Set(candidates)).filter(
8264
- (fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
10115
+ (fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
8265
10116
  );
8266
10117
  }
8267
- function stringArray(value) {
10118
+ function stringArray2(value) {
8268
10119
  if (!Array.isArray(value)) {
8269
10120
  return [];
8270
10121
  }
@@ -8299,7 +10150,7 @@ function inspectServices(repoPath) {
8299
10150
  const services = [];
8300
10151
  for (const fileName of composeFiles(repoPath)) {
8301
10152
  try {
8302
- const parsed = import_yaml5.default.parse(import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8"));
10153
+ const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
8303
10154
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
8304
10155
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
8305
10156
  const service = asRecord3(value);
@@ -8313,7 +10164,7 @@ function inspectServices(repoPath) {
8313
10164
  kind: kind.kind,
8314
10165
  source: fileName,
8315
10166
  image,
8316
- ports: stringArray(service.ports),
10167
+ ports: stringArray2(service.ports),
8317
10168
  hasHealthcheck: service.healthcheck !== void 0,
8318
10169
  envNames: envNames(service.environment),
8319
10170
  confidence: kind.confidence
@@ -8334,8 +10185,8 @@ function inspectServices(repoPath) {
8334
10185
  return services;
8335
10186
  }
8336
10187
  function inspectEnvFiles(repoPath) {
8337
- const files = import_node_fs20.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
8338
- const content = import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8");
10188
+ const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10189
+ const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
8339
10190
  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();
8340
10191
  return { path: fileName, names };
8341
10192
  });
@@ -8349,18 +10200,18 @@ function inspectEnvFiles(repoPath) {
8349
10200
  };
8350
10201
  }
8351
10202
  function inspectDevcontainer(repoPath) {
8352
- const dir = import_node_path20.default.join(repoPath, ".devcontainer");
8353
- if (!import_node_fs20.default.existsSync(dir)) {
10203
+ const dir = import_node_path22.default.join(repoPath, ".devcontainer");
10204
+ if (!import_node_fs22.default.existsSync(dir)) {
8354
10205
  return { exists: false, files: [] };
8355
10206
  }
8356
10207
  return {
8357
10208
  exists: true,
8358
- 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}`)
10209
+ 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}`)
8359
10210
  };
8360
10211
  }
8361
10212
  function inspectDevrouter(repoPath) {
8362
10213
  const configPath = getRepoConfigPath(repoPath);
8363
- if (!import_node_fs20.default.existsSync(configPath)) {
10214
+ if (!import_node_fs22.default.existsSync(configPath)) {
8364
10215
  return {
8365
10216
  exists: false,
8366
10217
  configPath,
@@ -8404,15 +10255,15 @@ function inspectAgentGuidance(repoPath) {
8404
10255
  ["AGENTS.md", "agents"],
8405
10256
  ["CLAUDE.md", "claude"]
8406
10257
  ]) {
8407
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10258
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
8408
10259
  results.push({ path: fileName, kind });
8409
10260
  }
8410
10261
  }
8411
- const skillsDir = import_node_path20.default.join(repoPath, ".agents", "skills");
8412
- if (import_node_fs20.default.existsSync(skillsDir)) {
8413
- for (const name of import_node_fs20.default.readdirSync(skillsDir).sort()) {
8414
- const skillPath = import_node_path20.default.join(skillsDir, name, "SKILL.md");
8415
- if (import_node_fs20.default.existsSync(skillPath)) {
10262
+ const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
10263
+ if (import_node_fs22.default.existsSync(skillsDir)) {
10264
+ for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
10265
+ const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
10266
+ if (import_node_fs22.default.existsSync(skillPath)) {
8416
10267
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
8417
10268
  }
8418
10269
  }
@@ -8455,7 +10306,7 @@ function buildIssues(report) {
8455
10306
  }
8456
10307
  function inspectRepo(options = {}) {
8457
10308
  const repoPath = resolveRepoPath(options.repo);
8458
- const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
10309
+ const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
8459
10310
  const scripts = inspectScripts(pkg);
8460
10311
  const reportWithoutIssues = {
8461
10312
  repoPath,
@@ -8474,13 +10325,13 @@ function inspectRepo(options = {}) {
8474
10325
  issues: buildIssues(reportWithoutIssues)
8475
10326
  };
8476
10327
  }
8477
- var import_node_fs20, import_node_path20, import_yaml5;
10328
+ var import_node_fs22, import_node_path22, import_yaml6;
8478
10329
  var init_repo_inspect = __esm({
8479
10330
  "src/core/repo-inspect.ts"() {
8480
10331
  "use strict";
8481
- import_node_fs20 = __toESM(require("fs"));
8482
- import_node_path20 = __toESM(require("path"));
8483
- import_yaml5 = __toESM(require("yaml"));
10332
+ import_node_fs22 = __toESM(require("fs"));
10333
+ import_node_path22 = __toESM(require("path"));
10334
+ import_yaml6 = __toESM(require("yaml"));
8484
10335
  init_repo_config();
8485
10336
  }
8486
10337
  });
@@ -8584,7 +10435,7 @@ function requiredFileChecks(repoPath) {
8584
10435
  ".devcontainer/docker-compose.yml",
8585
10436
  ".devrouter.yml"
8586
10437
  ];
8587
- const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
10438
+ const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
8588
10439
  return {
8589
10440
  id: "repo.devcontainer.verify-files",
8590
10441
  level: missing.length === 0 ? "ok" : "error",
@@ -8803,12 +10654,12 @@ async function verifyDevcontainer(options = {}) {
8803
10654
  nextSteps: collectNextSteps3(checks)
8804
10655
  };
8805
10656
  }
8806
- var import_node_fs21, import_node_path21;
10657
+ var import_node_fs23, import_node_path23;
8807
10658
  var init_devcontainer_verify = __esm({
8808
10659
  "src/core/devcontainer-verify.ts"() {
8809
10660
  "use strict";
8810
- import_node_fs21 = __toESM(require("fs"));
8811
- import_node_path21 = __toESM(require("path"));
10661
+ import_node_fs23 = __toESM(require("fs"));
10662
+ import_node_path23 = __toESM(require("path"));
8812
10663
  init_capabilities();
8813
10664
  init_doctor();
8814
10665
  init_host_routes();
@@ -9104,7 +10955,7 @@ function packageManagerIssues(repo) {
9104
10955
  }
9105
10956
  function plannedFiles(repoPath, version) {
9106
10957
  const repo = inspectRepo({ repo: repoPath });
9107
- const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
10958
+ const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
9108
10959
  const nodeMajor = majorVersion(repo.node?.version, "24");
9109
10960
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
9110
10961
  const port = inferPort2(repo);
@@ -9149,8 +11000,8 @@ function plannedFiles(repoPath, version) {
9149
11000
  };
9150
11001
  }
9151
11002
  function classifyFile(repoPath, file) {
9152
- const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
9153
- if (!import_node_fs22.default.existsSync(absolutePath)) {
11003
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11004
+ if (!import_node_fs24.default.existsSync(absolutePath)) {
9154
11005
  return {
9155
11006
  path: file.relativePath,
9156
11007
  action: "create",
@@ -9158,7 +11009,7 @@ function classifyFile(repoPath, file) {
9158
11009
  bytes: Buffer.byteLength(file.content)
9159
11010
  };
9160
11011
  }
9161
- const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
11012
+ const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
9162
11013
  if (!current.includes(MANAGED_MARKER2)) {
9163
11014
  return {
9164
11015
  path: file.relativePath,
@@ -9215,11 +11066,11 @@ function buildPlan(repoPath, dryRun, version) {
9215
11066
  };
9216
11067
  }
9217
11068
  function writeFile(repoPath, file) {
9218
- const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
9219
- import_node_fs22.default.mkdirSync(import_node_path22.default.dirname(absolutePath), { recursive: true });
9220
- import_node_fs22.default.writeFileSync(absolutePath, file.content, "utf-8");
11069
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11070
+ import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
11071
+ import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
9221
11072
  if (file.executable) {
9222
- import_node_fs22.default.chmodSync(absolutePath, 493);
11073
+ import_node_fs24.default.chmodSync(absolutePath, 493);
9223
11074
  }
9224
11075
  }
9225
11076
  function writeDevcontainer(options = {}) {
@@ -9257,12 +11108,12 @@ function writeDevcontainer(options = {}) {
9257
11108
  nextSteps: postWriteNextSteps(repoPath)
9258
11109
  };
9259
11110
  }
9260
- var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11111
+ var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
9261
11112
  var init_devcontainer_write = __esm({
9262
11113
  "src/core/devcontainer-write.ts"() {
9263
11114
  "use strict";
9264
- import_node_fs22 = __toESM(require("fs"));
9265
- import_node_path22 = __toESM(require("path"));
11115
+ import_node_fs24 = __toESM(require("fs"));
11116
+ import_node_path24 = __toESM(require("path"));
9266
11117
  init_repo_config();
9267
11118
  init_repo_inspect();
9268
11119
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -9653,7 +11504,7 @@ function sanitizeRouterId(value) {
9653
11504
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
9654
11505
  }
9655
11506
  function repoHash(repoPath) {
9656
- return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
11507
+ return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
9657
11508
  }
9658
11509
  function asDockerApp(app) {
9659
11510
  return app.runtime === "docker";
@@ -9732,11 +11583,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
9732
11583
  if (dockerApps.length === 0) {
9733
11584
  throw new Error("No docker apps selected to prepare compose overlay.");
9734
11585
  }
9735
- const cachePath = import_node_path23.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
9736
- import_node_fs23.default.mkdirSync(cachePath, { recursive: true });
9737
- const overlayPath = import_node_path23.default.join(cachePath, "compose.devrouter.yml");
11586
+ const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
11587
+ import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
11588
+ const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
9738
11589
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
9739
- import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
11590
+ import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
9740
11591
  return {
9741
11592
  overlayPath,
9742
11593
  composeFiles: ensureComposeFiles(dockerApps),
@@ -9750,7 +11601,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
9750
11601
  fileArgs.push("-f", resolved);
9751
11602
  }
9752
11603
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
9753
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11604
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9754
11605
  encoding: "utf-8",
9755
11606
  cwd: repoPath
9756
11607
  });
@@ -9778,7 +11629,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
9778
11629
  "--services",
9779
11630
  ...services
9780
11631
  ];
9781
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11632
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9782
11633
  encoding: "utf-8",
9783
11634
  cwd: repoPath
9784
11635
  });
@@ -9804,7 +11655,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
9804
11655
  fileArgs.push("-f", resolved);
9805
11656
  }
9806
11657
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
9807
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11658
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9808
11659
  encoding: "utf-8",
9809
11660
  cwd: repoPath
9810
11661
  });
@@ -9830,7 +11681,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
9830
11681
  String(tail),
9831
11682
  ...services
9832
11683
  ];
9833
- (0, import_node_child_process19.spawnSync)("docker", args, {
11684
+ (0, import_node_child_process20.spawnSync)("docker", args, {
9834
11685
  stdio: "inherit",
9835
11686
  cwd: repoPath
9836
11687
  });
@@ -9842,7 +11693,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9842
11693
  fileArgs.push("-f", resolved);
9843
11694
  }
9844
11695
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
9845
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11696
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9846
11697
  encoding: "utf-8",
9847
11698
  cwd: repoPath
9848
11699
  });
@@ -9856,15 +11707,15 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9856
11707
  const port = Number(match[1]);
9857
11708
  return Number.isInteger(port) && port > 0 ? port : void 0;
9858
11709
  }
9859
- var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
11710
+ var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
9860
11711
  var init_docker_run = __esm({
9861
11712
  "src/core/docker-run.ts"() {
9862
11713
  "use strict";
9863
- import_node_child_process19 = require("child_process");
9864
- import_node_crypto6 = require("crypto");
9865
- import_node_fs23 = __toESM(require("fs"));
9866
- import_node_path23 = __toESM(require("path"));
9867
- import_yaml6 = __toESM(require("yaml"));
11714
+ import_node_child_process20 = require("child_process");
11715
+ import_node_crypto8 = require("crypto");
11716
+ import_node_fs25 = __toESM(require("fs"));
11717
+ import_node_path25 = __toESM(require("path"));
11718
+ import_yaml7 = __toESM(require("yaml"));
9868
11719
  init_docker_error_guidance();
9869
11720
  init_paths();
9870
11721
  init_router();
@@ -9953,7 +11804,7 @@ function isProcessRunning(pid) {
9953
11804
  }
9954
11805
  }
9955
11806
  function readProcessTree(rootPid) {
9956
- const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
11807
+ const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
9957
11808
  if (result.status !== 0) {
9958
11809
  return [rootPid];
9959
11810
  }
@@ -10064,12 +11915,12 @@ function detectListeningPorts(pids) {
10064
11915
  if (pids.length === 0) {
10065
11916
  return [];
10066
11917
  }
10067
- const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
11918
+ const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
10068
11919
  encoding: "utf-8"
10069
11920
  });
10070
11921
  if (result.error && result.error.code === "ENOENT") {
10071
11922
  if (process.platform === "linux") {
10072
- const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
11923
+ const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
10073
11924
  if (ssResult.status === 0 && ssResult.stdout) {
10074
11925
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
10075
11926
  }
@@ -10128,7 +11979,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
10128
11979
  app.hostRun.command,
10129
11980
  true
10130
11981
  ) : app.hostRun.command;
10131
- const child = (0, import_node_child_process20.spawn)(spawnCommand, {
11982
+ const child = (0, import_node_child_process21.spawn)(spawnCommand, {
10132
11983
  cwd: commandCwd,
10133
11984
  stdio: "inherit",
10134
11985
  shell: true,
@@ -10496,7 +12347,7 @@ async function execWithAppEnv(options) {
10496
12347
  options.command[0],
10497
12348
  true
10498
12349
  );
10499
- child = (0, import_node_child_process20.spawn)(wrapped, {
12350
+ child = (0, import_node_child_process21.spawn)(wrapped, {
10500
12351
  cwd: deps.repoPath,
10501
12352
  stdio: "inherit",
10502
12353
  shell: true,
@@ -10510,7 +12361,7 @@ async function execWithAppEnv(options) {
10510
12361
  false
10511
12362
  );
10512
12363
  const [cmd, ...wrappedArgs] = wrapped;
10513
- child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
12364
+ child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
10514
12365
  cwd: deps.repoPath,
10515
12366
  stdio: "inherit",
10516
12367
  shell: false,
@@ -10518,7 +12369,7 @@ async function execWithAppEnv(options) {
10518
12369
  });
10519
12370
  }
10520
12371
  } else if (options.shell) {
10521
- child = (0, import_node_child_process20.spawn)(options.command[0], {
12372
+ child = (0, import_node_child_process21.spawn)(options.command[0], {
10522
12373
  cwd: deps.repoPath,
10523
12374
  stdio: "inherit",
10524
12375
  shell: true,
@@ -10526,7 +12377,7 @@ async function execWithAppEnv(options) {
10526
12377
  });
10527
12378
  } else {
10528
12379
  const [command, ...args] = options.command;
10529
- child = (0, import_node_child_process20.spawn)(command, args, {
12380
+ child = (0, import_node_child_process21.spawn)(command, args, {
10530
12381
  cwd: deps.repoPath,
10531
12382
  stdio: "inherit",
10532
12383
  shell: false,
@@ -10547,11 +12398,11 @@ async function execWithAppEnv(options) {
10547
12398
  deps.stopDeps();
10548
12399
  }
10549
12400
  }
10550
- var import_node_child_process20, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
12401
+ var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
10551
12402
  var init_app_run = __esm({
10552
12403
  "src/core/app-run.ts"() {
10553
12404
  "use strict";
10554
- import_node_child_process20 = require("child_process");
12405
+ import_node_child_process21 = require("child_process");
10555
12406
  import_node_net = __toESM(require("net"));
10556
12407
  import_node_process = require("process");
10557
12408
  import_promises = require("readline/promises");
@@ -10685,13 +12536,13 @@ function measureWorktreeConsumption(worktreePath, options) {
10685
12536
  const startedAt = Date.now();
10686
12537
  let rootStat;
10687
12538
  try {
10688
- rootStat = import_node_fs24.default.lstatSync(worktreePath);
12539
+ rootStat = import_node_fs26.default.lstatSync(worktreePath);
10689
12540
  } catch (error) {
10690
12541
  return { status: "unknown", reason: describeError(error, worktreePath) };
10691
12542
  }
10692
12543
  let rootEntries;
10693
12544
  try {
10694
- rootEntries = import_node_fs24.default.readdirSync(worktreePath, { withFileTypes: true });
12545
+ rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
10695
12546
  } catch (error) {
10696
12547
  return { status: "unknown", reason: describeError(error, worktreePath) };
10697
12548
  }
@@ -10717,10 +12568,10 @@ function measureWorktreeConsumption(worktreePath, options) {
10717
12568
  timedOut = true;
10718
12569
  break;
10719
12570
  }
10720
- const entryPath = import_node_path24.default.join(dirPath, entry.name);
12571
+ const entryPath = import_node_path26.default.join(dirPath, entry.name);
10721
12572
  let entryStat;
10722
12573
  try {
10723
- entryStat = import_node_fs24.default.lstatSync(entryPath);
12574
+ entryStat = import_node_fs26.default.lstatSync(entryPath);
10724
12575
  } catch (error) {
10725
12576
  if (error?.code === "ENOENT") continue;
10726
12577
  unreadableReason = describeIncompleteWalk(error);
@@ -10730,7 +12581,7 @@ function measureWorktreeConsumption(worktreePath, options) {
10730
12581
  if (!entryStat.isDirectory()) continue;
10731
12582
  let childEntries;
10732
12583
  try {
10733
- childEntries = import_node_fs24.default.readdirSync(entryPath, { withFileTypes: true });
12584
+ childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
10734
12585
  } catch (error) {
10735
12586
  unreadableReason = describeIncompleteWalk(error);
10736
12587
  break;
@@ -10794,12 +12645,12 @@ function describeError(error, worktreePath) {
10794
12645
  }
10795
12646
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
10796
12647
  }
10797
- var import_node_fs24, import_node_path24, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
12648
+ var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
10798
12649
  var init_workspace_consumption = __esm({
10799
12650
  "src/core/workspace-consumption.ts"() {
10800
12651
  "use strict";
10801
- import_node_fs24 = __toESM(require("fs"));
10802
- import_node_path24 = __toESM(require("path"));
12652
+ import_node_fs26 = __toESM(require("fs"));
12653
+ import_node_path26 = __toESM(require("path"));
10803
12654
  init_devpod_environment();
10804
12655
  DEFAULT_DEADLINE_MS = 1e4;
10805
12656
  BLOCK_SIZE_BYTES = 512;
@@ -10900,7 +12751,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
10900
12751
  }
10901
12752
  function readGitSnapshot(worktree, commandRunner) {
10902
12753
  const comparablePath = comparableWorkspacePath(worktree.path);
10903
- if (worktree.prunable || !import_node_fs25.default.existsSync(comparablePath)) {
12754
+ if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
10904
12755
  return { worktree, checkout: "missing", head: null, committerDate: null };
10905
12756
  }
10906
12757
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -11431,7 +13282,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11431
13282
  containers = measureContainersFn(worktreePaths);
11432
13283
  } catch (error) {
11433
13284
  const reason = `container measurement failed: ${describeCause(error)}`;
11434
- containers = new Map(worktreePaths.map((path24) => [path24, unknownContainers(reason)]));
13285
+ containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
11435
13286
  }
11436
13287
  }
11437
13288
  const rows = records.map((record) => {
@@ -11479,12 +13330,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11479
13330
  workspaces: rows
11480
13331
  };
11481
13332
  }
11482
- var import_node_child_process21, import_node_fs25, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
13333
+ var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
11483
13334
  var init_workspace_cleanup = __esm({
11484
13335
  "src/core/workspace-cleanup.ts"() {
11485
13336
  "use strict";
11486
- import_node_child_process21 = require("child_process");
11487
- import_node_fs25 = __toESM(require("fs"));
13337
+ import_node_child_process22 = require("child_process");
13338
+ import_node_fs27 = __toESM(require("fs"));
11488
13339
  init_devpod_workspaces();
11489
13340
  init_host_routes();
11490
13341
  init_repo_config();
@@ -11501,7 +13352,7 @@ var init_workspace_cleanup = __esm({
11501
13352
  ];
11502
13353
  defaultCommandRunner = (command, args, input2) => {
11503
13354
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
11504
- const result = (0, import_node_child_process21.spawnSync)(command, args, {
13355
+ const result = (0, import_node_child_process22.spawnSync)(command, args, {
11505
13356
  encoding: "utf-8",
11506
13357
  env: environment,
11507
13358
  ...input2 === void 0 ? {} : { input: input2 }
@@ -11661,7 +13512,7 @@ var init_version = __esm({
11661
13512
 
11662
13513
  // src/cli.ts
11663
13514
  var import_commander = require("commander");
11664
- var CLI_VERSION = true ? "0.0.38" : "0.0.0-dev";
13515
+ var CLI_VERSION = true ? "0.0.40" : "0.0.0-dev";
11665
13516
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
11666
13517
  function withErrorHandling(action2) {
11667
13518
  return async (...args) => {