@devrouter/cli 0.0.62 → 0.0.63
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/dist/devrouter-lifecycle-worker.js +1391 -1268
- package/dist/devrouter.js +355 -96
- package/package.json +1 -1
- package/upgrade-prompts/0.0.63.md +36 -0
package/dist/devrouter.js
CHANGED
|
@@ -2450,7 +2450,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs6.default.readF
|
|
|
2450
2450
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2451
2451
|
const requiredVersion = config.devrouter?.version;
|
|
2452
2452
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2453
|
-
const cliVersion = true ? "0.0.
|
|
2453
|
+
const cliVersion = true ? "0.0.63" : "0.0.0-dev";
|
|
2454
2454
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2455
2455
|
hasWarnedVersionMismatch = true;
|
|
2456
2456
|
process.stderr.write(
|
|
@@ -4634,6 +4634,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
|
4634
4634
|
assertUniqueContainerIds(ids);
|
|
4635
4635
|
return ids;
|
|
4636
4636
|
}
|
|
4637
|
+
function hasExactManagedStopMissingObjectError(stderr, id3) {
|
|
4638
|
+
if (typeof stderr !== "string") return false;
|
|
4639
|
+
const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
|
|
4640
|
+
return line.toLowerCase() === `error: no such object: ${id3}`;
|
|
4641
|
+
}
|
|
4642
|
+
function assertManagedStopContainersAbsent(endpoint, ids) {
|
|
4643
|
+
assertManagedStopEndpoint(endpoint);
|
|
4644
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
|
|
4645
|
+
throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
|
|
4646
|
+
}
|
|
4647
|
+
ids.forEach(assertFullContainerId);
|
|
4648
|
+
assertUniqueContainerIds(ids);
|
|
4649
|
+
const env = { ...process.env };
|
|
4650
|
+
delete env.DOCKER_CONTEXT;
|
|
4651
|
+
delete env.DOCKER_HOST;
|
|
4652
|
+
for (const id3 of ids) {
|
|
4653
|
+
const result = (0, import_node_child_process6.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
|
|
4654
|
+
env,
|
|
4655
|
+
encoding: "utf-8",
|
|
4656
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
4657
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
4658
|
+
});
|
|
4659
|
+
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id3)) {
|
|
4660
|
+
throw new Error("Managed stop Docker inspection did not prove container absence.");
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4637
4664
|
function stopPinnedManagedContainer(endpoint, containerId) {
|
|
4638
4665
|
assertFullContainerId(containerId);
|
|
4639
4666
|
runManagedStopDocker(["stop", containerId], endpoint, 3e4);
|
|
@@ -7142,6 +7169,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
7142
7169
|
"- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
|
|
7143
7170
|
"- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
|
|
7144
7171
|
"- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
|
|
7172
|
+
"- Busy exec waits up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
|
|
7145
7173
|
"- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not prevent OOM or activate capacity-managed recovery.",
|
|
7146
7174
|
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
|
|
7147
7175
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
@@ -8725,6 +8753,9 @@ var init_devcontainer_diagnostics = __esm({
|
|
|
8725
8753
|
});
|
|
8726
8754
|
|
|
8727
8755
|
// src/core/route-state.ts
|
|
8756
|
+
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
8757
|
+
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
8758
|
+
}
|
|
8728
8759
|
function listRoutesForWorktreePaths(worktreePaths) {
|
|
8729
8760
|
const routes = listHostRouteState();
|
|
8730
8761
|
const byWorktreePath = /* @__PURE__ */ new Map();
|
|
@@ -9584,11 +9615,13 @@ var init_status = __esm({
|
|
|
9584
9615
|
});
|
|
9585
9616
|
|
|
9586
9617
|
// src/core/devpod-registry.ts
|
|
9587
|
-
function listDevpodWorkspacesRaw() {
|
|
9618
|
+
function listDevpodWorkspacesRaw(options = {}) {
|
|
9588
9619
|
const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
|
|
9589
9620
|
encoding: "utf-8"
|
|
9590
9621
|
});
|
|
9591
|
-
if (result.
|
|
9622
|
+
if (options.allowMissingExecutable && result.error?.code === "ENOENT")
|
|
9623
|
+
return [];
|
|
9624
|
+
if (result.error || result.status !== 0) {
|
|
9592
9625
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
9593
9626
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
9594
9627
|
}
|
|
@@ -10954,7 +10987,56 @@ function proveRetainedManagedStop(state) {
|
|
|
10954
10987
|
}
|
|
10955
10988
|
return result;
|
|
10956
10989
|
}
|
|
10990
|
+
function absentRegistrationIdentity(state) {
|
|
10991
|
+
resetWorkspaceRuntimeCaches();
|
|
10992
|
+
if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
|
|
10993
|
+
throw new Error("Stop provider changed.");
|
|
10994
|
+
if (!state.workspace || !isLinkedWorktree(state.repoPath))
|
|
10995
|
+
throw new Error("Absent stop requires a linked workspace.");
|
|
10996
|
+
const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
|
|
10997
|
+
if (!record4 || record4.workspace !== state.workspace || record4.devpodId !== state.devpodId || !sameWorkspacePath(record4.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record4, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
|
|
10998
|
+
throw new Error("Absent stop workspace ownership changed.");
|
|
10999
|
+
for (const entries of [
|
|
11000
|
+
listDevsyWorkspaces(),
|
|
11001
|
+
listDevpodWorkspacesRaw({ allowMissingExecutable: true })
|
|
11002
|
+
]) {
|
|
11003
|
+
if (entries.some(
|
|
11004
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
11005
|
+
))
|
|
11006
|
+
throw new Error("Absent stop requires both provider registrations to remain absent.");
|
|
11007
|
+
}
|
|
11008
|
+
return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
|
|
11009
|
+
}
|
|
11010
|
+
function proveManagedStop(state) {
|
|
11011
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
11012
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
11013
|
+
listDevsyWorkspaces(),
|
|
11014
|
+
state.devpodId,
|
|
11015
|
+
state.repoPath
|
|
11016
|
+
);
|
|
11017
|
+
if (owner.status !== "absent")
|
|
11018
|
+
return { status: "retained", containers: proveRetainedManagedStop(state) };
|
|
11019
|
+
if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
|
|
11020
|
+
const before = absentRegistrationIdentity(state);
|
|
11021
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
11022
|
+
throw new Error("Retained stop generation changed.");
|
|
11023
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
11024
|
+
throw new Error("Stop daemon changed.");
|
|
11025
|
+
assertManagedStopContainersAbsent(
|
|
11026
|
+
baseline.endpoint,
|
|
11027
|
+
baseline.containers.map((container) => container.id)
|
|
11028
|
+
);
|
|
11029
|
+
const uidBytes = Buffer.byteLength(baseline.uid);
|
|
11030
|
+
const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
|
|
11031
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
|
|
11032
|
+
throw new Error("Absent stop observed a remaining workspace population.");
|
|
11033
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util2.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
11034
|
+
throw new Error("Absent stop authority changed during inspection.");
|
|
11035
|
+
return { status: "proven-absent", containers: [] };
|
|
11036
|
+
}
|
|
10957
11037
|
function stopFromManagedBaseline(state) {
|
|
11038
|
+
const proof = proveManagedStop(state);
|
|
11039
|
+
if (proof.status === "proven-absent") return proof.status;
|
|
10958
11040
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
10959
11041
|
const stopped = /* @__PURE__ */ new Set();
|
|
10960
11042
|
for (const initial of proveRetainedManagedStop(state))
|
|
@@ -10977,6 +11059,12 @@ function stopFromManagedBaseline(state) {
|
|
|
10977
11059
|
}
|
|
10978
11060
|
if (inspect().some((container) => container.state.Running))
|
|
10979
11061
|
throw new Error("Retained workloads remain running.");
|
|
11062
|
+
return "retained";
|
|
11063
|
+
}
|
|
11064
|
+
function managedStopRouteReferences(state) {
|
|
11065
|
+
return state.desired.apps.flatMap(
|
|
11066
|
+
(name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
|
|
11067
|
+
);
|
|
10980
11068
|
}
|
|
10981
11069
|
var import_node_os2, import_node_path31, import_node_util2;
|
|
10982
11070
|
var init_managed_stop_recovery = __esm({
|
|
@@ -10986,6 +11074,7 @@ var init_managed_stop_recovery = __esm({
|
|
|
10986
11074
|
import_node_path31 = __toESM(require("path"));
|
|
10987
11075
|
import_node_util2 = require("util");
|
|
10988
11076
|
init_devpod_environment();
|
|
11077
|
+
init_devpod_registry();
|
|
10989
11078
|
init_devsy_workspaces();
|
|
10990
11079
|
init_managed_compose_population();
|
|
10991
11080
|
init_managed_runtime_state();
|
|
@@ -11095,8 +11184,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
11095
11184
|
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
11096
11185
|
}
|
|
11097
11186
|
if (state.stopBaseline) {
|
|
11098
|
-
stopFromManagedBaseline(state);
|
|
11099
|
-
return true;
|
|
11187
|
+
return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
|
|
11100
11188
|
}
|
|
11101
11189
|
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
11102
11190
|
function registration2() {
|
|
@@ -11280,12 +11368,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
|
|
|
11280
11368
|
}
|
|
11281
11369
|
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
11282
11370
|
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
11283
|
-
if (action2 === "stop"
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11371
|
+
if (action2 === "stop") {
|
|
11372
|
+
const stopped = stopRetainedManagedDevsyWorkspace({
|
|
11373
|
+
repoPath: worktreePath,
|
|
11374
|
+
devsyId,
|
|
11375
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
11376
|
+
});
|
|
11377
|
+
if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
|
|
11378
|
+
}
|
|
11289
11379
|
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
11290
11380
|
if (before.status === "absent") return { status: "absent" };
|
|
11291
11381
|
runDevsyAction(action2, devsyId);
|
|
@@ -12068,7 +12158,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
12068
12158
|
const config = runtimeConfig.config;
|
|
12069
12159
|
loadedConfig = config;
|
|
12070
12160
|
loadedWorkspace = runtimeConfig.workspace;
|
|
12071
|
-
const cliVersion = true ? "0.0.
|
|
12161
|
+
const cliVersion = true ? "0.0.63" : "0.0.0-dev";
|
|
12072
12162
|
const configVersion = config.devrouter?.version;
|
|
12073
12163
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
12074
12164
|
addCheck(checks, {
|
|
@@ -13362,7 +13452,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
13362
13452
|
case "not-started":
|
|
13363
13453
|
if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
|
|
13364
13454
|
return unchanged(next, "blocked");
|
|
13365
|
-
if (next.operation.id !== event.operationId || next.operation.status
|
|
13455
|
+
if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
|
|
13366
13456
|
return unchanged(next, "stale");
|
|
13367
13457
|
next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
|
|
13368
13458
|
return transition(next, "accepted");
|
|
@@ -13417,6 +13507,11 @@ function sameFence2(record4, fence) {
|
|
|
13417
13507
|
([key, value]) => value === fence[key]
|
|
13418
13508
|
);
|
|
13419
13509
|
}
|
|
13510
|
+
function hasDuplicateOperation(record4, requestId, operationId) {
|
|
13511
|
+
return record4.state.operationHistory.some(
|
|
13512
|
+
(operation) => operation.key === requestId || operation.id === operationId
|
|
13513
|
+
);
|
|
13514
|
+
}
|
|
13420
13515
|
function workerGroupAbsent(pid) {
|
|
13421
13516
|
try {
|
|
13422
13517
|
process.kill(-pid, 0);
|
|
@@ -13433,7 +13528,7 @@ function signalOwnedGroup(child, signal) {
|
|
|
13433
13528
|
if (error.code !== "ESRCH") throw error;
|
|
13434
13529
|
}
|
|
13435
13530
|
}
|
|
13436
|
-
async function runLifecycleWorker(request) {
|
|
13531
|
+
async function runLifecycleWorker(request, beforeAdmission) {
|
|
13437
13532
|
if (process.platform === "win32")
|
|
13438
13533
|
throw new Error("Lifecycle workers require POSIX process-group ownership.");
|
|
13439
13534
|
const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
|
|
@@ -13445,13 +13540,18 @@ async function runLifecycleWorker(request) {
|
|
|
13445
13540
|
execArgv: []
|
|
13446
13541
|
});
|
|
13447
13542
|
let ready = false;
|
|
13543
|
+
let closed = false;
|
|
13544
|
+
let registered = false;
|
|
13545
|
+
let dispatchAcknowledged = false;
|
|
13546
|
+
let sendAttempted = false;
|
|
13547
|
+
let readinessTimer;
|
|
13448
13548
|
let result;
|
|
13449
13549
|
let cancelled = false;
|
|
13450
13550
|
let forceTimer;
|
|
13451
13551
|
let monitor;
|
|
13452
13552
|
let failure;
|
|
13453
13553
|
const cancel = () => {
|
|
13454
|
-
if (cancelled) return;
|
|
13554
|
+
if (cancelled || closed) return;
|
|
13455
13555
|
cancelled = true;
|
|
13456
13556
|
try {
|
|
13457
13557
|
signalOwnedGroup(child, "SIGTERM");
|
|
@@ -13474,20 +13574,56 @@ async function runLifecycleWorker(request) {
|
|
|
13474
13574
|
child.once("error", (error) => {
|
|
13475
13575
|
failure = error;
|
|
13476
13576
|
});
|
|
13477
|
-
|
|
13577
|
+
readinessTimer = setTimeout(() => {
|
|
13578
|
+
failure = new Error("Lifecycle worker readiness timed out before dispatch.");
|
|
13579
|
+
cancel();
|
|
13580
|
+
}, 3e4);
|
|
13581
|
+
child.on("message", async (message) => {
|
|
13478
13582
|
if (!message || typeof message !== "object") return;
|
|
13479
13583
|
if ("ready" in message && message.ready === true && !ready) {
|
|
13480
13584
|
ready = true;
|
|
13585
|
+
if (readinessTimer) clearTimeout(readinessTimer);
|
|
13481
13586
|
try {
|
|
13587
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
13588
|
+
if (closed) return;
|
|
13482
13589
|
if (cancelled || !child.pid)
|
|
13483
13590
|
throw new Error("Lifecycle invocation was cancelled before dispatch.");
|
|
13484
13591
|
if (request.kind !== "stop") {
|
|
13485
13592
|
const birth = processBirthIdentity(child.pid);
|
|
13486
13593
|
if (!birth) throw new Error("Could not prove lifecycle worker incarnation.");
|
|
13487
13594
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13488
|
-
|
|
13595
|
+
beforeAdmission?.();
|
|
13596
|
+
if (!request.admission)
|
|
13597
|
+
throw new Error("Lifecycle operation admission is required.");
|
|
13598
|
+
if (hasDuplicateOperation(record4, request.requestId, request.operationId))
|
|
13599
|
+
throw new Error(
|
|
13600
|
+
"Lifecycle operation request conflicts with an existing operation."
|
|
13601
|
+
);
|
|
13602
|
+
if (!sameFence2(record4, request.fence))
|
|
13489
13603
|
throw new Error("Lifecycle intent changed before dispatch.");
|
|
13604
|
+
if (record4.worker || record4.revision !== request.admission.expectedRevision) {
|
|
13605
|
+
throw new LifecycleWorkerAdmissionBusyError();
|
|
13490
13606
|
}
|
|
13607
|
+
const admitted = stepReliability(
|
|
13608
|
+
record4.state,
|
|
13609
|
+
{
|
|
13610
|
+
...request.fence,
|
|
13611
|
+
type: "operation-request",
|
|
13612
|
+
kind: request.kind === "exec" ? "exec" : "ensure",
|
|
13613
|
+
key: request.requestId,
|
|
13614
|
+
operationId: request.operationId,
|
|
13615
|
+
profile: request.admission.profile,
|
|
13616
|
+
consumer: request.admission.consumer,
|
|
13617
|
+
runtimeRunning: request.admission.runtimeRunning,
|
|
13618
|
+
...request.admission.recoverInterruptedEnsure ? { recoverInterruptedEnsure: true } : {}
|
|
13619
|
+
},
|
|
13620
|
+
Date.now()
|
|
13621
|
+
);
|
|
13622
|
+
if (admitted.outcome !== "accepted")
|
|
13623
|
+
throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
|
|
13624
|
+
record4.state = admitted.state;
|
|
13625
|
+
record4.outcome = null;
|
|
13626
|
+
request.fence = reliabilityFence(record4.state);
|
|
13491
13627
|
record4.worker = {
|
|
13492
13628
|
id: request.workerId,
|
|
13493
13629
|
operationId: request.operationId,
|
|
@@ -13503,6 +13639,7 @@ async function runLifecycleWorker(request) {
|
|
|
13503
13639
|
throw new Error("Lifecycle dispatch is blocked.");
|
|
13504
13640
|
record4.state = transition2.state;
|
|
13505
13641
|
});
|
|
13642
|
+
registered = true;
|
|
13506
13643
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13507
13644
|
const transition2 = stepReliability(
|
|
13508
13645
|
record4.state,
|
|
@@ -13513,12 +13650,18 @@ async function runLifecycleWorker(request) {
|
|
|
13513
13650
|
},
|
|
13514
13651
|
Date.now()
|
|
13515
13652
|
);
|
|
13516
|
-
record4.state = transition2.state;
|
|
13517
13653
|
if (!transition2.effects.some((effect2) => effect2.kind === "launch")) {
|
|
13518
13654
|
throw new Error("Lifecycle dispatch persistence was not acknowledged.");
|
|
13519
13655
|
}
|
|
13656
|
+
record4.state = transition2.state;
|
|
13520
13657
|
});
|
|
13658
|
+
dispatchAcknowledged = true;
|
|
13521
13659
|
}
|
|
13660
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
13661
|
+
if (closed) return;
|
|
13662
|
+
beforeAdmission?.();
|
|
13663
|
+
if (cancelled) throw new Error("Lifecycle invocation was cancelled before dispatch.");
|
|
13664
|
+
sendAttempted = true;
|
|
13522
13665
|
child.send({ request }, (error) => {
|
|
13523
13666
|
if (error) {
|
|
13524
13667
|
failure = error;
|
|
@@ -13543,8 +13686,9 @@ async function runLifecycleWorker(request) {
|
|
|
13543
13686
|
}
|
|
13544
13687
|
});
|
|
13545
13688
|
child.once("close", () => {
|
|
13689
|
+
closed = true;
|
|
13546
13690
|
try {
|
|
13547
|
-
if (request.kind !== "stop") {
|
|
13691
|
+
if (request.kind !== "stop" && registered) {
|
|
13548
13692
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13549
13693
|
if (record4.worker?.id !== request.workerId || record4.worker.pid !== child.pid) return;
|
|
13550
13694
|
if (!result) {
|
|
@@ -13552,7 +13696,7 @@ async function runLifecycleWorker(request) {
|
|
|
13552
13696
|
record4.state,
|
|
13553
13697
|
{
|
|
13554
13698
|
...reliabilityFence(record4.state),
|
|
13555
|
-
type: "interrupted",
|
|
13699
|
+
type: request.kind === "exec" && dispatchAcknowledged && !sendAttempted ? "not-started" : "interrupted",
|
|
13556
13700
|
operationId: request.operationId
|
|
13557
13701
|
},
|
|
13558
13702
|
Date.now()
|
|
@@ -13586,6 +13730,7 @@ async function runLifecycleWorker(request) {
|
|
|
13586
13730
|
});
|
|
13587
13731
|
});
|
|
13588
13732
|
} finally {
|
|
13733
|
+
if (readinessTimer) clearTimeout(readinessTimer);
|
|
13589
13734
|
if (monitor) clearInterval(monitor);
|
|
13590
13735
|
if (forceTimer) clearTimeout(forceTimer);
|
|
13591
13736
|
process.removeListener("SIGINT", onSignal);
|
|
@@ -13595,7 +13740,7 @@ async function runLifecycleWorker(request) {
|
|
|
13595
13740
|
function newLifecycleIds() {
|
|
13596
13741
|
return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
|
|
13597
13742
|
}
|
|
13598
|
-
var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38;
|
|
13743
|
+
var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38, LifecycleWorkerAdmissionBusyError;
|
|
13599
13744
|
var init_reliability_worker = __esm({
|
|
13600
13745
|
"src/core/reliability-worker.ts"() {
|
|
13601
13746
|
"use strict";
|
|
@@ -13607,17 +13752,12 @@ var init_reliability_worker = __esm({
|
|
|
13607
13752
|
init_reliability_contract();
|
|
13608
13753
|
init_reliability_model();
|
|
13609
13754
|
init_reliability_operation_store();
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
"src/core/managed-host-preparation.ts"() {
|
|
13617
|
-
"use strict";
|
|
13618
|
-
import_node_child_process22 = require("child_process");
|
|
13619
|
-
import_node_fs32 = __toESM(require("fs"));
|
|
13620
|
-
init_repo_config();
|
|
13755
|
+
LifecycleWorkerAdmissionBusyError = class extends Error {
|
|
13756
|
+
constructor() {
|
|
13757
|
+
super("Lifecycle admission lost to another operation.");
|
|
13758
|
+
this.name = "LifecycleWorkerAdmissionBusyError";
|
|
13759
|
+
}
|
|
13760
|
+
};
|
|
13621
13761
|
}
|
|
13622
13762
|
});
|
|
13623
13763
|
|
|
@@ -13626,7 +13766,7 @@ function sleep(ms) {
|
|
|
13626
13766
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13627
13767
|
}
|
|
13628
13768
|
function inspectRouterApi(protocol) {
|
|
13629
|
-
const result = (0,
|
|
13769
|
+
const result = (0, import_node_child_process22.spawnSync)(
|
|
13630
13770
|
"curl",
|
|
13631
13771
|
[
|
|
13632
13772
|
"--silent",
|
|
@@ -13653,6 +13793,11 @@ function inspectRouterApi(protocol) {
|
|
|
13653
13793
|
if (!Array.isArray(value)) {
|
|
13654
13794
|
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
13655
13795
|
}
|
|
13796
|
+
if (value.some((item) => !isRecord4(item) || typeof item.name !== "string" || !item.name))
|
|
13797
|
+
return {
|
|
13798
|
+
ok: false,
|
|
13799
|
+
details: `${protocol.toUpperCase()} router API returned malformed entries`
|
|
13800
|
+
};
|
|
13656
13801
|
return {
|
|
13657
13802
|
ok: true,
|
|
13658
13803
|
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
@@ -13665,6 +13810,9 @@ function inspectRouterApi(protocol) {
|
|
|
13665
13810
|
)
|
|
13666
13811
|
};
|
|
13667
13812
|
}
|
|
13813
|
+
function isRecord4(value) {
|
|
13814
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13815
|
+
}
|
|
13668
13816
|
function inspectExpectedRoutes(routes, expectation) {
|
|
13669
13817
|
const mismatched = [];
|
|
13670
13818
|
const failures = [];
|
|
@@ -13725,7 +13873,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
13725
13873
|
throw new Error(
|
|
13726
13874
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
13727
13875
|
);
|
|
13728
|
-
|
|
13876
|
+
import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
13729
13877
|
return withFileLock(
|
|
13730
13878
|
ROUTER_RELOAD_LOCK_FILE,
|
|
13731
13879
|
{
|
|
@@ -13756,12 +13904,12 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
13756
13904
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
13757
13905
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
13758
13906
|
}
|
|
13759
|
-
var
|
|
13907
|
+
var import_node_child_process22, import_node_fs32, import_node_path39, 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;
|
|
13760
13908
|
var init_traefik_route_health = __esm({
|
|
13761
13909
|
"src/core/traefik-route-health.ts"() {
|
|
13762
13910
|
"use strict";
|
|
13763
|
-
|
|
13764
|
-
|
|
13911
|
+
import_node_child_process22 = require("child_process");
|
|
13912
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
13765
13913
|
import_node_path39 = __toESM(require("path"));
|
|
13766
13914
|
init_file_lock();
|
|
13767
13915
|
init_host_routes();
|
|
@@ -13777,6 +13925,17 @@ var init_traefik_route_health = __esm({
|
|
|
13777
13925
|
}
|
|
13778
13926
|
});
|
|
13779
13927
|
|
|
13928
|
+
// src/core/managed-host-preparation.ts
|
|
13929
|
+
var import_node_child_process23, import_node_fs33;
|
|
13930
|
+
var init_managed_host_preparation = __esm({
|
|
13931
|
+
"src/core/managed-host-preparation.ts"() {
|
|
13932
|
+
"use strict";
|
|
13933
|
+
import_node_child_process23 = require("child_process");
|
|
13934
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
13935
|
+
init_repo_config();
|
|
13936
|
+
}
|
|
13937
|
+
});
|
|
13938
|
+
|
|
13780
13939
|
// src/core/workspace-ensure.ts
|
|
13781
13940
|
function resolveLinkedTarget(repoPath) {
|
|
13782
13941
|
const snapshots = getWorkspaceRegistrySnapshots();
|
|
@@ -13839,6 +13998,11 @@ var init_workspace_ensure = __esm({
|
|
|
13839
13998
|
});
|
|
13840
13999
|
|
|
13841
14000
|
// src/core/reliability-lifecycle.ts
|
|
14001
|
+
function matchesFence(record4, fence) {
|
|
14002
|
+
return Object.entries(reliabilityFence(record4.state)).every(
|
|
14003
|
+
([key, value]) => value === fence[key]
|
|
14004
|
+
);
|
|
14005
|
+
}
|
|
13842
14006
|
async function withLifecycleOperationLock(repoPath, operation) {
|
|
13843
14007
|
if (activeWorker && lockHeld) {
|
|
13844
14008
|
if (!sameWorkspacePath(repoPath, activeWorker.repoPath))
|
|
@@ -13886,6 +14050,23 @@ function reconcileDrained(record4) {
|
|
|
13886
14050
|
record4.worker = null;
|
|
13887
14051
|
}
|
|
13888
14052
|
}
|
|
14053
|
+
function reportBusyExecWait(operationId, waitedMs) {
|
|
14054
|
+
process.stderr.write(
|
|
14055
|
+
`Lifecycle exec ${operationId} is waiting for the existing worker (${Math.ceil(waitedMs / 1e3)}s).
|
|
14056
|
+
`
|
|
14057
|
+
);
|
|
14058
|
+
}
|
|
14059
|
+
function installLifecycleWaitSignals(cancellation) {
|
|
14060
|
+
const onSignal = () => {
|
|
14061
|
+
cancellation.requested = true;
|
|
14062
|
+
};
|
|
14063
|
+
process.once("SIGINT", onSignal);
|
|
14064
|
+
process.once("SIGTERM", onSignal);
|
|
14065
|
+
return () => {
|
|
14066
|
+
process.removeListener("SIGINT", onSignal);
|
|
14067
|
+
process.removeListener("SIGTERM", onSignal);
|
|
14068
|
+
};
|
|
14069
|
+
}
|
|
13889
14070
|
async function superviseLifecycle(kind, repoPath, options = {}, command) {
|
|
13890
14071
|
repoPath = comparableWorkspacePath(repoPath);
|
|
13891
14072
|
if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
|
|
@@ -13898,58 +14079,113 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
|
|
|
13898
14079
|
provider: resolveWorkspaceRuntimeOrDefault(repoPath)
|
|
13899
14080
|
};
|
|
13900
14081
|
const ids = newLifecycleIds();
|
|
13901
|
-
|
|
13902
|
-
const
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
}) : void 0;
|
|
13907
|
-
let retainedExecProof;
|
|
13908
|
-
if (previous && !previous.worker && canExecAfterInterruptedEnsure(previous.state)) {
|
|
13909
|
-
retainedExecProof = withMutationLock(
|
|
13910
|
-
"Prove retained exec",
|
|
13911
|
-
repoPath,
|
|
13912
|
-
() => captureDevsyExecProof(repoPath)
|
|
13913
|
-
);
|
|
13914
|
-
}
|
|
13915
|
-
updateReliabilityOperation(identity2, (record4) => {
|
|
13916
|
-
reconcileDrained(record4);
|
|
13917
|
-
if (retainedExecProof && record4.state.operation?.id !== previous?.state.operation?.id)
|
|
13918
|
-
throw new Error("Lifecycle operation changed during retained exec proof.");
|
|
13919
|
-
if (kind === "stop") {
|
|
14082
|
+
const copiedOptions = { ...options };
|
|
14083
|
+
const copiedCommand = command ? [...command] : void 0;
|
|
14084
|
+
if (kind === "stop") {
|
|
14085
|
+
const fence = updateReliabilityOperation(identity2, (record4) => {
|
|
14086
|
+
reconcileDrained(record4);
|
|
13920
14087
|
stepRecord(record4, { ...reliabilityFence(record4.state), type: "stop" });
|
|
13921
|
-
|
|
13922
|
-
|
|
14088
|
+
return reliabilityFence(record4.state);
|
|
14089
|
+
});
|
|
14090
|
+
return runLifecycleWorker({ kind, repoPath, identity: identity2, ...ids, fence, options: copiedOptions });
|
|
14091
|
+
}
|
|
14092
|
+
const initial = readReliabilityOperation(identity2) ?? updateReliabilityOperation(identity2, (record4) => record4);
|
|
14093
|
+
const expectedFence = reliabilityFence(initial.state);
|
|
14094
|
+
const deadline = Date.now() + BUSY_EXEC_WAIT_MS;
|
|
14095
|
+
const cancellation = { requested: false };
|
|
14096
|
+
const removeSignals = installLifecycleWaitSignals(cancellation);
|
|
14097
|
+
let lastProgress = -Infinity;
|
|
14098
|
+
const assertWaiting = () => {
|
|
14099
|
+
if (cancellation.requested)
|
|
14100
|
+
throw new Error(`Lifecycle exec ${ids.operationId} cancelled before dispatch.`);
|
|
14101
|
+
if (Date.now() >= deadline)
|
|
14102
|
+
throw new Error(
|
|
14103
|
+
`Lifecycle exec ${ids.operationId} admission timed out; command was not launched.`
|
|
14104
|
+
);
|
|
14105
|
+
};
|
|
14106
|
+
try {
|
|
14107
|
+
for (; ; ) {
|
|
14108
|
+
assertWaiting();
|
|
14109
|
+
let previous = readReliabilityOperation(identity2);
|
|
14110
|
+
if (!previous || !matchesFence(previous, expectedFence))
|
|
13923
14111
|
throw new Error(
|
|
13924
|
-
"
|
|
14112
|
+
"Lifecycle intent changed while awaiting admission; command was not launched."
|
|
13925
14113
|
);
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
14114
|
+
if (hasDuplicateOperation(previous, ids.requestId, ids.operationId))
|
|
14115
|
+
throw new Error("Lifecycle request identity already exists; command was not replayed.");
|
|
14116
|
+
if (previous.worker) {
|
|
14117
|
+
const worker = previous.worker;
|
|
14118
|
+
const birth = processBirthIdentity(worker.pid);
|
|
14119
|
+
if (birth !== worker.birth && workerGroupAbsent(worker.pid)) {
|
|
14120
|
+
previous = updateReliabilityOperation(identity2, (record4) => {
|
|
14121
|
+
reconcileDrained(record4);
|
|
14122
|
+
return record4;
|
|
14123
|
+
});
|
|
14124
|
+
} else {
|
|
14125
|
+
if (!birth || birth !== worker.birth || previous.state.operation?.status === "COMPLETION_UNKNOWN")
|
|
14126
|
+
throw new Error(
|
|
14127
|
+
"Existing lifecycle worker identity or completion is uncertain; preserve its evidence."
|
|
14128
|
+
);
|
|
14129
|
+
if (kind !== "exec")
|
|
14130
|
+
throw new Error("A lifecycle worker is active; wait for its completion before ensure.");
|
|
14131
|
+
const now = Date.now();
|
|
14132
|
+
if (now - lastProgress >= BUSY_EXEC_PROGRESS_MS) {
|
|
14133
|
+
reportBusyExecWait(ids.operationId, now - (deadline - BUSY_EXEC_WAIT_MS));
|
|
14134
|
+
lastProgress = now;
|
|
14135
|
+
}
|
|
14136
|
+
await new Promise(
|
|
14137
|
+
(resolve) => setTimeout(resolve, Math.min(BUSY_EXEC_POLL_MS, deadline - now))
|
|
14138
|
+
);
|
|
14139
|
+
continue;
|
|
14140
|
+
}
|
|
14141
|
+
}
|
|
14142
|
+
if (!previous.worker && previous.state.operation?.status === "NOT_STARTED" && !previous.state.operation.drained)
|
|
14143
|
+
previous = updateReliabilityOperation(identity2, (record4) => {
|
|
14144
|
+
reconcileDrained(record4);
|
|
14145
|
+
return record4;
|
|
14146
|
+
});
|
|
14147
|
+
assertWaiting();
|
|
14148
|
+
const runtimeRunning = kind === "exec" && Boolean(resolveRunningWorkspaceContainer(repoPath));
|
|
14149
|
+
let retainedExecProof;
|
|
14150
|
+
if (kind === "exec" && identity2.provider === "devsy" && canExecAfterInterruptedEnsure(previous.state))
|
|
14151
|
+
retainedExecProof = withMutationLock(
|
|
14152
|
+
"Prove retained exec",
|
|
14153
|
+
repoPath,
|
|
14154
|
+
() => captureDevsyExecProof(repoPath)
|
|
14155
|
+
);
|
|
14156
|
+
assertWaiting();
|
|
14157
|
+
try {
|
|
14158
|
+
return await runLifecycleWorker(
|
|
14159
|
+
{
|
|
14160
|
+
kind,
|
|
14161
|
+
repoPath,
|
|
14162
|
+
identity: identity2,
|
|
14163
|
+
...ids,
|
|
14164
|
+
fence: { ...expectedFence },
|
|
14165
|
+
options: copiedOptions,
|
|
14166
|
+
...copiedCommand ? { command: copiedCommand } : {},
|
|
14167
|
+
...retainedExecProof ? { retainedExecProof } : {},
|
|
14168
|
+
admission: {
|
|
14169
|
+
expectedRevision: previous.revision,
|
|
14170
|
+
profile: copiedOptions.profile ?? previous.state.profile ?? "full",
|
|
14171
|
+
consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
|
|
14172
|
+
runtimeRunning,
|
|
14173
|
+
...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
|
|
14174
|
+
}
|
|
14175
|
+
},
|
|
14176
|
+
assertWaiting
|
|
14177
|
+
);
|
|
14178
|
+
} catch (error) {
|
|
14179
|
+
if (!(error instanceof LifecycleWorkerAdmissionBusyError) || kind !== "exec") throw error;
|
|
14180
|
+
assertWaiting();
|
|
14181
|
+
await new Promise((resolve) => setTimeout(resolve, BUSY_EXEC_POLL_MS));
|
|
14182
|
+
}
|
|
13938
14183
|
}
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
kind,
|
|
13943
|
-
repoPath,
|
|
13944
|
-
identity: identity2,
|
|
13945
|
-
...ids,
|
|
13946
|
-
fence,
|
|
13947
|
-
options,
|
|
13948
|
-
...command ? { command } : {},
|
|
13949
|
-
...retainedExecProof ? { retainedExecProof } : {}
|
|
13950
|
-
});
|
|
14184
|
+
} finally {
|
|
14185
|
+
removeSignals();
|
|
14186
|
+
}
|
|
13951
14187
|
}
|
|
13952
|
-
var import_node_path41, activeWorker, lockHeld;
|
|
14188
|
+
var import_node_path41, activeWorker, lockHeld, BUSY_EXEC_WAIT_MS, BUSY_EXEC_POLL_MS, BUSY_EXEC_PROGRESS_MS;
|
|
13953
14189
|
var init_reliability_lifecycle = __esm({
|
|
13954
14190
|
"src/core/reliability-lifecycle.ts"() {
|
|
13955
14191
|
"use strict";
|
|
@@ -13966,11 +14202,15 @@ var init_reliability_lifecycle = __esm({
|
|
|
13966
14202
|
init_reliability_model();
|
|
13967
14203
|
init_reliability_operation_store();
|
|
13968
14204
|
init_reliability_worker();
|
|
14205
|
+
init_traefik_route_health();
|
|
13969
14206
|
init_workspace();
|
|
13970
14207
|
init_workspace_ensure();
|
|
13971
14208
|
init_workspace_runtime();
|
|
13972
14209
|
init_reliability_context();
|
|
13973
14210
|
lockHeld = false;
|
|
14211
|
+
BUSY_EXEC_WAIT_MS = 30 * 60 * 1e3;
|
|
14212
|
+
BUSY_EXEC_POLL_MS = 250;
|
|
14213
|
+
BUSY_EXEC_PROGRESS_MS = 1e4;
|
|
13974
14214
|
}
|
|
13975
14215
|
});
|
|
13976
14216
|
|
|
@@ -15632,11 +15872,11 @@ var init_repo_devcontainer = __esm({
|
|
|
15632
15872
|
function fail3(message) {
|
|
15633
15873
|
throw new Error(message);
|
|
15634
15874
|
}
|
|
15635
|
-
function
|
|
15875
|
+
function isRecord5(value) {
|
|
15636
15876
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15637
15877
|
}
|
|
15638
15878
|
function requireRecord(value, label) {
|
|
15639
|
-
if (!
|
|
15879
|
+
if (!isRecord5(value)) fail3(`${label} must be a mapping.`);
|
|
15640
15880
|
return value;
|
|
15641
15881
|
}
|
|
15642
15882
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -17416,7 +17656,7 @@ var init_workspace_consumption = __esm({
|
|
|
17416
17656
|
});
|
|
17417
17657
|
|
|
17418
17658
|
// src/core/workspace-cleanup.ts
|
|
17419
|
-
function
|
|
17659
|
+
function isRecord6(value) {
|
|
17420
17660
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17421
17661
|
}
|
|
17422
17662
|
function isSha(value) {
|
|
@@ -17560,14 +17800,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
17560
17800
|
if (!Array.isArray(value)) return void 0;
|
|
17561
17801
|
const changes = [];
|
|
17562
17802
|
for (const item of value) {
|
|
17563
|
-
if (!
|
|
17564
|
-
const repository =
|
|
17565
|
-
const headRepository =
|
|
17803
|
+
if (!isRecord6(item)) return void 0;
|
|
17804
|
+
const repository = isRecord6(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
17805
|
+
const headRepository = isRecord6(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
17566
17806
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
17567
17807
|
continue;
|
|
17568
17808
|
}
|
|
17569
17809
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
17570
|
-
const mergeCommit =
|
|
17810
|
+
const mergeCommit = isRecord6(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
17571
17811
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
17572
17812
|
changes.push({
|
|
17573
17813
|
sourceBranch: branch,
|
|
@@ -17584,14 +17824,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
17584
17824
|
if (!Array.isArray(value)) return void 0;
|
|
17585
17825
|
const changes = [];
|
|
17586
17826
|
for (const item of value) {
|
|
17587
|
-
if (!
|
|
17827
|
+
if (!isRecord6(item)) return void 0;
|
|
17588
17828
|
const sourceProjectId = item.source_project_id;
|
|
17589
17829
|
const targetProjectId = item.target_project_id;
|
|
17590
17830
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
17591
17831
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
17592
17832
|
continue;
|
|
17593
17833
|
}
|
|
17594
|
-
const diffRefs =
|
|
17834
|
+
const diffRefs = isRecord6(item.diff_refs) ? item.diff_refs : void 0;
|
|
17595
17835
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
17596
17836
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
17597
17837
|
changes.push({
|
|
@@ -18413,8 +18653,24 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
18413
18653
|
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
18414
18654
|
);
|
|
18415
18655
|
}
|
|
18416
|
-
const
|
|
18417
|
-
|
|
18656
|
+
const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
|
|
18657
|
+
let absent = false;
|
|
18658
|
+
const remove = () => {
|
|
18659
|
+
if (retained?.stopBaseline) {
|
|
18660
|
+
absent = proveManagedStop(retained).status === "proven-absent";
|
|
18661
|
+
if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
|
|
18662
|
+
(route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
|
|
18663
|
+
))
|
|
18664
|
+
throw new Error("Absent stop contains routes outside the retained desired set.");
|
|
18665
|
+
}
|
|
18666
|
+
claimLifecycleEffect();
|
|
18667
|
+
return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
18668
|
+
};
|
|
18669
|
+
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
|
|
18670
|
+
await ensureTraefikRoutesRemoved([
|
|
18671
|
+
...routes,
|
|
18672
|
+
...absent && retained ? managedStopRouteReferences(retained) : []
|
|
18673
|
+
]);
|
|
18418
18674
|
if (!quiet) {
|
|
18419
18675
|
process.stdout.write(
|
|
18420
18676
|
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
@@ -18425,6 +18681,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
18425
18681
|
...mutation.status === "changed" ? { devpodId } : {},
|
|
18426
18682
|
freedRoutes: routes.length,
|
|
18427
18683
|
providerChanged: mutation.status === "changed",
|
|
18684
|
+
...absent ? { runtimeAbsent: true } : {},
|
|
18428
18685
|
workspace: resolved.workspace
|
|
18429
18686
|
};
|
|
18430
18687
|
}
|
|
@@ -18487,7 +18744,9 @@ var init_workspace_lifecycle = __esm({
|
|
|
18487
18744
|
import_node_path49 = __toESM(require("path"));
|
|
18488
18745
|
init_devpod_mutation();
|
|
18489
18746
|
init_devpod_workspaces();
|
|
18747
|
+
init_devsy_mutation();
|
|
18490
18748
|
init_managed_runtime_state();
|
|
18749
|
+
init_managed_stop_recovery();
|
|
18491
18750
|
init_reliability_lifecycle();
|
|
18492
18751
|
init_repo_config();
|
|
18493
18752
|
init_route_state();
|
|
@@ -18643,7 +18902,7 @@ var init_version = __esm({
|
|
|
18643
18902
|
|
|
18644
18903
|
// src/cli.ts
|
|
18645
18904
|
var import_commander = require("commander");
|
|
18646
|
-
var CLI_VERSION = true ? "0.0.
|
|
18905
|
+
var CLI_VERSION = true ? "0.0.63" : "0.0.0-dev";
|
|
18647
18906
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
18648
18907
|
function withErrorHandling(action2) {
|
|
18649
18908
|
return async (...args) => {
|