@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/README.md +7 -0
- package/bin/devrouter-process +97 -9
- package/dist/devrouter.js +2303 -469
- package/package.json +2 -1
- package/upgrade-prompts/0.0.39.md +48 -0
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
|
|
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
|
|
2069
|
-
if (!
|
|
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(
|
|
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
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
-
|
|
2170
|
-
|
|
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 {
|
|
2409
|
+
return {
|
|
2410
|
+
name: defaultName,
|
|
2411
|
+
profile: normalizeSelectedProfile(
|
|
2412
|
+
defaultName,
|
|
2413
|
+
profiles[defaultName],
|
|
2414
|
+
config.managedRuntime
|
|
2415
|
+
)
|
|
2416
|
+
};
|
|
2186
2417
|
}
|
|
2187
|
-
return {
|
|
2418
|
+
return {
|
|
2419
|
+
name: "full",
|
|
2420
|
+
profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
|
|
2421
|
+
};
|
|
2188
2422
|
}
|
|
2189
|
-
return {
|
|
2423
|
+
return {
|
|
2424
|
+
name: "full",
|
|
2425
|
+
profile: config.managedRuntime ? { apps: ["*"], devcontainerServices: ["*"], processes: ["*"] } : void 0
|
|
2426
|
+
};
|
|
2190
2427
|
}
|
|
2191
2428
|
function applyProfile(config, profile) {
|
|
2192
|
-
|
|
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.
|
|
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 {
|
|
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/
|
|
4372
|
-
function
|
|
4373
|
-
|
|
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
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
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
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4438
|
-
|
|
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
|
-
|
|
4444
|
-
|
|
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
|
-
|
|
4450
|
-
|
|
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
|
|
4457
|
-
|
|
4458
|
-
|
|
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 (
|
|
4470
|
-
|
|
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 (
|
|
4473
|
-
|
|
4743
|
+
if (candidate.status !== "ready" && candidate.status !== "degraded") {
|
|
4744
|
+
throw new Error("Managed runtime state has an invalid transition status.");
|
|
4474
4745
|
}
|
|
4475
|
-
if (
|
|
4476
|
-
|
|
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
|
-
|
|
4489
|
-
|
|
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
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
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
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
4551
|
-
if (!
|
|
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
|
|
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 (!
|
|
5943
|
+
if (!import_node_fs15.default.existsSync(CERT_FILE)) {
|
|
4618
5944
|
return [];
|
|
4619
5945
|
}
|
|
4620
|
-
const pem =
|
|
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
|
|
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
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
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,
|
|
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 =
|
|
4790
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
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 =
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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" || !
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
6656
|
+
const filePath = import_node_path17.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
5328
6657
|
try {
|
|
5329
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
6686
|
+
import_node_fs18.default.mkdirSync(directory, { recursive: true });
|
|
5358
6687
|
return withFileLockSync(
|
|
5359
|
-
|
|
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) &&
|
|
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
|
|
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
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
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 =
|
|
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 =
|
|
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 = `${
|
|
5444
|
-
return
|
|
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
|
|
7016
|
+
var import_node_path18;
|
|
5688
7017
|
var init_workspace_gc = __esm({
|
|
5689
7018
|
"src/core/workspace-gc.ts"() {
|
|
5690
7019
|
"use strict";
|
|
5691
|
-
|
|
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 (!
|
|
7090
|
+
if (!import_node_fs19.default.existsSync(absolutePath)) {
|
|
5762
7091
|
continue;
|
|
5763
7092
|
}
|
|
5764
7093
|
try {
|
|
5765
|
-
const raw =
|
|
5766
|
-
const parsed =
|
|
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.
|
|
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:
|
|
7403
|
+
absolutePath: import_node_path19.default.resolve(repo.path, filePath)
|
|
6059
7404
|
}))
|
|
6060
|
-
).filter((entry) => !
|
|
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:
|
|
6104
|
-
})).filter((entry) => !
|
|
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
|
|
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
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
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,
|
|
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,
|
|
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
|
|
7890
|
+
var import_node_child_process13;
|
|
6546
7891
|
var init_ports = __esm({
|
|
6547
7892
|
"src/util/ports.ts"() {
|
|
6548
7893
|
"use strict";
|
|
6549
|
-
|
|
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
|
-
|
|
7937
|
+
startRouterStack();
|
|
7938
|
+
process.stdout.write("devrouter is up.\n");
|
|
6657
7939
|
}
|
|
6658
|
-
var
|
|
6659
|
-
|
|
6660
|
-
"src/core/devpod-environment.ts"() {
|
|
7940
|
+
var init_up = __esm({
|
|
7941
|
+
"src/commands/up.ts"() {
|
|
6661
7942
|
"use strict";
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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 =
|
|
7009
|
-
if (!
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
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
|
|
9081
|
+
return import_node_path21.default.join(mainRepo, "trees", ws);
|
|
7248
9082
|
}
|
|
7249
9083
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
7250
|
-
const ignored = (0,
|
|
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 '${
|
|
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
|
|
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 || !
|
|
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,
|
|
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 ?
|
|
7400
|
-
if (
|
|
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,
|
|
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,
|
|
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 &&
|
|
7528
|
-
const rm = (0,
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
7834
|
-
|
|
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,
|
|
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
|
|
9860
|
+
var import_node_child_process18;
|
|
8027
9861
|
var init_open = __esm({
|
|
8028
9862
|
"src/commands/open.ts"() {
|
|
8029
9863
|
"use strict";
|
|
8030
|
-
|
|
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,
|
|
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,
|
|
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
|
|
9907
|
+
var import_node_child_process19;
|
|
8074
9908
|
var init_logs = __esm({
|
|
8075
9909
|
"src/commands/logs.ts"() {
|
|
8076
9910
|
"use strict";
|
|
8077
|
-
|
|
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 (!
|
|
9951
|
+
if (!import_node_fs22.default.existsSync(filePath)) {
|
|
8118
9952
|
return void 0;
|
|
8119
9953
|
}
|
|
8120
9954
|
try {
|
|
8121
|
-
return JSON.parse(
|
|
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
|
|
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 (
|
|
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 =
|
|
8184
|
-
if (
|
|
8185
|
-
const version =
|
|
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) => !
|
|
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) =>
|
|
10098
|
+
(fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
|
|
8265
10099
|
);
|
|
8266
10100
|
}
|
|
8267
|
-
function
|
|
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 =
|
|
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:
|
|
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 =
|
|
8338
|
-
const content =
|
|
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 =
|
|
8353
|
-
if (!
|
|
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:
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
8412
|
-
if (
|
|
8413
|
-
for (const name of
|
|
8414
|
-
const skillPath =
|
|
8415
|
-
if (
|
|
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(
|
|
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
|
|
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
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
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) => !
|
|
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
|
|
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
|
-
|
|
8811
|
-
|
|
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(
|
|
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 =
|
|
9153
|
-
if (!
|
|
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 =
|
|
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 =
|
|
9219
|
-
|
|
9220
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9265
|
-
|
|
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,
|
|
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 =
|
|
9736
|
-
|
|
9737
|
-
const overlayPath =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
12554
|
+
const entryPath = import_node_path26.default.join(dirPath, entry.name);
|
|
10721
12555
|
let entryStat;
|
|
10722
12556
|
try {
|
|
10723
|
-
entryStat =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
10802
|
-
|
|
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 || !
|
|
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((
|
|
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
|
|
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
|
-
|
|
11487
|
-
|
|
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,
|
|
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.
|
|
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) => {
|