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