@devrouter/cli 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bin/devrouter-process +97 -9
- package/dist/devrouter.js +2315 -464
- package/package.json +2 -1
- package/upgrade-prompts/0.0.39.md +48 -0
- package/upgrade-prompts/0.0.40.md +34 -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.40" : "0.0.0-dev";
|
|
2270
2508
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2271
2509
|
hasWarnedVersionMismatch = true;
|
|
2272
2510
|
process.stderr.write(
|
|
@@ -2563,7 +2801,12 @@ function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
|
|
|
2563
2801
|
const resolvedProfile = resolveProfile(raw, profileOverride);
|
|
2564
2802
|
const workspace = resolveWorkspace(resolved, workspaceOverride);
|
|
2565
2803
|
const config = applyProfile(applyWorkspace(raw, workspace, resolved), resolvedProfile.profile);
|
|
2566
|
-
return {
|
|
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.40" : "0.0.0-dev";
|
|
6029
7374
|
const configVersion = config.devrouter?.version;
|
|
6030
7375
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
6031
7376
|
addCheck(checks, {
|
|
@@ -6055,9 +7400,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
6055
7400
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
6056
7401
|
app: app.name,
|
|
6057
7402
|
filePath,
|
|
6058
|
-
absolutePath:
|
|
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
|
|
|
@@ -6578,95 +7923,26 @@ Debug commands:
|
|
|
6578
7923
|
- lsof -nP -iTCP:80 -sTCP:LISTEN
|
|
6579
7924
|
- lsof -nP -iTCP:443 -sTCP:LISTEN
|
|
6580
7925
|
- lsof -nP -iTCP:5432 -sTCP:LISTEN`;
|
|
6581
|
-
}
|
|
6582
|
-
async function runUpCommand() {
|
|
6583
|
-
await ensureNetwork(DEVNET_NAME);
|
|
6584
|
-
ensureRouterFiles();
|
|
6585
|
-
const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
|
|
6586
|
-
if (!routerRunning) {
|
|
6587
|
-
const message = buildPortConflictMessage();
|
|
6588
|
-
if (message) {
|
|
6589
|
-
throw new Error(message);
|
|
6590
|
-
}
|
|
6591
|
-
}
|
|
6592
|
-
startRouterStack();
|
|
6593
|
-
process.stdout.write("devrouter is up.\n");
|
|
6594
|
-
}
|
|
6595
|
-
var init_up = __esm({
|
|
6596
|
-
"src/commands/up.ts"() {
|
|
6597
|
-
"use strict";
|
|
6598
|
-
init_docker();
|
|
6599
|
-
init_router();
|
|
6600
|
-
init_ports();
|
|
6601
|
-
}
|
|
6602
|
-
});
|
|
6603
|
-
|
|
6604
|
-
// src/core/devpod-environment.ts
|
|
6605
|
-
function inspectWorkspaceContainers(options) {
|
|
6606
|
-
let ids = options?.ids;
|
|
6607
|
-
if (!ids) {
|
|
6608
|
-
const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
|
|
6609
|
-
encoding: "utf-8"
|
|
6610
|
-
});
|
|
6611
|
-
if (listed.status !== 0) {
|
|
6612
|
-
throw new Error(
|
|
6613
|
-
`docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
|
|
6614
|
-
);
|
|
6615
|
-
}
|
|
6616
|
-
ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
|
|
6617
|
-
}
|
|
6618
|
-
if (ids.length === 0) return [];
|
|
6619
|
-
const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
|
|
6620
|
-
const inspected = (0, import_node_child_process12.spawnSync)(
|
|
6621
|
-
"docker",
|
|
6622
|
-
["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
|
|
6623
|
-
{ encoding: "utf-8" }
|
|
6624
|
-
);
|
|
6625
|
-
if (inspected.status !== 0) {
|
|
6626
|
-
throw new Error(
|
|
6627
|
-
`docker inspect failed: ${(inspected.stderr || inspected.stdout || "unknown error").trim()}`
|
|
6628
|
-
);
|
|
6629
|
-
}
|
|
6630
|
-
return inspected.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
6631
|
-
}
|
|
6632
|
-
function workspaceAppContainers(containers, repoPath) {
|
|
6633
|
-
return containers.filter((container) => {
|
|
6634
|
-
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
6635
|
-
return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
|
|
6636
|
-
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
6637
|
-
);
|
|
6638
|
-
});
|
|
6639
|
-
}
|
|
6640
|
-
function resolveRunningWorkspaceContainer(repoPath) {
|
|
6641
|
-
const matches = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
|
|
6642
|
-
(container2) => container2.state.Running
|
|
6643
|
-
);
|
|
6644
|
-
if (matches.length !== 1) {
|
|
6645
|
-
throw new Error(
|
|
6646
|
-
`Expected one running workspace app container for '${repoPath}', found ${matches.length}.`
|
|
6647
|
-
);
|
|
6648
|
-
}
|
|
6649
|
-
const container = matches[0];
|
|
6650
|
-
const repoMount = container.mounts.find(
|
|
6651
|
-
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
6652
|
-
);
|
|
6653
|
-
if (!repoMount) {
|
|
6654
|
-
throw new Error(`Workspace app container no longer mounts '${repoPath}'.`);
|
|
7926
|
+
}
|
|
7927
|
+
async function runUpCommand() {
|
|
7928
|
+
await ensureNetwork(DEVNET_NAME);
|
|
7929
|
+
ensureRouterFiles();
|
|
7930
|
+
const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
|
|
7931
|
+
if (!routerRunning) {
|
|
7932
|
+
const message = buildPortConflictMessage();
|
|
7933
|
+
if (message) {
|
|
7934
|
+
throw new Error(message);
|
|
7935
|
+
}
|
|
6655
7936
|
}
|
|
6656
|
-
|
|
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,209 @@ function assertReady(container, label) {
|
|
|
6800
8076
|
throw new Error(`${label} container '${container.id}' is not healthy (${health}).`);
|
|
6801
8077
|
}
|
|
6802
8078
|
}
|
|
8079
|
+
function exactWorkspaceServiceContainers(containers, repoPath, composeProject, service, composeFiles2) {
|
|
8080
|
+
return containers.filter(
|
|
8081
|
+
(container) => hasExactComposeIdentity(container, {
|
|
8082
|
+
repoPath,
|
|
8083
|
+
composeProject,
|
|
8084
|
+
service,
|
|
8085
|
+
composeFiles: composeFiles2
|
|
8086
|
+
})
|
|
8087
|
+
);
|
|
8088
|
+
}
|
|
8089
|
+
function resolveComposeProject(containers, repoPath, appContainerId, primaryService, composeFiles2) {
|
|
8090
|
+
const app = containers.find((candidate) => candidate.id === appContainerId);
|
|
8091
|
+
if (!app) {
|
|
8092
|
+
throw new Error(
|
|
8093
|
+
`Workspace app container '${appContainerId}' disappeared during reconciliation.`
|
|
8094
|
+
);
|
|
8095
|
+
}
|
|
8096
|
+
if (!hasExactComposeIdentity(app, {
|
|
8097
|
+
repoPath,
|
|
8098
|
+
service: primaryService,
|
|
8099
|
+
composeFiles: composeFiles2
|
|
8100
|
+
})) {
|
|
8101
|
+
throw new Error(`Workspace app container '${appContainerId}' has an unexpected Compose model.`);
|
|
8102
|
+
}
|
|
8103
|
+
const project = app.labels["com.docker.compose.project"];
|
|
8104
|
+
if (!project) {
|
|
8105
|
+
throw new Error(`Workspace app container '${appContainerId}' has no Compose project label.`);
|
|
8106
|
+
}
|
|
8107
|
+
return project;
|
|
8108
|
+
}
|
|
8109
|
+
async function waitForManagedServices(plan, repoPath, composeProject, expectedServices, expectedAppId, timeoutMs) {
|
|
8110
|
+
const deadline = Date.now() + timeoutMs;
|
|
8111
|
+
let lastError;
|
|
8112
|
+
do {
|
|
8113
|
+
try {
|
|
8114
|
+
const containers = inspectWorkspaceContainers();
|
|
8115
|
+
for (const service of expectedServices) {
|
|
8116
|
+
const matches = exactWorkspaceServiceContainers(
|
|
8117
|
+
containers,
|
|
8118
|
+
repoPath,
|
|
8119
|
+
composeProject,
|
|
8120
|
+
service,
|
|
8121
|
+
plan.composeFiles
|
|
8122
|
+
);
|
|
8123
|
+
if (matches.length !== 1) {
|
|
8124
|
+
throw new Error(
|
|
8125
|
+
`Managed service '${service}' must have exactly one exact workspace container; found ${matches.length}.`
|
|
8126
|
+
);
|
|
8127
|
+
}
|
|
8128
|
+
if (service === plan.primaryService && matches[0].id !== expectedAppId) {
|
|
8129
|
+
throw new Error(
|
|
8130
|
+
`Managed primary service '${service}' changed container identity during profile transition.`
|
|
8131
|
+
);
|
|
8132
|
+
}
|
|
8133
|
+
assertReady(matches[0], `Managed service '${service}'`);
|
|
8134
|
+
}
|
|
8135
|
+
return;
|
|
8136
|
+
} catch (error) {
|
|
8137
|
+
lastError = error;
|
|
8138
|
+
}
|
|
8139
|
+
if (Date.now() < deadline) await sleep(POLL_INTERVAL_MS);
|
|
8140
|
+
} while (Date.now() < deadline);
|
|
8141
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
8142
|
+
}
|
|
8143
|
+
function stopDroppedManagedServices(plan, repoPath, composeProject) {
|
|
8144
|
+
const desired = new Set(plan.desiredProfileServices);
|
|
8145
|
+
const containers = inspectWorkspaceContainers();
|
|
8146
|
+
for (const service of plan.profileServices) {
|
|
8147
|
+
if (desired.has(service)) continue;
|
|
8148
|
+
const matches = exactWorkspaceServiceContainers(
|
|
8149
|
+
containers,
|
|
8150
|
+
repoPath,
|
|
8151
|
+
composeProject,
|
|
8152
|
+
service,
|
|
8153
|
+
plan.composeFiles
|
|
8154
|
+
);
|
|
8155
|
+
if (matches.length > 1) {
|
|
8156
|
+
throw new Error(
|
|
8157
|
+
`Managed service '${service}' has multiple exact workspace containers; refusing to stop any.`
|
|
8158
|
+
);
|
|
8159
|
+
}
|
|
8160
|
+
const match = matches[0];
|
|
8161
|
+
if (match?.state.Running) stopExactManagedService(match.id, service);
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
function assertDroppedManagedServicesStopped(plan, repoPath, composeProject) {
|
|
8165
|
+
const desired = new Set(plan.desiredProfileServices);
|
|
8166
|
+
const containers = inspectWorkspaceContainers();
|
|
8167
|
+
for (const service of plan.profileServices) {
|
|
8168
|
+
if (desired.has(service)) continue;
|
|
8169
|
+
const matches = exactWorkspaceServiceContainers(
|
|
8170
|
+
containers,
|
|
8171
|
+
repoPath,
|
|
8172
|
+
composeProject,
|
|
8173
|
+
service,
|
|
8174
|
+
plan.composeFiles
|
|
8175
|
+
);
|
|
8176
|
+
if (matches.some((match) => match.state.Running)) {
|
|
8177
|
+
throw new Error(`Managed service '${service}' remains running after exact stop.`);
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
function routeInputFromState(route) {
|
|
8182
|
+
return {
|
|
8183
|
+
name: route.name,
|
|
8184
|
+
host: route.host,
|
|
8185
|
+
protocol: route.protocol,
|
|
8186
|
+
tcpProtocol: route.tcpProtocol,
|
|
8187
|
+
repoPath: route.repoPath,
|
|
8188
|
+
port: route.port,
|
|
8189
|
+
mode: route.mode,
|
|
8190
|
+
upstreamHost: route.upstreamHost,
|
|
8191
|
+
pid: route.pid,
|
|
8192
|
+
command: route.command,
|
|
8193
|
+
workspace: route.workspace
|
|
8194
|
+
};
|
|
8195
|
+
}
|
|
8196
|
+
function isWildcard2(values) {
|
|
8197
|
+
return values?.length === 1 && values[0] === "*";
|
|
8198
|
+
}
|
|
8199
|
+
function desiredManagedProcesses(managedRuntime, profile) {
|
|
8200
|
+
if (!profile || isWildcard2(profile.processes)) return [...managedRuntime.processes];
|
|
8201
|
+
return [...profile.processes ?? []];
|
|
8202
|
+
}
|
|
8203
|
+
function restorePreviousManagedConfig(options) {
|
|
8204
|
+
const restoredPlan = inspectManagedDevcontainerConfig({
|
|
8205
|
+
repoPath: options.repoPath,
|
|
8206
|
+
config: options.config,
|
|
8207
|
+
profile: {
|
|
8208
|
+
apps: [],
|
|
8209
|
+
devcontainerServices: [...options.previousState.desired.services]
|
|
8210
|
+
},
|
|
8211
|
+
linked: options.linked
|
|
8212
|
+
});
|
|
8213
|
+
if (restoredPlan.effectiveConfigSha256 !== options.previousState.effectiveConfigSha256) {
|
|
8214
|
+
throw new Error(
|
|
8215
|
+
"The previous managed Dev Container configuration fingerprint no longer matches the current source."
|
|
8216
|
+
);
|
|
8217
|
+
}
|
|
8218
|
+
writeManagedDevcontainerConfig(restoredPlan);
|
|
8219
|
+
}
|
|
8220
|
+
function restoreFirstTransitionManagedConfig(options) {
|
|
8221
|
+
const restoredPlan = inspectManagedDevcontainerConfig({
|
|
8222
|
+
repoPath: options.repoPath,
|
|
8223
|
+
config: options.config,
|
|
8224
|
+
profile: {
|
|
8225
|
+
apps: [],
|
|
8226
|
+
devcontainerServices: [...options.services]
|
|
8227
|
+
},
|
|
8228
|
+
linked: options.linked
|
|
8229
|
+
});
|
|
8230
|
+
writeManagedDevcontainerConfig(restoredPlan);
|
|
8231
|
+
}
|
|
8232
|
+
function isWarmWorkspaceActive(repoPath) {
|
|
8233
|
+
try {
|
|
8234
|
+
return workspaceAppContainers(inspectWorkspaceContainers(), repoPath).some(
|
|
8235
|
+
(container) => container.state.Running
|
|
8236
|
+
);
|
|
8237
|
+
} catch {
|
|
8238
|
+
return true;
|
|
8239
|
+
}
|
|
8240
|
+
}
|
|
8241
|
+
function captureFirstTransitionBaseline(options) {
|
|
8242
|
+
const containers = inspectWorkspaceContainers();
|
|
8243
|
+
const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
|
|
8244
|
+
(container) => container.state.Running
|
|
8245
|
+
);
|
|
8246
|
+
if (runningPrimary.length === 0) return void 0;
|
|
8247
|
+
if (runningPrimary.length > 1) {
|
|
8248
|
+
throw new Error(
|
|
8249
|
+
"Cannot start the first managed transition while multiple workspace app containers are running."
|
|
8250
|
+
);
|
|
8251
|
+
}
|
|
8252
|
+
const primary = runningPrimary[0];
|
|
8253
|
+
const composeProject = primary.labels["com.docker.compose.project"];
|
|
8254
|
+
const workspacePath = primary.mounts.find(
|
|
8255
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, options.repoPath)
|
|
8256
|
+
)?.Destination;
|
|
8257
|
+
if (!composeProject || !workspacePath) {
|
|
8258
|
+
throw new Error(
|
|
8259
|
+
"Cannot prove the exact pre-transition Compose identity for the first managed transition."
|
|
8260
|
+
);
|
|
8261
|
+
}
|
|
8262
|
+
const services = options.plan.profileServices.filter(
|
|
8263
|
+
(service) => containers.some(
|
|
8264
|
+
(container) => container.state.Running && hasExactComposeIdentity(container, { repoPath: options.repoPath, service, composeProject })
|
|
8265
|
+
)
|
|
8266
|
+
);
|
|
8267
|
+
const activeProcesses = [];
|
|
8268
|
+
for (const process2 of options.processes) {
|
|
8269
|
+
const status = runManagedProcessAction({
|
|
8270
|
+
container: { id: primary.id, workspacePath },
|
|
8271
|
+
name: process2,
|
|
8272
|
+
action: "status",
|
|
8273
|
+
quiet: true
|
|
8274
|
+
});
|
|
8275
|
+
if (status === "running") activeProcesses.push(process2);
|
|
8276
|
+
}
|
|
8277
|
+
return {
|
|
8278
|
+
services,
|
|
8279
|
+
processes: activeProcesses
|
|
8280
|
+
};
|
|
8281
|
+
}
|
|
6803
8282
|
function validateWorkspaceContainers(containers, options) {
|
|
6804
8283
|
const appContainers = workspaceAppContainers(containers, options.repoPath);
|
|
6805
8284
|
if (appContainers.length !== 1) {
|
|
@@ -6860,7 +8339,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6860
8339
|
target
|
|
6861
8340
|
});
|
|
6862
8341
|
if (target.kind === "linked") {
|
|
6863
|
-
const workspaceEnv = (0,
|
|
8342
|
+
const workspaceEnv = (0, import_node_child_process15.spawnSync)(
|
|
6864
8343
|
"docker",
|
|
6865
8344
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
6866
8345
|
{ encoding: "utf-8" }
|
|
@@ -6870,7 +8349,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6870
8349
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
6871
8350
|
);
|
|
6872
8351
|
}
|
|
6873
|
-
const devrouterWorkspaceEnv = (0,
|
|
8352
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
|
|
6874
8353
|
"docker",
|
|
6875
8354
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
6876
8355
|
{ encoding: "utf-8" }
|
|
@@ -6881,7 +8360,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6881
8360
|
);
|
|
6882
8361
|
}
|
|
6883
8362
|
}
|
|
6884
|
-
const gitCheck = (0,
|
|
8363
|
+
const gitCheck = (0, import_node_child_process15.spawnSync)(
|
|
6885
8364
|
"docker",
|
|
6886
8365
|
[
|
|
6887
8366
|
"exec",
|
|
@@ -6968,14 +8447,14 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
6968
8447
|
}
|
|
6969
8448
|
function isPrimaryCheckout(repoPath) {
|
|
6970
8449
|
try {
|
|
6971
|
-
return
|
|
8450
|
+
return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
|
|
6972
8451
|
} catch {
|
|
6973
8452
|
return false;
|
|
6974
8453
|
}
|
|
6975
8454
|
}
|
|
6976
8455
|
function openUrls(urls) {
|
|
6977
8456
|
for (const url of urls) {
|
|
6978
|
-
const opened = (0,
|
|
8457
|
+
const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
6979
8458
|
if (opened.status !== 0) {
|
|
6980
8459
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
6981
8460
|
`);
|
|
@@ -7001,24 +8480,92 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7001
8480
|
}
|
|
7002
8481
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
7003
8482
|
let environmentStarted = false;
|
|
8483
|
+
let managedPlan;
|
|
8484
|
+
let previousManagedState;
|
|
8485
|
+
let managedComposeProject;
|
|
8486
|
+
let managedContainer;
|
|
8487
|
+
let previousRoutes = [];
|
|
8488
|
+
let candidateRoutesPublished = false;
|
|
8489
|
+
let transitionPhase = "validation";
|
|
8490
|
+
let managedProcessRegistry = [];
|
|
8491
|
+
let managedPostStartPlan;
|
|
8492
|
+
let managedRuntimeConfig;
|
|
8493
|
+
let managedConfigWritten = false;
|
|
8494
|
+
let managedWorkspaceEnv;
|
|
8495
|
+
let firstTransitionBaseline;
|
|
7004
8496
|
try {
|
|
7005
8497
|
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
7006
8498
|
let devpodId = target.devpodId;
|
|
7007
8499
|
if (target.kind === "linked") {
|
|
7008
|
-
const overlayPath =
|
|
7009
|
-
if (!
|
|
8500
|
+
const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
8501
|
+
if (!import_node_fs20.default.existsSync(overlayPath)) {
|
|
7010
8502
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
7011
8503
|
}
|
|
7012
8504
|
}
|
|
7013
8505
|
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
8506
|
+
managedPostStartPlan = managedPostStart;
|
|
7014
8507
|
const runtime = loadRuntimeConfig(
|
|
7015
8508
|
repoPath,
|
|
7016
8509
|
target.kind === "primary" ? "" : target.workspace,
|
|
7017
8510
|
options.profile
|
|
7018
8511
|
);
|
|
8512
|
+
const managedRuntime = runtime.config.managedRuntime;
|
|
8513
|
+
managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
|
|
8514
|
+
managedProcessRegistry = managedRuntime?.processes ?? [];
|
|
8515
|
+
const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
|
|
8516
|
+
if (managedRuntime) {
|
|
8517
|
+
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
8518
|
+
if (previousManagedState?.status === "degraded") {
|
|
8519
|
+
throw new Error(
|
|
8520
|
+
"Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
|
|
8521
|
+
);
|
|
8522
|
+
}
|
|
8523
|
+
if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
|
|
8524
|
+
throw new Error(
|
|
8525
|
+
`Managed runtime state names DevPod '${previousManagedState.devpodId}', not the exact target '${target.devpodId ?? "(absent)"}'.`
|
|
8526
|
+
);
|
|
8527
|
+
}
|
|
8528
|
+
if (previousManagedState?.desired.services.some(
|
|
8529
|
+
(service) => !managedRuntime.devcontainer.profileServices.includes(service)
|
|
8530
|
+
)) {
|
|
8531
|
+
throw new Error("Managed runtime state contains an unregistered profile service.");
|
|
8532
|
+
}
|
|
8533
|
+
if (previousManagedState?.desired.processes.some(
|
|
8534
|
+
(process2) => !managedRuntime.processes.includes(process2)
|
|
8535
|
+
)) {
|
|
8536
|
+
throw new Error("Managed runtime state contains an unregistered process marker.");
|
|
8537
|
+
}
|
|
8538
|
+
previousRoutes = listHostRouteState().filter(
|
|
8539
|
+
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
8540
|
+
);
|
|
8541
|
+
if (managedPostStart.kind !== "runtime") {
|
|
8542
|
+
throw new Error(
|
|
8543
|
+
"managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
managedPlan = inspectManagedDevcontainerConfig({
|
|
8547
|
+
repoPath,
|
|
8548
|
+
config: runtime.config,
|
|
8549
|
+
profile: runtime.resolvedProfile,
|
|
8550
|
+
linked: target.kind === "linked"
|
|
8551
|
+
});
|
|
8552
|
+
managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
|
|
8553
|
+
if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
|
|
8554
|
+
throw new Error(
|
|
8555
|
+
`Managed Dev Container source configuration changed since the last successful transition; a warm ensure cannot apply it to the existing runtime for '${repoPath}'. Stop and delete the exact runtime, then run ensure again to recreate it from the current source configuration.`
|
|
8556
|
+
);
|
|
8557
|
+
}
|
|
8558
|
+
if (!previousManagedState) {
|
|
8559
|
+
firstTransitionBaseline = captureFirstTransitionBaseline({
|
|
8560
|
+
repoPath,
|
|
8561
|
+
plan: managedPlan,
|
|
8562
|
+
processes: managedRuntime.processes
|
|
8563
|
+
});
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
7019
8566
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
7020
8567
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
7021
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
8568
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
|
|
7022
8569
|
for (const [index, app] of apps.entries()) {
|
|
7023
8570
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
7024
8571
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -7027,6 +8574,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7027
8574
|
);
|
|
7028
8575
|
}
|
|
7029
8576
|
}
|
|
8577
|
+
if (managedPlan) {
|
|
8578
|
+
writeManagedDevcontainerConfig(managedPlan);
|
|
8579
|
+
managedConfigWritten = true;
|
|
8580
|
+
}
|
|
7030
8581
|
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
7031
8582
|
const ownership = target.kind === "linked" ? {
|
|
7032
8583
|
workspace: target.workspace,
|
|
@@ -7044,6 +8595,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7044
8595
|
devpodId = startDevpodWorkspace({
|
|
7045
8596
|
repoPath,
|
|
7046
8597
|
devpodId: requestedTarget.devpodId,
|
|
8598
|
+
devcontainerPath: managedPlan?.generatedRelativePath,
|
|
7047
8599
|
recreate,
|
|
7048
8600
|
quiet: options.quiet,
|
|
7049
8601
|
...requestedTarget.kind === "linked" ? {
|
|
@@ -7072,7 +8624,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7072
8624
|
try {
|
|
7073
8625
|
startAndProveAttachment();
|
|
7074
8626
|
} catch (error) {
|
|
7075
|
-
if (!target.hadExactDevpod) {
|
|
8627
|
+
if (managedPlan || !target.hadExactDevpod) {
|
|
7076
8628
|
throw error;
|
|
7077
8629
|
}
|
|
7078
8630
|
container = await recreateAndPreflight();
|
|
@@ -7082,24 +8634,113 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7082
8634
|
try {
|
|
7083
8635
|
container = await preflight(0);
|
|
7084
8636
|
} catch (error) {
|
|
7085
|
-
if (!target.hadExactDevpod) {
|
|
8637
|
+
if (managedPlan || !target.hadExactDevpod) {
|
|
7086
8638
|
throw error;
|
|
7087
8639
|
}
|
|
7088
8640
|
container = await recreateAndPreflight();
|
|
7089
8641
|
recreated = true;
|
|
7090
8642
|
}
|
|
7091
8643
|
}
|
|
8644
|
+
managedContainer = container;
|
|
8645
|
+
if (managedPlan) {
|
|
8646
|
+
transitionPhase = "service-start";
|
|
8647
|
+
const initialContainers = inspectWorkspaceContainers();
|
|
8648
|
+
managedComposeProject = resolveComposeProject(
|
|
8649
|
+
initialContainers,
|
|
8650
|
+
repoPath,
|
|
8651
|
+
container.id,
|
|
8652
|
+
managedPlan.primaryService,
|
|
8653
|
+
managedPlan.composeFiles
|
|
8654
|
+
);
|
|
8655
|
+
if (previousManagedState && previousManagedState.composeProject !== managedComposeProject) {
|
|
8656
|
+
throw new Error(
|
|
8657
|
+
`Managed runtime state names Compose project '${previousManagedState.composeProject}', not '${managedComposeProject}'.`
|
|
8658
|
+
);
|
|
8659
|
+
}
|
|
8660
|
+
const missingServices = [];
|
|
8661
|
+
for (const service of managedPlan.desiredServices) {
|
|
8662
|
+
const matches = exactWorkspaceServiceContainers(
|
|
8663
|
+
initialContainers,
|
|
8664
|
+
repoPath,
|
|
8665
|
+
managedComposeProject,
|
|
8666
|
+
service,
|
|
8667
|
+
managedPlan.composeFiles
|
|
8668
|
+
);
|
|
8669
|
+
if (matches.length > 1) {
|
|
8670
|
+
throw new Error(
|
|
8671
|
+
`Managed service '${service}' has multiple exact workspace containers; refusing to reconcile it.`
|
|
8672
|
+
);
|
|
8673
|
+
}
|
|
8674
|
+
if (!matches[0]?.state.Running) missingServices.push(service);
|
|
8675
|
+
}
|
|
8676
|
+
startExactManagedServices({
|
|
8677
|
+
plan: managedPlan,
|
|
8678
|
+
composeProject: managedComposeProject,
|
|
8679
|
+
services: missingServices,
|
|
8680
|
+
quiet: options.quiet,
|
|
8681
|
+
workspace: managedWorkspaceEnv
|
|
8682
|
+
});
|
|
8683
|
+
await waitForManagedServices(
|
|
8684
|
+
managedPlan,
|
|
8685
|
+
repoPath,
|
|
8686
|
+
managedComposeProject,
|
|
8687
|
+
managedPlan.desiredServices,
|
|
8688
|
+
container.id,
|
|
8689
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
8690
|
+
);
|
|
8691
|
+
container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
8692
|
+
managedContainer = container;
|
|
8693
|
+
}
|
|
8694
|
+
transitionPhase = "process-start";
|
|
7092
8695
|
runManagedPostStart({
|
|
7093
8696
|
plan: managedPostStart,
|
|
7094
8697
|
container,
|
|
7095
8698
|
quiet: options.quiet,
|
|
7096
|
-
profile: runtime.profile
|
|
8699
|
+
profile: runtime.profile,
|
|
8700
|
+
...managedPlan ? { processes: desiredProcesses } : {}
|
|
7097
8701
|
});
|
|
8702
|
+
if (managedPlan && managedComposeProject) {
|
|
8703
|
+
const processRegistry = runtime.config.managedRuntime?.processes ?? [];
|
|
8704
|
+
const desiredProcessSet = new Set(desiredProcesses);
|
|
8705
|
+
for (const process2 of processRegistry) {
|
|
8706
|
+
if (desiredProcessSet.has(process2)) continue;
|
|
8707
|
+
runManagedProcessAction({
|
|
8708
|
+
container,
|
|
8709
|
+
name: process2,
|
|
8710
|
+
action: "stop",
|
|
8711
|
+
quiet: options.quiet
|
|
8712
|
+
});
|
|
8713
|
+
}
|
|
8714
|
+
for (const process2 of desiredProcesses) {
|
|
8715
|
+
const status = runManagedProcessAction({
|
|
8716
|
+
container,
|
|
8717
|
+
name: process2,
|
|
8718
|
+
action: "status",
|
|
8719
|
+
quiet: options.quiet
|
|
8720
|
+
});
|
|
8721
|
+
if (status !== "running") {
|
|
8722
|
+
throw new Error(`Managed process '${process2}' is not running (${status}).`);
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
transitionPhase = "service-stop";
|
|
8726
|
+
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
8727
|
+
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
8728
|
+
await waitForManagedServices(
|
|
8729
|
+
managedPlan,
|
|
8730
|
+
repoPath,
|
|
8731
|
+
managedComposeProject,
|
|
8732
|
+
managedPlan.desiredServices,
|
|
8733
|
+
container.id,
|
|
8734
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
8735
|
+
);
|
|
8736
|
+
}
|
|
8737
|
+
transitionPhase = "route-publication";
|
|
7098
8738
|
const publication = await replacePublishedProxyRoutes(
|
|
7099
8739
|
repoPath,
|
|
7100
8740
|
runtime.config,
|
|
7101
8741
|
target.workspace
|
|
7102
8742
|
);
|
|
8743
|
+
candidateRoutesPublished = true;
|
|
7103
8744
|
try {
|
|
7104
8745
|
await waitForHttpRoutes(
|
|
7105
8746
|
repoPath,
|
|
@@ -7107,6 +8748,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7107
8748
|
options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
7108
8749
|
);
|
|
7109
8750
|
} catch (error) {
|
|
8751
|
+
if (managedPlan) {
|
|
8752
|
+
throw error;
|
|
8753
|
+
}
|
|
7110
8754
|
replaceHostRoutesForRepo(repoPath, []);
|
|
7111
8755
|
if (!target.hadExactDevpod || recreated) {
|
|
7112
8756
|
throw error;
|
|
@@ -7137,6 +8781,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7137
8781
|
if (!devpodId) {
|
|
7138
8782
|
throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
|
|
7139
8783
|
}
|
|
8784
|
+
if (managedPlan && managedComposeProject) {
|
|
8785
|
+
writeManagedRuntimeState({
|
|
8786
|
+
version: 1,
|
|
8787
|
+
repoPath,
|
|
8788
|
+
...target.workspace !== void 0 ? { workspace: target.workspace } : {},
|
|
8789
|
+
devpodId,
|
|
8790
|
+
composeProject: managedComposeProject,
|
|
8791
|
+
profile: runtime.profile,
|
|
8792
|
+
desired: {
|
|
8793
|
+
apps: apps.map((app) => app.name).sort(),
|
|
8794
|
+
services: [...managedPlan.desiredProfileServices].sort(),
|
|
8795
|
+
processes: [...desiredProcesses].sort()
|
|
8796
|
+
},
|
|
8797
|
+
sourceConfigSha256: managedPlan.sourceConfigSha256,
|
|
8798
|
+
effectiveConfigSha256: managedPlan.effectiveConfigSha256,
|
|
8799
|
+
status: "ready",
|
|
8800
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8801
|
+
});
|
|
8802
|
+
}
|
|
8803
|
+
const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
|
|
8804
|
+
repoPath,
|
|
8805
|
+
workspace: target.workspace,
|
|
8806
|
+
config: runtime.config,
|
|
8807
|
+
profile: runtime.profile,
|
|
8808
|
+
resolvedProfile: runtime.resolvedProfile
|
|
8809
|
+
}) : void 0;
|
|
7140
8810
|
return {
|
|
7141
8811
|
kind: target.kind,
|
|
7142
8812
|
repoPath,
|
|
@@ -7145,9 +8815,187 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7145
8815
|
devpodId,
|
|
7146
8816
|
urls,
|
|
7147
8817
|
recreated,
|
|
7148
|
-
tlsRefreshed: publication.tlsRefreshed
|
|
8818
|
+
tlsRefreshed: publication.tlsRefreshed,
|
|
8819
|
+
...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
|
|
7149
8820
|
};
|
|
7150
8821
|
} catch (error) {
|
|
8822
|
+
if (managedPlan && managedContainer && managedComposeProject) {
|
|
8823
|
+
const rollbackErrors = [];
|
|
8824
|
+
const failedPhase = transitionPhase;
|
|
8825
|
+
transitionPhase = "rollback";
|
|
8826
|
+
const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
|
|
8827
|
+
const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
|
|
8828
|
+
const previousServiceSet = new Set(previousServices);
|
|
8829
|
+
const previousProcessSet = new Set(previousProcesses);
|
|
8830
|
+
try {
|
|
8831
|
+
if (managedConfigWritten) {
|
|
8832
|
+
if (previousManagedState && managedRuntimeConfig) {
|
|
8833
|
+
restorePreviousManagedConfig({
|
|
8834
|
+
repoPath,
|
|
8835
|
+
config: managedRuntimeConfig,
|
|
8836
|
+
linked,
|
|
8837
|
+
previousState: previousManagedState
|
|
8838
|
+
});
|
|
8839
|
+
} else if (managedRuntimeConfig) {
|
|
8840
|
+
restoreFirstTransitionManagedConfig({
|
|
8841
|
+
repoPath,
|
|
8842
|
+
config: managedRuntimeConfig,
|
|
8843
|
+
linked,
|
|
8844
|
+
services: previousServices
|
|
8845
|
+
});
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
} catch (rollbackError) {
|
|
8849
|
+
rollbackErrors.push(
|
|
8850
|
+
`configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8851
|
+
);
|
|
8852
|
+
}
|
|
8853
|
+
try {
|
|
8854
|
+
const previousAllServices = [
|
|
8855
|
+
managedPlan.primaryService,
|
|
8856
|
+
...managedPlan.baseServices,
|
|
8857
|
+
...previousServices
|
|
8858
|
+
];
|
|
8859
|
+
startExactManagedServices({
|
|
8860
|
+
plan: managedPlan,
|
|
8861
|
+
composeProject: managedComposeProject,
|
|
8862
|
+
services: previousAllServices,
|
|
8863
|
+
quiet: options.quiet,
|
|
8864
|
+
workspace: managedWorkspaceEnv
|
|
8865
|
+
});
|
|
8866
|
+
await waitForManagedServices(
|
|
8867
|
+
managedPlan,
|
|
8868
|
+
repoPath,
|
|
8869
|
+
managedComposeProject,
|
|
8870
|
+
previousAllServices,
|
|
8871
|
+
managedContainer.id,
|
|
8872
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
8873
|
+
);
|
|
8874
|
+
} catch (rollbackError) {
|
|
8875
|
+
rollbackErrors.push(
|
|
8876
|
+
`services: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8877
|
+
);
|
|
8878
|
+
}
|
|
8879
|
+
try {
|
|
8880
|
+
if (!managedPostStartPlan) {
|
|
8881
|
+
throw new Error("Managed post-start plan disappeared during rollback.");
|
|
8882
|
+
}
|
|
8883
|
+
runManagedPostStart({
|
|
8884
|
+
plan: managedPostStartPlan,
|
|
8885
|
+
container: managedContainer,
|
|
8886
|
+
quiet: options.quiet,
|
|
8887
|
+
profile: previousManagedState?.profile ?? "full",
|
|
8888
|
+
processes: previousProcesses
|
|
8889
|
+
});
|
|
8890
|
+
for (const process2 of managedProcessRegistry) {
|
|
8891
|
+
if (previousProcessSet.has(process2)) continue;
|
|
8892
|
+
runManagedProcessAction({
|
|
8893
|
+
container: managedContainer,
|
|
8894
|
+
name: process2,
|
|
8895
|
+
action: "stop",
|
|
8896
|
+
quiet: options.quiet
|
|
8897
|
+
});
|
|
8898
|
+
}
|
|
8899
|
+
} catch (rollbackError) {
|
|
8900
|
+
rollbackErrors.push(
|
|
8901
|
+
`processes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8902
|
+
);
|
|
8903
|
+
}
|
|
8904
|
+
try {
|
|
8905
|
+
const containers = inspectWorkspaceContainers();
|
|
8906
|
+
for (const service of managedPlan.profileServices) {
|
|
8907
|
+
if (previousServiceSet.has(service)) continue;
|
|
8908
|
+
const matches = exactWorkspaceServiceContainers(
|
|
8909
|
+
containers,
|
|
8910
|
+
repoPath,
|
|
8911
|
+
managedComposeProject,
|
|
8912
|
+
service,
|
|
8913
|
+
managedPlan.composeFiles
|
|
8914
|
+
);
|
|
8915
|
+
if (matches.length > 1) {
|
|
8916
|
+
throw new Error(`service '${service}' has multiple exact containers`);
|
|
8917
|
+
}
|
|
8918
|
+
if (matches[0]?.state.Running) stopExactManagedService(matches[0].id, service);
|
|
8919
|
+
}
|
|
8920
|
+
assertDroppedManagedServicesStopped(
|
|
8921
|
+
{
|
|
8922
|
+
...managedPlan,
|
|
8923
|
+
desiredProfileServices: previousServices,
|
|
8924
|
+
desiredServices: [
|
|
8925
|
+
managedPlan.primaryService,
|
|
8926
|
+
...managedPlan.baseServices,
|
|
8927
|
+
...previousServices
|
|
8928
|
+
]
|
|
8929
|
+
},
|
|
8930
|
+
repoPath,
|
|
8931
|
+
managedComposeProject
|
|
8932
|
+
);
|
|
8933
|
+
} catch (rollbackError) {
|
|
8934
|
+
rollbackErrors.push(
|
|
8935
|
+
`service cleanup: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8936
|
+
);
|
|
8937
|
+
}
|
|
8938
|
+
if (candidateRoutesPublished) {
|
|
8939
|
+
try {
|
|
8940
|
+
replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
|
|
8941
|
+
} catch (rollbackError) {
|
|
8942
|
+
rollbackErrors.push(
|
|
8943
|
+
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8944
|
+
);
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
if (rollbackErrors.length > 0 && previousManagedState) {
|
|
8948
|
+
try {
|
|
8949
|
+
markManagedRuntimeDegraded(previousManagedState, failedPhase);
|
|
8950
|
+
} catch (stateError) {
|
|
8951
|
+
rollbackErrors.push(
|
|
8952
|
+
`state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
|
|
8953
|
+
);
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
8957
|
+
const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : " Candidate runtime was rolled back.";
|
|
8958
|
+
throw new Error(`${original}${suffix}`);
|
|
8959
|
+
}
|
|
8960
|
+
if (managedPlan) {
|
|
8961
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
8962
|
+
const rollbackErrors = [];
|
|
8963
|
+
if (managedConfigWritten) {
|
|
8964
|
+
try {
|
|
8965
|
+
if (previousManagedState && managedRuntimeConfig) {
|
|
8966
|
+
restorePreviousManagedConfig({
|
|
8967
|
+
repoPath,
|
|
8968
|
+
config: managedRuntimeConfig,
|
|
8969
|
+
linked,
|
|
8970
|
+
previousState: previousManagedState
|
|
8971
|
+
});
|
|
8972
|
+
} else if (!environmentStarted) {
|
|
8973
|
+
removeManagedDevcontainerConfig(managedPlan);
|
|
8974
|
+
}
|
|
8975
|
+
} catch (rollbackError) {
|
|
8976
|
+
rollbackErrors.push(
|
|
8977
|
+
`configuration: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8978
|
+
);
|
|
8979
|
+
}
|
|
8980
|
+
}
|
|
8981
|
+
if (candidateRoutesPublished) {
|
|
8982
|
+
try {
|
|
8983
|
+
replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
|
|
8984
|
+
} catch (rollbackError) {
|
|
8985
|
+
rollbackErrors.push(
|
|
8986
|
+
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
8987
|
+
);
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
if (rollbackErrors.length > 0) {
|
|
8991
|
+
throw new Error(
|
|
8992
|
+
`${original} Rollback left degraded drift: ${rollbackErrors.join("; ")}.`
|
|
8993
|
+
);
|
|
8994
|
+
}
|
|
8995
|
+
throw new Error(
|
|
8996
|
+
`${original} Managed runtime transition did not reach a rollback boundary.`
|
|
8997
|
+
);
|
|
8998
|
+
}
|
|
7151
8999
|
if (environmentStarted) {
|
|
7152
9000
|
try {
|
|
7153
9001
|
replaceHostRoutesForRepo(repoPath, []);
|
|
@@ -7161,19 +9009,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7161
9009
|
}
|
|
7162
9010
|
});
|
|
7163
9011
|
}
|
|
7164
|
-
var
|
|
9012
|
+
var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
7165
9013
|
var init_workspace_ensure = __esm({
|
|
7166
9014
|
"src/core/workspace-ensure.ts"() {
|
|
7167
9015
|
"use strict";
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
9016
|
+
import_node_child_process15 = require("child_process");
|
|
9017
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
9018
|
+
import_node_path20 = __toESM(require("path"));
|
|
9019
|
+
init_devcontainer_profile();
|
|
7171
9020
|
init_devpod_environment();
|
|
7172
9021
|
init_devpod_mutation();
|
|
7173
9022
|
init_devpod_workspaces();
|
|
7174
9023
|
init_host_routes();
|
|
7175
9024
|
init_http_route_probe();
|
|
7176
9025
|
init_managed_post_start();
|
|
9026
|
+
init_managed_runtime_state();
|
|
9027
|
+
init_managed_runtime_status();
|
|
7177
9028
|
init_repo_config();
|
|
7178
9029
|
init_route_publication();
|
|
7179
9030
|
init_router();
|
|
@@ -7244,20 +9095,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
7244
9095
|
);
|
|
7245
9096
|
}
|
|
7246
9097
|
function defaultWorktreePath(mainRepo, ws) {
|
|
7247
|
-
return
|
|
9098
|
+
return import_node_path21.default.join(mainRepo, "trees", ws);
|
|
7248
9099
|
}
|
|
7249
9100
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
7250
|
-
const ignored = (0,
|
|
9101
|
+
const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
7251
9102
|
encoding: "utf-8"
|
|
7252
9103
|
});
|
|
7253
9104
|
if (ignored.status !== 0) {
|
|
7254
9105
|
throw new Error(
|
|
7255
|
-
`Default worktree root '${
|
|
9106
|
+
`Default worktree root '${import_node_path21.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path21.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
7256
9107
|
);
|
|
7257
9108
|
}
|
|
7258
9109
|
}
|
|
7259
9110
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
7260
|
-
return
|
|
9111
|
+
return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
|
|
7261
9112
|
}
|
|
7262
9113
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
7263
9114
|
const candidates = [
|
|
@@ -7369,13 +9220,13 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
7369
9220
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
7370
9221
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
7371
9222
|
}
|
|
7372
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
9223
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
|
|
7373
9224
|
if (target.worktree.locked) {
|
|
7374
9225
|
throw new Error(
|
|
7375
9226
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
7376
9227
|
);
|
|
7377
9228
|
}
|
|
7378
|
-
const status = (0,
|
|
9229
|
+
const status = (0, import_node_child_process16.spawnSync)(
|
|
7379
9230
|
"git",
|
|
7380
9231
|
["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
|
|
7381
9232
|
{ encoding: "utf-8" }
|
|
@@ -7396,8 +9247,8 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
7396
9247
|
if (!ws) {
|
|
7397
9248
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
7398
9249
|
}
|
|
7399
|
-
const worktreePath = opts.path ?
|
|
7400
|
-
if (
|
|
9250
|
+
const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
9251
|
+
if (import_node_fs21.default.existsSync(worktreePath)) {
|
|
7401
9252
|
const registered = listGitWorktrees(mainRepo).find(
|
|
7402
9253
|
(worktree) => sameWorkspacePath(worktree.path, worktreePath)
|
|
7403
9254
|
);
|
|
@@ -7415,11 +9266,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
7415
9266
|
if (!opts.path) {
|
|
7416
9267
|
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
7417
9268
|
}
|
|
7418
|
-
const add = (0,
|
|
9269
|
+
const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
7419
9270
|
encoding: "utf-8"
|
|
7420
9271
|
});
|
|
7421
9272
|
if (add.status !== 0) {
|
|
7422
|
-
const addNew = (0,
|
|
9273
|
+
const addNew = (0, import_node_child_process16.spawnSync)(
|
|
7423
9274
|
"git",
|
|
7424
9275
|
["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
|
|
7425
9276
|
{
|
|
@@ -7524,8 +9375,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
7524
9375
|
opts.quiet
|
|
7525
9376
|
);
|
|
7526
9377
|
if (removeWorktree) {
|
|
7527
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
7528
|
-
const rm = (0,
|
|
9378
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
|
|
9379
|
+
const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
7529
9380
|
encoding: "utf-8"
|
|
7530
9381
|
});
|
|
7531
9382
|
if (rm.status !== 0) {
|
|
@@ -7543,7 +9394,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
7543
9394
|
}
|
|
7544
9395
|
return result;
|
|
7545
9396
|
};
|
|
7546
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
9397
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
7547
9398
|
}
|
|
7548
9399
|
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
7549
9400
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
@@ -7578,13 +9429,13 @@ async function workspaceStop(target, opts = {}) {
|
|
|
7578
9429
|
async function workspaceDown(target, opts = {}) {
|
|
7579
9430
|
return runWorkspaceLifecycle("down", target, opts);
|
|
7580
9431
|
}
|
|
7581
|
-
var
|
|
9432
|
+
var import_node_child_process16, import_node_fs21, import_node_path21;
|
|
7582
9433
|
var init_workspace_lifecycle = __esm({
|
|
7583
9434
|
"src/core/workspace-lifecycle.ts"() {
|
|
7584
9435
|
"use strict";
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
9436
|
+
import_node_child_process16 = require("child_process");
|
|
9437
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
9438
|
+
import_node_path21 = __toESM(require("path"));
|
|
7588
9439
|
init_devpod_mutation();
|
|
7589
9440
|
init_devpod_workspaces();
|
|
7590
9441
|
init_repo_config();
|
|
@@ -7711,7 +9562,7 @@ function ensureGuidance(repoPath) {
|
|
|
7711
9562
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
7712
9563
|
}
|
|
7713
9564
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
7714
|
-
const result = (0,
|
|
9565
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
7715
9566
|
encoding: "utf-8"
|
|
7716
9567
|
});
|
|
7717
9568
|
if (result.status !== 0) {
|
|
@@ -7749,7 +9600,7 @@ async function devpodExec(repoPath, command) {
|
|
|
7749
9600
|
}
|
|
7750
9601
|
assertRunningDevpod(devpod.id, repoPath);
|
|
7751
9602
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
7752
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
9603
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto7.randomUUID)()}__:`;
|
|
7753
9604
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
7754
9605
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
7755
9606
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -7767,7 +9618,7 @@ async function devpodExec(repoPath, command) {
|
|
|
7767
9618
|
wrappedCommand
|
|
7768
9619
|
];
|
|
7769
9620
|
return new Promise((resolve, reject) => {
|
|
7770
|
-
const child = (0,
|
|
9621
|
+
const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
7771
9622
|
let pending = Buffer.alloc(0);
|
|
7772
9623
|
let statusBytes = Buffer.alloc(0);
|
|
7773
9624
|
let readingStatus = false;
|
|
@@ -7826,12 +9677,12 @@ async function devpodExec(repoPath, command) {
|
|
|
7826
9677
|
});
|
|
7827
9678
|
});
|
|
7828
9679
|
}
|
|
7829
|
-
var
|
|
9680
|
+
var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
|
|
7830
9681
|
var init_devpod_exec = __esm({
|
|
7831
9682
|
"src/core/devpod-exec.ts"() {
|
|
7832
9683
|
"use strict";
|
|
7833
|
-
|
|
7834
|
-
|
|
9684
|
+
import_node_child_process17 = require("child_process");
|
|
9685
|
+
import_node_crypto7 = require("crypto");
|
|
7835
9686
|
init_devpod_environment();
|
|
7836
9687
|
init_devpod_workspaces();
|
|
7837
9688
|
init_workspace();
|
|
@@ -8015,7 +9866,7 @@ async function runOpenCommand(name) {
|
|
|
8015
9866
|
);
|
|
8016
9867
|
return;
|
|
8017
9868
|
}
|
|
8018
|
-
const result = (0,
|
|
9869
|
+
const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
8019
9870
|
if (result.status !== 0) {
|
|
8020
9871
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
8021
9872
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -8023,11 +9874,11 @@ async function runOpenCommand(name) {
|
|
|
8023
9874
|
process.stdout.write(`Opened ${url}
|
|
8024
9875
|
`);
|
|
8025
9876
|
}
|
|
8026
|
-
var
|
|
9877
|
+
var import_node_child_process18;
|
|
8027
9878
|
var init_open = __esm({
|
|
8028
9879
|
"src/commands/open.ts"() {
|
|
8029
9880
|
"use strict";
|
|
8030
|
-
|
|
9881
|
+
import_node_child_process18 = require("child_process");
|
|
8031
9882
|
init_docker();
|
|
8032
9883
|
init_host_routes();
|
|
8033
9884
|
init_repo_config();
|
|
@@ -8050,7 +9901,7 @@ async function runLogsCommand(options) {
|
|
|
8050
9901
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
8051
9902
|
if (options.follow) {
|
|
8052
9903
|
args.splice(1, 0, "-f");
|
|
8053
|
-
const child = (0,
|
|
9904
|
+
const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
|
|
8054
9905
|
const onSignal = () => {
|
|
8055
9906
|
child.kill("SIGTERM");
|
|
8056
9907
|
};
|
|
@@ -8064,17 +9915,17 @@ async function runLogsCommand(options) {
|
|
|
8064
9915
|
});
|
|
8065
9916
|
});
|
|
8066
9917
|
} else {
|
|
8067
|
-
const result = (0,
|
|
9918
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
|
|
8068
9919
|
if (result.status !== 0) {
|
|
8069
9920
|
throw new Error("Failed to retrieve router logs.");
|
|
8070
9921
|
}
|
|
8071
9922
|
}
|
|
8072
9923
|
}
|
|
8073
|
-
var
|
|
9924
|
+
var import_node_child_process19;
|
|
8074
9925
|
var init_logs = __esm({
|
|
8075
9926
|
"src/commands/logs.ts"() {
|
|
8076
9927
|
"use strict";
|
|
8077
|
-
|
|
9928
|
+
import_node_child_process19 = require("child_process");
|
|
8078
9929
|
init_docker();
|
|
8079
9930
|
init_router();
|
|
8080
9931
|
}
|
|
@@ -8114,17 +9965,17 @@ function asRecord3(value) {
|
|
|
8114
9965
|
return value;
|
|
8115
9966
|
}
|
|
8116
9967
|
function readJson(filePath) {
|
|
8117
|
-
if (!
|
|
9968
|
+
if (!import_node_fs22.default.existsSync(filePath)) {
|
|
8118
9969
|
return void 0;
|
|
8119
9970
|
}
|
|
8120
9971
|
try {
|
|
8121
|
-
return JSON.parse(
|
|
9972
|
+
return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
|
|
8122
9973
|
} catch {
|
|
8123
9974
|
return void 0;
|
|
8124
9975
|
}
|
|
8125
9976
|
}
|
|
8126
9977
|
function relative(repoPath, filePath) {
|
|
8127
|
-
return
|
|
9978
|
+
return import_node_path22.default.relative(repoPath, filePath) || ".";
|
|
8128
9979
|
}
|
|
8129
9980
|
function redactEnvAssignments(value) {
|
|
8130
9981
|
return value.replace(
|
|
@@ -8165,7 +10016,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
8165
10016
|
["bun.lock", "bun"]
|
|
8166
10017
|
];
|
|
8167
10018
|
for (const [fileName, name] of lockfiles) {
|
|
8168
|
-
if (
|
|
10019
|
+
if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
|
|
8169
10020
|
return { name, source: fileName };
|
|
8170
10021
|
}
|
|
8171
10022
|
}
|
|
@@ -8180,9 +10031,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
8180
10031
|
if (typeof engines?.node === "string") {
|
|
8181
10032
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
8182
10033
|
}
|
|
8183
|
-
const nvmrc =
|
|
8184
|
-
if (
|
|
8185
|
-
const version =
|
|
10034
|
+
const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
|
|
10035
|
+
if (import_node_fs22.default.existsSync(nvmrc)) {
|
|
10036
|
+
const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
|
|
8186
10037
|
return { version, source: ".nvmrc" };
|
|
8187
10038
|
}
|
|
8188
10039
|
return void 0;
|
|
@@ -8243,7 +10094,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
8243
10094
|
const files = config.apps.filter(
|
|
8244
10095
|
(app) => app.runtime === "docker"
|
|
8245
10096
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
8246
|
-
(fileName) => !
|
|
10097
|
+
(fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
|
|
8247
10098
|
);
|
|
8248
10099
|
return Array.from(new Set(files));
|
|
8249
10100
|
} catch {
|
|
@@ -8261,10 +10112,10 @@ function composeFiles(repoPath) {
|
|
|
8261
10112
|
...configuredComposeFiles(repoPath)
|
|
8262
10113
|
];
|
|
8263
10114
|
return Array.from(new Set(candidates)).filter(
|
|
8264
|
-
(fileName) =>
|
|
10115
|
+
(fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
|
|
8265
10116
|
);
|
|
8266
10117
|
}
|
|
8267
|
-
function
|
|
10118
|
+
function stringArray2(value) {
|
|
8268
10119
|
if (!Array.isArray(value)) {
|
|
8269
10120
|
return [];
|
|
8270
10121
|
}
|
|
@@ -8299,7 +10150,7 @@ function inspectServices(repoPath) {
|
|
|
8299
10150
|
const services = [];
|
|
8300
10151
|
for (const fileName of composeFiles(repoPath)) {
|
|
8301
10152
|
try {
|
|
8302
|
-
const parsed =
|
|
10153
|
+
const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
|
|
8303
10154
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
8304
10155
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
8305
10156
|
const service = asRecord3(value);
|
|
@@ -8313,7 +10164,7 @@ function inspectServices(repoPath) {
|
|
|
8313
10164
|
kind: kind.kind,
|
|
8314
10165
|
source: fileName,
|
|
8315
10166
|
image,
|
|
8316
|
-
ports:
|
|
10167
|
+
ports: stringArray2(service.ports),
|
|
8317
10168
|
hasHealthcheck: service.healthcheck !== void 0,
|
|
8318
10169
|
envNames: envNames(service.environment),
|
|
8319
10170
|
confidence: kind.confidence
|
|
@@ -8334,8 +10185,8 @@ function inspectServices(repoPath) {
|
|
|
8334
10185
|
return services;
|
|
8335
10186
|
}
|
|
8336
10187
|
function inspectEnvFiles(repoPath) {
|
|
8337
|
-
const files =
|
|
8338
|
-
const content =
|
|
10188
|
+
const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
10189
|
+
const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
|
|
8339
10190
|
const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
|
|
8340
10191
|
return { path: fileName, names };
|
|
8341
10192
|
});
|
|
@@ -8349,18 +10200,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
8349
10200
|
};
|
|
8350
10201
|
}
|
|
8351
10202
|
function inspectDevcontainer(repoPath) {
|
|
8352
|
-
const dir =
|
|
8353
|
-
if (!
|
|
10203
|
+
const dir = import_node_path22.default.join(repoPath, ".devcontainer");
|
|
10204
|
+
if (!import_node_fs22.default.existsSync(dir)) {
|
|
8354
10205
|
return { exists: false, files: [] };
|
|
8355
10206
|
}
|
|
8356
10207
|
return {
|
|
8357
10208
|
exists: true,
|
|
8358
|
-
files:
|
|
10209
|
+
files: import_node_fs22.default.readdirSync(dir).filter((fileName) => import_node_fs22.default.statSync(import_node_path22.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
8359
10210
|
};
|
|
8360
10211
|
}
|
|
8361
10212
|
function inspectDevrouter(repoPath) {
|
|
8362
10213
|
const configPath = getRepoConfigPath(repoPath);
|
|
8363
|
-
if (!
|
|
10214
|
+
if (!import_node_fs22.default.existsSync(configPath)) {
|
|
8364
10215
|
return {
|
|
8365
10216
|
exists: false,
|
|
8366
10217
|
configPath,
|
|
@@ -8404,15 +10255,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
8404
10255
|
["AGENTS.md", "agents"],
|
|
8405
10256
|
["CLAUDE.md", "claude"]
|
|
8406
10257
|
]) {
|
|
8407
|
-
if (
|
|
10258
|
+
if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
|
|
8408
10259
|
results.push({ path: fileName, kind });
|
|
8409
10260
|
}
|
|
8410
10261
|
}
|
|
8411
|
-
const skillsDir =
|
|
8412
|
-
if (
|
|
8413
|
-
for (const name of
|
|
8414
|
-
const skillPath =
|
|
8415
|
-
if (
|
|
10262
|
+
const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
|
|
10263
|
+
if (import_node_fs22.default.existsSync(skillsDir)) {
|
|
10264
|
+
for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
|
|
10265
|
+
const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
|
|
10266
|
+
if (import_node_fs22.default.existsSync(skillPath)) {
|
|
8416
10267
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
8417
10268
|
}
|
|
8418
10269
|
}
|
|
@@ -8455,7 +10306,7 @@ function buildIssues(report) {
|
|
|
8455
10306
|
}
|
|
8456
10307
|
function inspectRepo(options = {}) {
|
|
8457
10308
|
const repoPath = resolveRepoPath(options.repo);
|
|
8458
|
-
const pkg = readJson(
|
|
10309
|
+
const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
|
|
8459
10310
|
const scripts = inspectScripts(pkg);
|
|
8460
10311
|
const reportWithoutIssues = {
|
|
8461
10312
|
repoPath,
|
|
@@ -8474,13 +10325,13 @@ function inspectRepo(options = {}) {
|
|
|
8474
10325
|
issues: buildIssues(reportWithoutIssues)
|
|
8475
10326
|
};
|
|
8476
10327
|
}
|
|
8477
|
-
var
|
|
10328
|
+
var import_node_fs22, import_node_path22, import_yaml6;
|
|
8478
10329
|
var init_repo_inspect = __esm({
|
|
8479
10330
|
"src/core/repo-inspect.ts"() {
|
|
8480
10331
|
"use strict";
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
10332
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
10333
|
+
import_node_path22 = __toESM(require("path"));
|
|
10334
|
+
import_yaml6 = __toESM(require("yaml"));
|
|
8484
10335
|
init_repo_config();
|
|
8485
10336
|
}
|
|
8486
10337
|
});
|
|
@@ -8584,7 +10435,7 @@ function requiredFileChecks(repoPath) {
|
|
|
8584
10435
|
".devcontainer/docker-compose.yml",
|
|
8585
10436
|
".devrouter.yml"
|
|
8586
10437
|
];
|
|
8587
|
-
const missing = required.filter((fileName) => !
|
|
10438
|
+
const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
|
|
8588
10439
|
return {
|
|
8589
10440
|
id: "repo.devcontainer.verify-files",
|
|
8590
10441
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -8803,12 +10654,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
8803
10654
|
nextSteps: collectNextSteps3(checks)
|
|
8804
10655
|
};
|
|
8805
10656
|
}
|
|
8806
|
-
var
|
|
10657
|
+
var import_node_fs23, import_node_path23;
|
|
8807
10658
|
var init_devcontainer_verify = __esm({
|
|
8808
10659
|
"src/core/devcontainer-verify.ts"() {
|
|
8809
10660
|
"use strict";
|
|
8810
|
-
|
|
8811
|
-
|
|
10661
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
10662
|
+
import_node_path23 = __toESM(require("path"));
|
|
8812
10663
|
init_capabilities();
|
|
8813
10664
|
init_doctor();
|
|
8814
10665
|
init_host_routes();
|
|
@@ -9104,7 +10955,7 @@ function packageManagerIssues(repo) {
|
|
|
9104
10955
|
}
|
|
9105
10956
|
function plannedFiles(repoPath, version) {
|
|
9106
10957
|
const repo = inspectRepo({ repo: repoPath });
|
|
9107
|
-
const projectName = sanitizeProjectName(
|
|
10958
|
+
const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
|
|
9108
10959
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
9109
10960
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
9110
10961
|
const port = inferPort2(repo);
|
|
@@ -9149,8 +11000,8 @@ function plannedFiles(repoPath, version) {
|
|
|
9149
11000
|
};
|
|
9150
11001
|
}
|
|
9151
11002
|
function classifyFile(repoPath, file) {
|
|
9152
|
-
const absolutePath =
|
|
9153
|
-
if (!
|
|
11003
|
+
const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
|
|
11004
|
+
if (!import_node_fs24.default.existsSync(absolutePath)) {
|
|
9154
11005
|
return {
|
|
9155
11006
|
path: file.relativePath,
|
|
9156
11007
|
action: "create",
|
|
@@ -9158,7 +11009,7 @@ function classifyFile(repoPath, file) {
|
|
|
9158
11009
|
bytes: Buffer.byteLength(file.content)
|
|
9159
11010
|
};
|
|
9160
11011
|
}
|
|
9161
|
-
const current =
|
|
11012
|
+
const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
|
|
9162
11013
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
9163
11014
|
return {
|
|
9164
11015
|
path: file.relativePath,
|
|
@@ -9215,11 +11066,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
9215
11066
|
};
|
|
9216
11067
|
}
|
|
9217
11068
|
function writeFile(repoPath, file) {
|
|
9218
|
-
const absolutePath =
|
|
9219
|
-
|
|
9220
|
-
|
|
11069
|
+
const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
|
|
11070
|
+
import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
|
|
11071
|
+
import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
9221
11072
|
if (file.executable) {
|
|
9222
|
-
|
|
11073
|
+
import_node_fs24.default.chmodSync(absolutePath, 493);
|
|
9223
11074
|
}
|
|
9224
11075
|
}
|
|
9225
11076
|
function writeDevcontainer(options = {}) {
|
|
@@ -9257,12 +11108,12 @@ function writeDevcontainer(options = {}) {
|
|
|
9257
11108
|
nextSteps: postWriteNextSteps(repoPath)
|
|
9258
11109
|
};
|
|
9259
11110
|
}
|
|
9260
|
-
var
|
|
11111
|
+
var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
9261
11112
|
var init_devcontainer_write = __esm({
|
|
9262
11113
|
"src/core/devcontainer-write.ts"() {
|
|
9263
11114
|
"use strict";
|
|
9264
|
-
|
|
9265
|
-
|
|
11115
|
+
import_node_fs24 = __toESM(require("fs"));
|
|
11116
|
+
import_node_path24 = __toESM(require("path"));
|
|
9266
11117
|
init_repo_config();
|
|
9267
11118
|
init_repo_inspect();
|
|
9268
11119
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -9653,7 +11504,7 @@ function sanitizeRouterId(value) {
|
|
|
9653
11504
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
9654
11505
|
}
|
|
9655
11506
|
function repoHash(repoPath) {
|
|
9656
|
-
return (0,
|
|
11507
|
+
return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
9657
11508
|
}
|
|
9658
11509
|
function asDockerApp(app) {
|
|
9659
11510
|
return app.runtime === "docker";
|
|
@@ -9732,11 +11583,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
9732
11583
|
if (dockerApps.length === 0) {
|
|
9733
11584
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
9734
11585
|
}
|
|
9735
|
-
const cachePath =
|
|
9736
|
-
|
|
9737
|
-
const overlayPath =
|
|
11586
|
+
const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
11587
|
+
import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
|
|
11588
|
+
const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
|
|
9738
11589
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
9739
|
-
|
|
11590
|
+
import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
9740
11591
|
return {
|
|
9741
11592
|
overlayPath,
|
|
9742
11593
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -9750,7 +11601,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
9750
11601
|
fileArgs.push("-f", resolved);
|
|
9751
11602
|
}
|
|
9752
11603
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
9753
|
-
const result = (0,
|
|
11604
|
+
const result = (0, import_node_child_process20.spawnSync)("docker", args, {
|
|
9754
11605
|
encoding: "utf-8",
|
|
9755
11606
|
cwd: repoPath
|
|
9756
11607
|
});
|
|
@@ -9778,7 +11629,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
9778
11629
|
"--services",
|
|
9779
11630
|
...services
|
|
9780
11631
|
];
|
|
9781
|
-
const result = (0,
|
|
11632
|
+
const result = (0, import_node_child_process20.spawnSync)("docker", args, {
|
|
9782
11633
|
encoding: "utf-8",
|
|
9783
11634
|
cwd: repoPath
|
|
9784
11635
|
});
|
|
@@ -9804,7 +11655,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
9804
11655
|
fileArgs.push("-f", resolved);
|
|
9805
11656
|
}
|
|
9806
11657
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
9807
|
-
const result = (0,
|
|
11658
|
+
const result = (0, import_node_child_process20.spawnSync)("docker", args, {
|
|
9808
11659
|
encoding: "utf-8",
|
|
9809
11660
|
cwd: repoPath
|
|
9810
11661
|
});
|
|
@@ -9830,7 +11681,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
9830
11681
|
String(tail),
|
|
9831
11682
|
...services
|
|
9832
11683
|
];
|
|
9833
|
-
(0,
|
|
11684
|
+
(0, import_node_child_process20.spawnSync)("docker", args, {
|
|
9834
11685
|
stdio: "inherit",
|
|
9835
11686
|
cwd: repoPath
|
|
9836
11687
|
});
|
|
@@ -9842,7 +11693,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9842
11693
|
fileArgs.push("-f", resolved);
|
|
9843
11694
|
}
|
|
9844
11695
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
9845
|
-
const result = (0,
|
|
11696
|
+
const result = (0, import_node_child_process20.spawnSync)("docker", args, {
|
|
9846
11697
|
encoding: "utf-8",
|
|
9847
11698
|
cwd: repoPath
|
|
9848
11699
|
});
|
|
@@ -9856,15 +11707,15 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9856
11707
|
const port = Number(match[1]);
|
|
9857
11708
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
9858
11709
|
}
|
|
9859
|
-
var
|
|
11710
|
+
var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
|
|
9860
11711
|
var init_docker_run = __esm({
|
|
9861
11712
|
"src/core/docker-run.ts"() {
|
|
9862
11713
|
"use strict";
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
11714
|
+
import_node_child_process20 = require("child_process");
|
|
11715
|
+
import_node_crypto8 = require("crypto");
|
|
11716
|
+
import_node_fs25 = __toESM(require("fs"));
|
|
11717
|
+
import_node_path25 = __toESM(require("path"));
|
|
11718
|
+
import_yaml7 = __toESM(require("yaml"));
|
|
9868
11719
|
init_docker_error_guidance();
|
|
9869
11720
|
init_paths();
|
|
9870
11721
|
init_router();
|
|
@@ -9953,7 +11804,7 @@ function isProcessRunning(pid) {
|
|
|
9953
11804
|
}
|
|
9954
11805
|
}
|
|
9955
11806
|
function readProcessTree(rootPid) {
|
|
9956
|
-
const result = (0,
|
|
11807
|
+
const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
9957
11808
|
if (result.status !== 0) {
|
|
9958
11809
|
return [rootPid];
|
|
9959
11810
|
}
|
|
@@ -10064,12 +11915,12 @@ function detectListeningPorts(pids) {
|
|
|
10064
11915
|
if (pids.length === 0) {
|
|
10065
11916
|
return [];
|
|
10066
11917
|
}
|
|
10067
|
-
const result = (0,
|
|
11918
|
+
const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
|
|
10068
11919
|
encoding: "utf-8"
|
|
10069
11920
|
});
|
|
10070
11921
|
if (result.error && result.error.code === "ENOENT") {
|
|
10071
11922
|
if (process.platform === "linux") {
|
|
10072
|
-
const ssResult = (0,
|
|
11923
|
+
const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
10073
11924
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
10074
11925
|
return parseSsListeningPorts(ssResult.stdout, new Set(pids));
|
|
10075
11926
|
}
|
|
@@ -10128,7 +11979,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
10128
11979
|
app.hostRun.command,
|
|
10129
11980
|
true
|
|
10130
11981
|
) : app.hostRun.command;
|
|
10131
|
-
const child = (0,
|
|
11982
|
+
const child = (0, import_node_child_process21.spawn)(spawnCommand, {
|
|
10132
11983
|
cwd: commandCwd,
|
|
10133
11984
|
stdio: "inherit",
|
|
10134
11985
|
shell: true,
|
|
@@ -10496,7 +12347,7 @@ async function execWithAppEnv(options) {
|
|
|
10496
12347
|
options.command[0],
|
|
10497
12348
|
true
|
|
10498
12349
|
);
|
|
10499
|
-
child = (0,
|
|
12350
|
+
child = (0, import_node_child_process21.spawn)(wrapped, {
|
|
10500
12351
|
cwd: deps.repoPath,
|
|
10501
12352
|
stdio: "inherit",
|
|
10502
12353
|
shell: true,
|
|
@@ -10510,7 +12361,7 @@ async function execWithAppEnv(options) {
|
|
|
10510
12361
|
false
|
|
10511
12362
|
);
|
|
10512
12363
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
10513
|
-
child = (0,
|
|
12364
|
+
child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
|
|
10514
12365
|
cwd: deps.repoPath,
|
|
10515
12366
|
stdio: "inherit",
|
|
10516
12367
|
shell: false,
|
|
@@ -10518,7 +12369,7 @@ async function execWithAppEnv(options) {
|
|
|
10518
12369
|
});
|
|
10519
12370
|
}
|
|
10520
12371
|
} else if (options.shell) {
|
|
10521
|
-
child = (0,
|
|
12372
|
+
child = (0, import_node_child_process21.spawn)(options.command[0], {
|
|
10522
12373
|
cwd: deps.repoPath,
|
|
10523
12374
|
stdio: "inherit",
|
|
10524
12375
|
shell: true,
|
|
@@ -10526,7 +12377,7 @@ async function execWithAppEnv(options) {
|
|
|
10526
12377
|
});
|
|
10527
12378
|
} else {
|
|
10528
12379
|
const [command, ...args] = options.command;
|
|
10529
|
-
child = (0,
|
|
12380
|
+
child = (0, import_node_child_process21.spawn)(command, args, {
|
|
10530
12381
|
cwd: deps.repoPath,
|
|
10531
12382
|
stdio: "inherit",
|
|
10532
12383
|
shell: false,
|
|
@@ -10547,11 +12398,11 @@ async function execWithAppEnv(options) {
|
|
|
10547
12398
|
deps.stopDeps();
|
|
10548
12399
|
}
|
|
10549
12400
|
}
|
|
10550
|
-
var
|
|
12401
|
+
var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
|
|
10551
12402
|
var init_app_run = __esm({
|
|
10552
12403
|
"src/core/app-run.ts"() {
|
|
10553
12404
|
"use strict";
|
|
10554
|
-
|
|
12405
|
+
import_node_child_process21 = require("child_process");
|
|
10555
12406
|
import_node_net = __toESM(require("net"));
|
|
10556
12407
|
import_node_process = require("process");
|
|
10557
12408
|
import_promises = require("readline/promises");
|
|
@@ -10685,13 +12536,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
10685
12536
|
const startedAt = Date.now();
|
|
10686
12537
|
let rootStat;
|
|
10687
12538
|
try {
|
|
10688
|
-
rootStat =
|
|
12539
|
+
rootStat = import_node_fs26.default.lstatSync(worktreePath);
|
|
10689
12540
|
} catch (error) {
|
|
10690
12541
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
10691
12542
|
}
|
|
10692
12543
|
let rootEntries;
|
|
10693
12544
|
try {
|
|
10694
|
-
rootEntries =
|
|
12545
|
+
rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
10695
12546
|
} catch (error) {
|
|
10696
12547
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
10697
12548
|
}
|
|
@@ -10717,10 +12568,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
10717
12568
|
timedOut = true;
|
|
10718
12569
|
break;
|
|
10719
12570
|
}
|
|
10720
|
-
const entryPath =
|
|
12571
|
+
const entryPath = import_node_path26.default.join(dirPath, entry.name);
|
|
10721
12572
|
let entryStat;
|
|
10722
12573
|
try {
|
|
10723
|
-
entryStat =
|
|
12574
|
+
entryStat = import_node_fs26.default.lstatSync(entryPath);
|
|
10724
12575
|
} catch (error) {
|
|
10725
12576
|
if (error?.code === "ENOENT") continue;
|
|
10726
12577
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -10730,7 +12581,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
10730
12581
|
if (!entryStat.isDirectory()) continue;
|
|
10731
12582
|
let childEntries;
|
|
10732
12583
|
try {
|
|
10733
|
-
childEntries =
|
|
12584
|
+
childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
|
|
10734
12585
|
} catch (error) {
|
|
10735
12586
|
unreadableReason = describeIncompleteWalk(error);
|
|
10736
12587
|
break;
|
|
@@ -10794,12 +12645,12 @@ function describeError(error, worktreePath) {
|
|
|
10794
12645
|
}
|
|
10795
12646
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
10796
12647
|
}
|
|
10797
|
-
var
|
|
12648
|
+
var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
10798
12649
|
var init_workspace_consumption = __esm({
|
|
10799
12650
|
"src/core/workspace-consumption.ts"() {
|
|
10800
12651
|
"use strict";
|
|
10801
|
-
|
|
10802
|
-
|
|
12652
|
+
import_node_fs26 = __toESM(require("fs"));
|
|
12653
|
+
import_node_path26 = __toESM(require("path"));
|
|
10803
12654
|
init_devpod_environment();
|
|
10804
12655
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
10805
12656
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -10900,7 +12751,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
10900
12751
|
}
|
|
10901
12752
|
function readGitSnapshot(worktree, commandRunner) {
|
|
10902
12753
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
10903
|
-
if (worktree.prunable || !
|
|
12754
|
+
if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
|
|
10904
12755
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
10905
12756
|
}
|
|
10906
12757
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -11431,7 +13282,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
11431
13282
|
containers = measureContainersFn(worktreePaths);
|
|
11432
13283
|
} catch (error) {
|
|
11433
13284
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
11434
|
-
containers = new Map(worktreePaths.map((
|
|
13285
|
+
containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
|
|
11435
13286
|
}
|
|
11436
13287
|
}
|
|
11437
13288
|
const rows = records.map((record) => {
|
|
@@ -11479,12 +13330,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
11479
13330
|
workspaces: rows
|
|
11480
13331
|
};
|
|
11481
13332
|
}
|
|
11482
|
-
var
|
|
13333
|
+
var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
11483
13334
|
var init_workspace_cleanup = __esm({
|
|
11484
13335
|
"src/core/workspace-cleanup.ts"() {
|
|
11485
13336
|
"use strict";
|
|
11486
|
-
|
|
11487
|
-
|
|
13337
|
+
import_node_child_process22 = require("child_process");
|
|
13338
|
+
import_node_fs27 = __toESM(require("fs"));
|
|
11488
13339
|
init_devpod_workspaces();
|
|
11489
13340
|
init_host_routes();
|
|
11490
13341
|
init_repo_config();
|
|
@@ -11501,7 +13352,7 @@ var init_workspace_cleanup = __esm({
|
|
|
11501
13352
|
];
|
|
11502
13353
|
defaultCommandRunner = (command, args, input2) => {
|
|
11503
13354
|
const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
|
|
11504
|
-
const result = (0,
|
|
13355
|
+
const result = (0, import_node_child_process22.spawnSync)(command, args, {
|
|
11505
13356
|
encoding: "utf-8",
|
|
11506
13357
|
env: environment,
|
|
11507
13358
|
...input2 === void 0 ? {} : { input: input2 }
|
|
@@ -11661,7 +13512,7 @@ var init_version = __esm({
|
|
|
11661
13512
|
|
|
11662
13513
|
// src/cli.ts
|
|
11663
13514
|
var import_commander = require("commander");
|
|
11664
|
-
var CLI_VERSION = true ? "0.0.
|
|
13515
|
+
var CLI_VERSION = true ? "0.0.40" : "0.0.0-dev";
|
|
11665
13516
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
11666
13517
|
function withErrorHandling(action2) {
|
|
11667
13518
|
return async (...args) => {
|