@devrouter/cli 0.0.51 → 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/bin/devrouter-process +4 -4
- package/dist/devrouter.js +600 -61
- package/package.json +1 -1
- package/upgrade-prompts/0.0.52.md +34 -0
package/bin/devrouter-process
CHANGED
|
@@ -187,9 +187,9 @@ process_owned() {
|
|
|
187
187
|
[ "$actual_pgid" = "$pgid" ] || return 1
|
|
188
188
|
[ "$pgid" = "$pid" ] || return 1
|
|
189
189
|
tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null |
|
|
190
|
-
grep -
|
|
190
|
+
grep -Fx "DEVROUTER_PROCESS_NAME=$name" >/dev/null || return 1
|
|
191
191
|
tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null |
|
|
192
|
-
grep -
|
|
192
|
+
grep -Fx "DEVROUTER_PROCESS_FINGERPRINT=$expected_fingerprint" >/dev/null
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
process_group_alive() {
|
|
@@ -217,7 +217,7 @@ is_helper_process() {
|
|
|
217
217
|
local candidate="$1"
|
|
218
218
|
|
|
219
219
|
[ -r "/proc/$candidate/cmdline" ] || return 1
|
|
220
|
-
tr '\0' '\n' <"/proc/$candidate/cmdline" 2>/dev/null | grep -
|
|
220
|
+
tr '\0' '\n' <"/proc/$candidate/cmdline" 2>/dev/null | grep -Fx "$0" >/dev/null
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
marked_process_exists() {
|
|
@@ -230,7 +230,7 @@ marked_process_exists() {
|
|
|
230
230
|
pid="${pid%/environ}"
|
|
231
231
|
process_alive "$pid" || continue
|
|
232
232
|
is_self_or_ancestor "$pid" && continue
|
|
233
|
-
if tr '\0' '\n' <"$environ" 2>/dev/null | grep -
|
|
233
|
+
if tr '\0' '\n' <"$environ" 2>/dev/null | grep -Fx "DEVROUTER_PROCESS_NAME=$name" >/dev/null; then
|
|
234
234
|
return 0
|
|
235
235
|
fi
|
|
236
236
|
done
|
package/dist/devrouter.js
CHANGED
|
@@ -400,7 +400,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
400
400
|
- \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
|
|
401
401
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
402
402
|
- \`devrouter setup --yes [--repo .] [--json] [--workspace-runtime <devpod|devsy>]\`: first-run machine setup plus structured diagnostics; explicit Devsy selection acquires its verified agent
|
|
403
|
-
- \`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
|
|
404
404
|
- \`devrouter profile resolve --repo <path> [--profile <selection>] [--json]\`: resolve exact profile resources for automation without starting or inspecting a runtime
|
|
405
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
|
|
406
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
|
|
@@ -425,7 +425,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
425
425
|
- \`devrouter app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
|
|
426
426
|
- \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
|
|
427
427
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
428
|
-
- \`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\`
|
|
429
429
|
- \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
|
|
430
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
|
|
431
431
|
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
@@ -2691,7 +2691,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2691
2691
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2692
2692
|
const requiredVersion = config.devrouter?.version;
|
|
2693
2693
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2694
|
-
const cliVersion = true ? "0.0.
|
|
2694
|
+
const cliVersion = true ? "0.0.52" : "0.0.0-dev";
|
|
2695
2695
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2696
2696
|
hasWarnedVersionMismatch = true;
|
|
2697
2697
|
process.stderr.write(
|
|
@@ -3273,6 +3273,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3273
3273
|
"- devrouter repo devcontainer write --repo <REPO_PATH> --yes",
|
|
3274
3274
|
"- devrouter repo devcontainer verify --repo <REPO_PATH> --json",
|
|
3275
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.",
|
|
3276
3277
|
"- Managed selective startup: devrouter ensure <REPO_PATH> --profile <name> --json",
|
|
3277
3278
|
"- Container seed/migration: devrouter exec <REPO_PATH> -- <command...>",
|
|
3278
3279
|
"- Compatibility verification only after ensure: devrouter repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
|
|
@@ -3391,7 +3392,7 @@ var init_ai_prompt = __esm({
|
|
|
3391
3392
|
purpose: "Write/update devrouter section in the repo's AGENTS.md and install the devrouter skill."
|
|
3392
3393
|
},
|
|
3393
3394
|
{
|
|
3394
|
-
command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
|
|
3395
|
+
command: "devrouter ensure [path] [--profile <name>] [--repair] [--open] [--json]",
|
|
3395
3396
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
3396
3397
|
},
|
|
3397
3398
|
{
|
|
@@ -5691,19 +5692,12 @@ function assertSafeContainerId(containerId) {
|
|
|
5691
5692
|
throw new Error(`Container '${containerId}' is not a safe exact identifier.`);
|
|
5692
5693
|
}
|
|
5693
5694
|
}
|
|
5694
|
-
function
|
|
5695
|
-
if (options.services.length === 0) return;
|
|
5696
|
-
const project = safeComposeProject(options.composeProject);
|
|
5697
|
-
const services = Array.from(new Set(options.services));
|
|
5698
|
-
if (services.some((service) => !options.plan.composeServices.includes(service))) {
|
|
5699
|
-
throw new Error("Exact Compose startup contains a service outside the validated model.");
|
|
5700
|
-
}
|
|
5701
|
-
const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
5695
|
+
function managedComposeEnvironment(workspace) {
|
|
5702
5696
|
const env = { ...process.env };
|
|
5703
|
-
if (
|
|
5704
|
-
env.WORKSPACE =
|
|
5705
|
-
env.DEVROUTER_WORKSPACE =
|
|
5706
|
-
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;
|
|
5707
5701
|
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
5708
5702
|
} else {
|
|
5709
5703
|
delete env.WORKSPACE;
|
|
@@ -5711,6 +5705,87 @@ function startExactManagedServices(options) {
|
|
|
5711
5705
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
5712
5706
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
5713
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);
|
|
5714
5789
|
const result = (0, import_node_child_process7.spawnSync)(
|
|
5715
5790
|
"docker",
|
|
5716
5791
|
[
|
|
@@ -5749,7 +5824,7 @@ function stopExactManagedService(containerId, service) {
|
|
|
5749
5824
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5750
5825
|
}
|
|
5751
5826
|
}
|
|
5752
|
-
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;
|
|
5753
5828
|
var init_devcontainer_profile = __esm({
|
|
5754
5829
|
"src/core/devcontainer-profile.ts"() {
|
|
5755
5830
|
"use strict";
|
|
@@ -5761,8 +5836,10 @@ var init_devcontainer_profile = __esm({
|
|
|
5761
5836
|
init_atomic_file();
|
|
5762
5837
|
init_devcontainer_config();
|
|
5763
5838
|
init_paths();
|
|
5839
|
+
init_workspace();
|
|
5764
5840
|
MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
|
|
5765
5841
|
MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
|
|
5842
|
+
COMPOSE_HASH_TIMEOUT_MS = 1e4;
|
|
5766
5843
|
}
|
|
5767
5844
|
});
|
|
5768
5845
|
|
|
@@ -5857,7 +5934,7 @@ var init_devpod_environment = __esm({
|
|
|
5857
5934
|
import_node_child_process8 = require("child_process");
|
|
5858
5935
|
import_node_path16 = __toESM(require("path"));
|
|
5859
5936
|
init_workspace();
|
|
5860
|
-
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}}}';
|
|
5861
5938
|
SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
|
|
5862
5939
|
/}$/,
|
|
5863
5940
|
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
@@ -6172,7 +6249,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
6172
6249
|
let state;
|
|
6173
6250
|
let stateReadFailed = false;
|
|
6174
6251
|
try {
|
|
6175
|
-
state = readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6252
|
+
state = options.candidateState ?? readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6176
6253
|
} catch {
|
|
6177
6254
|
stateReadFailed = true;
|
|
6178
6255
|
}
|
|
@@ -8054,6 +8131,12 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
8054
8131
|
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
8055
8132
|
};
|
|
8056
8133
|
}
|
|
8134
|
+
function recordPath(repoPath, workspace) {
|
|
8135
|
+
return import_node_path22.default.join(
|
|
8136
|
+
ownershipDirectory(repoPath),
|
|
8137
|
+
`${validateWorkspace(workspace, "workspace")}.json`
|
|
8138
|
+
);
|
|
8139
|
+
}
|
|
8057
8140
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
8058
8141
|
let parsed;
|
|
8059
8142
|
try {
|
|
@@ -8066,6 +8149,15 @@ function readRecordFile(filePath, expectedWorkspace) {
|
|
|
8066
8149
|
}
|
|
8067
8150
|
return validateRecord(parsed, expectedWorkspace);
|
|
8068
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
|
+
}
|
|
8069
8161
|
function listWorkspaceOwnership(repoPath) {
|
|
8070
8162
|
const directory = ownershipDirectory(repoPath);
|
|
8071
8163
|
return listWorkspaceOwnershipInDirectory(directory);
|
|
@@ -9015,7 +9107,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
9015
9107
|
const config = runtimeConfig.config;
|
|
9016
9108
|
loadedConfig = config;
|
|
9017
9109
|
loadedWorkspace = runtimeConfig.workspace;
|
|
9018
|
-
const cliVersion = true ? "0.0.
|
|
9110
|
+
const cliVersion = true ? "0.0.52" : "0.0.0-dev";
|
|
9019
9111
|
const configVersion = config.devrouter?.version;
|
|
9020
9112
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
9021
9113
|
addCheck(checks, {
|
|
@@ -9727,9 +9819,9 @@ async function ensureRouteInfrastructure(apps, options = {}) {
|
|
|
9727
9819
|
startRouterStack();
|
|
9728
9820
|
return tlsCoverage;
|
|
9729
9821
|
}
|
|
9730
|
-
async function replacePublishedProxyRoutes(repoPath, config, workspace) {
|
|
9822
|
+
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
9731
9823
|
const apps = proxyAppsFromConfig(config);
|
|
9732
|
-
const tlsCoverage = await ensureRouteInfrastructure(apps, { repoPath });
|
|
9824
|
+
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
9733
9825
|
const routes = apps.map((app) => {
|
|
9734
9826
|
const upstream = parseUpstream(app.upstream);
|
|
9735
9827
|
return {
|
|
@@ -9769,7 +9861,7 @@ function inspectRouterApi(protocol) {
|
|
|
9769
9861
|
"--show-error",
|
|
9770
9862
|
"--fail",
|
|
9771
9863
|
"--max-time",
|
|
9772
|
-
|
|
9864
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
9773
9865
|
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
9774
9866
|
],
|
|
9775
9867
|
{ encoding: "utf-8" }
|
|
@@ -9801,6 +9893,161 @@ function inspectRouterApi(protocol) {
|
|
|
9801
9893
|
)
|
|
9802
9894
|
};
|
|
9803
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
|
+
}
|
|
9804
10051
|
function inspectExpectedRoutes(routes, expectation) {
|
|
9805
10052
|
const mismatched = [];
|
|
9806
10053
|
const failures = [];
|
|
@@ -9857,6 +10104,10 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9857
10104
|
pollIntervalMs
|
|
9858
10105
|
);
|
|
9859
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
|
+
);
|
|
9860
10111
|
import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
9861
10112
|
return withFileLock(
|
|
9862
10113
|
ROUTER_RELOAD_LOCK_FILE,
|
|
@@ -9887,10 +10138,21 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9887
10138
|
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
9888
10139
|
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
9889
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
|
+
}
|
|
9890
10152
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
9891
10153
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
9892
10154
|
}
|
|
9893
|
-
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;
|
|
9894
10156
|
var init_traefik_route_health = __esm({
|
|
9895
10157
|
"src/core/traefik-route-health.ts"() {
|
|
9896
10158
|
"use strict";
|
|
@@ -9902,6 +10164,7 @@ var init_traefik_route_health = __esm({
|
|
|
9902
10164
|
init_router();
|
|
9903
10165
|
ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
9904
10166
|
ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
10167
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
9905
10168
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
9906
10169
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
9907
10170
|
DEFAULT_POLL_INTERVAL_MS = 250;
|
|
@@ -10165,7 +10428,7 @@ function validateWorkspaceContainers(containers, options) {
|
|
|
10165
10428
|
if (options.target.kind === "linked") {
|
|
10166
10429
|
assertOverlay(appContainer, options.repoPath);
|
|
10167
10430
|
}
|
|
10168
|
-
assertReady(appContainer, "Workspace app");
|
|
10431
|
+
if (!options.allowStopped) assertReady(appContainer, "Workspace app");
|
|
10169
10432
|
if (options.target.kind === "linked") {
|
|
10170
10433
|
const gitCommonDir = options.target.gitCommonDir;
|
|
10171
10434
|
const gitMount = appContainer.mounts.find(
|
|
@@ -10317,6 +10580,142 @@ function resolveLinkedTarget(repoPath) {
|
|
|
10317
10580
|
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
10318
10581
|
};
|
|
10319
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
|
+
}
|
|
10320
10719
|
function resolvePrimaryTarget(repoPath) {
|
|
10321
10720
|
const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
10322
10721
|
return {
|
|
@@ -10358,7 +10757,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10358
10757
|
);
|
|
10359
10758
|
}
|
|
10360
10759
|
}
|
|
10361
|
-
|
|
10760
|
+
const ensureLocked = async () => {
|
|
10761
|
+
let repairMutationStarted = false;
|
|
10762
|
+
let retainedRepairContainers = [];
|
|
10362
10763
|
let environmentStarted = false;
|
|
10363
10764
|
let managedPlan;
|
|
10364
10765
|
let previousManagedState;
|
|
@@ -10376,10 +10777,11 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10376
10777
|
let firstTransitionBaseline;
|
|
10377
10778
|
const routeLoadOptions = {
|
|
10378
10779
|
initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
|
|
10379
|
-
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 } : {}
|
|
10380
10782
|
};
|
|
10381
10783
|
try {
|
|
10382
|
-
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10784
|
+
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10383
10785
|
let devpodId = target.devpodId;
|
|
10384
10786
|
if (target.kind === "linked") {
|
|
10385
10787
|
const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
@@ -10389,12 +10791,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10389
10791
|
}
|
|
10390
10792
|
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
10391
10793
|
managedPostStartPlan = managedPostStart;
|
|
10794
|
+
if (options.repair)
|
|
10795
|
+
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10392
10796
|
const runtime = loadRuntimeConfig(
|
|
10393
10797
|
repoPath,
|
|
10394
10798
|
target.kind === "primary" ? "" : target.workspace,
|
|
10395
|
-
options.profile
|
|
10799
|
+
options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
|
|
10396
10800
|
);
|
|
10397
10801
|
const managedRuntime = runtime.config.managedRuntime;
|
|
10802
|
+
if (options.repair && !managedRuntime)
|
|
10803
|
+
throw new Error("Repair requires managedRuntime configuration.");
|
|
10398
10804
|
previousRoutes = listHostRouteState().filter(
|
|
10399
10805
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
10400
10806
|
);
|
|
@@ -10403,12 +10809,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10403
10809
|
const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
|
|
10404
10810
|
if (managedRuntime) {
|
|
10405
10811
|
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10406
|
-
if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
10812
|
+
if (!options.repair && previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
10407
10813
|
previousManagedState = void 0;
|
|
10408
10814
|
}
|
|
10409
|
-
if (previousManagedState?.status === "degraded") {
|
|
10815
|
+
if (!options.repair && previousManagedState?.status === "degraded") {
|
|
10410
10816
|
throw new Error(
|
|
10411
|
-
"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."
|
|
10412
10818
|
);
|
|
10413
10819
|
}
|
|
10414
10820
|
if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
|
|
@@ -10462,7 +10868,20 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10462
10868
|
);
|
|
10463
10869
|
}
|
|
10464
10870
|
}
|
|
10465
|
-
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) {
|
|
10466
10885
|
writeManagedDevcontainerConfig(managedPlan);
|
|
10467
10886
|
managedConfigWritten = true;
|
|
10468
10887
|
}
|
|
@@ -10498,7 +10917,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10498
10917
|
let recreated = false;
|
|
10499
10918
|
let container;
|
|
10500
10919
|
try {
|
|
10501
|
-
await startAndProveAttachment();
|
|
10920
|
+
if (!options.repair) await startAndProveAttachment();
|
|
10502
10921
|
} catch (error) {
|
|
10503
10922
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10504
10923
|
throw error;
|
|
@@ -10508,7 +10927,45 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10508
10927
|
}
|
|
10509
10928
|
if (!container) {
|
|
10510
10929
|
try {
|
|
10511
|
-
|
|
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);
|
|
10512
10969
|
} catch (error) {
|
|
10513
10970
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10514
10971
|
throw error;
|
|
@@ -10549,13 +11006,18 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10549
11006
|
}
|
|
10550
11007
|
if (!matches[0]?.state.Running) missingServices.push(service);
|
|
10551
11008
|
}
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
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
|
+
});
|
|
10559
11021
|
await waitForManagedServices(
|
|
10560
11022
|
managedPlan,
|
|
10561
11023
|
repoPath,
|
|
@@ -10567,6 +11029,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10567
11029
|
container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
10568
11030
|
managedContainer = container;
|
|
10569
11031
|
}
|
|
11032
|
+
if (options.repair && managedPlan)
|
|
11033
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
10570
11034
|
transitionPhase = "process-start";
|
|
10571
11035
|
runManagedPostStart({
|
|
10572
11036
|
plan: managedPostStart,
|
|
@@ -10580,6 +11044,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10580
11044
|
const desiredProcessSet = new Set(desiredProcesses);
|
|
10581
11045
|
for (const process2 of processRegistry) {
|
|
10582
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
|
+
}
|
|
10583
11058
|
runManagedProcessAction({
|
|
10584
11059
|
container,
|
|
10585
11060
|
name: process2,
|
|
@@ -10599,7 +11074,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10599
11074
|
}
|
|
10600
11075
|
}
|
|
10601
11076
|
transitionPhase = "service-stop";
|
|
10602
|
-
|
|
11077
|
+
if (!options.repair)
|
|
11078
|
+
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
10603
11079
|
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
10604
11080
|
await waitForManagedServices(
|
|
10605
11081
|
managedPlan,
|
|
@@ -10614,11 +11090,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10614
11090
|
const publication = await replacePublishedProxyRoutes(
|
|
10615
11091
|
repoPath,
|
|
10616
11092
|
runtime.config,
|
|
10617
|
-
target.workspace
|
|
11093
|
+
target.workspace,
|
|
11094
|
+
options.repair ? { prepareInfrastructure: false } : void 0
|
|
10618
11095
|
);
|
|
10619
11096
|
candidateRoutes = publication.routes;
|
|
10620
11097
|
candidateRoutesPublished = true;
|
|
10621
|
-
await
|
|
11098
|
+
if (options.repair) await ensureTraefikRoutesMatch(publication.routes, routeLoadOptions);
|
|
11099
|
+
else await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
|
|
10622
11100
|
await ensureTraefikRoutesRemoved(
|
|
10623
11101
|
removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
|
|
10624
11102
|
routeLoadOptions
|
|
@@ -10665,8 +11143,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10665
11143
|
if (!devpodId) {
|
|
10666
11144
|
throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
|
|
10667
11145
|
}
|
|
11146
|
+
let candidateState;
|
|
10668
11147
|
if (managedPlan && managedComposeProject) {
|
|
10669
|
-
|
|
11148
|
+
candidateState = {
|
|
10670
11149
|
version: 1,
|
|
10671
11150
|
repoPath,
|
|
10672
11151
|
...target.workspace !== void 0 ? { workspace: target.workspace } : {},
|
|
@@ -10682,15 +11161,24 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10682
11161
|
effectiveConfigSha256: managedPlan.effectiveConfigSha256,
|
|
10683
11162
|
status: "ready",
|
|
10684
11163
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10685
|
-
}
|
|
11164
|
+
};
|
|
11165
|
+
if (!options.repair) writeManagedRuntimeState(candidateState);
|
|
10686
11166
|
}
|
|
10687
11167
|
const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
|
|
10688
11168
|
repoPath,
|
|
10689
11169
|
workspace: target.workspace,
|
|
10690
11170
|
config: runtime.config,
|
|
10691
11171
|
profile: runtime.profile,
|
|
10692
|
-
resolvedProfile: runtime.resolvedProfile
|
|
11172
|
+
resolvedProfile: runtime.resolvedProfile,
|
|
11173
|
+
...options.repair ? { candidateState } : {}
|
|
10693
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
|
+
}
|
|
10694
11182
|
return {
|
|
10695
11183
|
kind: target.kind,
|
|
10696
11184
|
repoPath,
|
|
@@ -10703,6 +11191,37 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10703
11191
|
...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
|
|
10704
11192
|
};
|
|
10705
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
|
+
}
|
|
10706
11225
|
if (managedPlan && managedContainer && managedComposeProject) {
|
|
10707
11226
|
const rollbackErrors = [];
|
|
10708
11227
|
const failedPhase = transitionPhase;
|
|
@@ -10904,6 +11423,22 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10904
11423
|
}
|
|
10905
11424
|
throw error;
|
|
10906
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
|
+
);
|
|
10907
11442
|
});
|
|
10908
11443
|
}
|
|
10909
11444
|
var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
@@ -10917,6 +11452,7 @@ var init_workspace_ensure = __esm({
|
|
|
10917
11452
|
init_devpod_environment();
|
|
10918
11453
|
init_devpod_mutation();
|
|
10919
11454
|
init_devpod_workspaces();
|
|
11455
|
+
init_file_lock();
|
|
10920
11456
|
init_host_routes();
|
|
10921
11457
|
init_http_route_probe();
|
|
10922
11458
|
init_managed_post_start();
|
|
@@ -10964,7 +11500,8 @@ async function runEnsureCommand(options) {
|
|
|
10964
11500
|
const result = await workspaceEnsure(repoPath, {
|
|
10965
11501
|
open: options.open,
|
|
10966
11502
|
quiet: Boolean(options.json),
|
|
10967
|
-
profile: options.profile
|
|
11503
|
+
profile: options.profile,
|
|
11504
|
+
...options.repair ? { repair: true } : {}
|
|
10968
11505
|
});
|
|
10969
11506
|
if (options.json) {
|
|
10970
11507
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -13268,11 +13805,11 @@ var init_profile_resolution = __esm({
|
|
|
13268
13805
|
function fail(message) {
|
|
13269
13806
|
throw new Error(message);
|
|
13270
13807
|
}
|
|
13271
|
-
function
|
|
13808
|
+
function isRecord3(value) {
|
|
13272
13809
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13273
13810
|
}
|
|
13274
13811
|
function requireRecord(value, label) {
|
|
13275
|
-
if (!
|
|
13812
|
+
if (!isRecord3(value)) fail(`${label} must be a mapping.`);
|
|
13276
13813
|
return value;
|
|
13277
13814
|
}
|
|
13278
13815
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -15052,7 +15589,7 @@ var init_workspace_consumption = __esm({
|
|
|
15052
15589
|
});
|
|
15053
15590
|
|
|
15054
15591
|
// src/core/workspace-cleanup.ts
|
|
15055
|
-
function
|
|
15592
|
+
function isRecord4(value) {
|
|
15056
15593
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15057
15594
|
}
|
|
15058
15595
|
function isSha(value) {
|
|
@@ -15196,14 +15733,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
15196
15733
|
if (!Array.isArray(value)) return void 0;
|
|
15197
15734
|
const changes = [];
|
|
15198
15735
|
for (const item of value) {
|
|
15199
|
-
if (!
|
|
15200
|
-
const repository =
|
|
15201
|
-
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;
|
|
15202
15739
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
15203
15740
|
continue;
|
|
15204
15741
|
}
|
|
15205
15742
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
15206
|
-
const mergeCommit =
|
|
15743
|
+
const mergeCommit = isRecord4(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
15207
15744
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
15208
15745
|
changes.push({
|
|
15209
15746
|
sourceBranch: branch,
|
|
@@ -15220,14 +15757,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
15220
15757
|
if (!Array.isArray(value)) return void 0;
|
|
15221
15758
|
const changes = [];
|
|
15222
15759
|
for (const item of value) {
|
|
15223
|
-
if (!
|
|
15760
|
+
if (!isRecord4(item)) return void 0;
|
|
15224
15761
|
const sourceProjectId = item.source_project_id;
|
|
15225
15762
|
const targetProjectId = item.target_project_id;
|
|
15226
15763
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
15227
15764
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
15228
15765
|
continue;
|
|
15229
15766
|
}
|
|
15230
|
-
const diffRefs =
|
|
15767
|
+
const diffRefs = isRecord4(item.diff_refs) ? item.diff_refs : void 0;
|
|
15231
15768
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
15232
15769
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
15233
15770
|
changes.push({
|
|
@@ -15901,7 +16438,7 @@ var init_version = __esm({
|
|
|
15901
16438
|
|
|
15902
16439
|
// src/cli.ts
|
|
15903
16440
|
var import_commander = require("commander");
|
|
15904
|
-
var CLI_VERSION = true ? "0.0.
|
|
16441
|
+
var CLI_VERSION = true ? "0.0.52" : "0.0.0-dev";
|
|
15905
16442
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
15906
16443
|
function withErrorHandling(action2) {
|
|
15907
16444
|
return async (...args) => {
|
|
@@ -15956,13 +16493,14 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
|
|
|
15956
16493
|
await runUpCommand2();
|
|
15957
16494
|
})
|
|
15958
16495
|
);
|
|
15959
|
-
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(
|
|
15960
16497
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
15961
16498
|
const options = command.opts();
|
|
15962
16499
|
const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
|
|
15963
16500
|
await runEnsureCommand2({
|
|
15964
16501
|
path: repoPath,
|
|
15965
16502
|
profile: options.profile,
|
|
16503
|
+
repair: options.repair,
|
|
15966
16504
|
open: Boolean(options.open),
|
|
15967
16505
|
json: Boolean(options.json)
|
|
15968
16506
|
});
|
|
@@ -16184,7 +16722,7 @@ workspaceCommand2.command("up").description(
|
|
|
16184
16722
|
);
|
|
16185
16723
|
workspaceCommand2.command("ensure").description(
|
|
16186
16724
|
"Start and prove a primary or linked checkout's workspace runtime, upstreams, and routes"
|
|
16187
|
-
).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(
|
|
16188
16726
|
withErrorHandling(
|
|
16189
16727
|
async (worktreePath, _options, command) => {
|
|
16190
16728
|
const options = command.opts();
|
|
@@ -16192,6 +16730,7 @@ workspaceCommand2.command("ensure").description(
|
|
|
16192
16730
|
await runEnsureCommand2({
|
|
16193
16731
|
path: worktreePath,
|
|
16194
16732
|
profile: options.profile,
|
|
16733
|
+
repair: options.repair,
|
|
16195
16734
|
open: Boolean(options.open),
|
|
16196
16735
|
json: Boolean(options.json)
|
|
16197
16736
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.52
|
|
2
|
+
|
|
3
|
+
Managed process ownership now handles large environments correctly. Explicit
|
|
4
|
+
`ensure --repair` recovers a retained degraded runtime without deleting its data
|
|
5
|
+
or invoking provider bootstrap. Ordinary `ensure` retains its degraded-state guard.
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
1. Install the published CLI with `npm install -g @devrouter/cli@0.0.52`.
|
|
10
|
+
2. Bump `devrouter.version` in the consumer `.devrouter.yml` to `0.0.52`.
|
|
11
|
+
3. For an approved exact workspace recovery, inspect status and run
|
|
12
|
+
`devrouter ensure <checkout> --repair --json`. Omit the profile to reuse the
|
|
13
|
+
recorded profile. Preserve required non-secret Compose interpolation settings.
|
|
14
|
+
4. Stop at ownership or configuration drift. Repair requires retained containers;
|
|
15
|
+
it never creates missing resources or changes the recorded profile.
|
|
16
|
+
|
|
17
|
+
## Validation
|
|
18
|
+
|
|
19
|
+
Verify installed/configured versions with `devrouter -V --repo <checkout>`.
|
|
20
|
+
Repair accepts a stopped primary only with every project container explicitly
|
|
21
|
+
stopped and no checkout routes. It starts retained IDs, including their existing
|
|
22
|
+
entrypoints, then replays the current repository adapter. Owned processes may
|
|
23
|
+
restart when fingerprints change. Failure can leave owned resources running.
|
|
24
|
+
|
|
25
|
+
Require zero command exit status, ready services/processes, exact active route
|
|
26
|
+
rules and targets, and no managed drift. Check the application separately.
|
|
27
|
+
No live recovery is implied by installing the CLI. No schema migration or manual
|
|
28
|
+
state-file edit is required.
|
|
29
|
+
|
|
30
|
+
## Report
|
|
31
|
+
|
|
32
|
+
Report installed/configured versions, exact checkout/profile, repair exit status,
|
|
33
|
+
managed state, application check, and remaining blockers. Distinguish source
|
|
34
|
+
checks from live runtime proof. Data deletion remains a separate explicit action.
|