@devrouter/cli 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -48,6 +48,7 @@ function buildDevrouterSection() {
48
48
  "- `devrouter tls install` (required when repo defines tcp/postgres apps)",
49
49
  "- `devrouter app ls --repo .`",
50
50
  "- Primary or linked devcontainer checkout: `devrouter ensure . --json`",
51
+ "- Managed selective profile: `devrouter ensure . --profile <name> --json`",
51
52
  "- Host/docker runtime app only: `devrouter app run <host-app> --repo . --yes`",
52
53
  "- `devrouter ls`",
53
54
  DEVROUTER_END_SENTINEL
@@ -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.39" : "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.39" : "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
 
@@ -6573,100 +7918,31 @@ ${details}
6573
7918
  Mitigation:
6574
7919
  1) Stop the conflicting process/container
6575
7920
  2) Re-run: dev up
6576
-
6577
- Debug commands:
6578
- - lsof -nP -iTCP:80 -sTCP:LISTEN
6579
- - lsof -nP -iTCP:443 -sTCP:LISTEN
6580
- - 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}'.`);
7921
+
7922
+ Debug commands:
7923
+ - lsof -nP -iTCP:80 -sTCP:LISTEN
7924
+ - lsof -nP -iTCP:443 -sTCP:LISTEN
7925
+ - lsof -nP -iTCP:5432 -sTCP:LISTEN`;
7926
+ }
7927
+ async function runUpCommand() {
7928
+ await ensureNetwork(DEVNET_NAME);
7929
+ ensureRouterFiles();
7930
+ const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
7931
+ if (!routerRunning) {
7932
+ const message = buildPortConflictMessage();
7933
+ if (message) {
7934
+ throw new Error(message);
7935
+ }
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,197 @@ 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 isWarmWorkspaceActive(repoPath) {
8221
+ try {
8222
+ return workspaceAppContainers(inspectWorkspaceContainers(), repoPath).some(
8223
+ (container) => container.state.Running
8224
+ );
8225
+ } catch {
8226
+ return true;
8227
+ }
8228
+ }
8229
+ function captureFirstTransitionBaseline(options) {
8230
+ const containers = inspectWorkspaceContainers();
8231
+ const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
8232
+ (container) => container.state.Running
8233
+ );
8234
+ if (runningPrimary.length === 0) return void 0;
8235
+ if (runningPrimary.length > 1) {
8236
+ throw new Error(
8237
+ "Cannot start the first managed transition while multiple workspace app containers are running."
8238
+ );
8239
+ }
8240
+ const primary = runningPrimary[0];
8241
+ const composeProject = primary.labels["com.docker.compose.project"];
8242
+ const workspacePath = primary.mounts.find(
8243
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, options.repoPath)
8244
+ )?.Destination;
8245
+ if (!composeProject || !workspacePath) {
8246
+ throw new Error(
8247
+ "Cannot prove the exact pre-transition Compose identity for the first managed transition."
8248
+ );
8249
+ }
8250
+ const services = options.plan.profileServices.filter(
8251
+ (service) => containers.some(
8252
+ (container) => container.state.Running && hasExactComposeIdentity(container, { repoPath: options.repoPath, service, composeProject })
8253
+ )
8254
+ );
8255
+ const activeProcesses = [];
8256
+ for (const process2 of options.processes) {
8257
+ const status = runManagedProcessAction({
8258
+ container: { id: primary.id, workspacePath },
8259
+ name: process2,
8260
+ action: "status",
8261
+ quiet: true
8262
+ });
8263
+ if (status === "running") activeProcesses.push(process2);
8264
+ }
8265
+ return {
8266
+ services,
8267
+ processes: activeProcesses
8268
+ };
8269
+ }
6803
8270
  function validateWorkspaceContainers(containers, options) {
6804
8271
  const appContainers = workspaceAppContainers(containers, options.repoPath);
6805
8272
  if (appContainers.length !== 1) {
@@ -6860,7 +8327,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6860
8327
  target
6861
8328
  });
6862
8329
  if (target.kind === "linked") {
6863
- const workspaceEnv = (0, import_node_child_process14.spawnSync)(
8330
+ const workspaceEnv = (0, import_node_child_process15.spawnSync)(
6864
8331
  "docker",
6865
8332
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
6866
8333
  { encoding: "utf-8" }
@@ -6870,7 +8337,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6870
8337
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
6871
8338
  );
6872
8339
  }
6873
- const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
8340
+ const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
6874
8341
  "docker",
6875
8342
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
6876
8343
  { encoding: "utf-8" }
@@ -6881,7 +8348,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
6881
8348
  );
6882
8349
  }
6883
8350
  }
6884
- const gitCheck = (0, import_node_child_process14.spawnSync)(
8351
+ const gitCheck = (0, import_node_child_process15.spawnSync)(
6885
8352
  "docker",
6886
8353
  [
6887
8354
  "exec",
@@ -6968,14 +8435,14 @@ function resolvePrimaryTarget(repoPath) {
6968
8435
  }
6969
8436
  function isPrimaryCheckout(repoPath) {
6970
8437
  try {
6971
- return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
8438
+ return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
6972
8439
  } catch {
6973
8440
  return false;
6974
8441
  }
6975
8442
  }
6976
8443
  function openUrls(urls) {
6977
8444
  for (const url of urls) {
6978
- const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
8445
+ const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
6979
8446
  if (opened.status !== 0) {
6980
8447
  process.stderr.write(`Warning: could not open '${url}'.
