@devrouter/cli 0.0.62 → 0.0.64
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 +1382 -1257
- package/dist/devrouter.js +360 -97
- package/package.json +1 -1
- package/upgrade-prompts/0.0.63.md +36 -0
- package/upgrade-prompts/0.0.64.md +34 -0
package/dist/devrouter.js
CHANGED
|
@@ -671,7 +671,9 @@ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
|
|
|
671
671
|
function acquireFileLock(lockPath, options) {
|
|
672
672
|
const processBirth = processBirthIdentity(process.pid);
|
|
673
673
|
if (!processBirth) {
|
|
674
|
-
throw new Error(
|
|
674
|
+
throw new Error(
|
|
675
|
+
`could not determine process identity for ${options.activity} lock: process inspection may be unavailable or sandbox-denied; run the canonical command in a permitted host context`
|
|
676
|
+
);
|
|
675
677
|
}
|
|
676
678
|
const ownerId = (0, import_node_crypto2.randomUUID)();
|
|
677
679
|
const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
|
|
@@ -2450,7 +2452,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs6.default.readF
|
|
|
2450
2452
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2451
2453
|
const requiredVersion = config.devrouter?.version;
|
|
2452
2454
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2453
|
-
const cliVersion = true ? "0.0.
|
|
2455
|
+
const cliVersion = true ? "0.0.64" : "0.0.0-dev";
|
|
2454
2456
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2455
2457
|
hasWarnedVersionMismatch = true;
|
|
2456
2458
|
process.stderr.write(
|
|
@@ -4634,6 +4636,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
|
4634
4636
|
assertUniqueContainerIds(ids);
|
|
4635
4637
|
return ids;
|
|
4636
4638
|
}
|
|
4639
|
+
function hasExactManagedStopMissingObjectError(stderr, id3) {
|
|
4640
|
+
if (typeof stderr !== "string") return false;
|
|
4641
|
+
const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
|
|
4642
|
+
return line.toLowerCase() === `error: no such object: ${id3}`;
|
|
4643
|
+
}
|
|
4644
|
+
function assertManagedStopContainersAbsent(endpoint, ids) {
|
|
4645
|
+
assertManagedStopEndpoint(endpoint);
|
|
4646
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
|
|
4647
|
+
throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
|
|
4648
|
+
}
|
|
4649
|
+
ids.forEach(assertFullContainerId);
|
|
4650
|
+
assertUniqueContainerIds(ids);
|
|
4651
|
+
const env = { ...process.env };
|
|
4652
|
+
delete env.DOCKER_CONTEXT;
|
|
4653
|
+
delete env.DOCKER_HOST;
|
|
4654
|
+
for (const id3 of ids) {
|
|
4655
|
+
const result = (0, import_node_child_process6.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
|
|
4656
|
+
env,
|
|
4657
|
+
encoding: "utf-8",
|
|
4658
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
4659
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
4660
|
+
});
|
|
4661
|
+
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id3)) {
|
|
4662
|
+
throw new Error("Managed stop Docker inspection did not prove container absence.");
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4637
4666
|
function stopPinnedManagedContainer(endpoint, containerId) {
|
|
4638
4667
|
assertFullContainerId(containerId);
|
|
4639
4668
|
runManagedStopDocker(["stop", containerId], endpoint, 3e4);
|
|
@@ -7142,6 +7171,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
7142
7171
|
"- 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
7172
|
"- 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
7173
|
"- 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.",
|
|
7174
|
+
"- Busy ensure and exec wait 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
7175
|
"- 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
7176
|
"- 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
7177
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
@@ -8725,6 +8755,9 @@ var init_devcontainer_diagnostics = __esm({
|
|
|
8725
8755
|
});
|
|
8726
8756
|
|
|
8727
8757
|
// src/core/route-state.ts
|
|
8758
|
+
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
8759
|
+
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
8760
|
+
}
|
|
8728
8761
|
function listRoutesForWorktreePaths(worktreePaths) {
|
|
8729
8762
|
const routes = listHostRouteState();
|
|
8730
8763
|
const byWorktreePath = /* @__PURE__ */ new Map();
|
|
@@ -9584,11 +9617,13 @@ var init_status = __esm({
|
|
|
9584
9617
|
});
|
|
9585
9618
|
|
|
9586
9619
|
// src/core/devpod-registry.ts
|
|
9587
|
-
function listDevpodWorkspacesRaw() {
|
|
9620
|
+
function listDevpodWorkspacesRaw(options = {}) {
|
|
9588
9621
|
const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
|
|
9589
9622
|
encoding: "utf-8"
|
|
9590
9623
|
});
|
|
9591
|
-
if (result.
|
|
9624
|
+
if (options.allowMissingExecutable && result.error?.code === "ENOENT")
|
|
9625
|
+
return [];
|
|
9626
|
+
if (result.error || result.status !== 0) {
|
|
9592
9627
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
9593
9628
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
9594
9629
|
}
|
|
@@ -10954,7 +10989,56 @@ function proveRetainedManagedStop(state) {
|
|
|
10954
10989
|
}
|
|
10955
10990
|
return result;
|
|
10956
10991
|
}
|
|
10992
|
+
function absentRegistrationIdentity(state) {
|
|
10993
|
+
resetWorkspaceRuntimeCaches();
|
|
10994
|
+
if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
|
|
10995
|
+
throw new Error("Stop provider changed.");
|
|
10996
|
+
if (!state.workspace || !isLinkedWorktree(state.repoPath))
|
|
10997
|
+
throw new Error("Absent stop requires a linked workspace.");
|
|
10998
|
+
const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
|
|
10999
|
+
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")
|
|
11000
|
+
throw new Error("Absent stop workspace ownership changed.");
|
|
11001
|
+
for (const entries of [
|
|
11002
|
+
listDevsyWorkspaces(),
|
|
11003
|
+
listDevpodWorkspacesRaw({ allowMissingExecutable: true })
|
|
11004
|
+
]) {
|
|
11005
|
+
if (entries.some(
|
|
11006
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
11007
|
+
))
|
|
11008
|
+
throw new Error("Absent stop requires both provider registrations to remain absent.");
|
|
11009
|
+
}
|
|
11010
|
+
return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
|
|
11011
|
+
}
|
|
11012
|
+
function proveManagedStop(state) {
|
|
11013
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
11014
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
11015
|
+
listDevsyWorkspaces(),
|
|
11016
|
+
state.devpodId,
|
|
11017
|
+
state.repoPath
|
|
11018
|
+
);
|
|
11019
|
+
if (owner.status !== "absent")
|
|
11020
|
+
return { status: "retained", containers: proveRetainedManagedStop(state) };
|
|
11021
|
+
if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
|
|
11022
|
+
const before = absentRegistrationIdentity(state);
|
|
11023
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
11024
|
+
throw new Error("Retained stop generation changed.");
|
|
11025
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
11026
|
+
throw new Error("Stop daemon changed.");
|
|
11027
|
+
assertManagedStopContainersAbsent(
|
|
11028
|
+
baseline.endpoint,
|
|
11029
|
+
baseline.containers.map((container) => container.id)
|
|
11030
|
+
);
|
|
11031
|
+
const uidBytes = Buffer.byteLength(baseline.uid);
|
|
11032
|
+
const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
|
|
11033
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
|
|
11034
|
+
throw new Error("Absent stop observed a remaining workspace population.");
|
|
11035
|
+
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))
|
|
11036
|
+
throw new Error("Absent stop authority changed during inspection.");
|
|
11037
|
+
return { status: "proven-absent", containers: [] };
|
|
11038
|
+
}
|
|
10957
11039
|
function stopFromManagedBaseline(state) {
|
|
11040
|
+
const proof = proveManagedStop(state);
|
|
11041
|
+
if (proof.status === "proven-absent") return proof.status;
|
|
10958
11042
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
10959
11043
|
const stopped = /* @__PURE__ */ new Set();
|
|
10960
11044
|
for (const initial of proveRetainedManagedStop(state))
|
|
@@ -10977,6 +11061,12 @@ function stopFromManagedBaseline(state) {
|
|
|
10977
11061
|
}
|
|
10978
11062
|
if (inspect().some((container) => container.state.Running))
|
|
10979
11063
|
throw new Error("Retained workloads remain running.");
|
|
11064
|
+
return "retained";
|
|
11065
|
+
}
|
|
11066
|
+
function managedStopRouteReferences(state) {
|
|
11067
|
+
return state.desired.apps.flatMap(
|
|
11068
|
+
(name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
|
|
11069
|
+
);
|
|
10980
11070
|
}
|
|
10981
11071
|
var import_node_os2, import_node_path31, import_node_util2;
|
|
10982
11072
|
var init_managed_stop_recovery = __esm({
|
|
@@ -10986,6 +11076,7 @@ var init_managed_stop_recovery = __esm({
|
|
|
10986
11076
|
import_node_path31 = __toESM(require("path"));
|
|
10987
11077
|
import_node_util2 = require("util");
|
|
10988
11078
|
init_devpod_environment();
|
|
11079
|
+
init_devpod_registry();
|
|
10989
11080
|
init_devsy_workspaces();
|
|
10990
11081
|
init_managed_compose_population();
|
|
10991
11082
|
init_managed_runtime_state();
|
|
@@ -11095,8 +11186,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
11095
11186
|
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
11096
11187
|
}
|
|
11097
11188
|
if (state.stopBaseline) {
|
|
11098
|
-
stopFromManagedBaseline(state);
|
|
11099
|
-
return true;
|
|
11189
|
+
return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
|
|
11100
11190
|
}
|
|
11101
11191
|
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
11102
11192
|
function registration2() {
|
|
@@ -11280,12 +11370,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
|
|
|
11280
11370
|
}
|
|
11281
11371
|
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
11282
11372
|
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
11283
|
-
if (action2 === "stop"
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11373
|
+
if (action2 === "stop") {
|
|
11374
|
+
const stopped = stopRetainedManagedDevsyWorkspace({
|
|
11375
|
+
repoPath: worktreePath,
|
|
11376
|
+
devsyId,
|
|
11377
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
11378
|
+
});
|
|
11379
|
+
if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
|
|
11380
|
+
}
|
|
11289
11381
|
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
11290
11382
|
if (before.status === "absent") return { status: "absent" };
|
|
11291
11383
|
runDevsyAction(action2, devsyId);
|
|
@@ -12068,7 +12160,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
12068
12160
|
const config = runtimeConfig.config;
|
|
12069
12161
|
loadedConfig = config;
|
|
12070
12162
|
loadedWorkspace = runtimeConfig.workspace;
|
|
12071
|
-
const cliVersion = true ? "0.0.
|
|
12163
|
+
const cliVersion = true ? "0.0.64" : "0.0.0-dev";
|
|
12072
12164
|
const configVersion = config.devrouter?.version;
|
|
12073
12165
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
12074
12166
|
addCheck(checks, {
|
|
@@ -13362,7 +13454,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
13362
13454
|
case "not-started":
|
|
13363
13455
|
if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
|
|
13364
13456
|
return unchanged(next, "blocked");
|
|
13365
|
-
if (next.operation.id !== event.operationId || next.operation.status
|
|
13457
|
+
if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
|
|
13366
13458
|
return unchanged(next, "stale");
|
|
13367
13459
|
next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
|
|
13368
13460
|
return transition(next, "accepted");
|
|
@@ -13417,6 +13509,11 @@ function sameFence2(record4, fence) {
|
|
|
13417
13509
|
([key, value]) => value === fence[key]
|
|
13418
13510
|
);
|
|
13419
13511
|
}
|
|
13512
|
+
function hasDuplicateOperation(record4, requestId, operationId) {
|
|
13513
|
+
return record4.state.operationHistory.some(
|
|
13514
|
+
(operation) => operation.key === requestId || operation.id === operationId
|
|
13515
|
+
);
|
|
13516
|
+
}
|
|
13420
13517
|
function workerGroupAbsent(pid) {
|
|
13421
13518
|
try {
|
|
13422
13519
|
process.kill(-pid, 0);
|
|
@@ -13433,7 +13530,7 @@ function signalOwnedGroup(child, signal) {
|
|
|
13433
13530
|
if (error.code !== "ESRCH") throw error;
|
|
13434
13531
|
}
|
|
13435
13532
|
}
|
|
13436
|
-
async function runLifecycleWorker(request) {
|
|
13533
|
+
async function runLifecycleWorker(request, beforeAdmission) {
|
|
13437
13534
|
if (process.platform === "win32")
|
|
13438
13535
|
throw new Error("Lifecycle workers require POSIX process-group ownership.");
|
|
13439
13536
|
const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
|
|
@@ -13445,13 +13542,18 @@ async function runLifecycleWorker(request) {
|
|
|
13445
13542
|
execArgv: []
|
|
13446
13543
|
});
|
|
13447
13544
|
let ready = false;
|
|
13545
|
+
let closed = false;
|
|
13546
|
+
let registered = false;
|
|
13547
|
+
let dispatchAcknowledged = false;
|
|
13548
|
+
let sendAttempted = false;
|
|
13549
|
+
let readinessTimer;
|
|
13448
13550
|
let result;
|
|
13449
13551
|
let cancelled = false;
|
|
13450
13552
|
let forceTimer;
|
|
13451
13553
|
let monitor;
|
|
13452
13554
|
let failure;
|
|
13453
13555
|
const cancel = () => {
|
|
13454
|
-
if (cancelled) return;
|
|
13556
|
+
if (cancelled || closed) return;
|
|
13455
13557
|
cancelled = true;
|
|
13456
13558
|
try {
|
|
13457
13559
|
signalOwnedGroup(child, "SIGTERM");
|
|
@@ -13474,20 +13576,56 @@ async function runLifecycleWorker(request) {
|
|
|
13474
13576
|
child.once("error", (error) => {
|
|
13475
13577
|
failure = error;
|
|
13476
13578
|
});
|
|
13477
|
-
|
|
13579
|
+
readinessTimer = setTimeout(() => {
|
|
13580
|
+
failure = new Error("Lifecycle worker readiness timed out before dispatch.");
|
|
13581
|
+
cancel();
|
|
13582
|
+
}, 3e4);
|
|
13583
|
+
child.on("message", async (message) => {
|
|
13478
13584
|
if (!message || typeof message !== "object") return;
|
|
13479
13585
|
if ("ready" in message && message.ready === true && !ready) {
|
|
13480
13586
|
ready = true;
|
|
13587
|
+
if (readinessTimer) clearTimeout(readinessTimer);
|
|
13481
13588
|
try {
|
|
13589
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
13590
|
+
if (closed) return;
|
|
13482
13591
|
if (cancelled || !child.pid)
|
|
13483
13592
|
throw new Error("Lifecycle invocation was cancelled before dispatch.");
|
|
13484
13593
|
if (request.kind !== "stop") {
|
|
13485
13594
|
const birth = processBirthIdentity(child.pid);
|
|
13486
13595
|
if (!birth) throw new Error("Could not prove lifecycle worker incarnation.");
|
|
13487
13596
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13488
|
-
|
|
13597
|
+
beforeAdmission?.();
|
|
13598
|
+
if (!request.admission)
|
|
13599
|
+
throw new Error("Lifecycle operation admission is required.");
|
|
13600
|
+
if (hasDuplicateOperation(record4, request.requestId, request.operationId))
|
|
13601
|
+
throw new Error(
|
|
13602
|
+
"Lifecycle operation request conflicts with an existing operation."
|
|
13603
|
+
);
|
|
13604
|
+
if (!sameFence2(record4, request.fence))
|
|
13489
13605
|
throw new Error("Lifecycle intent changed before dispatch.");
|
|
13606
|
+
if (record4.worker || record4.revision !== request.admission.expectedRevision) {
|
|
13607
|
+
throw new LifecycleWorkerAdmissionBusyError();
|
|
13490
13608
|
}
|
|
13609
|
+
const admitted = stepReliability(
|
|
13610
|
+
record4.state,
|
|
13611
|
+
{
|
|
13612
|
+
...request.fence,
|
|
13613
|
+
type: "operation-request",
|
|
13614
|
+
kind: request.kind === "exec" ? "exec" : "ensure",
|
|
13615
|
+
key: request.requestId,
|
|
13616
|
+
operationId: request.operationId,
|
|
13617
|
+
profile: request.admission.profile,
|
|
13618
|
+
consumer: request.admission.consumer,
|
|
13619
|
+
runtimeRunning: request.admission.runtimeRunning,
|
|
13620
|
+
...request.admission.recoverInterruptedEnsure ? { recoverInterruptedEnsure: true } : {}
|
|
13621
|
+
},
|
|
13622
|
+
Date.now()
|
|
13623
|
+
);
|
|
13624
|
+
if (admitted.outcome !== "accepted")
|
|
13625
|
+
throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
|
|
13626
|
+
record4.state = admitted.state;
|
|
13627
|
+
record4.outcome = null;
|
|
13628
|
+
request.fence = reliabilityFence(record4.state);
|
|
13491
13629
|
record4.worker = {
|
|
13492
13630
|
id: request.workerId,
|
|
13493
13631
|
operationId: request.operationId,
|
|
@@ -13503,6 +13641,7 @@ async function runLifecycleWorker(request) {
|
|
|
13503
13641
|
throw new Error("Lifecycle dispatch is blocked.");
|
|
13504
13642
|
record4.state = transition2.state;
|
|
13505
13643
|
});
|
|
13644
|
+
registered = true;
|
|
13506
13645
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13507
13646
|
const transition2 = stepReliability(
|
|
13508
13647
|
record4.state,
|
|
@@ -13513,12 +13652,18 @@ async function runLifecycleWorker(request) {
|
|
|
13513
13652
|
},
|
|
13514
13653
|
Date.now()
|
|
13515
13654
|
);
|
|
13516
|
-
record4.state = transition2.state;
|
|
13517
13655
|
if (!transition2.effects.some((effect2) => effect2.kind === "launch")) {
|
|
13518
13656
|
throw new Error("Lifecycle dispatch persistence was not acknowledged.");
|
|
13519
13657
|
}
|
|
13658
|
+
record4.state = transition2.state;
|
|
13520
13659
|
});
|
|
13660
|
+
dispatchAcknowledged = true;
|
|
13521
13661
|
}
|
|
13662
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
13663
|
+
if (closed) return;
|
|
13664
|
+
beforeAdmission?.();
|
|
13665
|
+
if (cancelled) throw new Error("Lifecycle invocation was cancelled before dispatch.");
|
|
13666
|
+
sendAttempted = true;
|
|
13522
13667
|
child.send({ request }, (error) => {
|
|
13523
13668
|
if (error) {
|
|
13524
13669
|
failure = error;
|
|
@@ -13543,8 +13688,9 @@ async function runLifecycleWorker(request) {
|
|
|
13543
13688
|
}
|
|
13544
13689
|
});
|
|
13545
13690
|
child.once("close", () => {
|
|
13691
|
+
closed = true;
|
|
13546
13692
|
try {
|
|
13547
|
-
if (request.kind !== "stop") {
|
|
13693
|
+
if (request.kind !== "stop" && registered) {
|
|
13548
13694
|
updateReliabilityOperation(request.identity, (record4) => {
|
|
13549
13695
|
if (record4.worker?.id !== request.workerId || record4.worker.pid !== child.pid) return;
|
|
13550
13696
|
if (!result) {
|
|
@@ -13552,7 +13698,7 @@ async function runLifecycleWorker(request) {
|
|
|
13552
13698
|
record4.state,
|
|
13553
13699
|
{
|
|
13554
13700
|
...reliabilityFence(record4.state),
|
|
13555
|
-
type: "interrupted",
|
|
13701
|
+
type: request.kind === "exec" && dispatchAcknowledged && !sendAttempted ? "not-started" : "interrupted",
|
|
13556
13702
|
operationId: request.operationId
|
|
13557
13703
|
},
|
|
13558
13704
|
Date.now()
|
|
@@ -13586,6 +13732,7 @@ async function runLifecycleWorker(request) {
|
|
|
13586
13732
|
});
|
|
13587
13733
|
});
|
|
13588
13734
|
} finally {
|
|
13735
|
+
if (readinessTimer) clearTimeout(readinessTimer);
|
|
13589
13736
|
if (monitor) clearInterval(monitor);
|
|
13590
13737
|
if (forceTimer) clearTimeout(forceTimer);
|
|
13591
13738
|
process.removeListener("SIGINT", onSignal);
|
|
@@ -13595,7 +13742,7 @@ async function runLifecycleWorker(request) {
|
|
|
13595
13742
|
function newLifecycleIds() {
|
|
13596
13743
|
return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
|
|
13597
13744
|
}
|
|
13598
|
-
var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38;
|
|
13745
|
+
var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38, LifecycleWorkerAdmissionBusyError;
|
|
13599
13746
|
var init_reliability_worker = __esm({
|
|
13600
13747
|
"src/core/reliability-worker.ts"() {
|
|
13601
13748
|
"use strict";
|
|
@@ -13607,17 +13754,12 @@ var init_reliability_worker = __esm({
|
|
|
13607
13754
|
init_reliability_contract();
|
|
13608
13755
|
init_reliability_model();
|
|
13609
13756
|
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();
|
|
13757
|
+
LifecycleWorkerAdmissionBusyError = class extends Error {
|
|
13758
|
+
constructor() {
|
|
13759
|
+
super("Lifecycle admission lost to another operation.");
|
|
13760
|
+
this.name = "LifecycleWorkerAdmissionBusyError";
|
|
13761
|
+
}
|
|
13762
|
+
};
|
|
13621
13763
|
}
|
|
13622
13764
|
});
|
|
13623
13765
|
|
|
@@ -13626,7 +13768,7 @@ function sleep(ms) {
|
|
|
13626
13768
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13627
13769
|
}
|
|
13628
13770
|
function inspectRouterApi(protocol) {
|
|
13629
|
-
const result = (0,
|
|
13771
|
+
const result = (0, import_node_child_process22.spawnSync)(
|
|
13630
13772
|
"curl",
|
|
13631
13773
|
[
|
|
13632
13774
|
"--silent",
|
|
@@ -13653,6 +13795,11 @@ function inspectRouterApi(protocol) {
|
|
|
13653
13795
|
if (!Array.isArray(value)) {
|
|
13654
13796
|
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
13655
13797
|
}
|
|
13798
|
+
if (value.some((item) => !isRecord4(item) || typeof item.name !== "string" || !item.name))
|
|
13799
|
+
return {
|
|
13800
|
+
ok: false,
|
|
13801
|
+
details: `${protocol.toUpperCase()} router API returned malformed entries`
|
|
13802
|
+
};
|
|
13656
13803
|
return {
|
|
13657
13804
|
ok: true,
|
|
13658
13805
|
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
@@ -13665,6 +13812,9 @@ function inspectRouterApi(protocol) {
|
|
|
13665
13812
|
)
|
|
13666
13813
|
};
|
|
13667
13814
|
}
|
|
13815
|
+
function isRecord4(value) {
|
|
13816
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13817
|
+
}
|
|
13668
13818
|
function inspectExpectedRoutes(routes, expectation) {
|
|
13669
13819
|
const mismatched = [];
|
|
13670
13820
|
const failures = [];
|
|
@@ -13725,7 +13875,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
13725
13875
|
throw new Error(
|
|
13726
13876
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
13727
13877
|
);
|
|
13728
|
-
|
|
13878
|
+
import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
13729
13879
|
return withFileLock(
|
|
13730
13880
|
ROUTER_RELOAD_LOCK_FILE,
|
|
13731
13881
|
{
|
|
@@ -13756,12 +13906,12 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
13756
13906
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
13757
13907
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
13758
13908
|
}
|
|
13759
|
-
var
|
|
13909
|
+
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
13910
|
var init_traefik_route_health = __esm({
|
|
13761
13911
|
"src/core/traefik-route-health.ts"() {
|
|
13762
13912
|
"use strict";
|
|
13763
|
-
|
|
13764
|
-
|
|
13913
|
+
import_node_child_process22 = require("child_process");
|
|
13914
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
13765
13915
|
import_node_path39 = __toESM(require("path"));
|
|
13766
13916
|
init_file_lock();
|
|
13767
13917
|
init_host_routes();
|
|
@@ -13777,6 +13927,17 @@ var init_traefik_route_health = __esm({
|
|
|
13777
13927
|
}
|
|
13778
13928
|
});
|
|
13779
13929
|
|
|
13930
|
+
// src/core/managed-host-preparation.ts
|
|
13931
|
+
var import_node_child_process23, import_node_fs33;
|
|
13932
|
+
var init_managed_host_preparation = __esm({
|
|
13933
|
+
"src/core/managed-host-preparation.ts"() {
|
|
13934
|
+
"use strict";
|
|
13935
|
+
import_node_child_process23 = require("child_process");
|
|
13936
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
13937
|
+
init_repo_config();
|
|
13938
|
+
}
|
|
13939
|
+
});
|
|
13940
|
+
|
|
13780
13941
|
// src/core/workspace-ensure.ts
|
|
13781
13942
|
function resolveLinkedTarget(repoPath) {
|
|
13782
13943
|
const snapshots = getWorkspaceRegistrySnapshots();
|
|
@@ -13839,6 +14000,11 @@ var init_workspace_ensure = __esm({
|
|
|
13839
14000
|
});
|
|
13840
14001
|
|
|
13841
14002
|
// src/core/reliability-lifecycle.ts
|
|
14003
|
+
function matchesFence(record4, fence) {
|
|
14004
|
+
return Object.entries(reliabilityFence(record4.state)).every(
|
|
14005
|
+
([key, value]) => value === fence[key]
|
|
14006
|
+
);
|
|
14007
|
+
}
|
|
13842
14008
|
async function withLifecycleOperationLock(repoPath, operation) {
|
|
13843
14009
|
if (activeWorker && lockHeld) {
|
|
13844
14010
|
if (!sameWorkspacePath(repoPath, activeWorker.repoPath))
|
|
@@ -13886,6 +14052,23 @@ function reconcileDrained(record4) {
|
|
|
13886
14052
|
record4.worker = null;
|
|
13887
14053
|
}
|
|
13888
14054
|
}
|
|
14055
|
+
function reportBusyLifecycleWait(kind, operationId, waitedMs) {
|
|
14056
|
+
process.stderr.write(
|
|
14057
|
+
`Lifecycle ${kind} ${operationId} is waiting for the existing worker (${Math.ceil(waitedMs / 1e3)}s).
|
|
14058
|
+
`
|
|
14059
|
+
);
|
|
14060
|
+
}
|
|
14061
|
+
function installLifecycleWaitSignals(cancellation) {
|
|
14062
|
+
const onSignal = () => {
|
|
14063
|
+
cancellation.requested = true;
|
|
14064
|
+
};
|
|
14065
|
+
process.once("SIGINT", onSignal);
|
|
14066
|
+
process.once("SIGTERM", onSignal);
|
|
14067
|
+
return () => {
|
|
14068
|
+
process.removeListener("SIGINT", onSignal);
|
|
14069
|
+
process.removeListener("SIGTERM", onSignal);
|
|
14070
|
+
};
|
|
14071
|
+
}
|
|
13889
14072
|
async function superviseLifecycle(kind, repoPath, options = {}, command) {
|
|
13890
14073
|
repoPath = comparableWorkspacePath(repoPath);
|
|
13891
14074
|
if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
|
|
@@ -13898,58 +14081,115 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
|
|
|
13898
14081
|
provider: resolveWorkspaceRuntimeOrDefault(repoPath)
|
|
13899
14082
|
};
|
|
13900
14083
|
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") {
|
|
14084
|
+
const copiedOptions = { ...options };
|
|
14085
|
+
const copiedCommand = command ? [...command] : void 0;
|
|
14086
|
+
if (kind === "stop") {
|
|
14087
|
+
const fence = updateReliabilityOperation(identity2, (record4) => {
|
|
14088
|
+
reconcileDrained(record4);
|
|
13920
14089
|
stepRecord(record4, { ...reliabilityFence(record4.state), type: "stop" });
|
|
13921
|
-
|
|
13922
|
-
|
|
14090
|
+
return reliabilityFence(record4.state);
|
|
14091
|
+
});
|
|
14092
|
+
return runLifecycleWorker({ kind, repoPath, identity: identity2, ...ids, fence, options: copiedOptions });
|
|
14093
|
+
}
|
|
14094
|
+
const initial = readReliabilityOperation(identity2) ?? updateReliabilityOperation(identity2, (record4) => record4);
|
|
14095
|
+
const expectedFence = reliabilityFence(initial.state);
|
|
14096
|
+
const deadline = Date.now() + BUSY_LIFECYCLE_WAIT_MS;
|
|
14097
|
+
const cancellation = { requested: false };
|
|
14098
|
+
const removeSignals = installLifecycleWaitSignals(cancellation);
|
|
14099
|
+
let lastProgress = -Infinity;
|
|
14100
|
+
const assertWaiting = () => {
|
|
14101
|
+
if (cancellation.requested)
|
|
14102
|
+
throw new Error(`Lifecycle ${kind} ${ids.operationId} cancelled before dispatch.`);
|
|
14103
|
+
if (Date.now() >= deadline)
|
|
14104
|
+
throw new Error(
|
|
14105
|
+
`Lifecycle ${kind} ${ids.operationId} admission timed out; command was not launched.`
|
|
14106
|
+
);
|
|
14107
|
+
};
|
|
14108
|
+
try {
|
|
14109
|
+
for (; ; ) {
|
|
14110
|
+
assertWaiting();
|
|
14111
|
+
let previous = readReliabilityOperation(identity2);
|
|
14112
|
+
if (!previous || !matchesFence(previous, expectedFence))
|
|
13923
14113
|
throw new Error(
|
|
13924
|
-
"
|
|
14114
|
+
"Lifecycle intent changed while awaiting admission; command was not launched."
|
|
13925
14115
|
);
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
14116
|
+
if (hasDuplicateOperation(previous, ids.requestId, ids.operationId))
|
|
14117
|
+
throw new Error("Lifecycle request identity already exists; command was not replayed.");
|
|
14118
|
+
if (previous.worker) {
|
|
14119
|
+
const worker = previous.worker;
|
|
14120
|
+
const birth = processBirthIdentity(worker.pid);
|
|
14121
|
+
if (birth !== worker.birth && workerGroupAbsent(worker.pid)) {
|
|
14122
|
+
previous = updateReliabilityOperation(identity2, (record4) => {
|
|
14123
|
+
reconcileDrained(record4);
|
|
14124
|
+
return record4;
|
|
14125
|
+
});
|
|
14126
|
+
} else {
|
|
14127
|
+
if (!birth || birth !== worker.birth || previous.state.operation?.status === "COMPLETION_UNKNOWN")
|
|
14128
|
+
throw new Error(
|
|
14129
|
+
"Existing lifecycle worker identity or completion is uncertain; preserve its evidence."
|
|
14130
|
+
);
|
|
14131
|
+
const now = Date.now();
|
|
14132
|
+
if (now - lastProgress >= BUSY_LIFECYCLE_PROGRESS_MS) {
|
|
14133
|
+
reportBusyLifecycleWait(
|
|
14134
|
+
kind,
|
|
14135
|
+
ids.operationId,
|
|
14136
|
+
now - (deadline - BUSY_LIFECYCLE_WAIT_MS)
|
|
14137
|
+
);
|
|
14138
|
+
lastProgress = now;
|
|
14139
|
+
}
|
|
14140
|
+
await new Promise(
|
|
14141
|
+
(resolve) => setTimeout(resolve, Math.min(BUSY_LIFECYCLE_POLL_MS, deadline - now))
|
|
14142
|
+
);
|
|
14143
|
+
continue;
|
|
14144
|
+
}
|
|
14145
|
+
}
|
|
14146
|
+
if (!previous.worker && previous.state.operation?.status === "NOT_STARTED" && !previous.state.operation.drained)
|
|
14147
|
+
previous = updateReliabilityOperation(identity2, (record4) => {
|
|
14148
|
+
reconcileDrained(record4);
|
|
14149
|
+
return record4;
|
|
14150
|
+
});
|
|
14151
|
+
assertWaiting();
|
|
14152
|
+
const runtimeRunning = kind === "exec" && Boolean(resolveRunningWorkspaceContainer(repoPath));
|
|
14153
|
+
let retainedExecProof;
|
|
14154
|
+
if (kind === "exec" && identity2.provider === "devsy" && canExecAfterInterruptedEnsure(previous.state))
|
|
14155
|
+
retainedExecProof = withMutationLock(
|
|
14156
|
+
"Prove retained exec",
|
|
14157
|
+
repoPath,
|
|
14158
|
+
() => captureDevsyExecProof(repoPath)
|
|
14159
|
+
);
|
|
14160
|
+
assertWaiting();
|
|
14161
|
+
try {
|
|
14162
|
+
return await runLifecycleWorker(
|
|
14163
|
+
{
|
|
14164
|
+
kind,
|
|
14165
|
+
repoPath,
|
|
14166
|
+
identity: identity2,
|
|
14167
|
+
...ids,
|
|
14168
|
+
fence: { ...expectedFence },
|
|
14169
|
+
options: copiedOptions,
|
|
14170
|
+
...copiedCommand ? { command: copiedCommand } : {},
|
|
14171
|
+
...retainedExecProof ? { retainedExecProof } : {},
|
|
14172
|
+
admission: {
|
|
14173
|
+
expectedRevision: previous.revision,
|
|
14174
|
+
profile: copiedOptions.profile ?? previous.state.profile ?? "full",
|
|
14175
|
+
consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
|
|
14176
|
+
runtimeRunning,
|
|
14177
|
+
...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
|
|
14178
|
+
}
|
|
14179
|
+
},
|
|
14180
|
+
assertWaiting
|
|
14181
|
+
);
|
|
14182
|
+
} catch (error) {
|
|
14183
|
+
if (!(error instanceof LifecycleWorkerAdmissionBusyError)) throw error;
|
|
14184
|
+
assertWaiting();
|
|
14185
|
+
await new Promise((resolve) => setTimeout(resolve, BUSY_LIFECYCLE_POLL_MS));
|
|
14186
|
+
}
|
|
13938
14187
|
}
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
kind,
|
|
13943
|
-
repoPath,
|
|
13944
|
-
identity: identity2,
|
|
13945
|
-
...ids,
|
|
13946
|
-
fence,
|
|
13947
|
-
options,
|
|
13948
|
-
...command ? { command } : {},
|
|
13949
|
-
...retainedExecProof ? { retainedExecProof } : {}
|
|
13950
|
-
});
|
|
14188
|
+
} finally {
|
|
14189
|
+
removeSignals();
|
|
14190
|
+
}
|
|
13951
14191
|
}
|
|
13952
|
-
var import_node_path41, activeWorker, lockHeld;
|
|
14192
|
+
var import_node_path41, activeWorker, lockHeld, BUSY_LIFECYCLE_WAIT_MS, BUSY_LIFECYCLE_POLL_MS, BUSY_LIFECYCLE_PROGRESS_MS;
|
|
13953
14193
|
var init_reliability_lifecycle = __esm({
|
|
13954
14194
|
"src/core/reliability-lifecycle.ts"() {
|
|
13955
14195
|
"use strict";
|
|
@@ -13966,11 +14206,15 @@ var init_reliability_lifecycle = __esm({
|
|
|
13966
14206
|
init_reliability_model();
|
|
13967
14207
|
init_reliability_operation_store();
|
|
13968
14208
|
init_reliability_worker();
|
|
14209
|
+
init_traefik_route_health();
|
|
13969
14210
|
init_workspace();
|
|
13970
14211
|
init_workspace_ensure();
|
|
13971
14212
|
init_workspace_runtime();
|
|
13972
14213
|
init_reliability_context();
|
|
13973
14214
|
lockHeld = false;
|
|
14215
|
+
BUSY_LIFECYCLE_WAIT_MS = 30 * 60 * 1e3;
|
|
14216
|
+
BUSY_LIFECYCLE_POLL_MS = 250;
|
|
14217
|
+
BUSY_LIFECYCLE_PROGRESS_MS = 1e4;
|
|
13974
14218
|
}
|
|
13975
14219
|
});
|
|
13976
14220
|
|
|
@@ -15632,11 +15876,11 @@ var init_repo_devcontainer = __esm({
|
|
|
15632
15876
|
function fail3(message) {
|
|
15633
15877
|
throw new Error(message);
|
|
15634
15878
|
}
|
|
15635
|
-
function
|
|
15879
|
+
function isRecord5(value) {
|
|
15636
15880
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15637
15881
|
}
|
|
15638
15882
|
function requireRecord(value, label) {
|
|
15639
|
-
if (!
|
|
15883
|
+
if (!isRecord5(value)) fail3(`${label} must be a mapping.`);
|
|
15640
15884
|
return value;
|
|
15641
15885
|
}
|
|
15642
15886
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -17416,7 +17660,7 @@ var init_workspace_consumption = __esm({
|
|
|
17416
17660
|
});
|
|
17417
17661
|
|
|
17418
17662
|
// src/core/workspace-cleanup.ts
|
|
17419
|
-
function
|
|
17663
|
+
function isRecord6(value) {
|
|
17420
17664
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17421
17665
|
}
|
|
17422
17666
|
function isSha(value) {
|
|
@@ -17560,14 +17804,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
17560
17804
|
if (!Array.isArray(value)) return void 0;
|
|
17561
17805
|
const changes = [];
|
|
17562
17806
|
for (const item of value) {
|
|
17563
|
-
if (!
|
|
17564
|
-
const repository =
|
|
17565
|
-
const headRepository =
|
|
17807
|
+
if (!isRecord6(item)) return void 0;
|
|
17808
|
+
const repository = isRecord6(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
17809
|
+
const headRepository = isRecord6(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
17566
17810
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
17567
17811
|
continue;
|
|
17568
17812
|
}
|
|
17569
17813
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
17570
|
-
const mergeCommit =
|
|
17814
|
+
const mergeCommit = isRecord6(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
17571
17815
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
17572
17816
|
changes.push({
|
|
17573
17817
|
sourceBranch: branch,
|
|
@@ -17584,14 +17828,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
17584
17828
|
if (!Array.isArray(value)) return void 0;
|
|
17585
17829
|
const changes = [];
|
|
17586
17830
|
for (const item of value) {
|
|
17587
|
-
if (!
|
|
17831
|
+
if (!isRecord6(item)) return void 0;
|
|
17588
17832
|
const sourceProjectId = item.source_project_id;
|
|
17589
17833
|
const targetProjectId = item.target_project_id;
|
|
17590
17834
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
17591
17835
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
17592
17836
|
continue;
|
|
17593
17837
|
}
|
|
17594
|
-
const diffRefs =
|
|
17838
|
+
const diffRefs = isRecord6(item.diff_refs) ? item.diff_refs : void 0;
|
|
17595
17839
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
17596
17840
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
17597
17841
|
changes.push({
|
|
@@ -18413,8 +18657,24 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
18413
18657
|
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
18414
18658
|
);
|
|
18415
18659
|
}
|
|
18416
|
-
const
|
|
18417
|
-
|
|
18660
|
+
const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
|
|
18661
|
+
let absent = false;
|
|
18662
|
+
const remove = () => {
|
|
18663
|
+
if (retained?.stopBaseline) {
|
|
18664
|
+
absent = proveManagedStop(retained).status === "proven-absent";
|
|
18665
|
+
if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
|
|
18666
|
+
(route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
|
|
18667
|
+
))
|
|
18668
|
+
throw new Error("Absent stop contains routes outside the retained desired set.");
|
|
18669
|
+
}
|
|
18670
|
+
claimLifecycleEffect();
|
|
18671
|
+
return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
18672
|
+
};
|
|
18673
|
+
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
|
|
18674
|
+
await ensureTraefikRoutesRemoved([
|
|
18675
|
+
...routes,
|
|
18676
|
+
...absent && retained ? managedStopRouteReferences(retained) : []
|
|
18677
|
+
]);
|
|
18418
18678
|
if (!quiet) {
|
|
18419
18679
|
process.stdout.write(
|
|
18420
18680
|
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
@@ -18425,6 +18685,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
18425
18685
|
...mutation.status === "changed" ? { devpodId } : {},
|
|
18426
18686
|
freedRoutes: routes.length,
|
|
18427
18687
|
providerChanged: mutation.status === "changed",
|
|
18688
|
+
...absent ? { runtimeAbsent: true } : {},
|
|
18428
18689
|
workspace: resolved.workspace
|
|
18429
18690
|
};
|
|
18430
18691
|
}
|
|
@@ -18487,7 +18748,9 @@ var init_workspace_lifecycle = __esm({
|
|
|
18487
18748
|
import_node_path49 = __toESM(require("path"));
|
|
18488
18749
|
init_devpod_mutation();
|
|
18489
18750
|
init_devpod_workspaces();
|
|
18751
|
+
init_devsy_mutation();
|
|
18490
18752
|
init_managed_runtime_state();
|
|
18753
|
+
init_managed_stop_recovery();
|
|
18491
18754
|
init_reliability_lifecycle();
|
|
18492
18755
|
init_repo_config();
|
|
18493
18756
|
init_route_state();
|
|
@@ -18643,7 +18906,7 @@ var init_version = __esm({
|
|
|
18643
18906
|
|
|
18644
18907
|
// src/cli.ts
|
|
18645
18908
|
var import_commander = require("commander");
|
|
18646
|
-
var CLI_VERSION = true ? "0.0.
|
|
18909
|
+
var CLI_VERSION = true ? "0.0.64" : "0.0.0-dev";
|
|
18647
18910
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
18648
18911
|
function withErrorHandling(action2) {
|
|
18649
18912
|
return async (...args) => {
|