@devrouter/cli 0.0.50 → 0.0.52
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 +18 -0
- package/bin/devrouter-process +4 -4
- package/dist/devrouter.js +971 -80
- package/package.json +1 -1
- package/upgrade-prompts/0.0.51.md +40 -0
- package/upgrade-prompts/0.0.52.md +34 -0
package/dist/devrouter.js
CHANGED
|
@@ -50,6 +50,7 @@ function buildDevrouterSection() {
|
|
|
50
50
|
"- `devrouter app ls --repo .`",
|
|
51
51
|
"- Primary or linked devcontainer checkout: `devrouter ensure . --json`",
|
|
52
52
|
"- Managed selective profile: `devrouter ensure . --profile <name> --json`",
|
|
53
|
+
"- Side-effect-free automation: `devrouter profile resolve --repo . --profile <name> --json`; add `profile plan --contract <repo-relative-yaml>` when the repository needs literal bindings.",
|
|
53
54
|
"- Host/docker runtime app only: `devrouter app run <host-app> --repo . --yes`",
|
|
54
55
|
"- `devrouter ls`",
|
|
55
56
|
"- Managed devcontainer source configs with `postCreateCommand` and a managed post-start adapter must set `waitFor` exactly to `postCreateCommand` or `postStartCommand`; generated managed configs preserve lifecycle fields and change only `runServices`.",
|
|
@@ -300,6 +301,27 @@ profiles:
|
|
|
300
301
|
selective configuration preserves lifecycle fields and changes only
|
|
301
302
|
\`runServices\`.
|
|
302
303
|
|
|
304
|
+
## Automation profile plans
|
|
305
|
+
|
|
306
|
+
Use \`devrouter profile resolve --json\` when automation only needs exact selected
|
|
307
|
+
resources. Use \`devrouter profile plan\` when a repository-owned contract must
|
|
308
|
+
bind selected app names to build arguments, readiness URLs, or other literals:
|
|
309
|
+
|
|
310
|
+
\`\`\`bash
|
|
311
|
+
devrouter profile plan --repo . --profile <selection> \\
|
|
312
|
+
--contract <repo-relative-yaml> --output <plan.json> --json
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
The version-1 contract is separate from \`.devrouter.yml\`. It maps every allowed
|
|
316
|
+
app to named non-empty string arrays, constrains dependencies and managed
|
|
317
|
+
services to allowed sets, and requires an exact managed-process set. All
|
|
318
|
+
resource names must exist in \`.devrouter.yml\`. The path must be a regular,
|
|
319
|
+
non-symlink file inside the repository. Devrouter emits canonical, deduplicated
|
|
320
|
+
literal arrays and atomically writes mode \`0600\`; it never interprets binding
|
|
321
|
+
names, expands values, or runs a shell or runtime. Consumers validate expected
|
|
322
|
+
keys and pass values as literal data. Keep separate contracts for workloads
|
|
323
|
+
with different exact process policies.
|
|
324
|
+
|
|
303
325
|
## Env var injection
|
|
304
326
|
|
|
305
327
|
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, "_")\`):
|
|
@@ -378,8 +400,9 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
378
400
|
- \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
|
|
379
401
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
380
402
|
- \`devrouter setup --yes [--repo .] [--json] [--workspace-runtime <devpod|devsy>]\`: first-run machine setup plus structured diagnostics; explicit Devsy selection acquires its verified agent
|
|
381
|
-
- \`devrouter ensure [path] [--profile <name>] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts;
|
|
403
|
+
- \`devrouter ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts; explicit repair uses the recorded degraded profile and unchanged retained configuration
|
|
382
404
|
- \`devrouter profile resolve --repo <path> [--profile <selection>] [--json]\`: resolve exact profile resources for automation without starting or inspecting a runtime
|
|
405
|
+
- \`devrouter profile plan --repo <path> [--profile <selection>] --contract <repo-relative-yaml> [--output <path>] [--json]\`: validate repository-owned resource policy and emit literal bindings without runtime access
|
|
383
406
|
- \`devrouter stop [path] [--delete] [--json]\`: stop the exact workspace runtime and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
|
|
384
407
|
- \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running workspace runtime
|
|
385
408
|
- \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
|
|
@@ -402,7 +425,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
402
425
|
- \`devrouter app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
|
|
403
426
|
- \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
|
|
404
427
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
405
|
-
- \`devrouter workspace ensure [path] [--profile <name>] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
428
|
+
- \`devrouter workspace ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
406
429
|
- \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
|
|
407
430
|
- \`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
|
|
408
431
|
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
@@ -2668,7 +2691,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2668
2691
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2669
2692
|
const requiredVersion = config.devrouter?.version;
|
|
2670
2693
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2671
|
-
const cliVersion = true ? "0.0.
|
|
2694
|
+
const cliVersion = true ? "0.0.52" : "0.0.0-dev";
|
|
2672
2695
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2673
2696
|
hasWarnedVersionMismatch = true;
|
|
2674
2697
|
process.stderr.write(
|
|
@@ -3153,6 +3176,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3153
3176
|
"- managedRuntime (optional) registers devcontainer baseServices, profileServices, and managed process markers",
|
|
3154
3177
|
"- managed profiles may add devcontainerServices and processes independently; with managedRuntime, apps may be omitted for a route-free capability profile",
|
|
3155
3178
|
"- 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",
|
|
3179
|
+
"- automation may use a separate strict version-1 contract with `devrouter profile plan`; the contract maps apps to named literal arrays, constrains dependencies and services to allowed registries, requires an exact managed-process set, and remains repository-owned",
|
|
3180
|
+
"- profile plan values are data: Devrouter never interprets binding names, expands values, runs a shell, or starts/inspects a runtime; consumers validate expected keys and pass values literally",
|
|
3156
3181
|
"- native Dev Container clients continue to use the source configuration's full service set; managed ensure derives an ignored sibling and changes only runServices",
|
|
3157
3182
|
"",
|
|
3158
3183
|
"Validation rules to enforce:",
|
|
@@ -3248,6 +3273,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3248
3273
|
"- devrouter repo devcontainer write --repo <REPO_PATH> --yes",
|
|
3249
3274
|
"- devrouter repo devcontainer verify --repo <REPO_PATH> --json",
|
|
3250
3275
|
"- Normal primary or linked startup: devrouter ensure <REPO_PATH> --json",
|
|
3276
|
+
"- Degraded recovery: explicit devrouter ensure <REPO_PATH> --repair --json validates the recorded profile and unchanged retained configuration; stopped primary requires all project containers stopped and zero checkout routes. It starts only retained IDs, never provider bootstrap/recreation. Inspect failures before retrying.",
|
|
3251
3277
|
"- Managed selective startup: devrouter ensure <REPO_PATH> --profile <name> --json",
|
|
3252
3278
|
"- Container seed/migration: devrouter exec <REPO_PATH> -- <command...>",
|
|
3253
3279
|
"- Compatibility verification only after ensure: devrouter repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
|
|
@@ -3366,13 +3392,17 @@ var init_ai_prompt = __esm({
|
|
|
3366
3392
|
purpose: "Write/update devrouter section in the repo's AGENTS.md and install the devrouter skill."
|
|
3367
3393
|
},
|
|
3368
3394
|
{
|
|
3369
|
-
command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
|
|
3395
|
+
command: "devrouter ensure [path] [--profile <name>] [--repair] [--open] [--json]",
|
|
3370
3396
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
3371
3397
|
},
|
|
3372
3398
|
{
|
|
3373
3399
|
command: "devrouter profile resolve --repo <path> [--profile <selection>] [--json]",
|
|
3374
3400
|
purpose: "Resolve exact profile resources without starting or inspecting a runtime; fail closed on invalid selections or unknown report schemas."
|
|
3375
3401
|
},
|
|
3402
|
+
{
|
|
3403
|
+
command: "devrouter profile plan --repo <path> [--profile <selection>] --contract <repo-relative-yaml> [--output <path>] [--json]",
|
|
3404
|
+
purpose: "Validate exact profile resources against a repository-owned contract and emit named literal arrays without interpreting them or touching a runtime."
|
|
3405
|
+
},
|
|
3376
3406
|
{
|
|
3377
3407
|
command: "devrouter stop [path] [--delete] [--json]",
|
|
3378
3408
|
purpose: "Stop the exact checkout workspace runtime and routes; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
|
|
@@ -5662,19 +5692,12 @@ function assertSafeContainerId(containerId) {
|
|
|
5662
5692
|
throw new Error(`Container '${containerId}' is not a safe exact identifier.`);
|
|
5663
5693
|
}
|
|
5664
5694
|
}
|
|
5665
|
-
function
|
|
5666
|
-
if (options.services.length === 0) return;
|
|
5667
|
-
const project = safeComposeProject(options.composeProject);
|
|
5668
|
-
const services = Array.from(new Set(options.services));
|
|
5669
|
-
if (services.some((service) => !options.plan.composeServices.includes(service))) {
|
|
5670
|
-
throw new Error("Exact Compose startup contains a service outside the validated model.");
|
|
5671
|
-
}
|
|
5672
|
-
const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
5695
|
+
function managedComposeEnvironment(workspace) {
|
|
5673
5696
|
const env = { ...process.env };
|
|
5674
|
-
if (
|
|
5675
|
-
env.WORKSPACE =
|
|
5676
|
-
env.DEVROUTER_WORKSPACE =
|
|
5677
|
-
env.DEVROUTER_GIT_COMMON_DIR =
|
|
5697
|
+
if (workspace) {
|
|
5698
|
+
env.WORKSPACE = workspace.token;
|
|
5699
|
+
env.DEVROUTER_WORKSPACE = workspace.token;
|
|
5700
|
+
env.DEVROUTER_GIT_COMMON_DIR = workspace.gitCommonDir;
|
|
5678
5701
|
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
5679
5702
|
} else {
|
|
5680
5703
|
delete env.WORKSPACE;
|
|
@@ -5682,6 +5705,87 @@ function startExactManagedServices(options) {
|
|
|
5682
5705
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
5683
5706
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
5684
5707
|
}
|
|
5708
|
+
return env;
|
|
5709
|
+
}
|
|
5710
|
+
function assertReadableComposeFile(file) {
|
|
5711
|
+
try {
|
|
5712
|
+
if (!import_node_fs15.default.statSync(file).isFile()) throw new Error("not a regular file");
|
|
5713
|
+
import_node_fs15.default.accessSync(file, import_node_fs15.default.constants.R_OK);
|
|
5714
|
+
} catch {
|
|
5715
|
+
throw new Error("Managed container Compose configuration contains an unreadable file.");
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
function assertManagedContainerConfigUnchanged(options) {
|
|
5719
|
+
if (options.containers.length === 0) {
|
|
5720
|
+
throw new Error("Managed container configuration cannot be verified without containers.");
|
|
5721
|
+
}
|
|
5722
|
+
for (const container of options.containers) {
|
|
5723
|
+
const project = container.labels["com.docker.compose.project"];
|
|
5724
|
+
const service = container.labels["com.docker.compose.service"];
|
|
5725
|
+
const workingDirectory = container.labels["com.docker.compose.project.working_dir"];
|
|
5726
|
+
const recordedConfigFiles = container.labels["com.docker.compose.project.config_files"];
|
|
5727
|
+
const expectedHash = container.labels["com.docker.compose.config-hash"];
|
|
5728
|
+
if (!project || !service || !workingDirectory || !recordedConfigFiles || !expectedHash || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(service) || !/^[a-f0-9]{64}$/.test(expectedHash)) {
|
|
5729
|
+
throw new Error("Managed container is missing valid Compose identity and hash labels.");
|
|
5730
|
+
}
|
|
5731
|
+
const composeProject = safeComposeProject(project);
|
|
5732
|
+
const configFiles = recordedConfigFiles.split(",").map((file) => file.trim());
|
|
5733
|
+
if (configFiles.some((file) => file.length === 0)) {
|
|
5734
|
+
throw new Error("Managed container has an invalid recorded Compose file list.");
|
|
5735
|
+
}
|
|
5736
|
+
if (options.plan.composeFiles.some(
|
|
5737
|
+
(expected) => !configFiles.some((actual) => sameWorkspacePath(actual, expected))
|
|
5738
|
+
)) {
|
|
5739
|
+
throw new Error("Managed container Compose files do not include the managed plan files.");
|
|
5740
|
+
}
|
|
5741
|
+
for (const file of configFiles) assertReadableComposeFile(file);
|
|
5742
|
+
const fileArgs = configFiles.flatMap((file) => ["-f", file]);
|
|
5743
|
+
const result = (() => {
|
|
5744
|
+
try {
|
|
5745
|
+
return (0, import_node_child_process7.spawnSync)(
|
|
5746
|
+
"docker",
|
|
5747
|
+
[
|
|
5748
|
+
"compose",
|
|
5749
|
+
"--project-name",
|
|
5750
|
+
composeProject,
|
|
5751
|
+
"--project-directory",
|
|
5752
|
+
workingDirectory,
|
|
5753
|
+
...fileArgs,
|
|
5754
|
+
"config",
|
|
5755
|
+
"--hash",
|
|
5756
|
+
service
|
|
5757
|
+
],
|
|
5758
|
+
{
|
|
5759
|
+
cwd: workingDirectory,
|
|
5760
|
+
encoding: "utf-8",
|
|
5761
|
+
env: managedComposeEnvironment(options.workspace),
|
|
5762
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5763
|
+
timeout: COMPOSE_HASH_TIMEOUT_MS
|
|
5764
|
+
}
|
|
5765
|
+
);
|
|
5766
|
+
} catch {
|
|
5767
|
+
throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
|
|
5768
|
+
}
|
|
5769
|
+
})();
|
|
5770
|
+
if (result.status !== 0 || result.error) {
|
|
5771
|
+
throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
|
|
5772
|
+
}
|
|
5773
|
+
const output2 = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
5774
|
+
const match = /^(\S+)\s+([a-f0-9]{64})$/.exec(output2);
|
|
5775
|
+
if (!match || match[1] !== service || match[2] !== expectedHash) {
|
|
5776
|
+
throw new Error(`Managed Compose configuration changed for service '${service}'.`);
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
function startExactManagedServices(options) {
|
|
5781
|
+
if (options.services.length === 0) return;
|
|
5782
|
+
const project = safeComposeProject(options.composeProject);
|
|
5783
|
+
const services = Array.from(new Set(options.services));
|
|
5784
|
+
if (services.some((service) => !options.plan.composeServices.includes(service))) {
|
|
5785
|
+
throw new Error("Exact Compose startup contains a service outside the validated model.");
|
|
5786
|
+
}
|
|
5787
|
+
const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
5788
|
+
const env = managedComposeEnvironment(options.workspace);
|
|
5685
5789
|
const result = (0, import_node_child_process7.spawnSync)(
|
|
5686
5790
|
"docker",
|
|
5687
5791
|
[
|
|
@@ -5720,7 +5824,7 @@ function stopExactManagedService(containerId, service) {
|
|
|
5720
5824
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5721
5825
|
}
|
|
5722
5826
|
}
|
|
5723
|
-
var import_node_child_process7, import_node_crypto7, import_node_fs15, import_node_path15, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
|
|
5827
|
+
var import_node_child_process7, import_node_crypto7, import_node_fs15, import_node_path15, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER, COMPOSE_HASH_TIMEOUT_MS;
|
|
5724
5828
|
var init_devcontainer_profile = __esm({
|
|
5725
5829
|
"src/core/devcontainer-profile.ts"() {
|
|
5726
5830
|
"use strict";
|
|
@@ -5732,8 +5836,10 @@ var init_devcontainer_profile = __esm({
|
|
|
5732
5836
|
init_atomic_file();
|
|
5733
5837
|
init_devcontainer_config();
|
|
5734
5838
|
init_paths();
|
|
5839
|
+
init_workspace();
|
|
5735
5840
|
MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
|
|
5736
5841
|
MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
|
|
5842
|
+
COMPOSE_HASH_TIMEOUT_MS = 1e4;
|
|
5737
5843
|
}
|
|
5738
5844
|
});
|
|
5739
5845
|
|
|
@@ -5828,7 +5934,7 @@ var init_devpod_environment = __esm({
|
|
|
5828
5934
|
import_node_child_process8 = require("child_process");
|
|
5829
5935
|
import_node_path16 = __toESM(require("path"));
|
|
5830
5936
|
init_workspace();
|
|
5831
|
-
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}}}';
|
|
5937
|
+
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")}},"com.docker.compose.config-hash":{{json (index .Config.Labels "com.docker.compose.config-hash")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
|
|
5832
5938
|
SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
|
|
5833
5939
|
/}$/,
|
|
5834
5940
|
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
@@ -6143,7 +6249,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
6143
6249
|
let state;
|
|
6144
6250
|
let stateReadFailed = false;
|
|
6145
6251
|
try {
|
|
6146
|
-
state = readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6252
|
+
state = options.candidateState ?? readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6147
6253
|
} catch {
|
|
6148
6254
|
stateReadFailed = true;
|
|
6149
6255
|
}
|
|
@@ -8025,6 +8131,12 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
8025
8131
|
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
8026
8132
|
};
|
|
8027
8133
|
}
|
|
8134
|
+
function recordPath(repoPath, workspace) {
|
|
8135
|
+
return import_node_path22.default.join(
|
|
8136
|
+
ownershipDirectory(repoPath),
|
|
8137
|
+
`${validateWorkspace(workspace, "workspace")}.json`
|
|
8138
|
+
);
|
|
8139
|
+
}
|
|
8028
8140
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
8029
8141
|
let parsed;
|
|
8030
8142
|
try {
|
|
@@ -8037,6 +8149,15 @@ function readRecordFile(filePath, expectedWorkspace) {
|
|
|
8037
8149
|
}
|
|
8038
8150
|
return validateRecord(parsed, expectedWorkspace);
|
|
8039
8151
|
}
|
|
8152
|
+
function readWorkspaceOwnership(repoPath, workspace) {
|
|
8153
|
+
const filePath = recordPath(repoPath, workspace);
|
|
8154
|
+
try {
|
|
8155
|
+
return readRecordFile(filePath, workspace);
|
|
8156
|
+
} catch (error) {
|
|
8157
|
+
if (error.code === "ENOENT") return void 0;
|
|
8158
|
+
throw error;
|
|
8159
|
+
}
|
|
8160
|
+
}
|
|
8040
8161
|
function listWorkspaceOwnership(repoPath) {
|
|
8041
8162
|
const directory = ownershipDirectory(repoPath);
|
|
8042
8163
|
return listWorkspaceOwnershipInDirectory(directory);
|
|
@@ -8986,7 +9107,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
8986
9107
|
const config = runtimeConfig.config;
|
|
8987
9108
|
loadedConfig = config;
|
|
8988
9109
|
loadedWorkspace = runtimeConfig.workspace;
|
|
8989
|
-
const cliVersion = true ? "0.0.
|
|
9110
|
+
const cliVersion = true ? "0.0.52" : "0.0.0-dev";
|
|
8990
9111
|
const configVersion = config.devrouter?.version;
|
|
8991
9112
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
8992
9113
|
addCheck(checks, {
|
|
@@ -9698,9 +9819,9 @@ async function ensureRouteInfrastructure(apps, options = {}) {
|
|
|
9698
9819
|
startRouterStack();
|
|
9699
9820
|
return tlsCoverage;
|
|
9700
9821
|
}
|
|
9701
|
-
async function replacePublishedProxyRoutes(repoPath, config, workspace) {
|
|
9822
|
+
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
9702
9823
|
const apps = proxyAppsFromConfig(config);
|
|
9703
|
-
const tlsCoverage = await ensureRouteInfrastructure(apps, { repoPath });
|
|
9824
|
+
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
9704
9825
|
const routes = apps.map((app) => {
|
|
9705
9826
|
const upstream = parseUpstream(app.upstream);
|
|
9706
9827
|
return {
|
|
@@ -9740,7 +9861,7 @@ function inspectRouterApi(protocol) {
|
|
|
9740
9861
|
"--show-error",
|
|
9741
9862
|
"--fail",
|
|
9742
9863
|
"--max-time",
|
|
9743
|
-
|
|
9864
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
9744
9865
|
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
9745
9866
|
],
|
|
9746
9867
|
{ encoding: "utf-8" }
|
|
@@ -9772,6 +9893,161 @@ function inspectRouterApi(protocol) {
|
|
|
9772
9893
|
)
|
|
9773
9894
|
};
|
|
9774
9895
|
}
|
|
9896
|
+
function isRecord2(value) {
|
|
9897
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9898
|
+
}
|
|
9899
|
+
function inspectNamedRouterApi(protocol, resource, name) {
|
|
9900
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
9901
|
+
"curl",
|
|
9902
|
+
[
|
|
9903
|
+
"--silent",
|
|
9904
|
+
"--show-error",
|
|
9905
|
+
"--fail",
|
|
9906
|
+
"--max-time",
|
|
9907
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
9908
|
+
`${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
|
|
9909
|
+
],
|
|
9910
|
+
{ encoding: "utf-8" }
|
|
9911
|
+
);
|
|
9912
|
+
if (result.status !== 0) {
|
|
9913
|
+
return {
|
|
9914
|
+
ok: false,
|
|
9915
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
9916
|
+
};
|
|
9917
|
+
}
|
|
9918
|
+
let value;
|
|
9919
|
+
try {
|
|
9920
|
+
value = JSON.parse(result.stdout);
|
|
9921
|
+
} catch {
|
|
9922
|
+
return {
|
|
9923
|
+
ok: false,
|
|
9924
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
9925
|
+
};
|
|
9926
|
+
}
|
|
9927
|
+
if (!isRecord2(value)) {
|
|
9928
|
+
return {
|
|
9929
|
+
ok: false,
|
|
9930
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
9931
|
+
};
|
|
9932
|
+
}
|
|
9933
|
+
return { ok: true, value };
|
|
9934
|
+
}
|
|
9935
|
+
function renderExpectedRoute(route) {
|
|
9936
|
+
const protocol = route.protocol ?? "http";
|
|
9937
|
+
const routerName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
9938
|
+
const routeState = {
|
|
9939
|
+
id: buildHostRouteId(route.repoPath, route.name),
|
|
9940
|
+
name: route.name,
|
|
9941
|
+
host: route.host,
|
|
9942
|
+
protocol,
|
|
9943
|
+
tcpProtocol: route.tcpProtocol,
|
|
9944
|
+
repoPath: route.repoPath,
|
|
9945
|
+
port: route.port,
|
|
9946
|
+
mode: route.mode,
|
|
9947
|
+
upstreamHost: route.upstreamHost,
|
|
9948
|
+
pid: route.pid,
|
|
9949
|
+
command: route.command,
|
|
9950
|
+
workspace: route.workspace,
|
|
9951
|
+
createdAt: "",
|
|
9952
|
+
updatedAt: ""
|
|
9953
|
+
};
|
|
9954
|
+
const document = buildHostRoutesDocument([routeState], false);
|
|
9955
|
+
const protocolDocument = document[protocol];
|
|
9956
|
+
if (!isRecord2(protocolDocument)) return void 0;
|
|
9957
|
+
const routers = protocolDocument.routers;
|
|
9958
|
+
const services = protocolDocument.services;
|
|
9959
|
+
if (!isRecord2(routers) || !isRecord2(services)) return void 0;
|
|
9960
|
+
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
9961
|
+
const router = routers[routerKey];
|
|
9962
|
+
const service = services[routerKey];
|
|
9963
|
+
if (!isRecord2(router) || !isRecord2(service)) return void 0;
|
|
9964
|
+
const rule = router.rule;
|
|
9965
|
+
const serviceReference = router.service;
|
|
9966
|
+
const loadBalancer = service.loadBalancer;
|
|
9967
|
+
const servers = isRecord2(loadBalancer) ? loadBalancer.servers : void 0;
|
|
9968
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord2(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord2(servers[0])) {
|
|
9969
|
+
return void 0;
|
|
9970
|
+
}
|
|
9971
|
+
const serverField = protocol === "http" ? "url" : "address";
|
|
9972
|
+
const server = servers[0][serverField];
|
|
9973
|
+
if (typeof server !== "string") return void 0;
|
|
9974
|
+
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
9975
|
+
if (!provider.startsWith("@")) return void 0;
|
|
9976
|
+
return {
|
|
9977
|
+
protocol,
|
|
9978
|
+
routerName,
|
|
9979
|
+
serviceKey: routerKey,
|
|
9980
|
+
serviceName: `${routerKey}${provider}`,
|
|
9981
|
+
rule,
|
|
9982
|
+
serverField,
|
|
9983
|
+
server
|
|
9984
|
+
};
|
|
9985
|
+
}
|
|
9986
|
+
function inspectExpectedRouteMatch(route) {
|
|
9987
|
+
const expected = renderExpectedRoute(route);
|
|
9988
|
+
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
9989
|
+
if (!expected) {
|
|
9990
|
+
return {
|
|
9991
|
+
ok: false,
|
|
9992
|
+
routes: [routeName],
|
|
9993
|
+
details: "host-route renderer returned an incomplete route configuration"
|
|
9994
|
+
};
|
|
9995
|
+
}
|
|
9996
|
+
const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
|
|
9997
|
+
if (!routerResult.ok) {
|
|
9998
|
+
return { ok: false, routes: [routeName], details: routerResult.details };
|
|
9999
|
+
}
|
|
10000
|
+
if (routerResult.value.status !== "enabled" || routerResult.value.name !== expected.routerName || routerResult.value.rule !== expected.rule || routerResult.value.service !== expected.serviceKey && routerResult.value.service !== expected.serviceName) {
|
|
10001
|
+
return {
|
|
10002
|
+
ok: false,
|
|
10003
|
+
routes: [routeName],
|
|
10004
|
+
details: "active Traefik router does not match the rendered route"
|
|
10005
|
+
};
|
|
10006
|
+
}
|
|
10007
|
+
const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
|
|
10008
|
+
if (!serviceResult.ok) {
|
|
10009
|
+
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
10010
|
+
}
|
|
10011
|
+
const loadBalancer = serviceResult.value.loadBalancer;
|
|
10012
|
+
const servers = isRecord2(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10013
|
+
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
10014
|
+
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord2(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord2(server) || server[expected.serverField] !== expected.server) {
|
|
10015
|
+
return {
|
|
10016
|
+
ok: false,
|
|
10017
|
+
routes: [routeName],
|
|
10018
|
+
details: "active Traefik service does not match the rendered route"
|
|
10019
|
+
};
|
|
10020
|
+
}
|
|
10021
|
+
return { ok: true };
|
|
10022
|
+
}
|
|
10023
|
+
function inspectExpectedRouteMatches(routes) {
|
|
10024
|
+
const mismatched = [];
|
|
10025
|
+
const failures = [];
|
|
10026
|
+
for (const route of routes) {
|
|
10027
|
+
const result = inspectExpectedRouteMatch(route);
|
|
10028
|
+
if (!result.ok) {
|
|
10029
|
+
mismatched.push(...result.routes);
|
|
10030
|
+
failures.push(result.details);
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
10033
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
10034
|
+
ok: false,
|
|
10035
|
+
routes: mismatched,
|
|
10036
|
+
details: failures.join("; ")
|
|
10037
|
+
};
|
|
10038
|
+
}
|
|
10039
|
+
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
10040
|
+
const deadline = Date.now() + timeoutMs;
|
|
10041
|
+
let result;
|
|
10042
|
+
do {
|
|
10043
|
+
result = inspectExpectedRouteMatches(routes);
|
|
10044
|
+
if (result.ok) return result;
|
|
10045
|
+
if (Date.now() < deadline) {
|
|
10046
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
10047
|
+
}
|
|
10048
|
+
} while (Date.now() < deadline);
|
|
10049
|
+
return result;
|
|
10050
|
+
}
|
|
9775
10051
|
function inspectExpectedRoutes(routes, expectation) {
|
|
9776
10052
|
const mismatched = [];
|
|
9777
10053
|
const failures = [];
|
|
@@ -9828,6 +10104,10 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9828
10104
|
pollIntervalMs
|
|
9829
10105
|
);
|
|
9830
10106
|
if (initial.ok) return { restarted: false };
|
|
10107
|
+
if (options.allowRestart === false)
|
|
10108
|
+
throw new Error(
|
|
10109
|
+
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
10110
|
+
);
|
|
9831
10111
|
import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
9832
10112
|
return withFileLock(
|
|
9833
10113
|
ROUTER_RELOAD_LOCK_FILE,
|
|
@@ -9858,10 +10138,21 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9858
10138
|
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
9859
10139
|
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
9860
10140
|
}
|
|
10141
|
+
async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
10142
|
+
if (routes.length === 0) return;
|
|
10143
|
+
const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
10144
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
10145
|
+
const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
|
|
10146
|
+
if (!result.ok) {
|
|
10147
|
+
throw new Error(
|
|
10148
|
+
`Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
|
|
10149
|
+
);
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
9861
10152
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
9862
10153
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
9863
10154
|
}
|
|
9864
|
-
var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
|
|
10155
|
+
var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
|
|
9865
10156
|
var init_traefik_route_health = __esm({
|
|
9866
10157
|
"src/core/traefik-route-health.ts"() {
|
|
9867
10158
|
"use strict";
|
|
@@ -9873,6 +10164,7 @@ var init_traefik_route_health = __esm({
|
|
|
9873
10164
|
init_router();
|
|
9874
10165
|
ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
9875
10166
|
ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
10167
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
9876
10168
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
9877
10169
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
9878
10170
|
DEFAULT_POLL_INTERVAL_MS = 250;
|
|
@@ -10136,7 +10428,7 @@ function validateWorkspaceContainers(containers, options) {
|
|
|
10136
10428
|
if (options.target.kind === "linked") {
|
|
10137
10429
|
assertOverlay(appContainer, options.repoPath);
|
|
10138
10430
|
}
|
|
10139
|
-
assertReady(appContainer, "Workspace app");
|
|
10431
|
+
if (!options.allowStopped) assertReady(appContainer, "Workspace app");
|
|
10140
10432
|
if (options.target.kind === "linked") {
|
|
10141
10433
|
const gitCommonDir = options.target.gitCommonDir;
|
|
10142
10434
|
const gitMount = appContainer.mounts.find(
|
|
@@ -10288,6 +10580,142 @@ function resolveLinkedTarget(repoPath) {
|
|
|
10288
10580
|
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
10289
10581
|
};
|
|
10290
10582
|
}
|
|
10583
|
+
function resolveRepairTarget(repoPath, linked) {
|
|
10584
|
+
const workspace = linked ? readPersistedWorkspace(repoPath) : void 0;
|
|
10585
|
+
if (linked && !workspace) throw new Error("Repair requires an existing workspace identity.");
|
|
10586
|
+
const state = readManagedRuntimeState(repoPath, workspace);
|
|
10587
|
+
if (state?.status !== "degraded") {
|
|
10588
|
+
throw new Error("Repair requires a persisted degraded managed runtime.");
|
|
10589
|
+
}
|
|
10590
|
+
resetWorkspaceRuntimeCaches();
|
|
10591
|
+
const snapshots = getWorkspaceRegistrySnapshots();
|
|
10592
|
+
if (snapshots.unavailable.length) throw new Error("Repair cannot read every provider registry.");
|
|
10593
|
+
const matches = [...snapshots.devpod ?? [], ...snapshots.devsy ?? []].filter(
|
|
10594
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
10595
|
+
);
|
|
10596
|
+
if (matches.length !== 1 || matches[0].id !== state.devpodId || !sameWorkspacePath(matches[0].source.localFolder, repoPath)) {
|
|
10597
|
+
throw new Error("Repair requires one exact retained provider registration.");
|
|
10598
|
+
}
|
|
10599
|
+
const selected = snapshots[resolveWorkspaceRuntimeOrDefault(repoPath)] ?? [];
|
|
10600
|
+
if (!selected.some(
|
|
10601
|
+
(entry) => entry.id === state.devpodId && sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
10602
|
+
)) {
|
|
10603
|
+
throw new Error("Repair provider selection does not own the retained workspace.");
|
|
10604
|
+
}
|
|
10605
|
+
if (workspace) {
|
|
10606
|
+
const owner = readWorkspaceOwnership(repoPath, workspace);
|
|
10607
|
+
if (!owner || owner.devpodId !== state.devpodId || !sameWorkspacePath(owner.worktreePath, repoPath)) {
|
|
10608
|
+
throw new Error("Repair requires the existing exact workspace owner record.");
|
|
10609
|
+
}
|
|
10610
|
+
return {
|
|
10611
|
+
kind: "linked",
|
|
10612
|
+
workspace,
|
|
10613
|
+
devpodId: state.devpodId,
|
|
10614
|
+
hadExactDevpod: true,
|
|
10615
|
+
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
10616
|
+
};
|
|
10617
|
+
}
|
|
10618
|
+
return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
|
|
10619
|
+
}
|
|
10620
|
+
function assertRepairBaseline(options) {
|
|
10621
|
+
const { repoPath, target, state, plan, config, profile, processes } = options;
|
|
10622
|
+
const desired = {
|
|
10623
|
+
apps: proxyAppsFromConfig(config).map((app) => app.name),
|
|
10624
|
+
services: plan.desiredProfileServices,
|
|
10625
|
+
processes
|
|
10626
|
+
};
|
|
10627
|
+
if (state.profile !== profile || state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || Object.keys(desired).some((key) => {
|
|
10628
|
+
const resource = key;
|
|
10629
|
+
return JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort());
|
|
10630
|
+
}) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
|
|
10631
|
+
throw new Error(
|
|
10632
|
+
"Repair requires the recorded profile, resource sets, and unchanged managed configuration."
|
|
10633
|
+
);
|
|
10634
|
+
}
|
|
10635
|
+
if (options.routes.some(
|
|
10636
|
+
(route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
|
|
10637
|
+
)) {
|
|
10638
|
+
throw new Error("Repair refuses unexpected active routes.");
|
|
10639
|
+
}
|
|
10640
|
+
const containers = inspectWorkspaceContainers();
|
|
10641
|
+
const retained = plan.desiredServices.map((service) => {
|
|
10642
|
+
const matches = exactWorkspaceServiceContainers(
|
|
10643
|
+
containers,
|
|
10644
|
+
repoPath,
|
|
10645
|
+
state.composeProject,
|
|
10646
|
+
service,
|
|
10647
|
+
plan.composeFiles
|
|
10648
|
+
);
|
|
10649
|
+
if (matches.length !== 1)
|
|
10650
|
+
throw new Error(`Repair requires one retained container for '${service}'.`);
|
|
10651
|
+
return matches[0];
|
|
10652
|
+
});
|
|
10653
|
+
const primary = retained.find(
|
|
10654
|
+
(c) => c.labels["com.docker.compose.service"] === plan.primaryService
|
|
10655
|
+
);
|
|
10656
|
+
if (!primary) throw new Error("Repair cannot identify the retained primary container.");
|
|
10657
|
+
if (!primary.state.Running && (options.routes.length > 0 || containers.some(
|
|
10658
|
+
(container) => container.labels["com.docker.compose.project"] === state.composeProject && container.state.Running !== false
|
|
10659
|
+
))) {
|
|
10660
|
+
throw new Error(
|
|
10661
|
+
"Repair of a stopped primary requires all exact project containers stopped and no checkout routes."
|
|
10662
|
+
);
|
|
10663
|
+
}
|
|
10664
|
+
validateWorkspaceContainers(containers, {
|
|
10665
|
+
repoPath,
|
|
10666
|
+
target,
|
|
10667
|
+
upstreamHosts: [],
|
|
10668
|
+
allowStopped: !primary.state.Running
|
|
10669
|
+
});
|
|
10670
|
+
for (const container of containers) {
|
|
10671
|
+
if (container.labels["com.docker.compose.project"] !== state.composeProject) continue;
|
|
10672
|
+
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
10673
|
+
if (typeof container.state.Running !== "boolean" || !hasExactComposeIdentity(container, {
|
|
10674
|
+
repoPath,
|
|
10675
|
+
service,
|
|
10676
|
+
composeProject: state.composeProject,
|
|
10677
|
+
composeFiles: plan.composeFiles
|
|
10678
|
+
}) || container.state.Running && !plan.desiredServices.includes(service)) {
|
|
10679
|
+
throw new Error("Repair refuses foreign or unexpected active Compose resources.");
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10682
|
+
const workspacePath = primary.mounts.find(
|
|
10683
|
+
(m) => m.Type === "bind" && sameWorkspacePath(m.Source, repoPath)
|
|
10684
|
+
)?.Destination;
|
|
10685
|
+
if (!workspacePath) throw new Error("Repair cannot prove the primary workspace mount.");
|
|
10686
|
+
for (const name of primary.state.Running ? config.managedRuntime?.processes ?? [] : []) {
|
|
10687
|
+
if (!processes.includes(name) && runManagedProcessAction({
|
|
10688
|
+
container: { id: primary.id, workspacePath },
|
|
10689
|
+
name,
|
|
10690
|
+
action: "status",
|
|
10691
|
+
quiet: true
|
|
10692
|
+
}) !== "stopped") {
|
|
10693
|
+
throw new Error(`Repair refuses unexpected or unproven process '${name}'.`);
|
|
10694
|
+
}
|
|
10695
|
+
}
|
|
10696
|
+
assertManagedContainerConfigUnchanged({
|
|
10697
|
+
plan,
|
|
10698
|
+
containers: retained,
|
|
10699
|
+
workspace: options.workspace
|
|
10700
|
+
});
|
|
10701
|
+
return retained;
|
|
10702
|
+
}
|
|
10703
|
+
function assertRetainedRepairContainers(repoPath, plan, retained) {
|
|
10704
|
+
const current = inspectWorkspaceContainers();
|
|
10705
|
+
for (const previous of retained) {
|
|
10706
|
+
const service = previous.labels["com.docker.compose.service"] ?? "";
|
|
10707
|
+
const matches = exactWorkspaceServiceContainers(
|
|
10708
|
+
current,
|
|
10709
|
+
repoPath,
|
|
10710
|
+
previous.labels["com.docker.compose.project"] ?? "",
|
|
10711
|
+
service,
|
|
10712
|
+
plan.composeFiles
|
|
10713
|
+
);
|
|
10714
|
+
if (matches.length !== 1 || matches[0].id !== previous.id) {
|
|
10715
|
+
throw new Error(`Repair lost retained container identity for '${service}'.`);
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10291
10719
|
function resolvePrimaryTarget(repoPath) {
|
|
10292
10720
|
const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
10293
10721
|
return {
|
|
@@ -10329,7 +10757,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10329
10757
|
);
|
|
10330
10758
|
}
|
|
10331
10759
|
}
|
|
10332
|
-
|
|
10760
|
+
const ensureLocked = async () => {
|
|
10761
|
+
let repairMutationStarted = false;
|
|
10762
|
+
let retainedRepairContainers = [];
|
|
10333
10763
|
let environmentStarted = false;
|
|
10334
10764
|
let managedPlan;
|
|
10335
10765
|
let previousManagedState;
|
|
@@ -10347,10 +10777,11 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10347
10777
|
let firstTransitionBaseline;
|
|
10348
10778
|
const routeLoadOptions = {
|
|
10349
10779
|
initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
|
|
10350
|
-
recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4)
|
|
10780
|
+
recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4),
|
|
10781
|
+
...options.repair ? { allowRestart: false } : {}
|
|
10351
10782
|
};
|
|
10352
10783
|
try {
|
|
10353
|
-
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10784
|
+
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10354
10785
|
let devpodId = target.devpodId;
|
|
10355
10786
|
if (target.kind === "linked") {
|
|
10356
10787
|
const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
@@ -10360,12 +10791,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10360
10791
|
}
|
|
10361
10792
|
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
10362
10793
|
managedPostStartPlan = managedPostStart;
|
|
10794
|
+
if (options.repair)
|
|
10795
|
+
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10363
10796
|
const runtime = loadRuntimeConfig(
|
|
10364
10797
|
repoPath,
|
|
10365
10798
|
target.kind === "primary" ? "" : target.workspace,
|
|
10366
|
-
options.profile
|
|
10799
|
+
options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
|
|
10367
10800
|
);
|
|
10368
10801
|
const managedRuntime = runtime.config.managedRuntime;
|
|
10802
|
+
if (options.repair && !managedRuntime)
|
|
10803
|
+
throw new Error("Repair requires managedRuntime configuration.");
|
|
10369
10804
|
previousRoutes = listHostRouteState().filter(
|
|
10370
10805
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
10371
10806
|
);
|
|
@@ -10374,12 +10809,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10374
10809
|
const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
|
|
10375
10810
|
if (managedRuntime) {
|
|
10376
10811
|
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10377
|
-
if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
10812
|
+
if (!options.repair && previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
10378
10813
|
previousManagedState = void 0;
|
|
10379
10814
|
}
|
|
10380
|
-
if (previousManagedState?.status === "degraded") {
|
|
10815
|
+
if (!options.repair && previousManagedState?.status === "degraded") {
|
|
10381
10816
|
throw new Error(
|
|
10382
|
-
"Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
|
|
10817
|
+
"Managed runtime state is degraded; refusing a new profile transition until drift is repaired. Inspect status, then use ensure --repair for the recorded profile."
|
|
10383
10818
|
);
|
|
10384
10819
|
}
|
|
10385
10820
|
if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
|
|
@@ -10433,7 +10868,20 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10433
10868
|
);
|
|
10434
10869
|
}
|
|
10435
10870
|
}
|
|
10436
|
-
if (managedPlan) {
|
|
10871
|
+
if (options.repair && managedPlan && previousManagedState) {
|
|
10872
|
+
retainedRepairContainers = assertRepairBaseline({
|
|
10873
|
+
repoPath,
|
|
10874
|
+
target,
|
|
10875
|
+
state: previousManagedState,
|
|
10876
|
+
plan: managedPlan,
|
|
10877
|
+
config: runtime.config,
|
|
10878
|
+
profile: runtime.profile,
|
|
10879
|
+
processes: desiredProcesses,
|
|
10880
|
+
routes: previousRoutes,
|
|
10881
|
+
workspace: managedWorkspaceEnv
|
|
10882
|
+
});
|
|
10883
|
+
}
|
|
10884
|
+
if (managedPlan && !options.repair) {
|
|
10437
10885
|
writeManagedDevcontainerConfig(managedPlan);
|
|
10438
10886
|
managedConfigWritten = true;
|
|
10439
10887
|
}
|
|
@@ -10469,7 +10917,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10469
10917
|
let recreated = false;
|
|
10470
10918
|
let container;
|
|
10471
10919
|
try {
|
|
10472
|
-
await startAndProveAttachment();
|
|
10920
|
+
if (!options.repair) await startAndProveAttachment();
|
|
10473
10921
|
} catch (error) {
|
|
10474
10922
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10475
10923
|
throw error;
|
|
@@ -10479,7 +10927,45 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10479
10927
|
}
|
|
10480
10928
|
if (!container) {
|
|
10481
10929
|
try {
|
|
10482
|
-
|
|
10930
|
+
if (options.repair && managedPlan && previousManagedState) {
|
|
10931
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
10932
|
+
const primaryService = managedPlan.primaryService;
|
|
10933
|
+
const primary = retainedRepairContainers.find(
|
|
10934
|
+
(entry) => entry.labels["com.docker.compose.service"] === primaryService
|
|
10935
|
+
);
|
|
10936
|
+
if (!primary) throw new Error("Repair lost its retained primary container.");
|
|
10937
|
+
const stopped = retainedRepairContainers.filter((entry) => !entry.state.Running);
|
|
10938
|
+
for (const entry of stopped) {
|
|
10939
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(entry.id))
|
|
10940
|
+
throw new Error("Repair found an invalid retained container ID.");
|
|
10941
|
+
}
|
|
10942
|
+
if (stopped.length > 0) {
|
|
10943
|
+
repairMutationStarted = true;
|
|
10944
|
+
transitionPhase = "service-start";
|
|
10945
|
+
const result = (0, import_node_child_process21.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
10946
|
+
encoding: "utf-8",
|
|
10947
|
+
timeout: DEFAULT_READINESS_TIMEOUT_MS,
|
|
10948
|
+
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit"
|
|
10949
|
+
});
|
|
10950
|
+
if (result.status !== 0)
|
|
10951
|
+
throw new Error("Could not start retained repair containers.");
|
|
10952
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
10953
|
+
}
|
|
10954
|
+
await waitForManagedServices(
|
|
10955
|
+
managedPlan,
|
|
10956
|
+
repoPath,
|
|
10957
|
+
previousManagedState.composeProject,
|
|
10958
|
+
managedPlan.desiredServices,
|
|
10959
|
+
primary.id,
|
|
10960
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
10961
|
+
);
|
|
10962
|
+
}
|
|
10963
|
+
container = options.repair ? await waitForContainerPreflight(
|
|
10964
|
+
repoPath,
|
|
10965
|
+
currentTarget(),
|
|
10966
|
+
[],
|
|
10967
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
10968
|
+
) : await preflight(0);
|
|
10483
10969
|
} catch (error) {
|
|
10484
10970
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10485
10971
|
throw error;
|
|
@@ -10520,13 +11006,18 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10520
11006
|
}
|
|
10521
11007
|
if (!matches[0]?.state.Running) missingServices.push(service);
|
|
10522
11008
|
}
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
11009
|
+
if (options.repair) {
|
|
11010
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11011
|
+
repairMutationStarted = true;
|
|
11012
|
+
}
|
|
11013
|
+
if (!options.repair)
|
|
11014
|
+
startExactManagedServices({
|
|
11015
|
+
plan: managedPlan,
|
|
11016
|
+
composeProject: managedComposeProject,
|
|
11017
|
+
services: missingServices,
|
|
11018
|
+
quiet: options.quiet,
|
|
11019
|
+
workspace: managedWorkspaceEnv
|
|
11020
|
+
});
|
|
10530
11021
|
await waitForManagedServices(
|
|
10531
11022
|
managedPlan,
|
|
10532
11023
|
repoPath,
|
|
@@ -10538,6 +11029,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10538
11029
|
container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
10539
11030
|
managedContainer = container;
|
|
10540
11031
|
}
|
|
11032
|
+
if (options.repair && managedPlan)
|
|
11033
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
10541
11034
|
transitionPhase = "process-start";
|
|
10542
11035
|
runManagedPostStart({
|
|
10543
11036
|
plan: managedPostStart,
|
|
@@ -10551,6 +11044,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10551
11044
|
const desiredProcessSet = new Set(desiredProcesses);
|
|
10552
11045
|
for (const process2 of processRegistry) {
|
|
10553
11046
|
if (desiredProcessSet.has(process2)) continue;
|
|
11047
|
+
if (options.repair) {
|
|
11048
|
+
if (runManagedProcessAction({
|
|
11049
|
+
container,
|
|
11050
|
+
name: process2,
|
|
11051
|
+
action: "status",
|
|
11052
|
+
quiet: options.quiet
|
|
11053
|
+
}) !== "stopped") {
|
|
11054
|
+
throw new Error(`Repair found unexpected process '${process2}' after adapter replay.`);
|
|
11055
|
+
}
|
|
11056
|
+
continue;
|
|
11057
|
+
}
|
|
10554
11058
|
runManagedProcessAction({
|
|
10555
11059
|
container,
|
|
10556
11060
|
name: process2,
|
|
@@ -10570,7 +11074,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10570
11074
|
}
|
|
10571
11075
|
}
|
|
10572
11076
|
transitionPhase = "service-stop";
|
|
10573
|
-
|
|
11077
|
+
if (!options.repair)
|
|
11078
|
+
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
10574
11079
|
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
10575
11080
|
await waitForManagedServices(
|
|
10576
11081
|
managedPlan,
|
|
@@ -10585,11 +11090,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10585
11090
|
const publication = await replacePublishedProxyRoutes(
|
|
10586
11091
|
repoPath,
|
|
10587
11092
|
runtime.config,
|
|
10588
|
-
target.workspace
|
|
11093
|
+
target.workspace,
|
|
11094
|
+
options.repair ? { prepareInfrastructure: false } : void 0
|
|
10589
11095
|
);
|
|
10590
11096
|
candidateRoutes = publication.routes;
|
|
10591
11097
|
candidateRoutesPublished = true;
|
|
10592
|
-
await
|
|
11098
|
+
if (options.repair) await ensureTraefikRoutesMatch(publication.routes, routeLoadOptions);
|
|
11099
|
+
else await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
|
|
10593
11100
|
await ensureTraefikRoutesRemoved(
|
|
10594
11101
|
removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
|
|
10595
11102
|
routeLoadOptions
|
|
@@ -10636,8 +11143,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10636
11143
|
if (!devpodId) {
|
|
10637
11144
|
throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
|
|
10638
11145
|
}
|
|
11146
|
+
let candidateState;
|
|
10639
11147
|
if (managedPlan && managedComposeProject) {
|
|
10640
|
-
|
|
11148
|
+
candidateState = {
|
|
10641
11149
|
version: 1,
|
|
10642
11150
|
repoPath,
|
|
10643
11151
|
...target.workspace !== void 0 ? { workspace: target.workspace } : {},
|
|
@@ -10653,15 +11161,24 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10653
11161
|
effectiveConfigSha256: managedPlan.effectiveConfigSha256,
|
|
10654
11162
|
status: "ready",
|
|
10655
11163
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10656
|
-
}
|
|
11164
|
+
};
|
|
11165
|
+
if (!options.repair) writeManagedRuntimeState(candidateState);
|
|
10657
11166
|
}
|
|
10658
11167
|
const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
|
|
10659
11168
|
repoPath,
|
|
10660
11169
|
workspace: target.workspace,
|
|
10661
11170
|
config: runtime.config,
|
|
10662
11171
|
profile: runtime.profile,
|
|
10663
|
-
resolvedProfile: runtime.resolvedProfile
|
|
11172
|
+
resolvedProfile: runtime.resolvedProfile,
|
|
11173
|
+
...options.repair ? { candidateState } : {}
|
|
10664
11174
|
}) : void 0;
|
|
11175
|
+
if (options.repair && candidateState && managedPlan) {
|
|
11176
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11177
|
+
resolveRepairTarget(repoPath, linked);
|
|
11178
|
+
if (managedRuntimeStatus?.status !== "ready")
|
|
11179
|
+
throw new Error("Repaired candidate runtime did not pass final readiness validation.");
|
|
11180
|
+
writeManagedRuntimeState(candidateState);
|
|
11181
|
+
}
|
|
10665
11182
|
return {
|
|
10666
11183
|
kind: target.kind,
|
|
10667
11184
|
repoPath,
|
|
@@ -10674,6 +11191,37 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10674
11191
|
...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
|
|
10675
11192
|
};
|
|
10676
11193
|
} catch (error) {
|
|
11194
|
+
if (options.repair && !repairMutationStarted) throw error;
|
|
11195
|
+
if (options.repair && repairMutationStarted && previousManagedState && managedPlan) {
|
|
11196
|
+
const failures = [];
|
|
11197
|
+
if (transitionPhase === "route-publication") {
|
|
11198
|
+
try {
|
|
11199
|
+
const currentRoutes = listHostRouteState().filter((route) => sameWorkspacePath(route.repoPath, repoPath)).map(routeInputFromState);
|
|
11200
|
+
const rollbackRoutes = previousRoutes.map(routeInputFromState);
|
|
11201
|
+
replaceHostRoutesForRepo(repoPath, rollbackRoutes);
|
|
11202
|
+
await ensureTraefikRoutesMatch(rollbackRoutes, routeLoadOptions);
|
|
11203
|
+
await ensureTraefikRoutesRemoved(
|
|
11204
|
+
removedRoutesForReplacement(currentRoutes, rollbackRoutes),
|
|
11205
|
+
routeLoadOptions
|
|
11206
|
+
);
|
|
11207
|
+
} catch (rollbackError) {
|
|
11208
|
+
failures.push(
|
|
11209
|
+
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
11210
|
+
);
|
|
11211
|
+
}
|
|
11212
|
+
}
|
|
11213
|
+
try {
|
|
11214
|
+
markManagedRuntimeDegraded(previousManagedState, transitionPhase);
|
|
11215
|
+
} catch (stateError) {
|
|
11216
|
+
failures.push(
|
|
11217
|
+
`state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
|
|
11218
|
+
);
|
|
11219
|
+
}
|
|
11220
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
11221
|
+
throw new Error(
|
|
11222
|
+
`${original} Repair failed; owned resources may remain running.${failures.length ? ` Recovery errors: ${failures.join("; ")}.` : " State remains degraded."}`
|
|
11223
|
+
);
|
|
11224
|
+
}
|
|
10677
11225
|
if (managedPlan && managedContainer && managedComposeProject) {
|
|
10678
11226
|
const rollbackErrors = [];
|
|
10679
11227
|
const failedPhase = transitionPhase;
|
|
@@ -10875,6 +11423,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10875
11423
|
}
|
|
10876
11424
|
throw error;
|
|
10877
11425
|
}
|
|
11426
|
+
};
|
|
11427
|
+
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11428
|
+
if (!options.repair) return ensureLocked();
|
|
11429
|
+
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
11430
|
+
import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11431
|
+
return withFileLock(
|
|
11432
|
+
import_node_path26.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
11433
|
+
{
|
|
11434
|
+
activity: "Managed runtime repair",
|
|
11435
|
+
target: repoPath,
|
|
11436
|
+
fair: true,
|
|
11437
|
+
waitMs: 18e5,
|
|
11438
|
+
onWait: createStderrWaitReporter("Managed runtime repair", repoPath)
|
|
11439
|
+
},
|
|
11440
|
+
ensureLocked
|
|
11441
|
+
);
|
|
10878
11442
|
});
|
|
10879
11443
|
}
|
|
10880
11444
|
var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
@@ -10888,6 +11452,7 @@ var init_workspace_ensure = __esm({
|
|
|
10888
11452
|
init_devpod_environment();
|
|
10889
11453
|
init_devpod_mutation();
|
|
10890
11454
|
init_devpod_workspaces();
|
|
11455
|
+
init_file_lock();
|
|
10891
11456
|
init_host_routes();
|
|
10892
11457
|
init_http_route_probe();
|
|
10893
11458
|
init_managed_post_start();
|
|
@@ -10935,7 +11500,8 @@ async function runEnsureCommand(options) {
|
|
|
10935
11500
|
const result = await workspaceEnsure(repoPath, {
|
|
10936
11501
|
open: options.open,
|
|
10937
11502
|
quiet: Boolean(options.json),
|
|
10938
|
-
profile: options.profile
|
|
11503
|
+
profile: options.profile,
|
|
11504
|
+
...options.repair ? { repair: true } : {}
|
|
10939
11505
|
});
|
|
10940
11506
|
if (options.json) {
|
|
10941
11507
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -13235,9 +13801,294 @@ var init_profile_resolution = __esm({
|
|
|
13235
13801
|
}
|
|
13236
13802
|
});
|
|
13237
13803
|
|
|
13804
|
+
// src/core/profile-plan.ts
|
|
13805
|
+
function fail(message) {
|
|
13806
|
+
throw new Error(message);
|
|
13807
|
+
}
|
|
13808
|
+
function isRecord3(value) {
|
|
13809
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13810
|
+
}
|
|
13811
|
+
function requireRecord(value, label) {
|
|
13812
|
+
if (!isRecord3(value)) fail(`${label} must be a mapping.`);
|
|
13813
|
+
return value;
|
|
13814
|
+
}
|
|
13815
|
+
function requireExactKeys(value, allowed, label) {
|
|
13816
|
+
const allowedSet = new Set(allowed);
|
|
13817
|
+
const unknown = Object.keys(value).filter((key) => !allowedSet.has(key));
|
|
13818
|
+
if (unknown.length > 0) fail(`${label} has unsupported keys: ${unknown.sort().join(", ")}.`);
|
|
13819
|
+
const missing = allowed.filter((key) => !Object.hasOwn(value, key));
|
|
13820
|
+
if (missing.length > 0) fail(`${label} is missing keys: ${missing.join(", ")}.`);
|
|
13821
|
+
}
|
|
13822
|
+
function requireBoolean(value, label) {
|
|
13823
|
+
if (typeof value !== "boolean") fail(`${label} must be a boolean.`);
|
|
13824
|
+
return value;
|
|
13825
|
+
}
|
|
13826
|
+
function requireStringList(value, label, options = {}) {
|
|
13827
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
13828
|
+
fail(`${label} must be an array of strings.`);
|
|
13829
|
+
}
|
|
13830
|
+
if (options.allowEmpty === false && value.length === 0) fail(`${label} must not be empty.`);
|
|
13831
|
+
for (const entry of value) {
|
|
13832
|
+
if (entry.length === 0) fail(`${label} must not contain empty strings.`);
|
|
13833
|
+
if (entry.length > MAX_LITERAL_LENGTH) {
|
|
13834
|
+
fail(`${label} contains a value longer than ${MAX_LITERAL_LENGTH} characters.`);
|
|
13835
|
+
}
|
|
13836
|
+
}
|
|
13837
|
+
if (new Set(value).size !== value.length) fail(`${label} must not contain duplicates.`);
|
|
13838
|
+
return [...value];
|
|
13839
|
+
}
|
|
13840
|
+
function parseMappings(value) {
|
|
13841
|
+
const input2 = requireRecord(value, "apps.mappings");
|
|
13842
|
+
const entries = Object.entries(input2);
|
|
13843
|
+
if (entries.length > MAX_APP_MAPPINGS) {
|
|
13844
|
+
fail(`apps.mappings exceeds the limit of ${MAX_APP_MAPPINGS}.`);
|
|
13845
|
+
}
|
|
13846
|
+
const mappings = [];
|
|
13847
|
+
const allBindingKeys = /* @__PURE__ */ new Set();
|
|
13848
|
+
let bindingValues = 0;
|
|
13849
|
+
for (const [appName, rawMapping] of entries) {
|
|
13850
|
+
if (appName.length === 0) fail("apps.mappings must not contain an empty app name.");
|
|
13851
|
+
const mapping = requireRecord(rawMapping, `apps.mappings.${appName}`);
|
|
13852
|
+
requireExactKeys(mapping, ["bindings"], `apps.mappings.${appName}`);
|
|
13853
|
+
const rawBindings = requireRecord(mapping.bindings, `apps.mappings.${appName}.bindings`);
|
|
13854
|
+
if (Object.keys(rawBindings).length === 0) {
|
|
13855
|
+
fail(`apps.mappings.${appName}.bindings must not be empty.`);
|
|
13856
|
+
}
|
|
13857
|
+
const bindings = [];
|
|
13858
|
+
for (const [key, rawValues] of Object.entries(rawBindings)) {
|
|
13859
|
+
if (!BINDING_KEY.test(key)) {
|
|
13860
|
+
fail(`binding key '${key}' must match ${BINDING_KEY.source}.`);
|
|
13861
|
+
}
|
|
13862
|
+
const values = requireStringList(rawValues, `apps.mappings.${appName}.bindings.${key}`, {
|
|
13863
|
+
allowEmpty: false
|
|
13864
|
+
});
|
|
13865
|
+
allBindingKeys.add(key);
|
|
13866
|
+
bindingValues += values.length;
|
|
13867
|
+
bindings.push([key, values]);
|
|
13868
|
+
}
|
|
13869
|
+
mappings.push([appName, { bindings: Object.fromEntries(bindings) }]);
|
|
13870
|
+
}
|
|
13871
|
+
if (allBindingKeys.size > MAX_BINDING_KEYS) {
|
|
13872
|
+
fail(`contract exceeds the limit of ${MAX_BINDING_KEYS} binding keys.`);
|
|
13873
|
+
}
|
|
13874
|
+
if (bindingValues > MAX_BINDING_VALUES) {
|
|
13875
|
+
fail(`contract exceeds the limit of ${MAX_BINDING_VALUES} binding values.`);
|
|
13876
|
+
}
|
|
13877
|
+
return Object.fromEntries(mappings);
|
|
13878
|
+
}
|
|
13879
|
+
function parseProfilePlanContract(source) {
|
|
13880
|
+
const document = (0, import_yaml7.parseDocument)(source, {
|
|
13881
|
+
merge: false,
|
|
13882
|
+
schema: "core",
|
|
13883
|
+
uniqueKeys: true
|
|
13884
|
+
});
|
|
13885
|
+
if (document.errors.length > 0) {
|
|
13886
|
+
fail(`profile plan contract is invalid YAML: ${document.errors[0]?.message}`);
|
|
13887
|
+
}
|
|
13888
|
+
if (document.warnings.length > 0) {
|
|
13889
|
+
fail(`profile plan contract has unsupported YAML: ${document.warnings[0]?.message}`);
|
|
13890
|
+
}
|
|
13891
|
+
let raw;
|
|
13892
|
+
try {
|
|
13893
|
+
raw = document.toJS({ maxAliasCount: 0 });
|
|
13894
|
+
} catch (error) {
|
|
13895
|
+
fail(`profile plan contract could not be converted: ${error.message}`);
|
|
13896
|
+
}
|
|
13897
|
+
const contract = requireRecord(raw, "profile plan contract");
|
|
13898
|
+
requireExactKeys(
|
|
13899
|
+
contract,
|
|
13900
|
+
["version", "apps", "dependencies", "managedRuntime"],
|
|
13901
|
+
"profile plan contract"
|
|
13902
|
+
);
|
|
13903
|
+
if (contract.version !== CONTRACT_VERSION) {
|
|
13904
|
+
fail(`unsupported profile plan contract version ${String(contract.version)}.`);
|
|
13905
|
+
}
|
|
13906
|
+
const apps = requireRecord(contract.apps, "apps");
|
|
13907
|
+
requireExactKeys(apps, ["requireNonEmpty", "mappings"], "apps");
|
|
13908
|
+
const dependencies = requireRecord(contract.dependencies, "dependencies");
|
|
13909
|
+
requireExactKeys(dependencies, ["allowed"], "dependencies");
|
|
13910
|
+
const managedRuntime = requireRecord(contract.managedRuntime, "managedRuntime");
|
|
13911
|
+
requireExactKeys(managedRuntime, ["services", "processes"], "managedRuntime");
|
|
13912
|
+
const services = requireRecord(managedRuntime.services, "managedRuntime.services");
|
|
13913
|
+
requireExactKeys(services, ["allowed"], "managedRuntime.services");
|
|
13914
|
+
const processes = requireRecord(managedRuntime.processes, "managedRuntime.processes");
|
|
13915
|
+
requireExactKeys(processes, ["exact"], "managedRuntime.processes");
|
|
13916
|
+
return {
|
|
13917
|
+
version: CONTRACT_VERSION,
|
|
13918
|
+
apps: {
|
|
13919
|
+
requireNonEmpty: requireBoolean(apps.requireNonEmpty, "apps.requireNonEmpty"),
|
|
13920
|
+
mappings: parseMappings(apps.mappings)
|
|
13921
|
+
},
|
|
13922
|
+
dependencies: {
|
|
13923
|
+
allowed: requireStringList(dependencies.allowed, "dependencies.allowed")
|
|
13924
|
+
},
|
|
13925
|
+
managedRuntime: {
|
|
13926
|
+
services: {
|
|
13927
|
+
allowed: requireStringList(services.allowed, "managedRuntime.services.allowed")
|
|
13928
|
+
},
|
|
13929
|
+
processes: {
|
|
13930
|
+
exact: requireStringList(processes.exact, "managedRuntime.processes.exact")
|
|
13931
|
+
}
|
|
13932
|
+
}
|
|
13933
|
+
};
|
|
13934
|
+
}
|
|
13935
|
+
function sortedUnique3(values) {
|
|
13936
|
+
return Array.from(new Set(values)).sort();
|
|
13937
|
+
}
|
|
13938
|
+
function registryFor(config) {
|
|
13939
|
+
const managedRuntime = config.managedRuntime;
|
|
13940
|
+
return {
|
|
13941
|
+
apps: sortedUnique3(
|
|
13942
|
+
config.apps.filter((app) => app.kind !== "dependency").map((app) => app.name)
|
|
13943
|
+
),
|
|
13944
|
+
dependencies: sortedUnique3(
|
|
13945
|
+
config.apps.filter((app) => app.kind === "dependency").map((app) => app.name)
|
|
13946
|
+
),
|
|
13947
|
+
managedServices: sortedUnique3([
|
|
13948
|
+
...managedRuntime?.devcontainer.baseServices ?? [],
|
|
13949
|
+
...managedRuntime?.devcontainer.profileServices ?? []
|
|
13950
|
+
]),
|
|
13951
|
+
managedProcesses: sortedUnique3(managedRuntime?.processes ?? [])
|
|
13952
|
+
};
|
|
13953
|
+
}
|
|
13954
|
+
function assertKnown(values, registry, label) {
|
|
13955
|
+
const known = new Set(registry);
|
|
13956
|
+
const unknown = values.filter((value) => !known.has(value));
|
|
13957
|
+
if (unknown.length > 0) fail(`${label} contains unknown resources: ${unknown.join(", ")}.`);
|
|
13958
|
+
}
|
|
13959
|
+
function assertAllowed(selected, allowed, label) {
|
|
13960
|
+
const allowedSet = new Set(allowed);
|
|
13961
|
+
const unsupported = selected.filter((value) => !allowedSet.has(value));
|
|
13962
|
+
if (unsupported.length > 0) {
|
|
13963
|
+
fail(`${label} selects unsupported resources: ${unsupported.join(", ")}.`);
|
|
13964
|
+
}
|
|
13965
|
+
}
|
|
13966
|
+
function assertExact(selected, expected, label) {
|
|
13967
|
+
const left = sortedUnique3(selected);
|
|
13968
|
+
const right = sortedUnique3(expected);
|
|
13969
|
+
if (left.length !== right.length || left.some((value, index) => value !== right[index])) {
|
|
13970
|
+
fail(`${label} must equal [${right.join(", ")}], received [${left.join(", ")}].`);
|
|
13971
|
+
}
|
|
13972
|
+
}
|
|
13973
|
+
function buildProfilePlanReport(options) {
|
|
13974
|
+
const { report, contract, registry, contractPath } = options;
|
|
13975
|
+
assertKnown(Object.keys(contract.apps.mappings), registry.apps, "apps.mappings");
|
|
13976
|
+
assertKnown(contract.dependencies.allowed, registry.dependencies, "dependencies.allowed");
|
|
13977
|
+
assertKnown(
|
|
13978
|
+
contract.managedRuntime.services.allowed,
|
|
13979
|
+
registry.managedServices,
|
|
13980
|
+
"managedRuntime.services.allowed"
|
|
13981
|
+
);
|
|
13982
|
+
assertKnown(
|
|
13983
|
+
contract.managedRuntime.processes.exact,
|
|
13984
|
+
registry.managedProcesses,
|
|
13985
|
+
"managedRuntime.processes.exact"
|
|
13986
|
+
);
|
|
13987
|
+
if (contract.apps.requireNonEmpty && report.apps.length === 0) {
|
|
13988
|
+
fail("selected profile must contain at least one app for this contract.");
|
|
13989
|
+
}
|
|
13990
|
+
const unmapped = report.apps.filter((app) => !Object.hasOwn(contract.apps.mappings, app));
|
|
13991
|
+
if (unmapped.length > 0) fail(`selected apps have no contract mapping: ${unmapped.join(", ")}.`);
|
|
13992
|
+
assertAllowed(report.dependencies, contract.dependencies.allowed, "dependencies");
|
|
13993
|
+
assertAllowed(
|
|
13994
|
+
report.managedRuntime.services,
|
|
13995
|
+
contract.managedRuntime.services.allowed,
|
|
13996
|
+
"managedRuntime.services"
|
|
13997
|
+
);
|
|
13998
|
+
assertExact(
|
|
13999
|
+
report.managedRuntime.processes,
|
|
14000
|
+
contract.managedRuntime.processes.exact,
|
|
14001
|
+
"managedRuntime.processes"
|
|
14002
|
+
);
|
|
14003
|
+
const bindingValues = /* @__PURE__ */ new Map();
|
|
14004
|
+
const bindingSeen = /* @__PURE__ */ new Map();
|
|
14005
|
+
for (const app of report.apps) {
|
|
14006
|
+
if (!Object.hasOwn(contract.apps.mappings, app)) {
|
|
14007
|
+
fail(`selected app '${app}' has no contract mapping.`);
|
|
14008
|
+
}
|
|
14009
|
+
const mapping = contract.apps.mappings[app];
|
|
14010
|
+
if (!mapping) fail(`selected app '${app}' has no contract mapping.`);
|
|
14011
|
+
for (const key of Object.keys(mapping.bindings).sort()) {
|
|
14012
|
+
const values = bindingValues.get(key) ?? [];
|
|
14013
|
+
const seen = bindingSeen.get(key) ?? /* @__PURE__ */ new Set();
|
|
14014
|
+
for (const value of mapping.bindings[key] ?? []) {
|
|
14015
|
+
if (seen.has(value)) continue;
|
|
14016
|
+
values.push(value);
|
|
14017
|
+
seen.add(value);
|
|
14018
|
+
}
|
|
14019
|
+
bindingValues.set(key, values);
|
|
14020
|
+
bindingSeen.set(key, seen);
|
|
14021
|
+
}
|
|
14022
|
+
}
|
|
14023
|
+
const bindings = Object.fromEntries(
|
|
14024
|
+
Array.from(bindingValues.keys()).sort().map((key) => [key, bindingValues.get(key) ?? []])
|
|
14025
|
+
);
|
|
14026
|
+
return { ...report, contractPath, bindings };
|
|
14027
|
+
}
|
|
14028
|
+
function loadContract(repoPath, contractPath) {
|
|
14029
|
+
if (import_node_path31.default.isAbsolute(contractPath)) {
|
|
14030
|
+
fail("profile plan contract path must be relative to the repository.");
|
|
14031
|
+
}
|
|
14032
|
+
const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
|
|
14033
|
+
let metadata;
|
|
14034
|
+
try {
|
|
14035
|
+
metadata = import_node_fs30.default.lstatSync(absolutePath);
|
|
14036
|
+
} catch (error) {
|
|
14037
|
+
fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
|
|
14038
|
+
}
|
|
14039
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
14040
|
+
fail(`profile plan contract '${contractPath}' must be a regular file, not a symlink.`);
|
|
14041
|
+
}
|
|
14042
|
+
if (metadata.size > MAX_CONTRACT_BYTES) {
|
|
14043
|
+
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14044
|
+
}
|
|
14045
|
+
const realRepo = import_node_fs30.default.realpathSync(repoPath);
|
|
14046
|
+
const realContract = import_node_fs30.default.realpathSync(absolutePath);
|
|
14047
|
+
assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
|
|
14048
|
+
const source = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
|
|
14049
|
+
if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
|
|
14050
|
+
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14051
|
+
}
|
|
14052
|
+
return {
|
|
14053
|
+
contract: parseProfilePlanContract(source),
|
|
14054
|
+
contractPath: import_node_path31.default.relative(repoPath, absolutePath).split(import_node_path31.default.sep).join("/")
|
|
14055
|
+
};
|
|
14056
|
+
}
|
|
14057
|
+
function resolveProfilePlan(options) {
|
|
14058
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
14059
|
+
const config = loadRepoConfig(repoPath);
|
|
14060
|
+
const loaded = loadContract(repoPath, options.contract);
|
|
14061
|
+
return buildProfilePlanReport({
|
|
14062
|
+
report: buildProfileResolutionReport(config, repoPath, options.profile),
|
|
14063
|
+
contract: loaded.contract,
|
|
14064
|
+
registry: registryFor(config),
|
|
14065
|
+
contractPath: loaded.contractPath
|
|
14066
|
+
});
|
|
14067
|
+
}
|
|
14068
|
+
var import_node_fs30, import_node_path31, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
|
|
14069
|
+
var init_profile_plan = __esm({
|
|
14070
|
+
"src/core/profile-plan.ts"() {
|
|
14071
|
+
"use strict";
|
|
14072
|
+
import_node_fs30 = __toESM(require("fs"));
|
|
14073
|
+
import_node_path31 = __toESM(require("path"));
|
|
14074
|
+
import_yaml7 = require("yaml");
|
|
14075
|
+
init_paths();
|
|
14076
|
+
init_profile_resolution();
|
|
14077
|
+
init_repo_config();
|
|
14078
|
+
CONTRACT_VERSION = 1;
|
|
14079
|
+
MAX_CONTRACT_BYTES = 1024 * 1024;
|
|
14080
|
+
MAX_APP_MAPPINGS = 256;
|
|
14081
|
+
MAX_BINDING_KEYS = 64;
|
|
14082
|
+
MAX_BINDING_VALUES = 4096;
|
|
14083
|
+
MAX_LITERAL_LENGTH = 4096;
|
|
14084
|
+
BINDING_KEY = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
14085
|
+
}
|
|
14086
|
+
});
|
|
14087
|
+
|
|
13238
14088
|
// src/commands/profile.ts
|
|
13239
14089
|
var profile_exports = {};
|
|
13240
14090
|
__export(profile_exports, {
|
|
14091
|
+
runProfilePlanCommand: () => runProfilePlanCommand,
|
|
13241
14092
|
runProfileResolveCommand: () => runProfileResolveCommand
|
|
13242
14093
|
});
|
|
13243
14094
|
async function runProfileResolveCommand(options) {
|
|
@@ -13248,6 +14099,18 @@ async function runProfileResolveCommand(options) {
|
|
|
13248
14099
|
}
|
|
13249
14100
|
printProfileResolutionSummary(report);
|
|
13250
14101
|
}
|
|
14102
|
+
async function runProfilePlanCommand(options) {
|
|
14103
|
+
const report = resolveProfilePlan(options);
|
|
14104
|
+
if (options.output) {
|
|
14105
|
+
writeFileAtomically(import_node_path32.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
|
|
14106
|
+
`);
|
|
14107
|
+
}
|
|
14108
|
+
if (options.json) {
|
|
14109
|
+
printJSON(report);
|
|
14110
|
+
return;
|
|
14111
|
+
}
|
|
14112
|
+
printProfilePlanSummary(report, options.output);
|
|
14113
|
+
}
|
|
13251
14114
|
function renderValues(values) {
|
|
13252
14115
|
return values.join(", ") || "-";
|
|
13253
14116
|
}
|
|
@@ -13267,10 +14130,25 @@ function printProfileResolutionSummary(report) {
|
|
|
13267
14130
|
process.stdout.write(`Managed processes: ${renderValues(report.managedRuntime.processes)}
|
|
13268
14131
|
`);
|
|
13269
14132
|
}
|
|
14133
|
+
function printProfilePlanSummary(report, output2) {
|
|
14134
|
+
printProfileResolutionSummary(report);
|
|
14135
|
+
process.stdout.write(`Contract: ${report.contractPath}
|
|
14136
|
+
`);
|
|
14137
|
+
for (const key of Object.keys(report.bindings).sort()) {
|
|
14138
|
+
process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
|
|
14139
|
+
`);
|
|
14140
|
+
}
|
|
14141
|
+
if (output2) process.stdout.write(`Output: ${import_node_path32.default.resolve(output2)}
|
|
14142
|
+
`);
|
|
14143
|
+
}
|
|
14144
|
+
var import_node_path32;
|
|
13270
14145
|
var init_profile = __esm({
|
|
13271
14146
|
"src/commands/profile.ts"() {
|
|
13272
14147
|
"use strict";
|
|
14148
|
+
import_node_path32 = __toESM(require("path"));
|
|
14149
|
+
init_atomic_file();
|
|
13273
14150
|
init_output();
|
|
14151
|
+
init_profile_plan();
|
|
13274
14152
|
init_profile_resolution();
|
|
13275
14153
|
}
|
|
13276
14154
|
});
|
|
@@ -13557,7 +14435,7 @@ function sanitizeRouterId(value) {
|
|
|
13557
14435
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
13558
14436
|
}
|
|
13559
14437
|
function repoHash(repoPath) {
|
|
13560
|
-
return (0, import_node_crypto10.createHash)("sha1").update(
|
|
14438
|
+
return (0, import_node_crypto10.createHash)("sha1").update(import_node_path33.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
13561
14439
|
}
|
|
13562
14440
|
function asDockerApp(app) {
|
|
13563
14441
|
return app.runtime === "docker";
|
|
@@ -13636,11 +14514,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
13636
14514
|
if (dockerApps.length === 0) {
|
|
13637
14515
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
13638
14516
|
}
|
|
13639
|
-
const cachePath =
|
|
13640
|
-
|
|
13641
|
-
const overlayPath =
|
|
14517
|
+
const cachePath = import_node_path33.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
14518
|
+
import_node_fs31.default.mkdirSync(cachePath, { recursive: true });
|
|
14519
|
+
const overlayPath = import_node_path33.default.join(cachePath, "compose.devrouter.yml");
|
|
13642
14520
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
13643
|
-
|
|
14521
|
+
import_node_fs31.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
13644
14522
|
return {
|
|
13645
14523
|
overlayPath,
|
|
13646
14524
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -13760,15 +14638,15 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
13760
14638
|
const port = Number(match[1]);
|
|
13761
14639
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
13762
14640
|
}
|
|
13763
|
-
var import_node_child_process27, import_node_crypto10,
|
|
14641
|
+
var import_node_child_process27, import_node_crypto10, import_node_fs31, import_node_path33, import_yaml8;
|
|
13764
14642
|
var init_docker_run = __esm({
|
|
13765
14643
|
"src/core/docker-run.ts"() {
|
|
13766
14644
|
"use strict";
|
|
13767
14645
|
import_node_child_process27 = require("child_process");
|
|
13768
14646
|
import_node_crypto10 = require("crypto");
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
14647
|
+
import_node_fs31 = __toESM(require("fs"));
|
|
14648
|
+
import_node_path33 = __toESM(require("path"));
|
|
14649
|
+
import_yaml8 = __toESM(require("yaml"));
|
|
13772
14650
|
init_docker_error_guidance();
|
|
13773
14651
|
init_paths();
|
|
13774
14652
|
init_router();
|
|
@@ -14589,13 +15467,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14589
15467
|
const startedAt = Date.now();
|
|
14590
15468
|
let rootStat;
|
|
14591
15469
|
try {
|
|
14592
|
-
rootStat =
|
|
15470
|
+
rootStat = import_node_fs32.default.lstatSync(worktreePath);
|
|
14593
15471
|
} catch (error) {
|
|
14594
15472
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
14595
15473
|
}
|
|
14596
15474
|
let rootEntries;
|
|
14597
15475
|
try {
|
|
14598
|
-
rootEntries =
|
|
15476
|
+
rootEntries = import_node_fs32.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
14599
15477
|
} catch (error) {
|
|
14600
15478
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
14601
15479
|
}
|
|
@@ -14621,10 +15499,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14621
15499
|
timedOut = true;
|
|
14622
15500
|
break;
|
|
14623
15501
|
}
|
|
14624
|
-
const entryPath =
|
|
15502
|
+
const entryPath = import_node_path34.default.join(dirPath, entry.name);
|
|
14625
15503
|
let entryStat;
|
|
14626
15504
|
try {
|
|
14627
|
-
entryStat =
|
|
15505
|
+
entryStat = import_node_fs32.default.lstatSync(entryPath);
|
|
14628
15506
|
} catch (error) {
|
|
14629
15507
|
if (error?.code === "ENOENT") continue;
|
|
14630
15508
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -14634,7 +15512,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14634
15512
|
if (!entryStat.isDirectory()) continue;
|
|
14635
15513
|
let childEntries;
|
|
14636
15514
|
try {
|
|
14637
|
-
childEntries =
|
|
15515
|
+
childEntries = import_node_fs32.default.readdirSync(entryPath, { withFileTypes: true });
|
|
14638
15516
|
} catch (error) {
|
|
14639
15517
|
unreadableReason = describeIncompleteWalk(error);
|
|
14640
15518
|
break;
|
|
@@ -14698,12 +15576,12 @@ function describeError(error, worktreePath) {
|
|
|
14698
15576
|
}
|
|
14699
15577
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
14700
15578
|
}
|
|
14701
|
-
var
|
|
15579
|
+
var import_node_fs32, import_node_path34, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
14702
15580
|
var init_workspace_consumption = __esm({
|
|
14703
15581
|
"src/core/workspace-consumption.ts"() {
|
|
14704
15582
|
"use strict";
|
|
14705
|
-
|
|
14706
|
-
|
|
15583
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
15584
|
+
import_node_path34 = __toESM(require("path"));
|
|
14707
15585
|
init_devpod_environment();
|
|
14708
15586
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
14709
15587
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -14711,7 +15589,7 @@ var init_workspace_consumption = __esm({
|
|
|
14711
15589
|
});
|
|
14712
15590
|
|
|
14713
15591
|
// src/core/workspace-cleanup.ts
|
|
14714
|
-
function
|
|
15592
|
+
function isRecord4(value) {
|
|
14715
15593
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14716
15594
|
}
|
|
14717
15595
|
function isSha(value) {
|
|
@@ -14804,7 +15682,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
14804
15682
|
}
|
|
14805
15683
|
function readGitSnapshot(worktree, commandRunner) {
|
|
14806
15684
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
14807
|
-
if (worktree.prunable || !
|
|
15685
|
+
if (worktree.prunable || !import_node_fs33.default.existsSync(comparablePath)) {
|
|
14808
15686
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
14809
15687
|
}
|
|
14810
15688
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -14855,14 +15733,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
14855
15733
|
if (!Array.isArray(value)) return void 0;
|
|
14856
15734
|
const changes = [];
|
|
14857
15735
|
for (const item of value) {
|
|
14858
|
-
if (!
|
|
14859
|
-
const repository =
|
|
14860
|
-
const headRepository =
|
|
15736
|
+
if (!isRecord4(item)) return void 0;
|
|
15737
|
+
const repository = isRecord4(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
15738
|
+
const headRepository = isRecord4(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
14861
15739
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
14862
15740
|
continue;
|
|
14863
15741
|
}
|
|
14864
15742
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
14865
|
-
const mergeCommit =
|
|
15743
|
+
const mergeCommit = isRecord4(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
14866
15744
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
14867
15745
|
changes.push({
|
|
14868
15746
|
sourceBranch: branch,
|
|
@@ -14879,14 +15757,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
14879
15757
|
if (!Array.isArray(value)) return void 0;
|
|
14880
15758
|
const changes = [];
|
|
14881
15759
|
for (const item of value) {
|
|
14882
|
-
if (!
|
|
15760
|
+
if (!isRecord4(item)) return void 0;
|
|
14883
15761
|
const sourceProjectId = item.source_project_id;
|
|
14884
15762
|
const targetProjectId = item.target_project_id;
|
|
14885
15763
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
14886
15764
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
14887
15765
|
continue;
|
|
14888
15766
|
}
|
|
14889
|
-
const diffRefs =
|
|
15767
|
+
const diffRefs = isRecord4(item.diff_refs) ? item.diff_refs : void 0;
|
|
14890
15768
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
14891
15769
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
14892
15770
|
changes.push({
|
|
@@ -15329,7 +16207,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
15329
16207
|
containers = measureContainersFn(worktreePaths);
|
|
15330
16208
|
} catch (error) {
|
|
15331
16209
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
15332
|
-
containers = new Map(worktreePaths.map((
|
|
16210
|
+
containers = new Map(worktreePaths.map((path34) => [path34, unknownContainers(reason)]));
|
|
15333
16211
|
}
|
|
15334
16212
|
}
|
|
15335
16213
|
const rows = records.map((record) => {
|
|
@@ -15378,12 +16256,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
15378
16256
|
workspaces: rows
|
|
15379
16257
|
};
|
|
15380
16258
|
}
|
|
15381
|
-
var import_node_child_process29,
|
|
16259
|
+
var import_node_child_process29, import_node_fs33, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
15382
16260
|
var init_workspace_cleanup = __esm({
|
|
15383
16261
|
"src/core/workspace-cleanup.ts"() {
|
|
15384
16262
|
"use strict";
|
|
15385
16263
|
import_node_child_process29 = require("child_process");
|
|
15386
|
-
|
|
16264
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
15387
16265
|
init_devpod_workspaces();
|
|
15388
16266
|
init_host_routes();
|
|
15389
16267
|
init_repo_config();
|
|
@@ -15560,7 +16438,7 @@ var init_version = __esm({
|
|
|
15560
16438
|
|
|
15561
16439
|
// src/cli.ts
|
|
15562
16440
|
var import_commander = require("commander");
|
|
15563
|
-
var CLI_VERSION = true ? "0.0.
|
|
16441
|
+
var CLI_VERSION = true ? "0.0.52" : "0.0.0-dev";
|
|
15564
16442
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
15565
16443
|
function withErrorHandling(action2) {
|
|
15566
16444
|
return async (...args) => {
|
|
@@ -15615,13 +16493,14 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
|
|
|
15615
16493
|
await runUpCommand2();
|
|
15616
16494
|
})
|
|
15617
16495
|
);
|
|
15618
|
-
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(
|
|
16496
|
+
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("--repair", "Repair a degraded managed runtime from its recorded profile").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
15619
16497
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
15620
16498
|
const options = command.opts();
|
|
15621
16499
|
const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
|
|
15622
16500
|
await runEnsureCommand2({
|
|
15623
16501
|
path: repoPath,
|
|
15624
16502
|
profile: options.profile,
|
|
16503
|
+
repair: options.repair,
|
|
15625
16504
|
open: Boolean(options.open),
|
|
15626
16505
|
json: Boolean(options.json)
|
|
15627
16506
|
});
|
|
@@ -15728,6 +16607,17 @@ profileCommand.command("resolve").description("Print the exact apps, dependencie
|
|
|
15728
16607
|
await runProfileResolveCommand2(options);
|
|
15729
16608
|
})
|
|
15730
16609
|
);
|
|
16610
|
+
profileCommand.command("plan").description("Validate a profile against a repository contract and emit literal bindings").option("--repo <path>", "Repository path (defaults to current directory)").option(
|
|
16611
|
+
"--profile <name>",
|
|
16612
|
+
"Profile name or comma-separated selection (defaults to configured profile)"
|
|
16613
|
+
).requiredOption("--contract <path>", "Repository-relative profile plan contract").option("--output <path>", "Atomically write the JSON plan with mode 0600").option("--json", "Output stable JSON").action(
|
|
16614
|
+
withErrorHandling(
|
|
16615
|
+
async (options) => {
|
|
16616
|
+
const { runProfilePlanCommand: runProfilePlanCommand2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
16617
|
+
await runProfilePlanCommand2(options);
|
|
16618
|
+
}
|
|
16619
|
+
)
|
|
16620
|
+
);
|
|
15731
16621
|
var appCommand = program.command("app").description("Manage app entries and runtime actions from `.devrouter.yml`");
|
|
15732
16622
|
appCommand.command("add").description("Add or update one app definition in `.devrouter.yml`").requiredOption("--name <name>", "App name").option("--kind <kind>", "app or dependency", "app").option("--host <host>", "Hostname ending with .localhost (required for --kind app)").option("--protocol <protocol>", "http or tcp (required for --kind app)").option(
|
|
15733
16623
|
"--runtime <runtime>",
|
|
@@ -15832,7 +16722,7 @@ workspaceCommand2.command("up").description(
|
|
|
15832
16722
|
);
|
|
15833
16723
|
workspaceCommand2.command("ensure").description(
|
|
15834
16724
|
"Start and prove a primary or linked checkout's workspace runtime, upstreams, and routes"
|
|
15835
|
-
).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(
|
|
16725
|
+
).argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--repair", "Repair a degraded managed runtime from its recorded profile").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
15836
16726
|
withErrorHandling(
|
|
15837
16727
|
async (worktreePath, _options, command) => {
|
|
15838
16728
|
const options = command.opts();
|
|
@@ -15840,6 +16730,7 @@ workspaceCommand2.command("ensure").description(
|
|
|
15840
16730
|
await runEnsureCommand2({
|
|
15841
16731
|
path: worktreePath,
|
|
15842
16732
|
profile: options.profile,
|
|
16733
|
+
repair: options.repair,
|
|
15843
16734
|
open: Boolean(options.open),
|
|
15844
16735
|
json: Boolean(options.json)
|
|
15845
16736
|
});
|