6981
8448
  `);
@@ -7001,24 +8468,92 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7001
8468
  }
7002
8469
  return withWorkspaceLifecycleLock(repoPath, async () => {
7003
8470
  let environmentStarted = false;
8471
+ let managedPlan;
8472
+ let previousManagedState;
8473
+ let managedComposeProject;
8474
+ let managedContainer;
8475
+ let previousRoutes = [];
8476
+ let candidateRoutesPublished = false;
8477
+ let transitionPhase = "validation";
8478
+ let managedProcessRegistry = [];
8479
+ let managedPostStartPlan;
8480
+ let managedRuntimeConfig;
8481
+ let managedConfigWritten = false;
8482
+ let managedWorkspaceEnv;
8483
+ let firstTransitionBaseline;
7004
8484
  try {
7005
8485
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
7006
8486
  let devpodId = target.devpodId;
7007
8487
  if (target.kind === "linked") {
7008
- const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7009
- if (!import_node_fs18.default.existsSync(overlayPath)) {
8488
+ const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8489
+ if (!import_node_fs20.default.existsSync(overlayPath)) {
7010
8490
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
7011
8491
  }
7012
8492
  }
7013
8493
  const managedPostStart = resolveManagedPostStartPlan(repoPath);
8494
+ managedPostStartPlan = managedPostStart;
7014
8495
  const runtime = loadRuntimeConfig(
7015
8496
  repoPath,
7016
8497
  target.kind === "primary" ? "" : target.workspace,
7017
8498
  options.profile
7018
8499
  );
8500
+ const managedRuntime = runtime.config.managedRuntime;
8501
+ managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
8502
+ managedProcessRegistry = managedRuntime?.processes ?? [];
8503
+ const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
8504
+ if (managedRuntime) {
8505
+ previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
8506
+ if (previousManagedState?.status === "degraded") {
8507
+ throw new Error(
8508
+ "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
8509
+ );
8510
+ }
8511
+ if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
8512
+ throw new Error(
8513
+ `Managed runtime state names DevPod '${previousManagedState.devpodId}', not the exact target '${target.devpodId ?? "(absent)"}'.`
8514
+ );
8515
+ }
8516
+ if (previousManagedState?.desired.services.some(
8517
+ (service) => !managedRuntime.devcontainer.profileServices.includes(service)
8518
+ )) {
8519
+ throw new Error("Managed runtime state contains an unregistered profile service.");
8520
+ }
8521
+ if (previousManagedState?.desired.processes.some(
8522
+ (process2) => !managedRuntime.processes.includes(process2)
8523
+ )) {
8524
+ throw new Error("Managed runtime state contains an unregistered process marker.");
8525
+ }
8526
+ previousRoutes = listHostRouteState().filter(
8527
+ (route) => sameWorkspacePath(route.repoPath, repoPath)
8528
+ );
8529
+ if (managedPostStart.kind !== "runtime") {
8530
+ throw new Error(
8531
+ "managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
8532
+ );
8533
+ }
8534
+ managedPlan = inspectManagedDevcontainerConfig({
8535
+ repoPath,
8536
+ config: runtime.config,
8537
+ profile: runtime.resolvedProfile,
8538
+ linked: target.kind === "linked"
8539
+ });
8540
+ managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
8541
+ if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
8542
+ throw new Error(
8543
+ `Managed Dev Container source configuration changed since the last successful transition; a warm ensure cannot apply it to the existing runtime for '${repoPath}'. Stop and delete the exact runtime, then run ensure again to recreate it from the current source configuration.`
8544
+ );
8545
+ }
8546
+ if (!previousManagedState) {
8547
+ firstTransitionBaseline = captureFirstTransitionBaseline({
8548
+ repoPath,
8549
+ plan: managedPlan,
8550
+ processes: managedRuntime.processes
8551
+ });
8552
+ }
8553
+ }
7019
8554
  const apps = proxyAppsFromConfig(runtime.config);
7020
8555
  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";
8556
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
7022
8557
  for (const [index, app] of apps.entries()) {
7023
8558
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
7024
8559
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -7027,6 +8562,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7027
8562
  );
7028
8563
  }
7029
8564
  }
8565
+ if (managedPlan) {
8566
+ writeManagedDevcontainerConfig(managedPlan);
8567
+ managedConfigWritten = true;
8568
+ }
7030
8569
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
7031
8570
  const ownership = target.kind === "linked" ? {
7032
8571
  workspace: target.workspace,
@@ -7044,6 +8583,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7044
8583
  devpodId = startDevpodWorkspace({
7045
8584
  repoPath,
7046
8585
  devpodId: requestedTarget.devpodId,
8586
+ devcontainerPath: managedPlan?.generatedRelativePath,
7047
8587
  recreate,
7048
8588
  quiet: options.quiet,
7049
8589
  ...requestedTarget.kind === "linked" ? {
@@ -7072,7 +8612,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7072
8612
  try {
7073
8613
  startAndProveAttachment();
7074
8614
  } catch (error) {
7075
- if (!target.hadExactDevpod) {
8615
+ if (managedPlan || !target.hadExactDevpod) {
7076
8616
  throw error;
7077
8617
  }
7078
8618
  container = await recreateAndPreflight();
@@ -7082,24 +8622,113 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7082
8622
  try {
7083
8623
  container = await preflight(0);
7084
8624
  } catch (error) {
7085
- if (!target.hadExactDevpod) {
8625
+ if (managedPlan || !target.hadExactDevpod) {
7086
8626
  throw error;
7087
8627
  }
7088
8628
  container = await recreateAndPreflight();
7089
8629
  recreated = true;
7090
8630
  }
7091
8631
  }
8632
+ managedContainer = container;
8633
+ if (managedPlan) {
8634
+ transitionPhase = "service-start";
8635
+ const initialContainers = inspectWorkspaceContainers();
8636
+ managedComposeProject = resolveComposeProject(
8637
+ initialContainers,
8638
+ repoPath,
8639
+ container.id,
8640
+ managedPlan.primaryService,
8641
+ managedPlan.composeFiles
8642
+ );
8643
+ if (previousManagedState && previousManagedState.composeProject !== managedComposeProject) {
8644
+ throw new Error(
8645
+ `Managed runtime state names Compose project '${previousManagedState.composeProject}', not '${managedComposeProject}'.`
8646
+ );
8647
+ }
8648
+ const missingServices = [];
8649
+ for (const service of managedPlan.desiredServices) {
8650
+ const matches = exactWorkspaceServiceContainers(
8651
+ initialContainers,
8652
+ repoPath,
8653
+ managedComposeProject,
8654
+ service,
8655
+ managedPlan.composeFiles
8656
+ );
8657
+ if (matches.length > 1) {
8658
+ throw new Error(
8659
+ `Managed service '${service}' has multiple exact workspace containers; refusing to reconcile it.`
8660
+ );
8661
+ }
8662
+ if (!matches[0]?.state.Running) missingServices.push(service);
8663
+ }
8664
+ startExactManagedServices({
8665
+ plan: managedPlan,
8666
+ composeProject: managedComposeProject,
8667
+ services: missingServices,
8668
+ quiet: options.quiet,
8669
+ workspace: managedWorkspaceEnv
8670
+ });
8671
+ await waitForManagedServices(
8672
+ managedPlan,
8673
+ repoPath,
8674
+ managedComposeProject,
8675
+ managedPlan.desiredServices,
8676
+ container.id,
8677
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8678
+ );
8679
+ container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
8680
+ managedContainer = container;
8681
+ }
8682
+ transitionPhase = "process-start";
7092
8683
  runManagedPostStart({
7093
8684
  plan: managedPostStart,
7094
8685
  container,
7095
8686
  quiet: options.quiet,
7096
- profile: runtime.profile
8687
+ profile: runtime.profile,
8688
+ ...managedPlan ? { processes: desiredProcesses } : {}
7097
8689
  });
8690
+ if (managedPlan && managedComposeProject) {
8691
+ const processRegistry = runtime.config.managedRuntime?.processes ?? [];
8692
+ const desiredProcessSet = new Set(desiredProcesses);
8693
+ for (const process2 of processRegistry) {
8694
+ if (desiredProcessSet.has(process2)) continue;
8695
+ runManagedProcessAction({
8696
+ container,
8697
+ name: process2,
8698
+ action: "stop",
8699
+ quiet: options.quiet
8700
+ });
8701
+ }
8702
+ for (const process2 of desiredProcesses) {
8703
+ const status = runManagedProcessAction({
8704
+ container,
8705
+ name: process2,
8706
+ action: "status",
8707
+ quiet: options.quiet
8708
+ });
8709
+ if (status !== "running") {
8710
+ throw new Error(`Managed process '${process2}' is not running (${status}).`);
8711
+ }
8712
+ }
8713
+ transitionPhase = "service-stop";
8714
+ stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
8715
+ assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
8716
+ await waitForManagedServices(
8717
+ managedPlan,
8718
+ repoPath,
8719
+ managedComposeProject,
8720
+ managedPlan.desiredServices,
8721
+ container.id,
8722
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8723
+ );
8724
+ }
8725
+ transitionPhase = "route-publication";
7098
8726
  const publication = await replacePublishedProxyRoutes(
7099
8727
  repoPath,
7100
8728
  runtime.config,
7101
8729
  target.workspace
7102
8730
  );
8731
+ candidateRoutesPublished = true;
7103
8732
  try {
7104
8733
  await waitForHttpRoutes(
7105
8734
  repoPath,
@@ -7107,6 +8736,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7107
8736
  options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
7108
8737
  );
7109
8738
  } catch (error) {
8739
+ if (managedPlan) {
8740
+ throw error;
8741
+ }
7110
8742
  replaceHostRoutesForRepo(repoPath, []);
7111
8743
  if (!target.hadExactDevpod || recreated) {
7112
8744
  throw error;
@@ -7137,6 +8769,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7137
8769
  if (!devpodId) {
7138
8770
  throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
7139
8771
  }
8772
+ if (managedPlan && managedComposeProject) {
8773
+ writeManagedRuntimeState({
8774
+ version: 1,
8775
+ repoPath,
8776
+ ...target.workspace !== void 0 ? { workspace: target.workspace } : {},
8777
+ devpodId,
8778
+ composeProject: managedComposeProject,
8779
+ profile: runtime.profile,
8780
+ desired: {
8781
+ apps: apps.map((app) => app.name).sort(),
8782
+ services: [...managedPlan.desiredProfileServices].sort(),
8783
+ processes: [...desiredProcesses].sort()
8784
+ },
8785
+ sourceConfigSha256: managedPlan.sourceConfigSha256,
8786
+ effectiveConfigSha256: managedPlan.effectiveConfigSha256,
8787
+ status: "ready",
8788
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8789
+ });
8790
+ }
8791
+ const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
8792
+ repoPath,
8793
+ workspace: target.workspace,
8794
+ config: runtime.config,
8795
+ profile: runtime.profile,
8796
+ resolvedProfile: runtime.resolvedProfile
8797
+ }) : void 0;
7140
8798
  return {
7141
8799
  kind: target.kind,
7142
8800
  repoPath,
@@ -7145,9 +8803,182 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7145
8803
  devpodId,
7146
8804
  urls,
7147
8805
  recreated,
7148
- tlsRefreshed: publication.tlsRefreshed
8806
+ tlsRefreshed: publication.tlsRefreshed,
8807
+ ...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
7149
8808
  };
7150
8809
  } catch (error) {
8810
+ if (managedPlan && managedContainer && managedComposeProject) {
8811
+ const rollbackErrors = [];
8812
+ const failedPhase = transitionPhase;
8813
+ transitionPhase = "rollback";
8814
+ const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
8815
+ const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
8816
+ const previousServiceSet = new Set(previousServices);
8817
+ const previousProcessSet = new Set(previousProcesses);
8818
+ try {
8819
+ if (managedConfigWritten) {
8820
+ if (previousManagedState && managedRuntimeConfig) {
8821
+ restorePreviousManagedConfig({
8822
+ repoPath,
8823
+ config: managedRuntimeConfig,
8824
+ linked,
8825
+ previousState: previousManagedState
8826
+ });
8827
+ } else {
8828
+ removeManagedDevcontainerConfig(managedPlan);
8829
+ }
8830
+ }
8831
+ } catch (rollbackError) {
8832
+ rollbackErrors.push(
8833
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8834
+ );
8835
+ }
8836
+ try {
8837
+ const previousAllServices = [
8838
+ managedPlan.primaryService,
8839
+ ...managedPlan.baseServices,
8840
+ ...previousServices
8841
+ ];
8842
+ startExactManagedServices({
8843
+ plan: managedPlan,
8844
+ composeProject: managedComposeProject,
8845
+ services: previousAllServices,
8846
+ quiet: options.quiet,
8847
+ workspace: managedWorkspaceEnv
8848
+ });
8849
+ await waitForManagedServices(
8850
+ managedPlan,
8851
+ repoPath,
8852
+ managedComposeProject,
8853
+ previousAllServices,
8854
+ managedContainer.id,
8855
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8856
+ );
8857
+ } catch (rollbackError) {
8858
+ rollbackErrors.push(
8859
+ `services: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8860
+ );
8861
+ }
8862
+ try {
8863
+ if (!managedPostStartPlan) {
8864
+ throw new Error("Managed post-start plan disappeared during rollback.");
8865
+ }
8866
+ runManagedPostStart({
8867
+ plan: managedPostStartPlan,
8868
+ container: managedContainer,
8869
+ quiet: options.quiet,
8870
+ profile: previousManagedState?.profile ?? "full",
8871
+ processes: previousProcesses
8872
+ });
8873
+ for (const process2 of managedProcessRegistry) {
8874
+ if (previousProcessSet.has(process2)) continue;
8875
+ runManagedProcessAction({
8876
+ container: managedContainer,
8877
+ name: process2,
8878
+ action: "stop",
8879
+ quiet: options.quiet
8880
+ });
8881
+ }
8882
+ } catch (rollbackError) {
8883
+ rollbackErrors.push(
8884
+ `processes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8885
+ );
8886
+ }
8887
+ try {
8888
+ const containers = inspectWorkspaceContainers();
8889
+ for (const service of managedPlan.profileServices) {
8890
+ if (previousServiceSet.has(service)) continue;
8891
+ const matches = exactWorkspaceServiceContainers(
8892
+ containers,
8893
+ repoPath,
8894
+ managedComposeProject,
8895
+ service,
8896
+ managedPlan.composeFiles
8897
+ );
8898
+ if (matches.length > 1) {
8899
+ throw new Error(`service '${service}' has multiple exact containers`);
8900
+ }
8901
+ if (matches[0]?.state.Running) stopExactManagedService(matches[0].id, service);
8902
+ }
8903
+ assertDroppedManagedServicesStopped(
8904
+ {
8905
+ ...managedPlan,
8906
+ desiredProfileServices: previousServices,
8907
+ desiredServices: [
8908
+ managedPlan.primaryService,
8909
+ ...managedPlan.baseServices,
8910
+ ...previousServices
8911
+ ]
8912
+ },
8913
+ repoPath,
8914
+ managedComposeProject
8915
+ );
8916
+ } catch (rollbackError) {
8917
+ rollbackErrors.push(
8918
+ `service cleanup: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8919
+ );
8920
+ }
8921
+ if (candidateRoutesPublished) {
8922
+ try {
8923
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8924
+ } catch (rollbackError) {
8925
+ rollbackErrors.push(
8926
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8927
+ );
8928
+ }
8929
+ }
8930
+ if (rollbackErrors.length > 0 && previousManagedState) {
8931
+ try {
8932
+ markManagedRuntimeDegraded(previousManagedState, failedPhase);
8933
+ } catch (stateError) {
8934
+ rollbackErrors.push(
8935
+ `state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
8936
+ );
8937
+ }
8938
+ }
8939
+ const original = error instanceof Error ? error.message : String(error);
8940
+ const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : " Candidate runtime was rolled back.";
8941
+ throw new Error(`${original}${suffix}`);
8942
+ }
8943
+ if (managedPlan) {
8944
+ const original = error instanceof Error ? error.message : String(error);
8945
+ const rollbackErrors = [];
8946
+ if (managedConfigWritten) {
8947
+ try {
8948
+ if (previousManagedState && managedRuntimeConfig) {
8949
+ restorePreviousManagedConfig({
8950
+ repoPath,
8951
+ config: managedRuntimeConfig,
8952
+ linked,
8953
+ previousState: previousManagedState
8954
+ });
8955
+ } else {
8956
+ removeManagedDevcontainerConfig(managedPlan);
8957
+ }
8958
+ } catch (rollbackError) {
8959
+ rollbackErrors.push(
8960
+ `configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8961
+ );
8962
+ }
8963
+ }
8964
+ if (candidateRoutesPublished) {
8965
+ try {
8966
+ replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
8967
+ } catch (rollbackError) {
8968
+ rollbackErrors.push(
8969
+ `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
8970
+ );
8971
+ }
8972
+ }
8973
+ if (rollbackErrors.length > 0) {
8974
+ throw new Error(
8975
+ `${original} Rollback left degraded drift: ${rollbackErrors.join("; ")}.`
8976
+ );
8977
+ }
8978
+ throw new Error(
8979
+ `${original} Managed runtime transition did not reach a rollback boundary.`
8980
+ );
8981
+ }
7151
8982
  if (environmentStarted) {
7152
8983
  try {
7153
8984
  replaceHostRoutesForRepo(repoPath, []);
@@ -7161,19 +8992,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7161
8992
  }
7162
8993
  });
7163
8994
  }
7164
- var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
8995
+ var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
7165
8996
  var init_workspace_ensure = __esm({
7166
8997
  "src/core/workspace-ensure.ts"() {
7167
8998
  "use strict";
7168
- import_node_child_process14 = require("child_process");
7169
- import_node_fs18 = __toESM(require("fs"));
7170
- import_node_path18 = __toESM(require("path"));
8999
+ import_node_child_process15 = require("child_process");
9000
+ import_node_fs20 = __toESM(require("fs"));
9001
+ import_node_path20 = __toESM(require("path"));
9002
+ init_devcontainer_profile();
7171
9003
  init_devpod_environment();
7172
9004
  init_devpod_mutation();
7173
9005
  init_devpod_workspaces();
7174
9006
  init_host_routes();
7175
9007
  init_http_route_probe();
7176
9008
  init_managed_post_start();
9009
+ init_managed_runtime_state();
9010
+ init_managed_runtime_status();
7177
9011
  init_repo_config();
7178
9012
  init_route_publication();
7179
9013
  init_router();
@@ -7244,20 +9078,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
7244
9078
  );
7245
9079
  }
7246
9080
  function defaultWorktreePath(mainRepo, ws) {
7247
- return import_node_path19.default.join(mainRepo, "trees", ws);
9081
+ return import_node_path21.default.join(mainRepo, "trees", ws);
7248
9082
  }
7249
9083
  function assertDefaultWorktreeRootIgnored(mainRepo) {
7250
- const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9084
+ const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
7251
9085
  encoding: "utf-8"
7252
9086
  });
7253
9087
  if (ignored.status !== 0) {
7254
9088
  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.`
9089
+ `Default worktree root '${import_node_path21.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path21.default.join(mainRepo, ".gitignore")}' or use --path.`
7256
9090
  );
7257
9091
  }
7258
9092
  }
