@devrouter/cli 0.0.61 → 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 +917 -659
- package/dist/devrouter.js +600 -177
- package/package.json +1 -1
- package/upgrade-prompts/0.0.62.md +35 -0
- package/upgrade-prompts/0.0.63.md +36 -0
|
@@ -936,6 +936,28 @@ function inspectManagedStopRunnerId(endpoint, containerId) {
|
|
|
936
936
|
throw new Error("Provider container binding is unavailable.");
|
|
937
937
|
return value;
|
|
938
938
|
}
|
|
939
|
+
function inspectProviderRunnerContainers(endpoint, runnerId) {
|
|
940
|
+
if (!runnerId || runnerId.length > 4096 || /[\r\n\0]/.test(runnerId))
|
|
941
|
+
throw new Error("Provider runner identity is malformed.");
|
|
942
|
+
const ids = parseDockerLines(
|
|
943
|
+
runManagedStopDocker(
|
|
944
|
+
[
|
|
945
|
+
"ps",
|
|
946
|
+
"-a",
|
|
947
|
+
"--no-trunc",
|
|
948
|
+
"--filter",
|
|
949
|
+
`label=dev.containers.id=${runnerId}`,
|
|
950
|
+
"--format",
|
|
951
|
+
"{{.ID}}"
|
|
952
|
+
],
|
|
953
|
+
endpoint
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
if (ids.length > 256) throw new Error("Provider runner population exceeds its bound.");
|
|
957
|
+
ids.forEach(assertFullContainerId);
|
|
958
|
+
assertUniqueContainerIds(ids);
|
|
959
|
+
return ids;
|
|
960
|
+
}
|
|
939
961
|
function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
940
962
|
const ids = parseDockerLines(
|
|
941
963
|
runManagedStopDocker(
|
|
@@ -956,6 +978,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
|
956
978
|
assertUniqueContainerIds(ids);
|
|
957
979
|
return ids;
|
|
958
980
|
}
|
|
981
|
+
function hasExactManagedStopMissingObjectError(stderr, id2) {
|
|
982
|
+
if (typeof stderr !== "string") return false;
|
|
983
|
+
const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
|
|
984
|
+
return line.toLowerCase() === `error: no such object: ${id2}`;
|
|
985
|
+
}
|
|
986
|
+
function assertManagedStopContainersAbsent(endpoint, ids) {
|
|
987
|
+
assertManagedStopEndpoint(endpoint);
|
|
988
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
|
|
989
|
+
throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
|
|
990
|
+
}
|
|
991
|
+
ids.forEach(assertFullContainerId);
|
|
992
|
+
assertUniqueContainerIds(ids);
|
|
993
|
+
const env = { ...process.env };
|
|
994
|
+
delete env.DOCKER_CONTEXT;
|
|
995
|
+
delete env.DOCKER_HOST;
|
|
996
|
+
for (const id2 of ids) {
|
|
997
|
+
const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id2], {
|
|
998
|
+
env,
|
|
999
|
+
encoding: "utf-8",
|
|
1000
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
1001
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
1002
|
+
});
|
|
1003
|
+
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id2)) {
|
|
1004
|
+
throw new Error("Managed stop Docker inspection did not prove container absence.");
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
959
1008
|
function stopPinnedManagedContainer(endpoint, containerId) {
|
|
960
1009
|
assertFullContainerId(containerId);
|
|
961
1010
|
runManagedStopDocker(["stop", containerId], endpoint, 3e4);
|
|
@@ -966,11 +1015,13 @@ var import_node_child_process8 = require("child_process");
|
|
|
966
1015
|
|
|
967
1016
|
// src/core/devpod-registry.ts
|
|
968
1017
|
var import_node_child_process4 = require("child_process");
|
|
969
|
-
function listDevpodWorkspacesRaw() {
|
|
1018
|
+
function listDevpodWorkspacesRaw(options = {}) {
|
|
970
1019
|
const result = (0, import_node_child_process4.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
|
|
971
1020
|
encoding: "utf-8"
|
|
972
1021
|
});
|
|
973
|
-
if (result.
|
|
1022
|
+
if (options.allowMissingExecutable && result.error?.code === "ENOENT")
|
|
1023
|
+
return [];
|
|
1024
|
+
if (result.error || result.status !== 0) {
|
|
974
1025
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
975
1026
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
976
1027
|
}
|
|
@@ -1046,6 +1097,10 @@ function listDevsyWorkspaces() {
|
|
|
1046
1097
|
...typeof candidate.uid === "string" ? { uid: candidate.uid } : {},
|
|
1047
1098
|
...typeof candidate.context === "string" ? { context: candidate.context } : {}
|
|
1048
1099
|
};
|
|
1100
|
+
const provider = candidate.provider;
|
|
1101
|
+
if (provider && typeof provider.name === "string") workspace.providerName = provider.name;
|
|
1102
|
+
if (typeof provider?.options?.DOCKER_PATH?.value === "string")
|
|
1103
|
+
workspace.dockerPathOption = provider.options.DOCKER_PATH.value;
|
|
1049
1104
|
if ("lastUsed" in candidate) {
|
|
1050
1105
|
if (typeof candidate.lastUsed === "string") {
|
|
1051
1106
|
workspace.lastUsed = candidate.lastUsed;
|
|
@@ -1627,29 +1682,101 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
1627
1682
|
// src/core/devsy-exec.ts
|
|
1628
1683
|
var import_node_child_process22 = require("child_process");
|
|
1629
1684
|
|
|
1630
|
-
// src/core/
|
|
1631
|
-
var
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1685
|
+
// src/core/devsy-exec-proof.ts
|
|
1686
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
1687
|
+
var import_node_os2 = __toESM(require("os"));
|
|
1688
|
+
var import_node_path7 = __toESM(require("path"));
|
|
1689
|
+
var import_node_util = require("util");
|
|
1690
|
+
function proveLocalDockerSelection(workspace) {
|
|
1691
|
+
const name = workspace.providerName;
|
|
1692
|
+
const context = workspace.context;
|
|
1693
|
+
if (!name || !context || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(name) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(context))
|
|
1694
|
+
throw new Error("Retained exec provider configuration identity is unavailable.");
|
|
1695
|
+
const file = import_node_path7.default.join(
|
|
1696
|
+
process.env.DEVSY_HOME || import_node_path7.default.join(import_node_os2.default.homedir(), ".devsy"),
|
|
1697
|
+
"contexts",
|
|
1698
|
+
context,
|
|
1699
|
+
"providers",
|
|
1700
|
+
name,
|
|
1701
|
+
"provider.json"
|
|
1702
|
+
);
|
|
1703
|
+
try {
|
|
1704
|
+
if (import_node_fs6.default.statSync(file).size > 1024 * 1024) throw new Error();
|
|
1705
|
+
const config = JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
|
|
1706
|
+
const driver = config.agent?.driver ?? "docker";
|
|
1707
|
+
const configuredPath = config.agent?.docker?.path ?? "";
|
|
1708
|
+
if (typeof configuredPath !== "string") throw new Error();
|
|
1709
|
+
const command = configuredPath === "${DOCKER_PATH}" || configuredPath === "$DOCKER_PATH" ? workspace.dockerPathOption || process.env.DOCKER_PATH || "docker" : configuredPath || "docker";
|
|
1710
|
+
if (config.name !== name || driver !== "docker" || command !== "docker") throw new Error();
|
|
1711
|
+
} catch {
|
|
1712
|
+
throw new Error("Retained exec requires a provider configured for the local Docker command.");
|
|
1637
1713
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1714
|
+
return name;
|
|
1715
|
+
}
|
|
1716
|
+
function captureDevsyExecProof(repoPath) {
|
|
1717
|
+
repoPath = comparableWorkspacePath(repoPath);
|
|
1718
|
+
const registry = listDevsyWorkspaces();
|
|
1719
|
+
const workspace = selectDevsyWorkspace(registry, repoPath);
|
|
1720
|
+
if (!workspace?.uid || !workspace.context || workspace.source.container || inspectDevsyWorkspaceOwnership(registry, workspace.id, repoPath).status !== "owned" || inspectDevsyRuntimeStatus(workspace.id) !== "running")
|
|
1721
|
+
throw new Error("Retained exec requires one exact running Devsy registration.");
|
|
1722
|
+
const providerName = proveLocalDockerSelection(workspace);
|
|
1723
|
+
const endpoint = resolveManagedStopEndpoint();
|
|
1724
|
+
if (!supportsManagedStopBaseline(endpoint))
|
|
1725
|
+
throw new Error("Retained exec requires an exact local Docker endpoint.");
|
|
1726
|
+
const daemon = inspectManagedStopDaemon(endpoint);
|
|
1727
|
+
const candidates = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
|
|
1728
|
+
(container) => container.state.Running
|
|
1729
|
+
);
|
|
1730
|
+
if (candidates.length !== 1)
|
|
1731
|
+
throw new Error("Retained exec requires one exact running primary container.");
|
|
1732
|
+
const candidate = candidates[0];
|
|
1733
|
+
const project = candidate.labels["com.docker.compose.project"];
|
|
1734
|
+
if (!project) throw new Error("Retained exec primary has no Compose identity.");
|
|
1735
|
+
const pinned = workspaceAppContainers(
|
|
1736
|
+
inspectManagedStopContainers(project, endpoint),
|
|
1737
|
+
repoPath
|
|
1738
|
+
).filter((container) => container.state.Running);
|
|
1739
|
+
if (pinned.length !== 1 || pinned[0].id !== candidate.id)
|
|
1740
|
+
throw new Error("Retained exec primary changed during inspection.");
|
|
1741
|
+
const mounts = pinned[0].mounts.filter(
|
|
1742
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
1743
|
+
);
|
|
1744
|
+
if (mounts.length !== 1 || !mounts[0].Destination.startsWith("/"))
|
|
1745
|
+
throw new Error("Retained exec requires one exact source mount.");
|
|
1746
|
+
const uidBytes = Buffer.byteLength(workspace.uid);
|
|
1747
|
+
const runner = uidBytes === 16 || uidBytes === 40 ? workspace.uid : workspace.id;
|
|
1748
|
+
const runnerContainers = inspectProviderRunnerContainers(endpoint, runner);
|
|
1749
|
+
if (runnerContainers.length !== 1 || runnerContainers[0] !== candidate.id)
|
|
1750
|
+
throw new Error("Retained exec runner does not select the exact primary container.");
|
|
1751
|
+
if (inspectManagedStopRunnerId(endpoint, candidate.id) !== runner || inspectManagedStopDaemon(endpoint) !== daemon)
|
|
1752
|
+
throw new Error("Retained exec provider or Docker identity changed.");
|
|
1753
|
+
return {
|
|
1754
|
+
repoPath,
|
|
1755
|
+
id: workspace.id,
|
|
1756
|
+
uid: workspace.uid,
|
|
1757
|
+
context: workspace.context,
|
|
1758
|
+
providerName,
|
|
1759
|
+
endpoint,
|
|
1760
|
+
daemon,
|
|
1761
|
+
containerId: candidate.id,
|
|
1762
|
+
workspacePath: mounts[0].Destination
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function revalidateDevsyExecProof(repoPath, proof) {
|
|
1766
|
+
if (!(0, import_node_util.isDeepStrictEqual)(captureDevsyExecProof(repoPath), proof))
|
|
1767
|
+
throw new Error("Retained exec identity changed before command launch.");
|
|
1768
|
+
}
|
|
1642
1769
|
|
|
1643
1770
|
// src/core/devsy-mutation.ts
|
|
1644
1771
|
var import_node_child_process14 = require("child_process");
|
|
1645
|
-
var
|
|
1646
|
-
var
|
|
1772
|
+
var import_node_fs16 = __toESM(require("fs"));
|
|
1773
|
+
var import_node_path21 = __toESM(require("path"));
|
|
1647
1774
|
|
|
1648
1775
|
// src/core/devsy-agent.ts
|
|
1649
1776
|
var import_node_child_process9 = require("child_process");
|
|
1650
1777
|
var import_node_crypto4 = require("crypto");
|
|
1651
|
-
var
|
|
1652
|
-
var
|
|
1778
|
+
var import_node_fs7 = __toESM(require("fs"));
|
|
1779
|
+
var import_node_path8 = __toESM(require("path"));
|
|
1653
1780
|
var SUPPORTED_DEVSY_VERSION = "1.16.2";
|
|
1654
1781
|
var DEVSY_AGENT_SETUP_COMMAND = "devrouter setup --yes --workspace-runtime devsy";
|
|
1655
1782
|
var DEVSY_AGENT_ASSETS = [
|
|
@@ -1675,7 +1802,7 @@ var DevsyAgentReadinessError = class extends Error {
|
|
|
1675
1802
|
this.name = "DevsyAgentReadinessError";
|
|
1676
1803
|
}
|
|
1677
1804
|
};
|
|
1678
|
-
var DEVSY_AGENT_CACHE_LOCK =
|
|
1805
|
+
var DEVSY_AGENT_CACHE_LOCK = import_node_path8.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
|
|
1679
1806
|
function parseVersion(output) {
|
|
1680
1807
|
return output?.match(
|
|
1681
1808
|
/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
|
|
@@ -1699,14 +1826,14 @@ function nativeAssetName(platform, arch) {
|
|
|
1699
1826
|
return void 0;
|
|
1700
1827
|
}
|
|
1701
1828
|
function managedBinaryPath(cacheRoot, asset) {
|
|
1702
|
-
return
|
|
1829
|
+
return import_node_path8.default.join(cacheRoot, `v${SUPPORTED_DEVSY_VERSION}`, asset.name);
|
|
1703
1830
|
}
|
|
1704
1831
|
function hashOpenFile(file) {
|
|
1705
1832
|
const digest = (0, import_node_crypto4.createHash)("sha256");
|
|
1706
1833
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
1707
1834
|
let position = 0;
|
|
1708
1835
|
for (; ; ) {
|
|
1709
|
-
const bytesRead =
|
|
1836
|
+
const bytesRead = import_node_fs7.default.readSync(file, buffer, 0, buffer.length, position);
|
|
1710
1837
|
if (bytesRead === 0) break;
|
|
1711
1838
|
digest.update(buffer.subarray(0, bytesRead));
|
|
1712
1839
|
position += bytesRead;
|
|
@@ -1716,8 +1843,8 @@ function hashOpenFile(file) {
|
|
|
1716
1843
|
function inspectBinary(binaryPath, assets, missingState) {
|
|
1717
1844
|
let file;
|
|
1718
1845
|
try {
|
|
1719
|
-
file =
|
|
1720
|
-
const stat =
|
|
1846
|
+
file = import_node_fs7.default.openSync(binaryPath, import_node_fs7.default.constants.O_RDONLY | import_node_fs7.default.constants.O_NOFOLLOW);
|
|
1847
|
+
const stat = import_node_fs7.default.fstatSync(file);
|
|
1721
1848
|
if (!stat.isFile()) {
|
|
1722
1849
|
return { state: "invalid", reason: "the selected source is not a regular file" };
|
|
1723
1850
|
}
|
|
@@ -1732,7 +1859,7 @@ function inspectBinary(binaryPath, assets, missingState) {
|
|
|
1732
1859
|
const code = error.code;
|
|
1733
1860
|
return code === "ENOENT" ? { state: missingState, reason: "the selected source is missing" } : { state: "invalid", reason: "the selected source is not a readable regular file" };
|
|
1734
1861
|
} finally {
|
|
1735
|
-
if (file !== void 0)
|
|
1862
|
+
if (file !== void 0) import_node_fs7.default.closeSync(file);
|
|
1736
1863
|
}
|
|
1737
1864
|
}
|
|
1738
1865
|
function resolvedOptions(options) {
|
|
@@ -1741,7 +1868,7 @@ function resolvedOptions(options) {
|
|
|
1741
1868
|
platform: options.platform ?? process.platform,
|
|
1742
1869
|
arch: options.arch ?? process.arch,
|
|
1743
1870
|
versionOutput: options.versionOutput ?? installedVersionOutput(),
|
|
1744
|
-
cacheRoot: options.cacheRoot ??
|
|
1871
|
+
cacheRoot: options.cacheRoot ?? import_node_path8.default.join(CACHE_DIR, "devsy", "agents"),
|
|
1745
1872
|
lockPath: options.lockPath ?? DEVSY_AGENT_CACHE_LOCK,
|
|
1746
1873
|
assets: options.assets ?? DEVSY_AGENT_ASSETS,
|
|
1747
1874
|
nativeAssetName: options.nativeAssetName
|
|
@@ -1802,19 +1929,19 @@ function requireReadyDevsyAgent(options = {}) {
|
|
|
1802
1929
|
}
|
|
1803
1930
|
|
|
1804
1931
|
// src/core/managed-devsy-stop.ts
|
|
1805
|
-
var
|
|
1806
|
-
var
|
|
1932
|
+
var import_node_os4 = __toESM(require("os"));
|
|
1933
|
+
var import_node_path20 = __toESM(require("path"));
|
|
1807
1934
|
|
|
1808
1935
|
// src/core/devcontainer-profile.ts
|
|
1809
1936
|
var import_node_child_process10 = require("child_process");
|
|
1810
1937
|
var import_node_crypto5 = require("crypto");
|
|
1811
|
-
var
|
|
1812
|
-
var
|
|
1938
|
+
var import_node_fs9 = __toESM(require("fs"));
|
|
1939
|
+
var import_node_path11 = __toESM(require("path"));
|
|
1813
1940
|
var import_yaml = __toESM(require("yaml"));
|
|
1814
1941
|
|
|
1815
1942
|
// src/core/devcontainer-config.ts
|
|
1816
|
-
var
|
|
1817
|
-
var
|
|
1943
|
+
var import_node_fs8 = __toESM(require("fs"));
|
|
1944
|
+
var import_node_path9 = __toESM(require("path"));
|
|
1818
1945
|
var import_jsonc_parser = require("jsonc-parser");
|
|
1819
1946
|
function isObject(value) {
|
|
1820
1947
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1837,11 +1964,11 @@ function parseDevcontainerConfig(contents, sourcePath) {
|
|
|
1837
1964
|
return parsed;
|
|
1838
1965
|
}
|
|
1839
1966
|
function readDevcontainerConfig(repoPath) {
|
|
1840
|
-
const sourcePath =
|
|
1841
|
-
if (!
|
|
1967
|
+
const sourcePath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
1968
|
+
if (!import_node_fs8.default.existsSync(sourcePath) || !import_node_fs8.default.lstatSync(sourcePath).isFile()) {
|
|
1842
1969
|
throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
|
|
1843
1970
|
}
|
|
1844
|
-
const sourceContents =
|
|
1971
|
+
const sourceContents = import_node_fs8.default.readFileSync(sourcePath, "utf-8");
|
|
1845
1972
|
return {
|
|
1846
1973
|
sourcePath,
|
|
1847
1974
|
sourceContents,
|
|
@@ -1859,11 +1986,11 @@ function assertManagedDevcontainerLifecycle(repoPath) {
|
|
|
1859
1986
|
}
|
|
1860
1987
|
|
|
1861
1988
|
// src/core/paths.ts
|
|
1862
|
-
var
|
|
1989
|
+
var import_node_path10 = __toESM(require("path"));
|
|
1863
1990
|
function assertPathWithinRepo(filePath, repoRoot, label) {
|
|
1864
|
-
const resolvedRoot =
|
|
1865
|
-
const resolved =
|
|
1866
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot +
|
|
1991
|
+
const resolvedRoot = import_node_path10.default.resolve(repoRoot);
|
|
1992
|
+
const resolved = import_node_path10.default.resolve(repoRoot, filePath);
|
|
1993
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path10.default.sep)) {
|
|
1867
1994
|
throw new Error(`${label} path '${filePath}' escapes the repository root.`);
|
|
1868
1995
|
}
|
|
1869
1996
|
return resolved;
|
|
@@ -1917,14 +2044,14 @@ function resolveComposeFiles(source, repoPath, linked) {
|
|
|
1917
2044
|
"managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
|
|
1918
2045
|
);
|
|
1919
2046
|
}
|
|
1920
|
-
const directory =
|
|
2047
|
+
const directory = import_node_path11.default.join(repoPath, ".devcontainer");
|
|
1921
2048
|
const files = references.map((reference) => {
|
|
1922
2049
|
const resolved2 = assertPathWithinRepo(
|
|
1923
2050
|
resolveComposeReference(reference, linked),
|
|
1924
2051
|
directory,
|
|
1925
2052
|
"dockerComposeFile"
|
|
1926
2053
|
);
|
|
1927
|
-
if (!
|
|
2054
|
+
if (!import_node_fs9.default.existsSync(resolved2) || !import_node_fs9.default.lstatSync(resolved2).isFile()) {
|
|
1928
2055
|
throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
|
|
1929
2056
|
}
|
|
1930
2057
|
return resolved2;
|
|
@@ -1932,7 +2059,7 @@ function resolveComposeFiles(source, repoPath, linked) {
|
|
|
1932
2059
|
for (const file of files) {
|
|
1933
2060
|
let parsed;
|
|
1934
2061
|
try {
|
|
1935
|
-
parsed = import_yaml.default.parse(
|
|
2062
|
+
parsed = import_yaml.default.parse(import_node_fs9.default.readFileSync(file, "utf-8"));
|
|
1936
2063
|
} catch (error) {
|
|
1937
2064
|
throw new Error(
|
|
1938
2065
|
`Could not parse managed Dev Container compose file '${file}': ${String(error)}`
|
|
@@ -1981,12 +2108,12 @@ function assertIgnoredGeneratedPath(repoPath, generatedPath) {
|
|
|
1981
2108
|
}
|
|
1982
2109
|
}
|
|
1983
2110
|
function assertGeneratedPathOwnership(generatedPath) {
|
|
1984
|
-
if (!
|
|
1985
|
-
const stat =
|
|
2111
|
+
if (!import_node_fs9.default.existsSync(generatedPath)) return;
|
|
2112
|
+
const stat = import_node_fs9.default.lstatSync(generatedPath);
|
|
1986
2113
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1987
2114
|
throw new Error(`Managed Dev Container path '${generatedPath}' is not a regular file.`);
|
|
1988
2115
|
}
|
|
1989
|
-
const firstLine =
|
|
2116
|
+
const firstLine = import_node_fs9.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
|
|
1990
2117
|
if (firstLine !== MANAGED_DEVCONTAINER_MARKER) {
|
|
1991
2118
|
throw new Error(
|
|
1992
2119
|
`Managed Dev Container path '${generatedPath}' exists without the devrouter ownership marker.`
|
|
@@ -2062,7 +2189,7 @@ function inspectManagedDevcontainerConfig(options) {
|
|
|
2062
2189
|
const contents = `${MANAGED_DEVCONTAINER_MARKER}
|
|
2063
2190
|
${JSON.stringify(effective, null, 2)}
|
|
2064
2191
|
`;
|
|
2065
|
-
const generatedPath =
|
|
2192
|
+
const generatedPath = import_node_path11.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
|
|
2066
2193
|
assertIgnoredGeneratedPath(options.repoPath, generatedPath);
|
|
2067
2194
|
assertGeneratedPathOwnership(generatedPath);
|
|
2068
2195
|
return {
|
|
@@ -2089,13 +2216,13 @@ function writeManagedDevcontainerConfig(plan) {
|
|
|
2089
2216
|
function inspectManagedDevcontainerGeneratedConfig(plan) {
|
|
2090
2217
|
let stat;
|
|
2091
2218
|
try {
|
|
2092
|
-
stat =
|
|
2219
|
+
stat = import_node_fs9.default.lstatSync(plan.generatedPath);
|
|
2093
2220
|
} catch (error) {
|
|
2094
2221
|
if (error.code === "ENOENT") return { status: "missing" };
|
|
2095
2222
|
throw error;
|
|
2096
2223
|
}
|
|
2097
2224
|
if (!stat.isFile() || stat.isSymbolicLink()) return { status: "foreign" };
|
|
2098
|
-
const contents =
|
|
2225
|
+
const contents = import_node_fs9.default.readFileSync(plan.generatedPath, "utf-8");
|
|
2099
2226
|
if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
|
|
2100
2227
|
`)) {
|
|
2101
2228
|
return { status: "foreign" };
|
|
@@ -2107,17 +2234,17 @@ function inspectManagedDevcontainerGeneratedConfig(plan) {
|
|
|
2107
2234
|
};
|
|
2108
2235
|
}
|
|
2109
2236
|
function removeManagedDevcontainerConfig(plan) {
|
|
2110
|
-
if (!
|
|
2111
|
-
const stat =
|
|
2237
|
+
if (!import_node_fs9.default.existsSync(plan.generatedPath)) return;
|
|
2238
|
+
const stat = import_node_fs9.default.lstatSync(plan.generatedPath);
|
|
2112
2239
|
if (!stat.isFile()) {
|
|
2113
2240
|
throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not a regular file.`);
|
|
2114
2241
|
}
|
|
2115
|
-
const contents =
|
|
2242
|
+
const contents = import_node_fs9.default.readFileSync(plan.generatedPath, "utf-8");
|
|
2116
2243
|
if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
|
|
2117
2244
|
`)) {
|
|
2118
2245
|
throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not devrouter-owned.`);
|
|
2119
2246
|
}
|
|
2120
|
-
|
|
2247
|
+
import_node_fs9.default.unlinkSync(plan.generatedPath);
|
|
2121
2248
|
}
|
|
2122
2249
|
function assertSafeContainerId(containerId) {
|
|
2123
2250
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
|
|
@@ -2141,8 +2268,8 @@ function managedComposeEnvironment(workspace) {
|
|
|
2141
2268
|
}
|
|
2142
2269
|
function assertReadableComposeFile(file) {
|
|
2143
2270
|
try {
|
|
2144
|
-
if (!
|
|
2145
|
-
|
|
2271
|
+
if (!import_node_fs9.default.statSync(file).isFile()) throw new Error("not a regular file");
|
|
2272
|
+
import_node_fs9.default.accessSync(file, import_node_fs9.default.constants.R_OK);
|
|
2146
2273
|
} catch {
|
|
2147
2274
|
throw new Error("Managed container Compose configuration contains an unreadable file.");
|
|
2148
2275
|
}
|
|
@@ -2274,8 +2401,8 @@ function stopExactManagedService(containerId, service, options) {
|
|
|
2274
2401
|
}
|
|
2275
2402
|
|
|
2276
2403
|
// src/core/managed-compose-population.ts
|
|
2277
|
-
var
|
|
2278
|
-
var
|
|
2404
|
+
var import_node_fs10 = __toESM(require("fs"));
|
|
2405
|
+
var import_node_path12 = __toESM(require("path"));
|
|
2279
2406
|
function proveManagedComposePopulation(options) {
|
|
2280
2407
|
const { plan, repoPath, composeProject, featureDirectory, devsyRoot, containers } = options;
|
|
2281
2408
|
const services = /* @__PURE__ */ new Set();
|
|
@@ -2289,13 +2416,13 @@ function proveManagedComposePopulation(options) {
|
|
|
2289
2416
|
}
|
|
2290
2417
|
services.add(service);
|
|
2291
2418
|
const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
|
|
2292
|
-
if (files.some((file) => !file || !
|
|
2419
|
+
if (files.some((file) => !file || !import_node_path12.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path12.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
|
|
2293
2420
|
throw new Error("Managed stop Compose file identity changed.");
|
|
2294
2421
|
}
|
|
2295
2422
|
for (const file of files.slice(plan.composeFiles.length)) {
|
|
2296
|
-
if (
|
|
2297
|
-
|
|
2298
|
-
) ||
|
|
2423
|
+
if (import_node_path12.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
2424
|
+
import_node_path12.default.basename(file)
|
|
2425
|
+
) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(devsyRoot), import_node_path12.default.relative(devsyRoot, file))) {
|
|
2299
2426
|
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
2300
2427
|
}
|
|
2301
2428
|
}
|
|
@@ -2316,11 +2443,11 @@ function proveManagedComposePopulation(options) {
|
|
|
2316
2443
|
|
|
2317
2444
|
// src/core/managed-runtime-state.ts
|
|
2318
2445
|
var import_node_crypto6 = require("crypto");
|
|
2319
|
-
var
|
|
2320
|
-
var
|
|
2446
|
+
var import_node_fs11 = __toESM(require("fs"));
|
|
2447
|
+
var import_node_path14 = __toESM(require("path"));
|
|
2321
2448
|
|
|
2322
2449
|
// src/core/managed-stop-baseline.ts
|
|
2323
|
-
var
|
|
2450
|
+
var import_node_path13 = __toESM(require("path"));
|
|
2324
2451
|
function record(value, keys2) {
|
|
2325
2452
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== keys2.length || Object.keys(value).some((key) => !keys2.includes(key))) {
|
|
2326
2453
|
throw new Error("Invalid managed stop baseline fields.");
|
|
@@ -2335,7 +2462,7 @@ function text(value, empty = false) {
|
|
|
2335
2462
|
}
|
|
2336
2463
|
function absolute(value) {
|
|
2337
2464
|
const result = text(value);
|
|
2338
|
-
if (!
|
|
2465
|
+
if (!import_node_path13.default.isAbsolute(result) || import_node_path13.default.normalize(result) !== result) {
|
|
2339
2466
|
throw new Error("Invalid managed stop baseline path.");
|
|
2340
2467
|
}
|
|
2341
2468
|
return result;
|
|
@@ -2413,8 +2540,8 @@ function validateManagedStopBaseline(value, state) {
|
|
|
2413
2540
|
const c = record(entry, ["id", "service", "configFiles", "mounts"]);
|
|
2414
2541
|
const configFiles = list(c.configFiles, absolute);
|
|
2415
2542
|
if (configFiles.length < result.composeFiles.length || result.composeFiles.some((file, index) => configFiles[index] !== file) || configFiles.slice(result.composeFiles.length).some(
|
|
2416
|
-
(file) =>
|
|
2417
|
-
|
|
2543
|
+
(file) => import_node_path13.default.dirname(file) !== result.featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
2544
|
+
import_node_path13.default.basename(file)
|
|
2418
2545
|
)
|
|
2419
2546
|
)) {
|
|
2420
2547
|
throw new Error("Invalid stop baseline Compose provenance.");
|
|
@@ -2453,7 +2580,7 @@ function stateKey(repoPath, workspace) {
|
|
|
2453
2580
|
return (0, import_node_crypto6.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
|
|
2454
2581
|
}
|
|
2455
2582
|
function managedRuntimeStatePath(repoPath, workspace) {
|
|
2456
|
-
return
|
|
2583
|
+
return import_node_path14.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
|
|
2457
2584
|
}
|
|
2458
2585
|
function isStringArray(value) {
|
|
2459
2586
|
return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
|
|
@@ -2518,9 +2645,9 @@ function validateState(value, repoPath, workspace) {
|
|
|
2518
2645
|
}
|
|
2519
2646
|
return result;
|
|
2520
2647
|
}
|
|
2521
|
-
function readManagedRuntimeState(repoPath, workspace, read = (file) =>
|
|
2648
|
+
function readManagedRuntimeState(repoPath, workspace, read = (file) => import_node_fs11.default.readFileSync(file, "utf-8")) {
|
|
2522
2649
|
const statePath = managedRuntimeStatePath(repoPath, workspace);
|
|
2523
|
-
if (!
|
|
2650
|
+
if (!import_node_fs11.default.existsSync(statePath)) return void 0;
|
|
2524
2651
|
let parsed;
|
|
2525
2652
|
try {
|
|
2526
2653
|
parsed = JSON.parse(read(statePath));
|
|
@@ -2547,9 +2674,9 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
|
|
|
2547
2674
|
}
|
|
2548
2675
|
|
|
2549
2676
|
// src/core/managed-stop-recovery.ts
|
|
2550
|
-
var
|
|
2551
|
-
var
|
|
2552
|
-
var
|
|
2677
|
+
var import_node_os3 = __toESM(require("os"));
|
|
2678
|
+
var import_node_path16 = __toESM(require("path"));
|
|
2679
|
+
var import_node_util2 = require("util");
|
|
2553
2680
|
|
|
2554
2681
|
// src/core/reliability-context.ts
|
|
2555
2682
|
var claim;
|
|
@@ -2563,11 +2690,11 @@ function claimLifecycleEffect() {
|
|
|
2563
2690
|
|
|
2564
2691
|
// src/core/workspace-ownership.ts
|
|
2565
2692
|
var import_node_child_process11 = require("child_process");
|
|
2566
|
-
var
|
|
2567
|
-
var
|
|
2693
|
+
var import_node_fs12 = __toESM(require("fs"));
|
|
2694
|
+
var import_node_path15 = __toESM(require("path"));
|
|
2568
2695
|
var READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
2569
2696
|
var OWNERSHIP_VERSION = 1;
|
|
2570
|
-
var OWNERSHIP_DIR =
|
|
2697
|
+
var OWNERSHIP_DIR = import_node_path15.default.join("devrouter", "workspaces");
|
|
2571
2698
|
function commandError(command, repoPath, stderr) {
|
|
2572
2699
|
return new Error(
|
|
2573
2700
|
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
@@ -2582,7 +2709,7 @@ function resolveGitCommonDir(repoPath) {
|
|
|
2582
2709
|
if (result.status !== 0 || !output) {
|
|
2583
2710
|
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
2584
2711
|
}
|
|
2585
|
-
return comparableWorkspacePath(
|
|
2712
|
+
return comparableWorkspacePath(import_node_path15.default.isAbsolute(output) ? output : import_node_path15.default.resolve(repoPath, output));
|
|
2586
2713
|
}
|
|
2587
2714
|
function listGitWorktrees(repoPath) {
|
|
2588
2715
|
const result = (0, import_node_child_process11.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
@@ -2622,7 +2749,7 @@ function listGitWorktrees(repoPath) {
|
|
|
2622
2749
|
return worktrees;
|
|
2623
2750
|
}
|
|
2624
2751
|
function ownershipDirectory(repoPath) {
|
|
2625
|
-
return
|
|
2752
|
+
return import_node_path15.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
2626
2753
|
}
|
|
2627
2754
|
function validateWorkspace(value, label) {
|
|
2628
2755
|
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
@@ -2650,7 +2777,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
2650
2777
|
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
2651
2778
|
);
|
|
2652
2779
|
}
|
|
2653
|
-
if (typeof candidate.worktreePath !== "string" || !
|
|
2780
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path15.default.isAbsolute(candidate.worktreePath)) {
|
|
2654
2781
|
throw new Error("invalid workspace ownership worktreePath");
|
|
2655
2782
|
}
|
|
2656
2783
|
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
@@ -2668,7 +2795,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
2668
2795
|
};
|
|
2669
2796
|
}
|
|
2670
2797
|
function recordPath(repoPath, workspace) {
|
|
2671
|
-
return
|
|
2798
|
+
return import_node_path15.default.join(
|
|
2672
2799
|
ownershipDirectory(repoPath),
|
|
2673
2800
|
`${validateWorkspace(workspace, "workspace")}.json`
|
|
2674
2801
|
);
|
|
@@ -2676,7 +2803,7 @@ function recordPath(repoPath, workspace) {
|
|
|
2676
2803
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
2677
2804
|
let parsed;
|
|
2678
2805
|
try {
|
|
2679
|
-
parsed = JSON.parse(
|
|
2806
|
+
parsed = JSON.parse(import_node_fs12.default.readFileSync(filePath, "utf-8"));
|
|
2680
2807
|
} catch (error) {
|
|
2681
2808
|
if (error instanceof SyntaxError) {
|
|
2682
2809
|
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
@@ -2701,7 +2828,7 @@ function listWorkspaceOwnership(repoPath) {
|
|
|
2701
2828
|
function listWorkspaceOwnershipInDirectory(directory) {
|
|
2702
2829
|
let entries;
|
|
2703
2830
|
try {
|
|
2704
|
-
entries =
|
|
2831
|
+
entries = import_node_fs12.default.readdirSync(directory, { withFileTypes: true });
|
|
2705
2832
|
} catch (error) {
|
|
2706
2833
|
if (error.code === "ENOENT") return [];
|
|
2707
2834
|
throw error;
|
|
@@ -2709,14 +2836,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
|
|
|
2709
2836
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
2710
2837
|
const workspace = entry.name.slice(0, -".json".length);
|
|
2711
2838
|
validateWorkspace(workspace, "filename");
|
|
2712
|
-
return readRecordFile(
|
|
2839
|
+
return readRecordFile(import_node_path15.default.join(directory, entry.name), workspace);
|
|
2713
2840
|
});
|
|
2714
2841
|
}
|
|
2715
2842
|
function writeWorkspaceOwnershipInDirectory(directory, input) {
|
|
2716
2843
|
const workspace = validateWorkspace(input.workspace, "workspace");
|
|
2717
2844
|
const devpodId = validateWorkspace(input.devpodId, "devpodId");
|
|
2718
2845
|
const worktreePath = comparableWorkspacePath(input.worktreePath);
|
|
2719
|
-
const filePath =
|
|
2846
|
+
const filePath = import_node_path15.default.join(directory, `${workspace}.json`);
|
|
2720
2847
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2721
2848
|
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
2722
2849
|
const existing = records.find((record5) => record5.workspace === workspace);
|
|
@@ -2752,9 +2879,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input) {
|
|
|
2752
2879
|
return record4;
|
|
2753
2880
|
}
|
|
2754
2881
|
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
2755
|
-
const filePath =
|
|
2882
|
+
const filePath = import_node_path15.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
2756
2883
|
try {
|
|
2757
|
-
|
|
2884
|
+
import_node_fs12.default.rmSync(filePath);
|
|
2758
2885
|
return true;
|
|
2759
2886
|
} catch (error) {
|
|
2760
2887
|
if (error.code === "ENOENT") return false;
|
|
@@ -2765,7 +2892,7 @@ function sameOwnershipRecord(left, right) {
|
|
|
2765
2892
|
return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
|
|
2766
2893
|
}
|
|
2767
2894
|
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
2768
|
-
const filePath =
|
|
2895
|
+
const filePath = import_node_path15.default.join(
|
|
2769
2896
|
directory,
|
|
2770
2897
|
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
2771
2898
|
);
|
|
@@ -2777,14 +2904,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
|
2777
2904
|
throw error;
|
|
2778
2905
|
}
|
|
2779
2906
|
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
2780
|
-
|
|
2907
|
+
import_node_fs12.default.rmSync(filePath);
|
|
2781
2908
|
return "removed";
|
|
2782
2909
|
}
|
|
2783
2910
|
function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
|
|
2784
2911
|
const directory = ownershipDirectory(repoPath);
|
|
2785
|
-
|
|
2912
|
+
import_node_fs12.default.mkdirSync(directory, { recursive: true });
|
|
2786
2913
|
return withFileLockSync(
|
|
2787
|
-
|
|
2914
|
+
import_node_path15.default.join(directory, ".lock"),
|
|
2788
2915
|
{
|
|
2789
2916
|
activity: "workspace ownership transaction",
|
|
2790
2917
|
target: `'${repoPath}'`,
|
|
@@ -2817,7 +2944,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
|
2817
2944
|
try {
|
|
2818
2945
|
workspace = readPersistedWorkspace(worktree.path);
|
|
2819
2946
|
} catch (error) {
|
|
2820
|
-
if (!
|
|
2947
|
+
if (!import_node_fs12.default.existsSync(worktree.path)) continue;
|
|
2821
2948
|
throw error;
|
|
2822
2949
|
}
|
|
2823
2950
|
if (!workspace) continue;
|
|
@@ -2974,7 +3101,7 @@ function inspectWorkspaceOwnership(record4, worktrees, devpods) {
|
|
|
2974
3101
|
if (worktree?.locked) {
|
|
2975
3102
|
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
2976
3103
|
}
|
|
2977
|
-
if ((!worktree || worktree.prunable) &&
|
|
3104
|
+
if ((!worktree || worktree.prunable) && import_node_fs12.default.existsSync(record4.worktreePath)) {
|
|
2978
3105
|
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
2979
3106
|
}
|
|
2980
3107
|
if (!worktree || worktree.prunable) {
|
|
@@ -3052,7 +3179,7 @@ function prove(state, baseline) {
|
|
|
3052
3179
|
const containers = inspectManagedStopContainers(baseline.project, baseline.endpoint);
|
|
3053
3180
|
const expected = baseline.containers.map((container) => ({ ...container, mounts: orderedMounts(container.mounts) })).sort((a, b) => compareText(a.id, b.id));
|
|
3054
3181
|
const observed = containers.map(identity).sort((a, b) => compareText(a.id, b.id));
|
|
3055
|
-
if (!(0,
|
|
3182
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(expected, observed) || containers.some(
|
|
3056
3183
|
(container) => container.labels["com.docker.compose.project.working_dir"] !== baseline.composeDirectory
|
|
3057
3184
|
)) {
|
|
3058
3185
|
throw new Error("Retained container population or immutable identity changed.");
|
|
@@ -3061,7 +3188,7 @@ function prove(state, baseline) {
|
|
|
3061
3188
|
baseline.endpoint,
|
|
3062
3189
|
baseline.composeDirectory
|
|
3063
3190
|
).sort();
|
|
3064
|
-
if (!(0,
|
|
3191
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(workspaceIds, expected.map((container) => container.id).sort())) {
|
|
3065
3192
|
throw new Error("Workspace contains an unrecorded container population.");
|
|
3066
3193
|
}
|
|
3067
3194
|
const primary = baseline.containers.find(
|
|
@@ -3073,7 +3200,7 @@ function prove(state, baseline) {
|
|
|
3073
3200
|
if (!baseline.sourceContainer && inspectManagedStopRunnerId(baseline.endpoint, primary.id) !== runnerId) {
|
|
3074
3201
|
throw new Error("Provider-to-container binding changed.");
|
|
3075
3202
|
}
|
|
3076
|
-
if (!(0,
|
|
3203
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(registration(state), owner) || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId) {
|
|
3077
3204
|
throw new Error("Stop authority changed during inspection.");
|
|
3078
3205
|
}
|
|
3079
3206
|
return containers;
|
|
@@ -3082,8 +3209,8 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
3082
3209
|
const owner = registration(state);
|
|
3083
3210
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(owner.context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(owner.providerId))
|
|
3084
3211
|
throw new Error("Invalid capture provider identity.");
|
|
3085
|
-
const devsyRoot =
|
|
3086
|
-
const featureDirectory =
|
|
3212
|
+
const devsyRoot = import_node_path16.default.resolve(process.env.DEVSY_HOME || import_node_path16.default.join(import_node_os3.default.homedir(), ".devsy"));
|
|
3213
|
+
const featureDirectory = import_node_path16.default.join(
|
|
3087
3214
|
devsyRoot,
|
|
3088
3215
|
"contexts",
|
|
3089
3216
|
owner.context,
|
|
@@ -3133,16 +3260,65 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
3133
3260
|
}
|
|
3134
3261
|
function proveRetainedManagedStop(state) {
|
|
3135
3262
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3136
|
-
if (!(0,
|
|
3263
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
|
|
3137
3264
|
throw new Error("Retained stop generation changed.");
|
|
3138
3265
|
}
|
|
3139
3266
|
const result = prove(state, baseline);
|
|
3140
|
-
if (!(0,
|
|
3267
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
|
|
3141
3268
|
throw new Error("Retained stop generation changed during inspection.");
|
|
3142
3269
|
}
|
|
3143
3270
|
return result;
|
|
3144
3271
|
}
|
|
3272
|
+
function absentRegistrationIdentity(state) {
|
|
3273
|
+
resetWorkspaceRuntimeCaches();
|
|
3274
|
+
if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
|
|
3275
|
+
throw new Error("Stop provider changed.");
|
|
3276
|
+
if (!state.workspace || !isLinkedWorktree(state.repoPath))
|
|
3277
|
+
throw new Error("Absent stop requires a linked workspace.");
|
|
3278
|
+
const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
|
|
3279
|
+
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")
|
|
3280
|
+
throw new Error("Absent stop workspace ownership changed.");
|
|
3281
|
+
for (const entries of [
|
|
3282
|
+
listDevsyWorkspaces(),
|
|
3283
|
+
listDevpodWorkspacesRaw({ allowMissingExecutable: true })
|
|
3284
|
+
]) {
|
|
3285
|
+
if (entries.some(
|
|
3286
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
3287
|
+
))
|
|
3288
|
+
throw new Error("Absent stop requires both provider registrations to remain absent.");
|
|
3289
|
+
}
|
|
3290
|
+
return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
|
|
3291
|
+
}
|
|
3292
|
+
function proveManagedStop(state) {
|
|
3293
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3294
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
3295
|
+
listDevsyWorkspaces(),
|
|
3296
|
+
state.devpodId,
|
|
3297
|
+
state.repoPath
|
|
3298
|
+
);
|
|
3299
|
+
if (owner.status !== "absent")
|
|
3300
|
+
return { status: "retained", containers: proveRetainedManagedStop(state) };
|
|
3301
|
+
if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
|
|
3302
|
+
const before = absentRegistrationIdentity(state);
|
|
3303
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
3304
|
+
throw new Error("Retained stop generation changed.");
|
|
3305
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
3306
|
+
throw new Error("Stop daemon changed.");
|
|
3307
|
+
assertManagedStopContainersAbsent(
|
|
3308
|
+
baseline.endpoint,
|
|
3309
|
+
baseline.containers.map((container) => container.id)
|
|
3310
|
+
);
|
|
3311
|
+
const uidBytes = Buffer.byteLength(baseline.uid);
|
|
3312
|
+
const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
|
|
3313
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
|
|
3314
|
+
throw new Error("Absent stop observed a remaining workspace population.");
|
|
3315
|
+
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))
|
|
3316
|
+
throw new Error("Absent stop authority changed during inspection.");
|
|
3317
|
+
return { status: "proven-absent", containers: [] };
|
|
3318
|
+
}
|
|
3145
3319
|
function stopFromManagedBaseline(state) {
|
|
3320
|
+
const proof = proveManagedStop(state);
|
|
3321
|
+
if (proof.status === "proven-absent") return proof.status;
|
|
3146
3322
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3147
3323
|
const stopped = /* @__PURE__ */ new Set();
|
|
3148
3324
|
for (const initial of proveRetainedManagedStop(state))
|
|
@@ -3165,11 +3341,17 @@ function stopFromManagedBaseline(state) {
|
|
|
3165
3341
|
}
|
|
3166
3342
|
if (inspect().some((container) => container.state.Running))
|
|
3167
3343
|
throw new Error("Retained workloads remain running.");
|
|
3344
|
+
return "retained";
|
|
3345
|
+
}
|
|
3346
|
+
function managedStopRouteReferences(state) {
|
|
3347
|
+
return state.desired.apps.flatMap(
|
|
3348
|
+
(name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
|
|
3349
|
+
);
|
|
3168
3350
|
}
|
|
3169
3351
|
|
|
3170
3352
|
// src/core/repo-config.ts
|
|
3171
|
-
var
|
|
3172
|
-
var
|
|
3353
|
+
var import_node_fs14 = __toESM(require("fs"));
|
|
3354
|
+
var import_node_path18 = __toESM(require("path"));
|
|
3173
3355
|
var import_yaml3 = __toESM(require("yaml"));
|
|
3174
3356
|
|
|
3175
3357
|
// src/core/capabilities.ts
|
|
@@ -3195,9 +3377,9 @@ function formatSupportedTcpProtocols() {
|
|
|
3195
3377
|
}
|
|
3196
3378
|
|
|
3197
3379
|
// src/core/host-routes.ts
|
|
3198
|
-
var
|
|
3199
|
-
var
|
|
3200
|
-
var
|
|
3380
|
+
var import_node_fs13 = __toESM(require("fs"));
|
|
3381
|
+
var import_node_path17 = __toESM(require("path"));
|
|
3382
|
+
var import_node_util3 = require("util");
|
|
3201
3383
|
var import_yaml2 = __toESM(require("yaml"));
|
|
3202
3384
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
|
|
3203
3385
|
var UPSTREAM_RE = /^([a-zA-Z0-9._-]+):(\d{1,5})$/;
|
|
@@ -3380,7 +3562,7 @@ function writeRouteGeneration(routes, tlsEnabled) {
|
|
|
3380
3562
|
writeFileAtomically(HOST_ROUTES_STATE_FILE, renderCompatibilityState(routes));
|
|
3381
3563
|
writeFileAtomically(TRAEFIK_HOST_ROUTES_FILE, renderCanonicalState(routes, tlsEnabled));
|
|
3382
3564
|
}
|
|
3383
|
-
function readCompatibilityState(read = (file) =>
|
|
3565
|
+
function readCompatibilityState(read = (file) => import_node_fs13.default.readFileSync(file, "utf-8")) {
|
|
3384
3566
|
let raw;
|
|
3385
3567
|
try {
|
|
3386
3568
|
raw = read(HOST_ROUTES_STATE_FILE);
|
|
@@ -3437,14 +3619,14 @@ function parseCanonicalState(raw) {
|
|
|
3437
3619
|
throw new Error(`Canonical host-route document is invalid: ${error.message}`);
|
|
3438
3620
|
}
|
|
3439
3621
|
const expectedDocument = buildHostRoutesDocument(routes, metadata.tlsEnabled);
|
|
3440
|
-
if (!(0,
|
|
3622
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(document, expectedDocument)) {
|
|
3441
3623
|
throw new Error("Canonical host-route document does not match its metadata.");
|
|
3442
3624
|
}
|
|
3443
3625
|
return { kind: "canonical", metadata };
|
|
3444
3626
|
}
|
|
3445
3627
|
function ensureHostRouteStorage() {
|
|
3446
|
-
|
|
3447
|
-
|
|
3628
|
+
import_node_fs13.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
3629
|
+
import_node_fs13.default.mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
|
|
3448
3630
|
}
|
|
3449
3631
|
function writeState(routes) {
|
|
3450
3632
|
ensureHostRouteStorage();
|
|
@@ -3463,8 +3645,8 @@ function refreshHostRoutesDynamicFile() {
|
|
|
3463
3645
|
}
|
|
3464
3646
|
function readHostRouteStateLocked() {
|
|
3465
3647
|
ensureHostRouteStorage();
|
|
3466
|
-
if (!
|
|
3467
|
-
if (
|
|
3648
|
+
if (!import_node_fs13.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
|
|
3649
|
+
if (import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
3468
3650
|
const routes = readCompatibilityState();
|
|
3469
3651
|
writeRouteGeneration(routes, isTLSEnabled());
|
|
3470
3652
|
return routes;
|
|
@@ -3472,10 +3654,10 @@ function readHostRouteStateLocked() {
|
|
|
3472
3654
|
writeRouteGeneration([], isTLSEnabled());
|
|
3473
3655
|
return [];
|
|
3474
3656
|
}
|
|
3475
|
-
const raw =
|
|
3657
|
+
const raw = import_node_fs13.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
|
|
3476
3658
|
const canonical = parseCanonicalState(raw);
|
|
3477
3659
|
if (canonical.kind === "legacy") {
|
|
3478
|
-
if (!
|
|
3660
|
+
if (!import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
3479
3661
|
throw new Error(
|
|
3480
3662
|
"Headerless host-route document requires a valid compatibility state file for migration."
|
|
3481
3663
|
);
|
|
@@ -3485,9 +3667,9 @@ function readHostRouteStateLocked() {
|
|
|
3485
3667
|
return routes;
|
|
3486
3668
|
}
|
|
3487
3669
|
let mirrorMatches = false;
|
|
3488
|
-
if (
|
|
3670
|
+
if (import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
3489
3671
|
try {
|
|
3490
|
-
mirrorMatches = (0,
|
|
3672
|
+
mirrorMatches = (0, import_node_util3.isDeepStrictEqual)(readCompatibilityState(), canonical.metadata.routes);
|
|
3491
3673
|
} catch {
|
|
3492
3674
|
mirrorMatches = false;
|
|
3493
3675
|
}
|
|
@@ -4505,15 +4687,15 @@ function applyProfile(config, profile) {
|
|
|
4505
4687
|
return next;
|
|
4506
4688
|
}
|
|
4507
4689
|
function resolveRepoPath(repoPath) {
|
|
4508
|
-
return
|
|
4690
|
+
return import_node_path18.default.resolve(repoPath ?? process.cwd());
|
|
4509
4691
|
}
|
|
4510
4692
|
function getRepoConfigPath(repoPath) {
|
|
4511
|
-
return
|
|
4693
|
+
return import_node_path18.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
4512
4694
|
}
|
|
4513
|
-
function loadRepoConfig(repoPath, read = (file) =>
|
|
4695
|
+
function loadRepoConfig(repoPath, read = (file) => import_node_fs14.default.readFileSync(file, "utf-8")) {
|
|
4514
4696
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
4515
4697
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
4516
|
-
if (!
|
|
4698
|
+
if (!import_node_fs14.default.existsSync(configPath)) {
|
|
4517
4699
|
throw new Error(
|
|
4518
4700
|
`Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
|
|
4519
4701
|
);
|
|
@@ -4523,7 +4705,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs13.default.read
|
|
|
4523
4705
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4524
4706
|
const requiredVersion = config.devrouter?.version;
|
|
4525
4707
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4526
|
-
const cliVersion = true ? "0.0.
|
|
4708
|
+
const cliVersion = true ? "0.0.63" : "0.0.0-dev";
|
|
4527
4709
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4528
4710
|
hasWarnedVersionMismatch = true;
|
|
4529
4711
|
process.stderr.write(
|
|
@@ -4543,7 +4725,7 @@ function namespaceHost(host, workspace) {
|
|
|
4543
4725
|
return `${base}.${workspace}${suffix}`;
|
|
4544
4726
|
}
|
|
4545
4727
|
function applyWorkspace(config, workspace, repoPath) {
|
|
4546
|
-
const defaultToken = wsFromBranch(config.project?.name ??
|
|
4728
|
+
const defaultToken = wsFromBranch(config.project?.name ?? import_node_path18.default.basename(import_node_path18.default.resolve(repoPath))) ?? "app";
|
|
4547
4729
|
const substitutionToken = workspace ?? defaultToken;
|
|
4548
4730
|
const next = structuredClone(config);
|
|
4549
4731
|
for (const app of next.apps) {
|
|
@@ -4704,10 +4886,10 @@ async function ensureNetwork(name) {
|
|
|
4704
4886
|
// src/core/tls.ts
|
|
4705
4887
|
var import_node_child_process13 = require("child_process");
|
|
4706
4888
|
var import_node_crypto7 = require("crypto");
|
|
4707
|
-
var
|
|
4708
|
-
var
|
|
4889
|
+
var import_node_fs15 = __toESM(require("fs"));
|
|
4890
|
+
var import_node_path19 = __toESM(require("path"));
|
|
4709
4891
|
var DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
4710
|
-
var TLS_CERTIFICATE_LOCK_FILE =
|
|
4892
|
+
var TLS_CERTIFICATE_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "tls-certificate.lock");
|
|
4711
4893
|
var TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
|
|
4712
4894
|
var TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
|
|
4713
4895
|
function runOrThrow(command, args) {
|
|
@@ -4739,8 +4921,8 @@ function tlsSetupCommand(repoPath) {
|
|
|
4739
4921
|
}
|
|
4740
4922
|
function getMkcertRootCAPath(options = {}) {
|
|
4741
4923
|
ensureMkcert();
|
|
4742
|
-
const rootCAPath =
|
|
4743
|
-
if (!
|
|
4924
|
+
const rootCAPath = import_node_path19.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
|
|
4925
|
+
if (!import_node_fs15.default.existsSync(rootCAPath)) {
|
|
4744
4926
|
throw new Error(
|
|
4745
4927
|
`mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
4746
4928
|
);
|
|
@@ -4841,11 +5023,11 @@ function compactTLSCertificateHosts(hosts) {
|
|
|
4841
5023
|
);
|
|
4842
5024
|
}
|
|
4843
5025
|
function readCurrentCertificateHosts(options = {}) {
|
|
4844
|
-
if (!
|
|
5026
|
+
if (!import_node_fs15.default.existsSync(CERT_FILE)) {
|
|
4845
5027
|
return [];
|
|
4846
5028
|
}
|
|
4847
5029
|
try {
|
|
4848
|
-
const pem =
|
|
5030
|
+
const pem = import_node_fs15.default.readFileSync(CERT_FILE, "utf-8");
|
|
4849
5031
|
return parseCertificateDnsHosts(pem);
|
|
4850
5032
|
} catch (error) {
|
|
4851
5033
|
if (options.replaceMalformed) {
|
|
@@ -5059,8 +5241,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5059
5241
|
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
5060
5242
|
}
|
|
5061
5243
|
if (state.stopBaseline) {
|
|
5062
|
-
stopFromManagedBaseline(state);
|
|
5063
|
-
return true;
|
|
5244
|
+
return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
|
|
5064
5245
|
}
|
|
5065
5246
|
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
5066
5247
|
function registration2() {
|
|
@@ -5084,8 +5265,8 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5084
5265
|
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
5085
5266
|
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
5086
5267
|
}
|
|
5087
|
-
const devsyRoot =
|
|
5088
|
-
const featureDirectory =
|
|
5268
|
+
const devsyRoot = import_node_path20.default.resolve(process.env.DEVSY_HOME || import_node_path20.default.join(import_node_os4.default.homedir(), ".devsy"));
|
|
5269
|
+
const featureDirectory = import_node_path20.default.join(
|
|
5089
5270
|
devsyRoot,
|
|
5090
5271
|
"contexts",
|
|
5091
5272
|
context,
|
|
@@ -5191,7 +5372,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5191
5372
|
}
|
|
5192
5373
|
|
|
5193
5374
|
// src/core/devsy-mutation.ts
|
|
5194
|
-
var DEVSY_MUTATION_LOCK_FILE =
|
|
5375
|
+
var DEVSY_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
|
|
5195
5376
|
var DEVSY_MUTATION_WAIT_MS = 18e5;
|
|
5196
5377
|
var DevsyStartPostconditionError = class extends Error {
|
|
5197
5378
|
};
|
|
@@ -5206,7 +5387,7 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
|
|
|
5206
5387
|
}
|
|
5207
5388
|
}
|
|
5208
5389
|
function withMutationLock(activity, target, operation) {
|
|
5209
|
-
|
|
5390
|
+
import_node_fs16.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
5210
5391
|
return withFileLockSync(
|
|
5211
5392
|
DEVSY_MUTATION_LOCK_FILE,
|
|
5212
5393
|
{
|
|
@@ -5220,7 +5401,7 @@ function withMutationLock(activity, target, operation) {
|
|
|
5220
5401
|
);
|
|
5221
5402
|
}
|
|
5222
5403
|
function withMutationLockAsync(activity, target, operation) {
|
|
5223
|
-
|
|
5404
|
+
import_node_fs16.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
5224
5405
|
return withFileLock(
|
|
5225
5406
|
DEVSY_MUTATION_LOCK_FILE,
|
|
5226
5407
|
{
|
|
@@ -5288,12 +5469,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
|
|
|
5288
5469
|
}
|
|
5289
5470
|
function mutateOwnedDevsyWorkspace(action, devsyId, worktreePath) {
|
|
5290
5471
|
return withMutationLock(`Devsy ${action}`, worktreePath, () => {
|
|
5291
|
-
if (action === "stop"
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5472
|
+
if (action === "stop") {
|
|
5473
|
+
const stopped = stopRetainedManagedDevsyWorkspace({
|
|
5474
|
+
repoPath: worktreePath,
|
|
5475
|
+
devsyId,
|
|
5476
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
5477
|
+
});
|
|
5478
|
+
if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
|
|
5479
|
+
}
|
|
5297
5480
|
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
5298
5481
|
if (before.status === "absent") return { status: "absent" };
|
|
5299
5482
|
runDevsyAction(action, devsyId);
|
|
@@ -5424,6 +5607,19 @@ async function startDevsyWorkspace(options) {
|
|
|
5424
5607
|
});
|
|
5425
5608
|
}
|
|
5426
5609
|
|
|
5610
|
+
// src/core/execution-outcome.ts
|
|
5611
|
+
var ExecutionOutcomeError = class extends Error {
|
|
5612
|
+
outcome;
|
|
5613
|
+
constructor(message, outcome) {
|
|
5614
|
+
super(message);
|
|
5615
|
+
this.name = "ExecutionOutcomeError";
|
|
5616
|
+
this.outcome = outcome;
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
|
|
5620
|
+
// src/core/reliability-lifecycle.ts
|
|
5621
|
+
var import_node_path28 = __toESM(require("path"));
|
|
5622
|
+
|
|
5427
5623
|
// src/core/reliability-contract.ts
|
|
5428
5624
|
var RELIABILITY_CONTRACT_VERSION = 2;
|
|
5429
5625
|
var RELIABILITY_MAX_ITEMS = 128;
|
|
@@ -5553,7 +5749,7 @@ function assertReliabilityEvent(value) {
|
|
|
5553
5749
|
valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
|
|
5554
5750
|
break;
|
|
5555
5751
|
case "operation-request":
|
|
5556
|
-
valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean";
|
|
5752
|
+
valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
|
|
5557
5753
|
break;
|
|
5558
5754
|
case "drained":
|
|
5559
5755
|
valid = isReliabilityId(value.operationId);
|
|
@@ -5736,6 +5932,10 @@ function handleRequest(state, event) {
|
|
|
5736
5932
|
});
|
|
5737
5933
|
return transition(state, "accepted");
|
|
5738
5934
|
}
|
|
5935
|
+
function canExecAfterInterruptedEnsure(state) {
|
|
5936
|
+
const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
|
|
5937
|
+
return state.executionPolicy === "manual" && state.desired === "running" && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && state.phase === "recovering" || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
|
|
5938
|
+
}
|
|
5739
5939
|
function handleOperationRequest(state, event) {
|
|
5740
5940
|
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
5741
5941
|
const previous = state.operationHistory.find((entry) => entry.key === event.key);
|
|
@@ -5746,9 +5946,11 @@ function handleOperationRequest(state, event) {
|
|
|
5746
5946
|
);
|
|
5747
5947
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
5748
5948
|
return unchanged(state, "conflict");
|
|
5949
|
+
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
5950
|
+
return unchanged(state, "blocked");
|
|
5749
5951
|
const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
5750
5952
|
const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
|
|
5751
|
-
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure))
|
|
5953
|
+
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
|
|
5752
5954
|
return unchanged(state, "blocked");
|
|
5753
5955
|
if (state.phase === "stopping" || state.desired === "parked-for-capacity")
|
|
5754
5956
|
return unchanged(state, "blocked");
|
|
@@ -5759,8 +5961,9 @@ function handleOperationRequest(state, event) {
|
|
|
5759
5961
|
if (rollover) {
|
|
5760
5962
|
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
|
|
5761
5963
|
return unchanged(state, "blocked");
|
|
5964
|
+
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
5762
5965
|
retired = state.operationHistory.findIndex(
|
|
5763
|
-
(entry) => entry.id !== state.operation?.id && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
|
|
5966
|
+
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
|
|
5764
5967
|
);
|
|
5765
5968
|
if (retired === -1) return unchanged(state, "blocked");
|
|
5766
5969
|
}
|
|
@@ -6088,7 +6291,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
6088
6291
|
case "not-started":
|
|
6089
6292
|
if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
|
|
6090
6293
|
return unchanged(next, "blocked");
|
|
6091
|
-
if (next.operation.id !== event.operationId || next.operation.status
|
|
6294
|
+
if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
|
|
6092
6295
|
return unchanged(next, "stale");
|
|
6093
6296
|
next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
|
|
6094
6297
|
return transition(next, "accepted");
|
|
@@ -6129,17 +6332,17 @@ function stepReliability(state, event, nowMs) {
|
|
|
6129
6332
|
|
|
6130
6333
|
// src/core/reliability-operation-store.ts
|
|
6131
6334
|
var import_node_crypto8 = require("crypto");
|
|
6132
|
-
var
|
|
6133
|
-
var
|
|
6335
|
+
var import_node_fs17 = __toESM(require("fs"));
|
|
6336
|
+
var import_node_path22 = __toESM(require("path"));
|
|
6134
6337
|
var MAX_RECORD_BYTES = 1048576;
|
|
6135
6338
|
function identityKey(identity2) {
|
|
6136
|
-
if (!
|
|
6339
|
+
if (!import_node_path22.default.isAbsolute(identity2.repoPath) || import_node_path22.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
|
|
6137
6340
|
throw new Error("Invalid reliability workspace identity.");
|
|
6138
6341
|
}
|
|
6139
6342
|
return (0, import_node_crypto8.createHash)("sha256").update(identity2.repoPath).digest("hex");
|
|
6140
6343
|
}
|
|
6141
6344
|
function reliabilityOperationPath(identity2) {
|
|
6142
|
-
return
|
|
6345
|
+
return import_node_path22.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
|
|
6143
6346
|
}
|
|
6144
6347
|
function keys(value, expected) {
|
|
6145
6348
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !expected.includes(key))) {
|
|
@@ -6218,21 +6421,21 @@ function readReliabilityOperation(identity2) {
|
|
|
6218
6421
|
const file = reliabilityOperationPath(identity2);
|
|
6219
6422
|
let descriptor;
|
|
6220
6423
|
try {
|
|
6221
|
-
descriptor =
|
|
6424
|
+
descriptor = import_node_fs17.default.openSync(
|
|
6222
6425
|
file,
|
|
6223
|
-
|
|
6426
|
+
import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NOFOLLOW | import_node_fs17.default.constants.O_NONBLOCK
|
|
6224
6427
|
);
|
|
6225
6428
|
} catch (error) {
|
|
6226
6429
|
if (error.code === "ENOENT") return void 0;
|
|
6227
6430
|
throw error;
|
|
6228
6431
|
}
|
|
6229
6432
|
try {
|
|
6230
|
-
const stat =
|
|
6433
|
+
const stat = import_node_fs17.default.fstatSync(descriptor);
|
|
6231
6434
|
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
6232
6435
|
throw new Error("Reliability record is not a bounded private file.");
|
|
6233
6436
|
}
|
|
6234
6437
|
const bytes = Buffer.alloc(MAX_RECORD_BYTES + 1);
|
|
6235
|
-
const count =
|
|
6438
|
+
const count = import_node_fs17.default.readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
6236
6439
|
if (count > MAX_RECORD_BYTES) throw new Error("Reliability record exceeds its byte limit.");
|
|
6237
6440
|
const record4 = JSON.parse(
|
|
6238
6441
|
bytes.subarray(0, count).toString("utf8")
|
|
@@ -6240,7 +6443,7 @@ function readReliabilityOperation(identity2) {
|
|
|
6240
6443
|
validate(record4, identity2);
|
|
6241
6444
|
return record4;
|
|
6242
6445
|
} finally {
|
|
6243
|
-
|
|
6446
|
+
import_node_fs17.default.closeSync(descriptor);
|
|
6244
6447
|
}
|
|
6245
6448
|
}
|
|
6246
6449
|
function persist(record4) {
|
|
@@ -6255,23 +6458,23 @@ function persist(record4) {
|
|
|
6255
6458
|
} catch (error) {
|
|
6256
6459
|
const reread = readReliabilityOperation(record4.identity);
|
|
6257
6460
|
if (!reread || JSON.stringify(reread) !== JSON.stringify(record4)) throw error;
|
|
6258
|
-
const descriptor =
|
|
6461
|
+
const descriptor = import_node_fs17.default.openSync(file, import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NOFOLLOW);
|
|
6259
6462
|
try {
|
|
6260
|
-
|
|
6463
|
+
import_node_fs17.default.fsyncSync(descriptor);
|
|
6261
6464
|
} finally {
|
|
6262
|
-
|
|
6465
|
+
import_node_fs17.default.closeSync(descriptor);
|
|
6263
6466
|
}
|
|
6264
|
-
const directory =
|
|
6467
|
+
const directory = import_node_fs17.default.openSync(import_node_path22.default.dirname(file), "r");
|
|
6265
6468
|
try {
|
|
6266
|
-
|
|
6469
|
+
import_node_fs17.default.fsyncSync(directory);
|
|
6267
6470
|
} finally {
|
|
6268
|
-
|
|
6471
|
+
import_node_fs17.default.closeSync(directory);
|
|
6269
6472
|
}
|
|
6270
6473
|
}
|
|
6271
6474
|
}
|
|
6272
6475
|
function updateReliabilityOperation(identity2, operation) {
|
|
6273
6476
|
const file = reliabilityOperationPath(identity2);
|
|
6274
|
-
|
|
6477
|
+
import_node_fs17.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true, mode: 448 });
|
|
6275
6478
|
return withFileLockSync(`${file}.lock`, { activity: "reliability intent", waitMs: 2e3 }, () => {
|
|
6276
6479
|
const record4 = readReliabilityOperation(identity2) ?? {
|
|
6277
6480
|
version: 1,
|
|
@@ -6298,8 +6501,8 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
6298
6501
|
// src/core/reliability-worker.ts
|
|
6299
6502
|
var import_node_child_process15 = require("child_process");
|
|
6300
6503
|
var import_node_crypto9 = require("crypto");
|
|
6301
|
-
var
|
|
6302
|
-
var
|
|
6504
|
+
var import_node_fs18 = __toESM(require("fs"));
|
|
6505
|
+
var import_node_path23 = __toESM(require("path"));
|
|
6303
6506
|
function workerGroupAbsent(pid) {
|
|
6304
6507
|
try {
|
|
6305
6508
|
process.kill(-pid, 0);
|
|
@@ -6309,53 +6512,377 @@ function workerGroupAbsent(pid) {
|
|
|
6309
6512
|
}
|
|
6310
6513
|
}
|
|
6311
6514
|
|
|
6312
|
-
// src/core/
|
|
6313
|
-
var import_node_child_process21 = require("child_process");
|
|
6314
|
-
var import_node_fs22 = __toESM(require("fs"));
|
|
6315
|
-
var import_node_path26 = __toESM(require("path"));
|
|
6316
|
-
|
|
6317
|
-
// src/core/devpod-mutation.ts
|
|
6515
|
+
// src/core/traefik-route-health.ts
|
|
6318
6516
|
var import_node_child_process16 = require("child_process");
|
|
6319
|
-
var
|
|
6320
|
-
var
|
|
6321
|
-
var
|
|
6322
|
-
var
|
|
6323
|
-
var
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
activity,
|
|
6331
|
-
target: `'${target}'`,
|
|
6332
|
-
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
6333
|
-
fair: true,
|
|
6334
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
6335
|
-
},
|
|
6336
|
-
operation
|
|
6337
|
-
);
|
|
6338
|
-
}
|
|
6339
|
-
function commandFailure2(result) {
|
|
6340
|
-
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
6517
|
+
var import_node_fs19 = __toESM(require("fs"));
|
|
6518
|
+
var import_node_path24 = __toESM(require("path"));
|
|
6519
|
+
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
6520
|
+
var ROUTER_RELOAD_LOCK_FILE = import_node_path24.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
6521
|
+
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
6522
|
+
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
6523
|
+
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
6524
|
+
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
6525
|
+
var ROUTER_API_PAGE_SIZE = 1e3;
|
|
6526
|
+
function sleep(ms) {
|
|
6527
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6341
6528
|
}
|
|
6342
|
-
function
|
|
6343
|
-
const
|
|
6344
|
-
|
|
6345
|
-
|
|
6529
|
+
function inspectRouterApi(protocol) {
|
|
6530
|
+
const result = (0, import_node_child_process16.spawnSync)(
|
|
6531
|
+
"curl",
|
|
6532
|
+
[
|
|
6533
|
+
"--silent",
|
|
6534
|
+
"--show-error",
|
|
6535
|
+
"--fail",
|
|
6536
|
+
"--max-time",
|
|
6537
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
6538
|
+
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
6539
|
+
],
|
|
6540
|
+
{ encoding: "utf-8" }
|
|
6541
|
+
);
|
|
6346
6542
|
if (result.status !== 0) {
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6543
|
+
return {
|
|
6544
|
+
ok: false,
|
|
6545
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
6546
|
+
};
|
|
6547
|
+
}
|
|
6548
|
+
let value;
|
|
6549
|
+
try {
|
|
6550
|
+
value = JSON.parse(result.stdout);
|
|
6551
|
+
} catch {
|
|
6552
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
|
|
6350
6553
|
}
|
|
6554
|
+
if (!Array.isArray(value)) {
|
|
6555
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
6556
|
+
}
|
|
6557
|
+
if (value.some((item) => !isRecord3(item) || typeof item.name !== "string" || !item.name))
|
|
6558
|
+
return {
|
|
6559
|
+
ok: false,
|
|
6560
|
+
details: `${protocol.toUpperCase()} router API returned malformed entries`
|
|
6561
|
+
};
|
|
6562
|
+
return {
|
|
6563
|
+
ok: true,
|
|
6564
|
+
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
6565
|
+
names: new Set(
|
|
6566
|
+
value.flatMap((item) => {
|
|
6567
|
+
if (!item || typeof item !== "object") return [];
|
|
6568
|
+
const name = item.name;
|
|
6569
|
+
return typeof name === "string" ? [name] : [];
|
|
6570
|
+
})
|
|
6571
|
+
)
|
|
6572
|
+
};
|
|
6351
6573
|
}
|
|
6352
|
-
function
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6574
|
+
function isRecord3(value) {
|
|
6575
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6576
|
+
}
|
|
6577
|
+
function inspectNamedRouterApi(protocol, resource, name) {
|
|
6578
|
+
const result = (0, import_node_child_process16.spawnSync)(
|
|
6579
|
+
"curl",
|
|
6580
|
+
[
|
|
6581
|
+
"--silent",
|
|
6582
|
+
"--show-error",
|
|
6583
|
+
"--fail",
|
|
6584
|
+
"--max-time",
|
|
6585
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
6586
|
+
`${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
|
|
6587
|
+
],
|
|
6588
|
+
{ encoding: "utf-8" }
|
|
6589
|
+
);
|
|
6590
|
+
if (result.status !== 0) {
|
|
6591
|
+
return {
|
|
6592
|
+
ok: false,
|
|
6593
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
let value;
|
|
6597
|
+
try {
|
|
6598
|
+
value = JSON.parse(result.stdout);
|
|
6599
|
+
} catch {
|
|
6600
|
+
return {
|
|
6601
|
+
ok: false,
|
|
6602
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
if (!isRecord3(value)) {
|
|
6606
|
+
return {
|
|
6607
|
+
ok: false,
|
|
6608
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
return { ok: true, value };
|
|
6612
|
+
}
|
|
6613
|
+
function renderExpectedRoute(route) {
|
|
6614
|
+
const protocol = route.protocol ?? "http";
|
|
6615
|
+
const routerName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
6616
|
+
const routeState = {
|
|
6617
|
+
id: buildHostRouteId(route.repoPath, route.name),
|
|
6618
|
+
name: route.name,
|
|
6619
|
+
host: route.host,
|
|
6620
|
+
protocol,
|
|
6621
|
+
tcpProtocol: route.tcpProtocol,
|
|
6622
|
+
repoPath: route.repoPath,
|
|
6623
|
+
port: route.port,
|
|
6624
|
+
mode: route.mode,
|
|
6625
|
+
upstreamHost: route.upstreamHost,
|
|
6626
|
+
pid: route.pid,
|
|
6627
|
+
command: route.command,
|
|
6628
|
+
workspace: route.workspace,
|
|
6629
|
+
createdAt: "",
|
|
6630
|
+
updatedAt: ""
|
|
6631
|
+
};
|
|
6632
|
+
const document = buildHostRoutesDocument([routeState], false);
|
|
6633
|
+
const protocolDocument = document[protocol];
|
|
6634
|
+
if (!isRecord3(protocolDocument)) return void 0;
|
|
6635
|
+
const routers = protocolDocument.routers;
|
|
6636
|
+
const services = protocolDocument.services;
|
|
6637
|
+
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
6638
|
+
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
6639
|
+
const router = routers[routerKey];
|
|
6640
|
+
const service = services[routerKey];
|
|
6641
|
+
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
6642
|
+
const rule = router.rule;
|
|
6643
|
+
const serviceReference = router.service;
|
|
6644
|
+
const loadBalancer = service.loadBalancer;
|
|
6645
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
6646
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
6647
|
+
return void 0;
|
|
6648
|
+
}
|
|
6649
|
+
const serverField = protocol === "http" ? "url" : "address";
|
|
6650
|
+
const server = servers[0][serverField];
|
|
6651
|
+
if (typeof server !== "string") return void 0;
|
|
6652
|
+
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
6653
|
+
if (!provider.startsWith("@")) return void 0;
|
|
6654
|
+
return {
|
|
6655
|
+
protocol,
|
|
6656
|
+
routerName,
|
|
6657
|
+
serviceKey: routerKey,
|
|
6658
|
+
serviceName: `${routerKey}${provider}`,
|
|
6659
|
+
rule,
|
|
6660
|
+
serverField,
|
|
6661
|
+
server
|
|
6662
|
+
};
|
|
6663
|
+
}
|
|
6664
|
+
function inspectExpectedRouteMatch(route) {
|
|
6665
|
+
const expected = renderExpectedRoute(route);
|
|
6666
|
+
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
6667
|
+
if (!expected) {
|
|
6668
|
+
return {
|
|
6669
|
+
ok: false,
|
|
6670
|
+
routes: [routeName],
|
|
6671
|
+
details: "host-route renderer returned an incomplete route configuration"
|
|
6672
|
+
};
|
|
6673
|
+
}
|
|
6674
|
+
const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
|
|
6675
|
+
if (!routerResult.ok) {
|
|
6676
|
+
return { ok: false, routes: [routeName], details: routerResult.details };
|
|
6677
|
+
}
|
|
6678
|
+
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) {
|
|
6679
|
+
return {
|
|
6680
|
+
ok: false,
|
|
6681
|
+
routes: [routeName],
|
|
6682
|
+
details: "active Traefik router does not match the rendered route"
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6685
|
+
const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
|
|
6686
|
+
if (!serviceResult.ok) {
|
|
6687
|
+
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
6688
|
+
}
|
|
6689
|
+
const loadBalancer = serviceResult.value.loadBalancer;
|
|
6690
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
6691
|
+
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
6692
|
+
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
|
|
6693
|
+
return {
|
|
6694
|
+
ok: false,
|
|
6695
|
+
routes: [routeName],
|
|
6696
|
+
details: "active Traefik service does not match the rendered route"
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
return { ok: true };
|
|
6700
|
+
}
|
|
6701
|
+
function inspectExpectedRouteMatches(routes) {
|
|
6702
|
+
const mismatched = [];
|
|
6703
|
+
const failures = [];
|
|
6704
|
+
for (const route of routes) {
|
|
6705
|
+
const result = inspectExpectedRouteMatch(route);
|
|
6706
|
+
if (!result.ok) {
|
|
6707
|
+
mismatched.push(...result.routes);
|
|
6708
|
+
failures.push(result.details);
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
6712
|
+
ok: false,
|
|
6713
|
+
routes: mismatched,
|
|
6714
|
+
details: failures.join("; ")
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
6718
|
+
const deadline = Date.now() + timeoutMs;
|
|
6719
|
+
let result;
|
|
6720
|
+
do {
|
|
6721
|
+
result = inspectExpectedRouteMatches(routes);
|
|
6722
|
+
if (result.ok) return result;
|
|
6723
|
+
if (Date.now() < deadline) {
|
|
6724
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
6725
|
+
}
|
|
6726
|
+
} while (Date.now() < deadline);
|
|
6727
|
+
return result;
|
|
6728
|
+
}
|
|
6729
|
+
function inspectExpectedRoutes(routes, expectation) {
|
|
6730
|
+
const mismatched = [];
|
|
6731
|
+
const failures = [];
|
|
6732
|
+
for (const protocol of ["http", "tcp"]) {
|
|
6733
|
+
const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
|
|
6734
|
+
if (expected.length === 0) continue;
|
|
6735
|
+
const result = inspectRouterApi(protocol);
|
|
6736
|
+
if (!result.ok) {
|
|
6737
|
+
failures.push(result.details);
|
|
6738
|
+
mismatched.push(...expected);
|
|
6739
|
+
continue;
|
|
6740
|
+
}
|
|
6741
|
+
const unexpected = expected.filter(
|
|
6742
|
+
(name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
|
|
6743
|
+
);
|
|
6744
|
+
const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
|
|
6745
|
+
if (boundedAbsenceIsUncertain) {
|
|
6746
|
+
failures.push(
|
|
6747
|
+
`${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
|
|
6748
|
+
);
|
|
6749
|
+
}
|
|
6750
|
+
mismatched.push(...unexpected);
|
|
6751
|
+
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
6752
|
+
mismatched.push(...expected);
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
6756
|
+
ok: false,
|
|
6757
|
+
routes: mismatched,
|
|
6758
|
+
details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
|
|
6759
|
+
};
|
|
6760
|
+
}
|
|
6761
|
+
async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
|
|
6762
|
+
const deadline = Date.now() + timeoutMs;
|
|
6763
|
+
let result;
|
|
6764
|
+
do {
|
|
6765
|
+
result = inspectExpectedRoutes(routes, expectation);
|
|
6766
|
+
if (result.ok) return result;
|
|
6767
|
+
if (Date.now() < deadline) {
|
|
6768
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
6769
|
+
}
|
|
6770
|
+
} while (Date.now() < deadline);
|
|
6771
|
+
return result;
|
|
6772
|
+
}
|
|
6773
|
+
async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
6774
|
+
if (routes.length === 0) return { restarted: false };
|
|
6775
|
+
const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
6776
|
+
const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
|
|
6777
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
6778
|
+
const initial = await waitForExpectedRoutes(
|
|
6779
|
+
routes,
|
|
6780
|
+
expectation,
|
|
6781
|
+
initialTimeoutMs,
|
|
6782
|
+
pollIntervalMs
|
|
6783
|
+
);
|
|
6784
|
+
if (initial.ok) return { restarted: false };
|
|
6785
|
+
if (options.allowRestart === false)
|
|
6786
|
+
throw new Error(
|
|
6787
|
+
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
6788
|
+
);
|
|
6789
|
+
import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
6790
|
+
return withFileLock(
|
|
6791
|
+
ROUTER_RELOAD_LOCK_FILE,
|
|
6792
|
+
{
|
|
6793
|
+
activity: "Traefik route reload recovery",
|
|
6794
|
+
waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
|
|
6795
|
+
fair: true,
|
|
6796
|
+
onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
|
|
6797
|
+
},
|
|
6798
|
+
async () => {
|
|
6799
|
+
const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
|
|
6800
|
+
if (recheck.ok) return { restarted: false };
|
|
6801
|
+
claimLifecycleEffect();
|
|
6802
|
+
restartRouterStack();
|
|
6803
|
+
const recovered = await waitForExpectedRoutes(
|
|
6804
|
+
routes,
|
|
6805
|
+
expectation,
|
|
6806
|
+
recoveryTimeoutMs,
|
|
6807
|
+
pollIntervalMs
|
|
6808
|
+
);
|
|
6809
|
+
if (recovered.ok) return { restarted: true };
|
|
6810
|
+
const action = expectation === "loaded" ? "load" : "remove";
|
|
6811
|
+
throw new Error(
|
|
6812
|
+
`Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
|
|
6813
|
+
);
|
|
6814
|
+
}
|
|
6815
|
+
);
|
|
6816
|
+
}
|
|
6817
|
+
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
6818
|
+
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
6819
|
+
}
|
|
6820
|
+
async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
6821
|
+
if (routes.length === 0) return;
|
|
6822
|
+
const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
6823
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
6824
|
+
const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
|
|
6825
|
+
if (!result.ok) {
|
|
6826
|
+
throw new Error(
|
|
6827
|
+
`Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
|
|
6828
|
+
);
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
6832
|
+
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
6833
|
+
}
|
|
6834
|
+
function assertTraefikRoutesRemoved(routes) {
|
|
6835
|
+
const result = inspectExpectedRoutes(routes, "removed");
|
|
6836
|
+
if (!result.ok) throw new Error(`Workspace route cessation is not proven: ${result.details}`);
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
// src/core/workspace-ensure.ts
|
|
6840
|
+
var import_node_child_process21 = require("child_process");
|
|
6841
|
+
var import_node_fs23 = __toESM(require("fs"));
|
|
6842
|
+
var import_node_path27 = __toESM(require("path"));
|
|
6843
|
+
|
|
6844
|
+
// src/core/devpod-mutation.ts
|
|
6845
|
+
var import_node_child_process17 = require("child_process");
|
|
6846
|
+
var import_node_fs20 = __toESM(require("fs"));
|
|
6847
|
+
var import_node_path25 = __toESM(require("path"));
|
|
6848
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
6849
|
+
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
6850
|
+
var DevpodStartPostconditionError = class extends Error {
|
|
6851
|
+
};
|
|
6852
|
+
function withMutationLock2(activity, target, operation) {
|
|
6853
|
+
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
6854
|
+
return withFileLockSync(
|
|
6855
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
6856
|
+
{
|
|
6857
|
+
activity,
|
|
6858
|
+
target: `'${target}'`,
|
|
6859
|
+
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
6860
|
+
fair: true,
|
|
6861
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
6862
|
+
},
|
|
6863
|
+
operation
|
|
6864
|
+
);
|
|
6865
|
+
}
|
|
6866
|
+
function commandFailure2(result) {
|
|
6867
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
6868
|
+
}
|
|
6869
|
+
function runDevpodAction(action, devpodId, force = false) {
|
|
6870
|
+
const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
|
|
6871
|
+
claimLifecycleEffect();
|
|
6872
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
6873
|
+
if (result.status !== 0) {
|
|
6874
|
+
throw new Error(
|
|
6875
|
+
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
|
|
6876
|
+
);
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
function inspectExactOwnership2(devpodId, worktreePath) {
|
|
6880
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
6881
|
+
listDevpodWorkspaces(worktreePath),
|
|
6882
|
+
devpodId,
|
|
6883
|
+
worktreePath
|
|
6884
|
+
);
|
|
6885
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
6359
6886
|
return ownership;
|
|
6360
6887
|
}
|
|
6361
6888
|
function mutateOwnedDevpodWorkspace(action, devpodId, worktreePath) {
|
|
@@ -6472,7 +6999,7 @@ async function startDevpodWorkspace(options) {
|
|
|
6472
6999
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
6473
7000
|
}
|
|
6474
7001
|
claimLifecycleEffect();
|
|
6475
|
-
const result = (0,
|
|
7002
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
|
|
6476
7003
|
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
6477
7004
|
env
|
|
6478
7005
|
});
|
|
@@ -6505,7 +7032,7 @@ async function startDevpodWorkspace(options) {
|
|
|
6505
7032
|
}
|
|
6506
7033
|
|
|
6507
7034
|
// src/core/http-route-probe.ts
|
|
6508
|
-
var
|
|
7035
|
+
var import_node_child_process18 = require("child_process");
|
|
6509
7036
|
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
6510
7037
|
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
6511
7038
|
var CONTRACT_MAX_BUFFER = 1024;
|
|
@@ -6538,7 +7065,7 @@ function probeHttpRoute(host, options = {}) {
|
|
|
6538
7065
|
args.push("--cacert", CERT_FILE);
|
|
6539
7066
|
}
|
|
6540
7067
|
args.push(url);
|
|
6541
|
-
const result = (0,
|
|
7068
|
+
const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
6542
7069
|
const stdout = result.stdout?.trim() ?? "";
|
|
6543
7070
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
6544
7071
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
@@ -6557,7 +7084,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
|
6557
7084
|
}
|
|
6558
7085
|
let result;
|
|
6559
7086
|
try {
|
|
6560
|
-
result = (0,
|
|
7087
|
+
result = (0, import_node_child_process18.spawnSync)("curl", args, {
|
|
6561
7088
|
encoding: "utf-8",
|
|
6562
7089
|
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
6563
7090
|
timeout: Math.ceil(parentTimeoutMs)
|
|
@@ -6641,8 +7168,8 @@ function transportFailure() {
|
|
|
6641
7168
|
}
|
|
6642
7169
|
|
|
6643
7170
|
// src/core/managed-host-preparation.ts
|
|
6644
|
-
var
|
|
6645
|
-
var
|
|
7171
|
+
var import_node_child_process19 = require("child_process");
|
|
7172
|
+
var import_node_fs21 = __toESM(require("fs"));
|
|
6646
7173
|
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
6647
7174
|
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
6648
7175
|
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
@@ -6653,7 +7180,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
6653
7180
|
return new Promise((resolve) => {
|
|
6654
7181
|
let child;
|
|
6655
7182
|
try {
|
|
6656
|
-
child = (0,
|
|
7183
|
+
child = (0, import_node_child_process19.spawn)(argv[0], argv.slice(1), {
|
|
6657
7184
|
cwd,
|
|
6658
7185
|
detached: true,
|
|
6659
7186
|
shell: false,
|
|
@@ -6685,7 +7212,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
6685
7212
|
}
|
|
6686
7213
|
function readConfigBytes(configPath) {
|
|
6687
7214
|
try {
|
|
6688
|
-
return
|
|
7215
|
+
return import_node_fs21.default.readFileSync(configPath);
|
|
6689
7216
|
} catch {
|
|
6690
7217
|
return void 0;
|
|
6691
7218
|
}
|
|
@@ -6719,10 +7246,10 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
6719
7246
|
}
|
|
6720
7247
|
|
|
6721
7248
|
// src/core/managed-post-start.ts
|
|
6722
|
-
var
|
|
7249
|
+
var import_node_child_process20 = require("child_process");
|
|
6723
7250
|
var import_node_crypto10 = require("crypto");
|
|
6724
|
-
var
|
|
6725
|
-
var
|
|
7251
|
+
var import_node_fs22 = __toESM(require("fs"));
|
|
7252
|
+
var import_node_path26 = __toESM(require("path"));
|
|
6726
7253
|
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
6727
7254
|
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
6728
7255
|
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
@@ -6735,7 +7262,7 @@ function renderDeliveryScript(targetPath) {
|
|
|
6735
7262
|
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
6736
7263
|
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
6737
7264
|
}
|
|
6738
|
-
const temporaryPrefix =
|
|
7265
|
+
const temporaryPrefix = import_node_path26.default.posix.basename(targetPath);
|
|
6739
7266
|
return `set -eu
|
|
6740
7267
|
umask 077
|
|
6741
7268
|
runtime_root=/tmp/devrouter
|
|
@@ -6759,7 +7286,7 @@ function commandFailure3(result) {
|
|
|
6759
7286
|
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
6760
7287
|
}
|
|
6761
7288
|
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
6762
|
-
const result = (0,
|
|
7289
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
6763
7290
|
"docker",
|
|
6764
7291
|
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
6765
7292
|
{ input: contents, encoding: "utf-8" }
|
|
@@ -6770,8 +7297,8 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
|
6770
7297
|
}
|
|
6771
7298
|
}
|
|
6772
7299
|
function readRegularFileBytes(filePath) {
|
|
6773
|
-
if (!
|
|
6774
|
-
return
|
|
7300
|
+
if (!import_node_fs22.default.existsSync(filePath) || !import_node_fs22.default.lstatSync(filePath).isFile()) return void 0;
|
|
7301
|
+
return import_node_fs22.default.readFileSync(filePath);
|
|
6775
7302
|
}
|
|
6776
7303
|
function readRegularFile(filePath) {
|
|
6777
7304
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
@@ -6781,21 +7308,21 @@ function adapterFingerprint(adapter) {
|
|
|
6781
7308
|
}
|
|
6782
7309
|
function resolveProcessHelperPath() {
|
|
6783
7310
|
const candidates = [
|
|
6784
|
-
|
|
6785
|
-
|
|
7311
|
+
import_node_path26.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
7312
|
+
import_node_path26.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
6786
7313
|
];
|
|
6787
|
-
const helperPath = candidates.find((candidate) =>
|
|
7314
|
+
const helperPath = candidates.find((candidate) => import_node_fs22.default.existsSync(candidate));
|
|
6788
7315
|
if (!helperPath) {
|
|
6789
7316
|
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
6790
7317
|
}
|
|
6791
7318
|
return helperPath;
|
|
6792
7319
|
}
|
|
6793
7320
|
function resolveManagedPostStartPlan(repoPath) {
|
|
6794
|
-
const adapterPath =
|
|
7321
|
+
const adapterPath = import_node_path26.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
6795
7322
|
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
6796
7323
|
const adapterText = adapterBytes?.toString("utf-8");
|
|
6797
|
-
const dockerfilePath =
|
|
6798
|
-
const devcontainerPath =
|
|
7324
|
+
const dockerfilePath = import_node_path26.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
7325
|
+
const devcontainerPath = import_node_path26.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
6799
7326
|
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
6800
7327
|
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
6801
7328
|
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
@@ -6836,7 +7363,7 @@ function resolveManagedPostStartPlan(repoPath) {
|
|
|
6836
7363
|
}
|
|
6837
7364
|
function runManagedPostStart(options) {
|
|
6838
7365
|
if (options.plan.kind !== "runtime") return;
|
|
6839
|
-
const helper =
|
|
7366
|
+
const helper = import_node_fs22.default.readFileSync(resolveProcessHelperPath());
|
|
6840
7367
|
deliverRuntimeFile(
|
|
6841
7368
|
options.container.id,
|
|
6842
7369
|
RUNTIME_HELPER_PATH,
|
|
@@ -6850,7 +7377,7 @@ function runManagedPostStart(options) {
|
|
|
6850
7377
|
options.plan.adapterContents,
|
|
6851
7378
|
"the managed post-start adapter"
|
|
6852
7379
|
);
|
|
6853
|
-
const
|
|
7380
|
+
const started2 = (0, import_node_child_process20.spawnSync)(
|
|
6854
7381
|
"docker",
|
|
6855
7382
|
[
|
|
6856
7383
|
"exec",
|
|
@@ -6876,8 +7403,8 @@ function runManagedPostStart(options) {
|
|
|
6876
7403
|
],
|
|
6877
7404
|
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
6878
7405
|
);
|
|
6879
|
-
if (
|
|
6880
|
-
const details = commandFailure3(
|
|
7406
|
+
if (started2.status !== 0) {
|
|
7407
|
+
const details = commandFailure3(started2);
|
|
6881
7408
|
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
6882
7409
|
}
|
|
6883
7410
|
}
|
|
@@ -6888,7 +7415,7 @@ function assertSafeProcessName(name) {
|
|
|
6888
7415
|
}
|
|
6889
7416
|
function runManagedProcessAction(options) {
|
|
6890
7417
|
assertSafeProcessName(options.name);
|
|
6891
|
-
const result = (0,
|
|
7418
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
6892
7419
|
"docker",
|
|
6893
7420
|
[
|
|
6894
7421
|
"exec",
|
|
@@ -7250,420 +7777,105 @@ function collectManagedRuntimeStatus(options) {
|
|
|
7250
7777
|
if (state) {
|
|
7251
7778
|
const desiredState = {
|
|
7252
7779
|
apps: sortedUnique(state.desired.apps),
|
|
7253
|
-
services: sortedUnique(state.desired.services),
|
|
7254
|
-
processes: sortedUnique(state.desired.processes)
|
|
7255
|
-
};
|
|
7256
|
-
const desired = {
|
|
7257
|
-
apps: desiredApps,
|
|
7258
|
-
services: desiredServices,
|
|
7259
|
-
processes: desiredProcesses
|
|
7260
|
-
};
|
|
7261
|
-
for (const [label, expected, actual] of [
|
|
7262
|
-
["apps", desired.apps, desiredState.apps],
|
|
7263
|
-
["services", desired.services, desiredState.services],
|
|
7264
|
-
["processes", desired.processes, desiredState.processes]
|
|
7265
|
-
]) {
|
|
7266
|
-
const mismatch = compareResourceSet(label, expected, actual);
|
|
7267
|
-
if (mismatch) drift.push(mismatch);
|
|
7268
|
-
}
|
|
7269
|
-
if (state.profile !== options.profile)
|
|
7270
|
-
drift.push("active profile differs from the selected profile");
|
|
7271
|
-
}
|
|
7272
|
-
const inspection = inspectManagedRuntime({
|
|
7273
|
-
repoPath: options.repoPath,
|
|
7274
|
-
config: options.config,
|
|
7275
|
-
registeredServices,
|
|
7276
|
-
registeredProcesses,
|
|
7277
|
-
desiredServices,
|
|
7278
|
-
desiredProcesses,
|
|
7279
|
-
workspace: options.workspace,
|
|
7280
|
-
state
|
|
7281
|
-
});
|
|
7282
|
-
drift.push(...inspection.drift);
|
|
7283
|
-
if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
|
|
7284
|
-
drift.push("managed Dev Container source configuration changed");
|
|
7285
|
-
}
|
|
7286
|
-
if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
|
|
7287
|
-
drift.push("managed Dev Container effective configuration changed");
|
|
7288
|
-
}
|
|
7289
|
-
if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
|
|
7290
|
-
drift.push("managed generated Dev Container configuration differs from last successful state");
|
|
7291
|
-
}
|
|
7292
|
-
const routes = exactRoutes(options.repoPath, options.workspace);
|
|
7293
|
-
drift.push(...routes.drift);
|
|
7294
|
-
const activeApps = routes.apps;
|
|
7295
|
-
const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
|
|
7296
|
-
if (activeApps.length > 0 && !inspection.primaryActive) {
|
|
7297
|
-
drift.push("active app routes exist without a running primary container");
|
|
7298
|
-
}
|
|
7299
|
-
if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
|
|
7300
|
-
drift.push("one or more selected app routes are missing");
|
|
7301
|
-
}
|
|
7302
|
-
if (activeApps.some((app) => !desiredApps.includes(app))) {
|
|
7303
|
-
drift.push("one or more active app routes are outside the selected profile");
|
|
7304
|
-
}
|
|
7305
|
-
if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
|
|
7306
|
-
drift.push("one or more active services are outside the selected profile");
|
|
7307
|
-
}
|
|
7308
|
-
if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
|
|
7309
|
-
drift.push("one or more active processes are outside the selected profile");
|
|
7310
|
-
}
|
|
7311
|
-
const positivelyStopped = !inspection.primaryActive && Object.values(inspection.baseStatuses).every(isStoppedOrMissingResourceStatus) && Object.values(inspection.serviceStatuses).every(isStoppedOrMissingResourceStatus) && activeApps.length === 0;
|
|
7312
|
-
const onlyDegradedIncident = state?.status === "degraded" && drift.length === 1;
|
|
7313
|
-
const requiredServicesReady = desiredServices.every(
|
|
7314
|
-
(service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
|
|
7315
|
-
);
|
|
7316
|
-
const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
|
|
7317
|
-
const processesReady = desiredProcesses.every(
|
|
7318
|
-
(process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
|
|
7319
|
-
);
|
|
7320
|
-
const routesReady = desiredApps.every((app) => activeApps.includes(app));
|
|
7321
|
-
const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
|
|
7322
|
-
const status = positivelyStopped && onlyDegradedIncident ? "stopped" : state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
|
|
7323
|
-
const active = {
|
|
7324
|
-
apps: activeApps,
|
|
7325
|
-
services: inspection.activeServices,
|
|
7326
|
-
processes: inspection.activeProcesses
|
|
7327
|
-
};
|
|
7328
|
-
return {
|
|
7329
|
-
mode: "managed",
|
|
7330
|
-
status,
|
|
7331
|
-
profile: options.profile,
|
|
7332
|
-
...state?.profile ? { activeProfile: state.profile } : {},
|
|
7333
|
-
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
7334
|
-
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
7335
|
-
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
7336
|
-
desired: {
|
|
7337
|
-
apps: desiredApps,
|
|
7338
|
-
services: desiredServices,
|
|
7339
|
-
processes: desiredProcesses
|
|
7340
|
-
},
|
|
7341
|
-
active,
|
|
7342
|
-
serviceStatuses: inspection.serviceStatuses,
|
|
7343
|
-
baseServiceStatuses: inspection.baseStatuses,
|
|
7344
|
-
processStatuses: inspection.processStatuses,
|
|
7345
|
-
drift: sortedUnique(drift),
|
|
7346
|
-
...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
|
|
7347
|
-
...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
|
|
7348
|
-
effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
|
|
7349
|
-
} : {},
|
|
7350
|
-
...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
|
|
7351
|
-
};
|
|
7352
|
-
}
|
|
7353
|
-
|
|
7354
|
-
// src/core/traefik-route-health.ts
|
|
7355
|
-
var import_node_child_process20 = require("child_process");
|
|
7356
|
-
var import_node_fs21 = __toESM(require("fs"));
|
|
7357
|
-
var import_node_path25 = __toESM(require("path"));
|
|
7358
|
-
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
7359
|
-
var ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
7360
|
-
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
7361
|
-
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
7362
|
-
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
7363
|
-
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
7364
|
-
var ROUTER_API_PAGE_SIZE = 1e3;
|
|
7365
|
-
function sleep(ms) {
|
|
7366
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7367
|
-
}
|
|
7368
|
-
function inspectRouterApi(protocol) {
|
|
7369
|
-
const result = (0, import_node_child_process20.spawnSync)(
|
|
7370
|
-
"curl",
|
|
7371
|
-
[
|
|
7372
|
-
"--silent",
|
|
7373
|
-
"--show-error",
|
|
7374
|
-
"--fail",
|
|
7375
|
-
"--max-time",
|
|
7376
|
-
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
7377
|
-
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
7378
|
-
],
|
|
7379
|
-
{ encoding: "utf-8" }
|
|
7380
|
-
);
|
|
7381
|
-
if (result.status !== 0) {
|
|
7382
|
-
return {
|
|
7383
|
-
ok: false,
|
|
7384
|
-
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
7385
|
-
};
|
|
7386
|
-
}
|
|
7387
|
-
let value;
|
|
7388
|
-
try {
|
|
7389
|
-
value = JSON.parse(result.stdout);
|
|
7390
|
-
} catch {
|
|
7391
|
-
return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
|
|
7392
|
-
}
|
|
7393
|
-
if (!Array.isArray(value)) {
|
|
7394
|
-
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
7395
|
-
}
|
|
7396
|
-
return {
|
|
7397
|
-
ok: true,
|
|
7398
|
-
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
7399
|
-
names: new Set(
|
|
7400
|
-
value.flatMap((item) => {
|
|
7401
|
-
if (!item || typeof item !== "object") return [];
|
|
7402
|
-
const name = item.name;
|
|
7403
|
-
return typeof name === "string" ? [name] : [];
|
|
7404
|
-
})
|
|
7405
|
-
)
|
|
7406
|
-
};
|
|
7407
|
-
}
|
|
7408
|
-
function isRecord3(value) {
|
|
7409
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7410
|
-
}
|
|
7411
|
-
function inspectNamedRouterApi(protocol, resource, name) {
|
|
7412
|
-
const result = (0, import_node_child_process20.spawnSync)(
|
|
7413
|
-
"curl",
|
|
7414
|
-
[
|
|
7415
|
-
"--silent",
|
|
7416
|
-
"--show-error",
|
|
7417
|
-
"--fail",
|
|
7418
|
-
"--max-time",
|
|
7419
|
-
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
7420
|
-
`${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
|
|
7421
|
-
],
|
|
7422
|
-
{ encoding: "utf-8" }
|
|
7423
|
-
);
|
|
7424
|
-
if (result.status !== 0) {
|
|
7425
|
-
return {
|
|
7426
|
-
ok: false,
|
|
7427
|
-
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
7428
|
-
};
|
|
7429
|
-
}
|
|
7430
|
-
let value;
|
|
7431
|
-
try {
|
|
7432
|
-
value = JSON.parse(result.stdout);
|
|
7433
|
-
} catch {
|
|
7434
|
-
return {
|
|
7435
|
-
ok: false,
|
|
7436
|
-
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
7437
|
-
};
|
|
7438
|
-
}
|
|
7439
|
-
if (!isRecord3(value)) {
|
|
7440
|
-
return {
|
|
7441
|
-
ok: false,
|
|
7442
|
-
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
7443
|
-
};
|
|
7444
|
-
}
|
|
7445
|
-
return { ok: true, value };
|
|
7446
|
-
}
|
|
7447
|
-
function renderExpectedRoute(route) {
|
|
7448
|
-
const protocol = route.protocol ?? "http";
|
|
7449
|
-
const routerName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
7450
|
-
const routeState = {
|
|
7451
|
-
id: buildHostRouteId(route.repoPath, route.name),
|
|
7452
|
-
name: route.name,
|
|
7453
|
-
host: route.host,
|
|
7454
|
-
protocol,
|
|
7455
|
-
tcpProtocol: route.tcpProtocol,
|
|
7456
|
-
repoPath: route.repoPath,
|
|
7457
|
-
port: route.port,
|
|
7458
|
-
mode: route.mode,
|
|
7459
|
-
upstreamHost: route.upstreamHost,
|
|
7460
|
-
pid: route.pid,
|
|
7461
|
-
command: route.command,
|
|
7462
|
-
workspace: route.workspace,
|
|
7463
|
-
createdAt: "",
|
|
7464
|
-
updatedAt: ""
|
|
7465
|
-
};
|
|
7466
|
-
const document = buildHostRoutesDocument([routeState], false);
|
|
7467
|
-
const protocolDocument = document[protocol];
|
|
7468
|
-
if (!isRecord3(protocolDocument)) return void 0;
|
|
7469
|
-
const routers = protocolDocument.routers;
|
|
7470
|
-
const services = protocolDocument.services;
|
|
7471
|
-
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
7472
|
-
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
7473
|
-
const router = routers[routerKey];
|
|
7474
|
-
const service = services[routerKey];
|
|
7475
|
-
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
7476
|
-
const rule = router.rule;
|
|
7477
|
-
const serviceReference = router.service;
|
|
7478
|
-
const loadBalancer = service.loadBalancer;
|
|
7479
|
-
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
7480
|
-
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
7481
|
-
return void 0;
|
|
7482
|
-
}
|
|
7483
|
-
const serverField = protocol === "http" ? "url" : "address";
|
|
7484
|
-
const server = servers[0][serverField];
|
|
7485
|
-
if (typeof server !== "string") return void 0;
|
|
7486
|
-
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
7487
|
-
if (!provider.startsWith("@")) return void 0;
|
|
7488
|
-
return {
|
|
7489
|
-
protocol,
|
|
7490
|
-
routerName,
|
|
7491
|
-
serviceKey: routerKey,
|
|
7492
|
-
serviceName: `${routerKey}${provider}`,
|
|
7493
|
-
rule,
|
|
7494
|
-
serverField,
|
|
7495
|
-
server
|
|
7496
|
-
};
|
|
7497
|
-
}
|
|
7498
|
-
function inspectExpectedRouteMatch(route) {
|
|
7499
|
-
const expected = renderExpectedRoute(route);
|
|
7500
|
-
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
7501
|
-
if (!expected) {
|
|
7502
|
-
return {
|
|
7503
|
-
ok: false,
|
|
7504
|
-
routes: [routeName],
|
|
7505
|
-
details: "host-route renderer returned an incomplete route configuration"
|
|
7780
|
+
services: sortedUnique(state.desired.services),
|
|
7781
|
+
processes: sortedUnique(state.desired.processes)
|
|
7782
|
+
};
|
|
7783
|
+
const desired = {
|
|
7784
|
+
apps: desiredApps,
|
|
7785
|
+
services: desiredServices,
|
|
7786
|
+
processes: desiredProcesses
|
|
7506
7787
|
};
|
|
7788
|
+
for (const [label, expected, actual] of [
|
|
7789
|
+
["apps", desired.apps, desiredState.apps],
|
|
7790
|
+
["services", desired.services, desiredState.services],
|
|
7791
|
+
["processes", desired.processes, desiredState.processes]
|
|
7792
|
+
]) {
|
|
7793
|
+
const mismatch = compareResourceSet(label, expected, actual);
|
|
7794
|
+
if (mismatch) drift.push(mismatch);
|
|
7795
|
+
}
|
|
7796
|
+
if (state.profile !== options.profile)
|
|
7797
|
+
drift.push("active profile differs from the selected profile");
|
|
7507
7798
|
}
|
|
7508
|
-
const
|
|
7509
|
-
|
|
7510
|
-
|
|
7799
|
+
const inspection = inspectManagedRuntime({
|
|
7800
|
+
repoPath: options.repoPath,
|
|
7801
|
+
config: options.config,
|
|
7802
|
+
registeredServices,
|
|
7803
|
+
registeredProcesses,
|
|
7804
|
+
desiredServices,
|
|
7805
|
+
desiredProcesses,
|
|
7806
|
+
workspace: options.workspace,
|
|
7807
|
+
state
|
|
7808
|
+
});
|
|
7809
|
+
drift.push(...inspection.drift);
|
|
7810
|
+
if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
|
|
7811
|
+
drift.push("managed Dev Container source configuration changed");
|
|
7511
7812
|
}
|
|
7512
|
-
if (
|
|
7513
|
-
|
|
7514
|
-
ok: false,
|
|
7515
|
-
routes: [routeName],
|
|
7516
|
-
details: "active Traefik router does not match the rendered route"
|
|
7517
|
-
};
|
|
7813
|
+
if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
|
|
7814
|
+
drift.push("managed Dev Container effective configuration changed");
|
|
7518
7815
|
}
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
7816
|
+
if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
|
|
7817
|
+
drift.push("managed generated Dev Container configuration differs from last successful state");
|
|
7522
7818
|
}
|
|
7523
|
-
const
|
|
7524
|
-
|
|
7525
|
-
const
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
routes: [routeName],
|
|
7530
|
-
details: "active Traefik service does not match the rendered route"
|
|
7531
|
-
};
|
|
7819
|
+
const routes = exactRoutes(options.repoPath, options.workspace);
|
|
7820
|
+
drift.push(...routes.drift);
|
|
7821
|
+
const activeApps = routes.apps;
|
|
7822
|
+
const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
|
|
7823
|
+
if (activeApps.length > 0 && !inspection.primaryActive) {
|
|
7824
|
+
drift.push("active app routes exist without a running primary container");
|
|
7532
7825
|
}
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
function inspectExpectedRouteMatches(routes) {
|
|
7536
|
-
const mismatched = [];
|
|
7537
|
-
const failures = [];
|
|
7538
|
-
for (const route of routes) {
|
|
7539
|
-
const result = inspectExpectedRouteMatch(route);
|
|
7540
|
-
if (!result.ok) {
|
|
7541
|
-
mismatched.push(...result.routes);
|
|
7542
|
-
failures.push(result.details);
|
|
7543
|
-
}
|
|
7826
|
+
if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
|
|
7827
|
+
drift.push("one or more selected app routes are missing");
|
|
7544
7828
|
}
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
routes: mismatched,
|
|
7548
|
-
details: failures.join("; ")
|
|
7549
|
-
};
|
|
7550
|
-
}
|
|
7551
|
-
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
7552
|
-
const deadline = Date.now() + timeoutMs;
|
|
7553
|
-
let result;
|
|
7554
|
-
do {
|
|
7555
|
-
result = inspectExpectedRouteMatches(routes);
|
|
7556
|
-
if (result.ok) return result;
|
|
7557
|
-
if (Date.now() < deadline) {
|
|
7558
|
-
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
7559
|
-
}
|
|
7560
|
-
} while (Date.now() < deadline);
|
|
7561
|
-
return result;
|
|
7562
|
-
}
|
|
7563
|
-
function inspectExpectedRoutes(routes, expectation) {
|
|
7564
|
-
const mismatched = [];
|
|
7565
|
-
const failures = [];
|
|
7566
|
-
for (const protocol of ["http", "tcp"]) {
|
|
7567
|
-
const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
|
|
7568
|
-
if (expected.length === 0) continue;
|
|
7569
|
-
const result = inspectRouterApi(protocol);
|
|
7570
|
-
if (!result.ok) {
|
|
7571
|
-
failures.push(result.details);
|
|
7572
|
-
mismatched.push(...expected);
|
|
7573
|
-
continue;
|
|
7574
|
-
}
|
|
7575
|
-
const unexpected = expected.filter(
|
|
7576
|
-
(name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
|
|
7577
|
-
);
|
|
7578
|
-
const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
|
|
7579
|
-
if (boundedAbsenceIsUncertain) {
|
|
7580
|
-
failures.push(
|
|
7581
|
-
`${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
|
|
7582
|
-
);
|
|
7583
|
-
}
|
|
7584
|
-
mismatched.push(...unexpected);
|
|
7585
|
-
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
7586
|
-
mismatched.push(...expected);
|
|
7587
|
-
}
|
|
7829
|
+
if (activeApps.some((app) => !desiredApps.includes(app))) {
|
|
7830
|
+
drift.push("one or more active app routes are outside the selected profile");
|
|
7588
7831
|
}
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
}
|
|
7595
|
-
|
|
7596
|
-
const
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
result = inspectExpectedRoutes(routes, expectation);
|
|
7600
|
-
if (result.ok) return result;
|
|
7601
|
-
if (Date.now() < deadline) {
|
|
7602
|
-
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
7603
|
-
}
|
|
7604
|
-
} while (Date.now() < deadline);
|
|
7605
|
-
return result;
|
|
7606
|
-
}
|
|
7607
|
-
async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
7608
|
-
if (routes.length === 0) return { restarted: false };
|
|
7609
|
-
const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
7610
|
-
const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
|
|
7611
|
-
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
7612
|
-
const initial = await waitForExpectedRoutes(
|
|
7613
|
-
routes,
|
|
7614
|
-
expectation,
|
|
7615
|
-
initialTimeoutMs,
|
|
7616
|
-
pollIntervalMs
|
|
7832
|
+
if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
|
|
7833
|
+
drift.push("one or more active services are outside the selected profile");
|
|
7834
|
+
}
|
|
7835
|
+
if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
|
|
7836
|
+
drift.push("one or more active processes are outside the selected profile");
|
|
7837
|
+
}
|
|
7838
|
+
const positivelyStopped = !inspection.primaryActive && Object.values(inspection.baseStatuses).every(isStoppedOrMissingResourceStatus) && Object.values(inspection.serviceStatuses).every(isStoppedOrMissingResourceStatus) && activeApps.length === 0;
|
|
7839
|
+
const onlyDegradedIncident = state?.status === "degraded" && drift.length === 1;
|
|
7840
|
+
const requiredServicesReady = desiredServices.every(
|
|
7841
|
+
(service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
|
|
7617
7842
|
);
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
7622
|
-
);
|
|
7623
|
-
import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7624
|
-
return withFileLock(
|
|
7625
|
-
ROUTER_RELOAD_LOCK_FILE,
|
|
7626
|
-
{
|
|
7627
|
-
activity: "Traefik route reload recovery",
|
|
7628
|
-
waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
|
|
7629
|
-
fair: true,
|
|
7630
|
-
onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
|
|
7631
|
-
},
|
|
7632
|
-
async () => {
|
|
7633
|
-
const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
|
|
7634
|
-
if (recheck.ok) return { restarted: false };
|
|
7635
|
-
claimLifecycleEffect();
|
|
7636
|
-
restartRouterStack();
|
|
7637
|
-
const recovered = await waitForExpectedRoutes(
|
|
7638
|
-
routes,
|
|
7639
|
-
expectation,
|
|
7640
|
-
recoveryTimeoutMs,
|
|
7641
|
-
pollIntervalMs
|
|
7642
|
-
);
|
|
7643
|
-
if (recovered.ok) return { restarted: true };
|
|
7644
|
-
const action = expectation === "loaded" ? "load" : "remove";
|
|
7645
|
-
throw new Error(
|
|
7646
|
-
`Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
|
|
7647
|
-
);
|
|
7648
|
-
}
|
|
7843
|
+
const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
|
|
7844
|
+
const processesReady = desiredProcesses.every(
|
|
7845
|
+
(process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
|
|
7649
7846
|
);
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
}
|
|
7665
|
-
|
|
7666
|
-
|
|
7847
|
+
const routesReady = desiredApps.every((app) => activeApps.includes(app));
|
|
7848
|
+
const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
|
|
7849
|
+
const status = positivelyStopped && onlyDegradedIncident ? "stopped" : state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
|
|
7850
|
+
const active = {
|
|
7851
|
+
apps: activeApps,
|
|
7852
|
+
services: inspection.activeServices,
|
|
7853
|
+
processes: inspection.activeProcesses
|
|
7854
|
+
};
|
|
7855
|
+
return {
|
|
7856
|
+
mode: "managed",
|
|
7857
|
+
status,
|
|
7858
|
+
profile: options.profile,
|
|
7859
|
+
...state?.profile ? { activeProfile: state.profile } : {},
|
|
7860
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
7861
|
+
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
7862
|
+
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
7863
|
+
desired: {
|
|
7864
|
+
apps: desiredApps,
|
|
7865
|
+
services: desiredServices,
|
|
7866
|
+
processes: desiredProcesses
|
|
7867
|
+
},
|
|
7868
|
+
active,
|
|
7869
|
+
serviceStatuses: inspection.serviceStatuses,
|
|
7870
|
+
baseServiceStatuses: inspection.baseStatuses,
|
|
7871
|
+
processStatuses: inspection.processStatuses,
|
|
7872
|
+
drift: sortedUnique(drift),
|
|
7873
|
+
...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
|
|
7874
|
+
...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
|
|
7875
|
+
effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
|
|
7876
|
+
} : {},
|
|
7877
|
+
...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
|
|
7878
|
+
};
|
|
7667
7879
|
}
|
|
7668
7880
|
|
|
7669
7881
|
// src/core/workspace-ensure.ts
|
|
@@ -7672,11 +7884,11 @@ var DEFAULT_READINESS_TIMEOUT_MS = 12e4;
|
|
|
7672
7884
|
var POLL_INTERVAL_MS = 1e3;
|
|
7673
7885
|
function assertOverlay(container, repoPath) {
|
|
7674
7886
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
7675
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
7887
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
|
|
7676
7888
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
7677
7889
|
}
|
|
7678
7890
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
7679
|
-
const expectedOverlay =
|
|
7891
|
+
const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
7680
7892
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
7681
7893
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
7682
7894
|
}
|
|
@@ -7871,7 +8083,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
7871
8083
|
}).some((container) => {
|
|
7872
8084
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
7873
8085
|
return Boolean(
|
|
7874
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
8086
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
|
|
7875
8087
|
);
|
|
7876
8088
|
});
|
|
7877
8089
|
} catch {
|
|
@@ -8255,7 +8467,7 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
8255
8467
|
}
|
|
8256
8468
|
function isPrimaryCheckout(repoPath) {
|
|
8257
8469
|
try {
|
|
8258
|
-
return
|
|
8470
|
+
return import_node_fs23.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
|
|
8259
8471
|
} catch {
|
|
8260
8472
|
return false;
|
|
8261
8473
|
}
|
|
@@ -8316,8 +8528,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8316
8528
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
8317
8529
|
let devpodId = target.devpodId;
|
|
8318
8530
|
if (target.kind === "linked") {
|
|
8319
|
-
const overlayPath =
|
|
8320
|
-
if (!
|
|
8531
|
+
const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
8532
|
+
if (!import_node_fs23.default.existsSync(overlayPath)) {
|
|
8321
8533
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
8322
8534
|
}
|
|
8323
8535
|
}
|
|
@@ -8387,7 +8599,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8387
8599
|
}
|
|
8388
8600
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
8389
8601
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
8390
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
8602
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
|
|
8391
8603
|
for (const [index, app] of apps.entries()) {
|
|
8392
8604
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
8393
8605
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -9157,9 +9369,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
9157
9369
|
}
|
|
9158
9370
|
const repairLocked = () => {
|
|
9159
9371
|
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
9160
|
-
|
|
9372
|
+
import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
9161
9373
|
return withFileLock(
|
|
9162
|
-
|
|
9374
|
+
import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
9163
9375
|
{
|
|
9164
9376
|
activity: "Managed runtime repair",
|
|
9165
9377
|
target: repoPath,
|
|
@@ -9230,6 +9442,7 @@ var cancelled = false;
|
|
|
9230
9442
|
var lockHeld = false;
|
|
9231
9443
|
var stopProjects = [];
|
|
9232
9444
|
var stopBaselineState;
|
|
9445
|
+
var BUSY_EXEC_WAIT_MS = 30 * 60 * 1e3;
|
|
9233
9446
|
function matchesFence(record4, fence) {
|
|
9234
9447
|
return Object.entries(reliabilityFence(record4.state)).every(
|
|
9235
9448
|
([key, value]) => value === fence[key]
|
|
@@ -9347,14 +9560,14 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
9347
9560
|
withMutationLock(
|
|
9348
9561
|
"Verify retained stop",
|
|
9349
9562
|
request.repoPath,
|
|
9350
|
-
() =>
|
|
9563
|
+
() => proveManagedStop(retained)
|
|
9351
9564
|
);
|
|
9352
9565
|
} else {
|
|
9353
9566
|
const containers = inspectWorkspaceContainers();
|
|
9354
9567
|
const owned = containers.filter(
|
|
9355
9568
|
(container) => sameWorkspacePath(
|
|
9356
9569
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
9357
|
-
|
|
9570
|
+
import_node_path28.default.join(request.repoPath, ".devcontainer")
|
|
9358
9571
|
)
|
|
9359
9572
|
);
|
|
9360
9573
|
const projects = owned.map((container) => container.labels["com.docker.compose.project"]);
|
|
@@ -9428,9 +9641,11 @@ function proveLifecycleStopped() {
|
|
|
9428
9641
|
const settle = () => {
|
|
9429
9642
|
claimLifecycleEffect();
|
|
9430
9643
|
if (stopBaselineState) {
|
|
9431
|
-
|
|
9644
|
+
const proof = proveManagedStop(stopBaselineState);
|
|
9645
|
+
if (proof.containers.some((container) => container.state.Running))
|
|
9432
9646
|
throw new Error("Retained workspace workloads remain running.");
|
|
9433
|
-
|
|
9647
|
+
if (proof.status === "proven-absent")
|
|
9648
|
+
assertTraefikRoutesRemoved(managedStopRouteReferences(stopBaselineState));
|
|
9434
9649
|
} else {
|
|
9435
9650
|
for (const project of stopProjects) {
|
|
9436
9651
|
const containers = inspectManagedStopContainers(project);
|
|
@@ -9440,7 +9655,7 @@ function proveLifecycleStopped() {
|
|
|
9440
9655
|
const remaining = inspectWorkspaceContainers().filter(
|
|
9441
9656
|
(container) => sameWorkspacePath(
|
|
9442
9657
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
9443
|
-
|
|
9658
|
+
import_node_path28.default.join(request.repoPath, ".devcontainer")
|
|
9444
9659
|
)
|
|
9445
9660
|
);
|
|
9446
9661
|
if (remaining.some(
|
|
@@ -9476,7 +9691,7 @@ function ensureGuidance(repoPath) {
|
|
|
9476
9691
|
function errorMessage(error) {
|
|
9477
9692
|
return error instanceof Error ? error.message : String(error);
|
|
9478
9693
|
}
|
|
9479
|
-
async function devsyExecOutcome(repoPath, command) {
|
|
9694
|
+
async function devsyExecOutcome(repoPath, command, retainedProof) {
|
|
9480
9695
|
if (command.length === 0) {
|
|
9481
9696
|
throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
|
|
9482
9697
|
}
|
|
@@ -9487,12 +9702,25 @@ async function devsyExecOutcome(repoPath, command) {
|
|
|
9487
9702
|
`No exact Devsy workspace exists for '${repoPath}'. ${ensureGuidance(repoPath)}`
|
|
9488
9703
|
);
|
|
9489
9704
|
}
|
|
9490
|
-
const args = [
|
|
9705
|
+
const args = [
|
|
9706
|
+
"workspace",
|
|
9707
|
+
"exec",
|
|
9708
|
+
"--result-format",
|
|
9709
|
+
"plain",
|
|
9710
|
+
...retainedProof ? ["--context", retainedProof.context] : [],
|
|
9711
|
+
retainedProof?.id ?? workspace.id,
|
|
9712
|
+
"--",
|
|
9713
|
+
...command
|
|
9714
|
+
];
|
|
9491
9715
|
return new Promise((resolve, reject) => {
|
|
9492
9716
|
let child;
|
|
9493
9717
|
try {
|
|
9494
|
-
|
|
9495
|
-
|
|
9718
|
+
const launch = () => {
|
|
9719
|
+
if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
|
|
9720
|
+
claimLifecycleEffect();
|
|
9721
|
+
return (0, import_node_child_process22.spawn)("devsy", args, { stdio: "inherit" });
|
|
9722
|
+
};
|
|
9723
|
+
child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
|
|
9496
9724
|
} catch (error) {
|
|
9497
9725
|
reject(
|
|
9498
9726
|
new ExecutionOutcomeError(`devsy exec failed: ${errorMessage(error)}`, {
|
|
@@ -9775,11 +10003,14 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
9775
10003
|
|
|
9776
10004
|
// src/core/workspace-lifecycle.ts
|
|
9777
10005
|
var import_node_child_process24 = require("child_process");
|
|
9778
|
-
var
|
|
9779
|
-
var
|
|
10006
|
+
var import_node_fs25 = __toESM(require("fs"));
|
|
10007
|
+
var import_node_path29 = __toESM(require("path"));
|
|
9780
10008
|
|
|
9781
10009
|
// src/core/route-state.ts
|
|
9782
|
-
var
|
|
10010
|
+
var import_node_fs24 = __toESM(require("fs"));
|
|
10011
|
+
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
10012
|
+
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
10013
|
+
}
|
|
9783
10014
|
function removeWorkspaceRoutesForWorktree(workspace, worktreePath) {
|
|
9784
10015
|
return removeHostRoutesWhere(
|
|
9785
10016
|
(route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)
|
|
@@ -9831,8 +10062,24 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
|
|
|
9831
10062
|
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
9832
10063
|
);
|
|
9833
10064
|
}
|
|
9834
|
-
const
|
|
9835
|
-
|
|
10065
|
+
const retained = action === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
|
|
10066
|
+
let absent = false;
|
|
10067
|
+
const remove = () => {
|
|
10068
|
+
if (retained?.stopBaseline) {
|
|
10069
|
+
absent = proveManagedStop(retained).status === "proven-absent";
|
|
10070
|
+
if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
|
|
10071
|
+
(route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
|
|
10072
|
+
))
|
|
10073
|
+
throw new Error("Absent stop contains routes outside the retained desired set.");
|
|
10074
|
+
}
|
|
10075
|
+
claimLifecycleEffect();
|
|
10076
|
+
return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
10077
|
+
};
|
|
10078
|
+
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
|
|
10079
|
+
await ensureTraefikRoutesRemoved([
|
|
10080
|
+
...routes,
|
|
10081
|
+
...absent && retained ? managedStopRouteReferences(retained) : []
|
|
10082
|
+
]);
|
|
9836
10083
|
if (!quiet) {
|
|
9837
10084
|
process.stdout.write(
|
|
9838
10085
|
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
@@ -9843,6 +10090,7 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
|
|
|
9843
10090
|
...mutation.status === "changed" ? { devpodId } : {},
|
|
9844
10091
|
freedRoutes: routes.length,
|
|
9845
10092
|
providerChanged: mutation.status === "changed",
|
|
10093
|
+
...absent ? { runtimeAbsent: true } : {},
|
|
9846
10094
|
workspace: resolved.workspace
|
|
9847
10095
|
};
|
|
9848
10096
|
}
|
|
@@ -9906,7 +10154,7 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
9906
10154
|
repoPath,
|
|
9907
10155
|
workspace: result.workspace,
|
|
9908
10156
|
...result.devpodId ? { devpodId: result.devpodId } : {},
|
|
9909
|
-
stopped: !options.delete && result.providerChanged,
|
|
10157
|
+
stopped: !options.delete && (result.providerChanged || result.runtimeAbsent === true),
|
|
9910
10158
|
...options.delete ? { deleted: result.providerChanged } : {},
|
|
9911
10159
|
freedRoutes: result.freedRoutes
|
|
9912
10160
|
};
|
|
@@ -9939,10 +10187,16 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
9939
10187
|
}
|
|
9940
10188
|
|
|
9941
10189
|
// src/lifecycle-worker.ts
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
10190
|
+
var started = false;
|
|
10191
|
+
function cancel() {
|
|
10192
|
+
cancelLifecycleWorker();
|
|
10193
|
+
if (!started && process.connected) process.disconnect();
|
|
10194
|
+
}
|
|
10195
|
+
process.on("SIGTERM", cancel);
|
|
10196
|
+
process.on("SIGINT", cancel);
|
|
10197
|
+
process.on("disconnect", cancel);
|
|
9945
10198
|
process.once("message", async (message) => {
|
|
10199
|
+
started = true;
|
|
9946
10200
|
let result;
|
|
9947
10201
|
try {
|
|
9948
10202
|
const request = message.request;
|
|
@@ -9953,7 +10207,11 @@ process.once("message", async (message) => {
|
|
|
9953
10207
|
return stopped;
|
|
9954
10208
|
}
|
|
9955
10209
|
if (request.kind === "exec") {
|
|
9956
|
-
const outcome =
|
|
10210
|
+
const outcome = request.retainedExecProof ? await devsyExecOutcome(
|
|
10211
|
+
request.repoPath,
|
|
10212
|
+
request.command ?? [],
|
|
10213
|
+
request.retainedExecProof
|
|
10214
|
+
) : await devpodExecOutcome(request.repoPath, request.command ?? []);
|
|
9957
10215
|
recordLifecycleOutcome(outcome);
|
|
9958
10216
|
return outcome;
|
|
9959
10217
|
}
|