7259
9093
  function legacyDefaultWorktreePath(mainRepo, ws) {
7260
- return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
9094
+ return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
7261
9095
  }
7262
9096
  function teardownFallbackPath(mainRepo, workspace) {
7263
9097
  const candidates = [
@@ -7369,13 +9203,13 @@ function assertFullDownPreflight(mainRepo, target) {
7369
9203
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
7370
9204
  throw new Error("Refusing to remove the primary Git checkout.");
7371
9205
  }
7372
- if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
9206
+ if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
7373
9207
  if (target.worktree.locked) {
7374
9208
  throw new Error(
7375
9209
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
7376
9210
  );
7377
9211
  }
7378
- const status = (0, import_node_child_process15.spawnSync)(
9212
+ const status = (0, import_node_child_process16.spawnSync)(
7379
9213
  "git",
7380
9214
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
7381
9215
  { encoding: "utf-8" }
@@ -7396,8 +9230,8 @@ async function workspaceUp(branch, opts = {}) {
7396
9230
  if (!ws) {
7397
9231
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
7398
9232
  }
7399
- const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
7400
- if (import_node_fs19.default.existsSync(worktreePath)) {
9233
+ const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9234
+ if (import_node_fs21.default.existsSync(worktreePath)) {
7401
9235
  const registered = listGitWorktrees(mainRepo).find(
7402
9236
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
7403
9237
  );
@@ -7415,11 +9249,11 @@ async function workspaceUp(branch, opts = {}) {
7415
9249
  if (!opts.path) {
7416
9250
  assertDefaultWorktreeRootIgnored(mainRepo);
7417
9251
  }
7418
- const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9252
+ const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
7419
9253
  encoding: "utf-8"
7420
9254
  });
7421
9255
  if (add.status !== 0) {
7422
- const addNew = (0, import_node_child_process15.spawnSync)(
9256
+ const addNew = (0, import_node_child_process16.spawnSync)(
7423
9257
  "git",
7424
9258
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
7425
9259
  {
@@ -7524,8 +9358,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7524
9358
  opts.quiet
7525
9359
  );
7526
9360
  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], {
9361
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
9362
+ const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
7529
9363
  encoding: "utf-8"
7530
9364
  });
7531
9365
  if (rm.status !== 0) {
@@ -7543,7 +9377,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
7543
9377
  }
7544
9378
  return result;
7545
9379
  };
7546
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9380
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
7547
9381
  }
7548
9382
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
7549
9383
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -7578,13 +9412,13 @@ async function workspaceStop(target, opts = {}) {
7578
9412
  async function workspaceDown(target, opts = {}) {
7579
9413
  return runWorkspaceLifecycle("down", target, opts);
7580
9414
  }
7581
- var import_node_child_process15, import_node_fs19, import_node_path19;
9415
+ var import_node_child_process16, import_node_fs21, import_node_path21;
7582
9416
  var init_workspace_lifecycle = __esm({
7583
9417
  "src/core/workspace-lifecycle.ts"() {
7584
9418
  "use strict";
7585
- import_node_child_process15 = require("child_process");
7586
- import_node_fs19 = __toESM(require("fs"));
7587
- import_node_path19 = __toESM(require("path"));
9419
+ import_node_child_process16 = require("child_process");
9420
+ import_node_fs21 = __toESM(require("fs"));
9421
+ import_node_path21 = __toESM(require("path"));
7588
9422
  init_devpod_mutation();
7589
9423
  init_devpod_workspaces();
7590
9424
  init_repo_config();
@@ -7711,7 +9545,7 @@ function ensureGuidance(repoPath) {
7711
9545
  return `Run 'devrouter ensure ${repoPath}' first.`;
7712
9546
  }
7713
9547
  function assertRunningDevpod(devpodId, repoPath) {
7714
- const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
9548
+ const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
7715
9549
  encoding: "utf-8"
7716
9550
  });
7717
9551
  if (result.status !== 0) {
@@ -7749,7 +9583,7 @@ async function devpodExec(repoPath, command) {
7749
9583
  }
7750
9584
  assertRunningDevpod(devpod.id, repoPath);
7751
9585
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
7752
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
9586
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto7.randomUUID)()}__:`;
7753
9587
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
7754
9588
  const literalCommand = command.map(quotePosixArg).join(" ");
7755
9589
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -7767,7 +9601,7 @@ async function devpodExec(repoPath, command) {
7767
9601
  wrappedCommand
7768
9602
  ];
7769
9603
  return new Promise((resolve, reject) => {
7770
- const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9604
+ const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
7771
9605
  let pending = Buffer.alloc(0);
7772
9606
  let statusBytes = Buffer.alloc(0);
7773
9607
  let readingStatus = false;
@@ -7826,12 +9660,12 @@ async function devpodExec(repoPath, command) {
7826
9660
  });
7827
9661
  });
7828
9662
  }
7829
- var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
9663
+ var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
7830
9664
  var init_devpod_exec = __esm({
7831
9665
  "src/core/devpod-exec.ts"() {
7832
9666
  "use strict";
7833
- import_node_child_process16 = require("child_process");
7834
- import_node_crypto5 = require("crypto");
9667
+ import_node_child_process17 = require("child_process");
9668
+ import_node_crypto7 = require("crypto");
7835
9669
  init_devpod_environment();
7836
9670
  init_devpod_workspaces();
7837
9671
  init_workspace();
@@ -8015,7 +9849,7 @@ async function runOpenCommand(name) {
8015
9849
  );
8016
9850
  return;
8017
9851
  }
8018
- const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
9852
+ const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
8019
9853
  if (result.status !== 0) {
8020
9854
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
8021
9855
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -8023,11 +9857,11 @@ async function runOpenCommand(name) {
8023
9857
  process.stdout.write(`Opened ${url}
8024
9858
  `);
8025
9859
  }
8026
- var import_node_child_process17;
9860
+ var import_node_child_process18;
8027
9861
  var init_open = __esm({
8028
9862
  "src/commands/open.ts"() {
8029
9863
  "use strict";
8030
- import_node_child_process17 = require("child_process");
9864
+ import_node_child_process18 = require("child_process");
8031
9865
  init_docker();
8032
9866
  init_host_routes();
8033
9867
  init_repo_config();
@@ -8050,7 +9884,7 @@ async function runLogsCommand(options) {
8050
9884
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
8051
9885
  if (options.follow) {
8052
9886
  args.splice(1, 0, "-f");
8053
- const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
9887
+ const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
8054
9888
  const onSignal = () => {
8055
9889
  child.kill("SIGTERM");
8056
9890
  };
@@ -8064,17 +9898,17 @@ async function runLogsCommand(options) {
8064
9898
  });
8065
9899
  });
8066
9900
  } else {
8067
- const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
9901
+ const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
8068
9902
  if (result.status !== 0) {
8069
9903
  throw new Error("Failed to retrieve router logs.");
8070
9904
  }
8071
9905
  }
8072
9906
  }
8073
- var import_node_child_process18;
9907
+ var import_node_child_process19;
8074
9908
  var init_logs = __esm({
8075
9909
  "src/commands/logs.ts"() {
8076
9910
  "use strict";
8077
- import_node_child_process18 = require("child_process");
9911
+ import_node_child_process19 = require("child_process");
8078
9912
  init_docker();
8079
9913
  init_router();
8080
9914
  }
@@ -8114,17 +9948,17 @@ function asRecord3(value) {
8114
9948
  return value;
8115
9949
  }
8116
9950
  function readJson(filePath) {
8117
- if (!import_node_fs20.default.existsSync(filePath)) {
9951
+ if (!import_node_fs22.default.existsSync(filePath)) {
8118
9952
  return void 0;
8119
9953
  }
8120
9954
  try {
8121
- return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
9955
+ return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
8122
9956
  } catch {
8123
9957
  return void 0;
8124
9958
  }
8125
9959
  }
8126
9960
  function relative(repoPath, filePath) {
8127
- return import_node_path20.default.relative(repoPath, filePath) || ".";
9961
+ return import_node_path22.default.relative(repoPath, filePath) || ".";
8128
9962
  }
8129
9963
  function redactEnvAssignments(value) {
8130
9964
  return value.replace(
@@ -8165,7 +9999,7 @@ function inspectPackageManager(repoPath, pkg) {
8165
9999
  ["bun.lock", "bun"]
8166
10000
  ];
8167
10001
  for (const [fileName, name] of lockfiles) {
8168
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10002
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
8169
10003
  return { name, source: fileName };
8170
10004
  }
8171
10005
  }
@@ -8180,9 +10014,9 @@ function inspectNode(repoPath, pkg) {
8180
10014
  if (typeof engines?.node === "string") {
8181
10015
  return { version: engines.node, source: "package.json:engines.node" };
8182
10016
  }
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();
10017
+ const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
10018
+ if (import_node_fs22.default.existsSync(nvmrc)) {
10019
+ const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
8186
10020
  return { version, source: ".nvmrc" };
8187
10021
  }
8188
10022
  return void 0;
@@ -8243,7 +10077,7 @@ function configuredComposeFiles(repoPath) {
8243
10077
  const files = config.apps.filter(
8244
10078
  (app) => app.runtime === "docker"
8245
10079
  ).flatMap((app) => app.docker.composeFiles).filter(
8246
- (fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
10080
+ (fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
8247
10081
  );
8248
10082
  return Array.from(new Set(files));
8249
10083
  } catch {
@@ -8261,10 +10095,10 @@ function composeFiles(repoPath) {
8261
10095
  ...configuredComposeFiles(repoPath)
8262
10096
  ];
8263
10097
  return Array.from(new Set(candidates)).filter(
8264
- (fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
10098
+ (fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
8265
10099
  );
8266
10100
  }
8267
- function stringArray(value) {
10101
+ function stringArray2(value) {
8268
10102
  if (!Array.isArray(value)) {
8269
10103
  return [];
8270
10104
  }
@@ -8299,7 +10133,7 @@ function inspectServices(repoPath) {
8299
10133
  const services = [];
8300
10134
  for (const fileName of composeFiles(repoPath)) {
8301
10135
  try {
8302
- const parsed = import_yaml5.default.parse(import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8"));
10136
+ const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
8303
10137
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
8304
10138
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
8305
10139
  const service = asRecord3(value);
@@ -8313,7 +10147,7 @@ function inspectServices(repoPath) {
8313
10147
  kind: kind.kind,
8314
10148
  source: fileName,
8315
10149
  image,
8316
- ports: stringArray(service.ports),
10150
+ ports: stringArray2(service.ports),
8317
10151
  hasHealthcheck: service.healthcheck !== void 0,
8318
10152
  envNames: envNames(service.environment),
8319
10153
  confidence: kind.confidence
@@ -8334,8 +10168,8 @@ function inspectServices(repoPath) {
8334
10168
  return services;
8335
10169
  }
8336
10170
  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");
10171
+ const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10172
+ const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
8339
10173
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
8340
10174
  return { path: fileName, names };
8341
10175
  });
@@ -8349,18 +10183,18 @@ function inspectEnvFiles(repoPath) {
8349
10183
  };
8350
10184
  }
8351
10185
  function inspectDevcontainer(repoPath) {
8352
- const dir = import_node_path20.default.join(repoPath, ".devcontainer");
8353
- if (!import_node_fs20.default.existsSync(dir)) {
10186
+ const dir = import_node_path22.default.join(repoPath, ".devcontainer");
10187
+ if (!import_node_fs22.default.existsSync(dir)) {
8354
10188
  return { exists: false, files: [] };
8355
10189
  }
8356
10190
  return {
8357
10191
  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}`)
10192
+ files: import_node_fs22.default.readdirSync(dir).filter((fileName) => import_node_fs22.default.statSync(import_node_path22.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
8359
10193
  };
8360
10194
  }
8361
10195
  function inspectDevrouter(repoPath) {
8362
10196
  const configPath = getRepoConfigPath(repoPath);
8363
- if (!import_node_fs20.default.existsSync(configPath)) {
10197
+ if (!import_node_fs22.default.existsSync(configPath)) {
8364
10198
  return {
8365
10199
  exists: false,
8366
10200
  configPath,
@@ -8404,15 +10238,15 @@ function inspectAgentGuidance(repoPath) {
8404
10238
  ["AGENTS.md", "agents"],
8405
10239
  ["CLAUDE.md", "claude"]
8406
10240
  ]) {
8407
- if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
10241
+ if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
8408
10242
  results.push({ path: fileName, kind });
8409
10243
  }
8410
10244
  }
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)) {
10245
+ const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
10246
+ if (import_node_fs22.default.existsSync(skillsDir)) {
10247
+ for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
10248
+ const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
10249
+ if (import_node_fs22.default.existsSync(skillPath)) {
8416
10250
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
8417
10251
  }
8418
10252
  }
@@ -8455,7 +10289,7 @@ function buildIssues(report) {
8455
10289
  }
8456
10290
  function inspectRepo(options = {}) {
8457
10291
  const repoPath = resolveRepoPath(options.repo);
8458
- const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
10292
+ const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
8459
10293
  const scripts = inspectScripts(pkg);
8460
10294
  const reportWithoutIssues = {
8461
10295
  repoPath,
@@ -8474,13 +10308,13 @@ function inspectRepo(options = {}) {
8474
10308
  issues: buildIssues(reportWithoutIssues)
8475
10309
  };
8476
10310
  }
8477
- var import_node_fs20, import_node_path20, import_yaml5;
10311
+ var import_node_fs22, import_node_path22, import_yaml6;
8478
10312
  var init_repo_inspect = __esm({
8479
10313
  "src/core/repo-inspect.ts"() {
8480
10314
  "use strict";
8481
- import_node_fs20 = __toESM(require("fs"));
8482
- import_node_path20 = __toESM(require("path"));
8483
- import_yaml5 = __toESM(require("yaml"));
10315
+ import_node_fs22 = __toESM(require("fs"));
10316
+ import_node_path22 = __toESM(require("path"));
10317
+ import_yaml6 = __toESM(require("yaml"));
8484
10318
  init_repo_config();
8485
10319
  }
8486
10320
  });
@@ -8584,7 +10418,7 @@ function requiredFileChecks(repoPath) {
8584
10418
  ".devcontainer/docker-compose.yml",
8585
10419
  ".devrouter.yml"
8586
10420
  ];
8587
- const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
10421
+ const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
8588
10422
  return {
8589
10423
  id: "repo.devcontainer.verify-files",
8590
10424
  level: missing.length === 0 ? "ok" : "error",
@@ -8803,12 +10637,12 @@ async function verifyDevcontainer(options = {}) {
8803
10637
  nextSteps: collectNextSteps3(checks)
8804
10638
  };
8805
10639
  }
8806
- var import_node_fs21, import_node_path21;
10640
+ var import_node_fs23, import_node_path23;
8807
10641
  var init_devcontainer_verify = __esm({
8808
10642
  "src/core/devcontainer-verify.ts"() {
8809
10643
  "use strict";
8810
- import_node_fs21 = __toESM(require("fs"));
8811
- import_node_path21 = __toESM(require("path"));
10644
+ import_node_fs23 = __toESM(require("fs"));
10645
+ import_node_path23 = __toESM(require("path"));
8812
10646
  init_capabilities();
8813
10647
  init_doctor();
8814
10648
  init_host_routes();
@@ -9104,7 +10938,7 @@ function packageManagerIssues(repo) {
9104
10938
  }
9105
10939
  function plannedFiles(repoPath, version) {
9106
10940
  const repo = inspectRepo({ repo: repoPath });
9107
- const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
10941
+ const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
9108
10942
  const nodeMajor = majorVersion(repo.node?.version, "24");
9109
10943
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
9110
10944
  const port = inferPort2(repo);
@@ -9149,8 +10983,8 @@ function plannedFiles(repoPath, version) {
9149
10983
  };
9150
10984
  }
9151
10985
  function classifyFile(repoPath, file) {
9152
- const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
9153
- if (!import_node_fs22.default.existsSync(absolutePath)) {
10986
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
10987
+ if (!import_node_fs24.default.existsSync(absolutePath)) {
9154
10988
  return {
9155
10989
  path: file.relativePath,
9156
10990
  action: "create",
@@ -9158,7 +10992,7 @@ function classifyFile(repoPath, file) {
9158
10992
  bytes: Buffer.byteLength(file.content)
9159
10993
  };
9160
10994
  }
9161
- const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
10995
+ const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
9162
10996
  if (!current.includes(MANAGED_MARKER2)) {
9163
10997
  return {
9164
10998
  path: file.relativePath,
@@ -9215,11 +11049,11 @@ function buildPlan(repoPath, dryRun, version) {
9215
11049
  };
9216
11050
  }
9217
11051
  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");
11052
+ const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11053
+ import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
11054
+ import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
9221
11055
  if (file.executable) {
9222
- import_node_fs22.default.chmodSync(absolutePath, 493);
11056
+ import_node_fs24.default.chmodSync(absolutePath, 493);
9223
11057
  }
9224
11058
  }
9225
11059
  function writeDevcontainer(options = {}) {
@@ -9257,12 +11091,12 @@ function writeDevcontainer(options = {}) {
9257
11091
  nextSteps: postWriteNextSteps(repoPath)
9258
11092
  };
9259
11093
  }
9260
- var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11094
+ var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
9261
11095
  var init_devcontainer_write = __esm({
9262
11096
  "src/core/devcontainer-write.ts"() {
9263
11097
  "use strict";
9264
- import_node_fs22 = __toESM(require("fs"));
9265
- import_node_path22 = __toESM(require("path"));
11098
+ import_node_fs24 = __toESM(require("fs"));
11099
+ import_node_path24 = __toESM(require("path"));
9266
11100
  init_repo_config();
9267
11101
  init_repo_inspect();
9268
11102
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -9653,7 +11487,7 @@ function sanitizeRouterId(value) {
9653
11487
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
9654
11488
  }
9655
11489
  function repoHash(repoPath) {
9656
- return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
11490
+ return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
9657
11491
  }
9658
11492
  function asDockerApp(app) {
9659
11493
  return app.runtime === "docker";
@@ -9732,11 +11566,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
9732
11566
  if (dockerApps.length === 0) {
9733
11567
  throw new Error("No docker apps selected to prepare compose overlay.");
9734
11568
  }
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");
11569
+ const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
11570
+ import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
11571
+ const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
9738
11572
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
9739
- import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
11573
+ import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
9740
11574
  return {
9741
11575
  overlayPath,
9742
11576
  composeFiles: ensureComposeFiles(dockerApps),
@@ -9750,7 +11584,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
9750
11584
  fileArgs.push("-f", resolved);
9751
11585
  }
9752
11586
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
9753
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11587
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9754
11588
  encoding: "utf-8",
9755
11589
  cwd: repoPath
9756
11590
  });
@@ -9778,7 +11612,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
9778
11612
  "--services",
9779
11613
  ...services
9780
11614
  ];
9781
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11615
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9782
11616
  encoding: "utf-8",
9783
11617
  cwd: repoPath
9784
11618
  });
@@ -9804,7 +11638,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
9804
11638
  fileArgs.push("-f", resolved);
9805
11639
  }
9806
11640
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
9807
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11641
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9808
11642
  encoding: "utf-8",
9809
11643
  cwd: repoPath
9810
11644
  });
@@ -9830,7 +11664,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
9830
11664
  String(tail),
9831
11665
  ...services
9832
11666
  ];
9833
- (0, import_node_child_process19.spawnSync)("docker", args, {
11667
+ (0, import_node_child_process20.spawnSync)("docker", args, {
9834
11668
  stdio: "inherit",
9835
11669
  cwd: repoPath
9836
11670
  });
@@ -9842,7 +11676,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9842
11676
  fileArgs.push("-f", resolved);
9843
11677
  }
9844
11678
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
9845
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11679
+ const result = (0, import_node_child_process20.spawnSync)("docker", args, {
9846
11680
  encoding: "utf-8",
9847
11681
  cwd: repoPath
9848
11682
  });
@@ -9856,15 +11690,15 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
9856
11690
  const port = Number(match[1]);
9857
11691
  return Number.isInteger(port) && port > 0 ? port : void 0;
9858
11692
  }
9859
- var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
11693
+ var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
9860
11694
  var init_docker_run = __esm({
9861
11695
  "src/core/docker-run.ts"() {
9862
11696
  "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"));
11697
+ import_node_child_process20 = require("child_process");
11698
+ import_node_crypto8 = require("crypto");
11699
+ import_node_fs25 = __toESM(require("fs"));
11700
+ import_node_path25 = __toESM(require("path"));
11701
+ import_yaml7 = __toESM(require("yaml"));
9868
11702
  init_docker_error_guidance();
9869
11703
  init_paths();
9870
11704
  init_router();
@@ -9953,7 +11787,7 @@ function isProcessRunning(pid) {
9953
11787
  }
9954
11788
  }
9955
11789
  function readProcessTree(rootPid) {
9956
- const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
11790
+ const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
9957
11791
  if (result.status !== 0) {
9958
11792
  return [rootPid];
9959
11793
  }
@@ -10064,12 +11898,12 @@ function detectListeningPorts(pids) {
10064
11898
  if (pids.length === 0) {
10065
11899
  return [];
10066
11900
  }
10067
- const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
11901
+ const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
10068
11902
  encoding: "utf-8"
10069
11903
  });
10070
11904
  if (result.error && result.error.code === "ENOENT") {
10071
11905
  if (process.platform === "linux") {
10072
- const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
11906
+ const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
10073
11907
  if (ssResult.status === 0 && ssResult.stdout) {
10074
11908
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
10075
11909
  }
@@ -10128,7 +11962,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
10128
11962
  app.hostRun.command,
10129
11963
  true
10130
11964
  ) : app.hostRun.command;
10131
- const child = (0, import_node_child_process20.spawn)(spawnCommand, {
11965
+ const child = (0, import_node_child_process21.spawn)(spawnCommand, {
10132
11966
  cwd: commandCwd,
10133
11967
  stdio: "inherit",
10134
11968
  shell: true,
@@ -10496,7 +12330,7 @@ async function execWithAppEnv(options) {
10496
12330
  options.command[0],
10497
12331
  true
10498
12332
  );
10499
- child = (0, import_node_child_process20.spawn)(wrapped, {
12333
+ child = (0, import_node_child_process21.spawn)(wrapped, {
10500
12334
  cwd: deps.repoPath,
10501
12335
  stdio: "inherit",
10502
12336
  shell: true,
@@ -10510,7 +12344,7 @@ async function execWithAppEnv(options) {
10510
12344
  false
10511
12345
  );
10512
12346
  const [cmd, ...wrappedArgs] = wrapped;
10513
- child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
12347
+ child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
10514
12348
  cwd: deps.repoPath,
10515
12349
  stdio: "inherit",
10516
12350
  shell: false,
@@ -10518,7 +12352,7 @@ async function execWithAppEnv(options) {
10518
12352
  });
10519
12353
  }
10520
12354
  } else if (options.shell) {
10521
- child = (0, import_node_child_process20.spawn)(options.command[0], {
12355
+ child = (0, import_node_child_process21.spawn)(options.command[0], {
10522
12356
  cwd: deps.repoPath,
10523
12357
  stdio: "inherit",
10524
12358
  shell: true,
@@ -10526,7 +12360,7 @@ async function execWithAppEnv(options) {
10526
12360
  });
10527
12361
  } else {
10528
12362
  const [command, ...args] = options.command;
10529
- child = (0, import_node_child_process20.spawn)(command, args, {
12363
+ child = (0, import_node_child_process21.spawn)(command, args, {
10530
12364
  cwd: deps.repoPath,
10531
12365
  stdio: "inherit",
10532
12366
  shell: false,
@@ -10547,11 +12381,11 @@ async function execWithAppEnv(options) {
10547
12381
  deps.stopDeps();
10548
12382
  }
10549
12383
  }
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;
12384
+ var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
10551
12385
  var init_app_run = __esm({
10552
12386
  "src/core/app-run.ts"() {
10553
12387
  "use strict";
10554
- import_node_child_process20 = require("child_process");
12388
+ import_node_child_process21 = require("child_process");
10555
12389
  import_node_net = __toESM(require("net"));
10556
12390
  import_node_process = require("process");
10557
12391
  import_promises = require("readline/promises");
@@ -10685,13 +12519,13 @@ function measureWorktreeConsumption(worktreePath, options) {
10685
12519
  const startedAt = Date.now();
10686
12520
  let rootStat;
10687
12521
  try {
10688
- rootStat = import_node_fs24.default.lstatSync(worktreePath);
12522
+ rootStat = import_node_fs26.default.lstatSync(worktreePath);
10689
12523
  } catch (error) {
10690
12524
  return { status: "unknown", reason: describeError(error, worktreePath) };
10691
12525
  }
10692
12526
  let rootEntries;
10693
12527
  try {
10694
- rootEntries = import_node_fs24.default.readdirSync(worktreePath, { withFileTypes: true });
12528
+ rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
10695
12529
  } catch (error) {
10696
12530
  return { status: "unknown", reason: describeError(error, worktreePath) };
10697
12531
  }
@@ -10717,10 +12551,10 @@ function measureWorktreeConsumption(worktreePath, options) {
10717
12551
  timedOut = true;
10718
12552
  break;
10719
12553
  }
10720
- const entryPath = import_node_path24.default.join(dirPath, entry.name);
12554
+ const entryPath = import_node_path26.default.join(dirPath, entry.name);
10721
12555
  let entryStat;
10722
12556
  try {
10723
- entryStat = import_node_fs24.default.lstatSync(entryPath);
12557
+ entryStat = import_node_fs26.default.lstatSync(entryPath);
10724
12558
  } catch (error) {
10725
12559
  if (error?.code === "ENOENT") continue;
10726
12560
  unreadableReason = describeIncompleteWalk(error);
@@ -10730,7 +12564,7 @@ function measureWorktreeConsumption(worktreePath, options) {
10730
12564
  if (!entryStat.isDirectory()) continue;
10731
12565
  let childEntries;
10732
12566
  try {
10733
- childEntries = import_node_fs24.default.readdirSync(entryPath, { withFileTypes: true });
12567
+ childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
10734
12568
  } catch (error) {
10735
12569
  unreadableReason = describeIncompleteWalk(error);
10736
12570
  break;
@@ -10794,12 +12628,12 @@ function describeError(error, worktreePath) {
10794
12628
  }
10795
12629
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
10796
12630
  }
10797
- var import_node_fs24, import_node_path24, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
12631
+ var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
10798
12632
  var init_workspace_consumption = __esm({
10799
12633
  "src/core/workspace-consumption.ts"() {
10800
12634
  "use strict";
10801
- import_node_fs24 = __toESM(require("fs"));
10802
- import_node_path24 = __toESM(require("path"));
12635
+ import_node_fs26 = __toESM(require("fs"));
12636
+ import_node_path26 = __toESM(require("path"));
10803
12637
  init_devpod_environment();
10804
12638
  DEFAULT_DEADLINE_MS = 1e4;
10805
12639
  BLOCK_SIZE_BYTES = 512;
@@ -10900,7 +12734,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
10900
12734
  }
10901
12735
  function readGitSnapshot(worktree, commandRunner) {
10902
12736
  const comparablePath = comparableWorkspacePath(worktree.path);
10903
- if (worktree.prunable || !import_node_fs25.default.existsSync(comparablePath)) {
12737
+ if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
10904
12738
  return { worktree, checkout: "missing", head: null, committerDate: null };
10905
12739
  }
10906
12740
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -11431,7 +13265,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11431
13265
  containers = measureContainersFn(worktreePaths);
11432
13266
  } catch (error) {
11433
13267
  const reason = `container measurement failed: ${describeCause(error)}`;
11434
- containers = new Map(worktreePaths.map((path24) => [path24, unknownContainers(reason)]));
13268
+ containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
11435
13269
  }
11436
13270
  }
11437
13271
  const rows = records.map((record) => {
@@ -11479,12 +13313,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
11479
13313
  workspaces: rows
11480
13314
  };
11481
13315
  }
11482
- var import_node_child_process21, import_node_fs25, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
13316
+ var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
11483
13317
  var init_workspace_cleanup = __esm({
11484
13318
  "src/core/workspace-cleanup.ts"() {
11485
13319
  "use strict";
11486
- import_node_child_process21 = require("child_process");
11487
- import_node_fs25 = __toESM(require("fs"));
13320
+ import_node_child_process22 = require("child_process");
13321
+ import_node_fs27 = __toESM(require("fs"));
11488
13322
  init_devpod_workspaces();
11489
13323
  init_host_routes();
11490
13324
  init_repo_config();
@@ -11501,7 +13335,7 @@ var init_workspace_cleanup = __esm({
11501
13335
  ];
11502
13336
  defaultCommandRunner = (command, args, input2) => {
11503
13337
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
11504
- const result = (0, import_node_child_process21.spawnSync)(command, args, {
13338
+ const result = (0, import_node_child_process22.spawnSync)(command, args, {
11505
13339
  encoding: "utf-8",
11506
13340
  env: environment,
11507
13341
  ...input2 === void 0 ? {} : { input: input2 }
@@ -11661,7 +13495,7 @@ var init_version = __esm({
11661
13495
 
11662
13496
  // src/cli.ts
11663
13497
  var import_commander = require("commander");
11664
- var CLI_VERSION = true ? "0.0.38" : "0.0.0-dev";
13498
+ var CLI_VERSION = true ? "0.0.39" : "0.0.0-dev";
11665
13499
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
11666
13500
  function withErrorHandling(action2) {
11667
13501
  return async (...args) => {