@devrouter/cli 0.0.51 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devrouter-process +91 -4
- package/dist/devrouter.js +1959 -1000
- package/package.json +1 -1
- package/upgrade-prompts/0.0.52.md +34 -0
- package/upgrade-prompts/0.0.53.md +42 -0
package/dist/devrouter.js
CHANGED
|
@@ -249,6 +249,8 @@ profiles:
|
|
|
249
249
|
- Validation is strict at config load: unknown keys, non-routed \`apps\`, non-dependency \`dependencies\`, \`readiness\` outside the profile's apps, and multiple defaults are rejected.
|
|
250
250
|
- Selection: \`devrouter ensure <path> --profile <name>\`. Comma-separated selections (\`--profile manage,pwa\`) merge with deduplication; the canonical name is sorted-unique so order never affects identity or fingerprints. A wildcard member collapses to everything.
|
|
251
251
|
- Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
|
|
252
|
+
- Adapters may pass \`--prepare-command <command>\` to \`devrouter-process ensure\` for synchronous dependency preparation under the process lock before application launch. Unchanged owned processes skip preparation; preparation participates in default fingerprints and must not daemonize or detach.
|
|
253
|
+
- Exact managed Devsy stop proves the complete retained container population, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup, and an original provider failure remains an error even if residual cleanup succeeds.
|
|
252
254
|
|
|
253
255
|
### Managed devcontainer resources
|
|
254
256
|
|
|
@@ -400,7 +402,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
400
402
|
- \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
|
|
401
403
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
402
404
|
- \`devrouter setup --yes [--repo .] [--json] [--workspace-runtime <devpod|devsy>]\`: first-run machine setup plus structured diagnostics; explicit Devsy selection acquires its verified agent
|
|
403
|
-
- \`devrouter ensure [path] [--profile <name>] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts;
|
|
405
|
+
- \`devrouter ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts; explicit repair uses the recorded degraded profile and unchanged retained configuration
|
|
404
406
|
- \`devrouter profile resolve --repo <path> [--profile <selection>] [--json]\`: resolve exact profile resources for automation without starting or inspecting a runtime
|
|
405
407
|
- \`devrouter profile plan --repo <path> [--profile <selection>] --contract <repo-relative-yaml> [--output <path>] [--json]\`: validate repository-owned resource policy and emit literal bindings without runtime access
|
|
406
408
|
- \`devrouter stop [path] [--delete] [--json]\`: stop the exact workspace runtime and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
|
|
@@ -425,7 +427,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
425
427
|
- \`devrouter app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
|
|
426
428
|
- \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
|
|
427
429
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
428
|
-
- \`devrouter workspace ensure [path] [--profile <name>] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
430
|
+
- \`devrouter workspace ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
429
431
|
- \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
|
|
430
432
|
- \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]\`: report-only cleanup evidence and exact guarded suggestions; no \`--yes\` or apply mode
|
|
431
433
|
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
@@ -2691,7 +2693,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2691
2693
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2692
2694
|
const requiredVersion = config.devrouter?.version;
|
|
2693
2695
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2694
|
-
const cliVersion = true ? "0.0.
|
|
2696
|
+
const cliVersion = true ? "0.0.53" : "0.0.0-dev";
|
|
2695
2697
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2696
2698
|
hasWarnedVersionMismatch = true;
|
|
2697
2699
|
process.stderr.write(
|
|
@@ -3227,6 +3229,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3227
3229
|
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
|
|
3228
3230
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
3229
3231
|
"- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
|
|
3232
|
+
"- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
|
|
3233
|
+
"- Exact managed Devsy stop validates complete retained container membership, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup; original provider failures remain errors even after successful residual cleanup.",
|
|
3230
3234
|
"- Owner status is `present`, `missing`, `locked`, or `conflict`. Dirty or locked full down fails before side effects. `workspace gc` is a dry run; only `--yes` deletes exact eligible missing resources and records, never Git worktrees, branches, or prune state.",
|
|
3231
3235
|
"- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, workspace runtime registration, runtime state (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. Local DevPod/Devsy list/status checks always run; `--check-merged` alone enables read-only origin and matching GitHub/GitLab checks. Treat `not-found` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit `gc`/`down` can remove exact stale registration only after expected-ID `NotFound` proof and ownership revalidation. Runtime `lastUsed` remains advisory.",
|
|
3232
3236
|
'- `--measure-size` adds per-workspace storage consumption and stays read-only, but walks each worktree and runs `docker ps` / `docker inspect --size`, so leave it off for a quick report. Each row reports `worktree` and `containerWritable` bytes, which are reclaimable; `imageShared` bytes, which are not, because those image layers are shared with other containers and overlap across rows; and `reclaimable`, the sum of the reclaimable fields. Never add `imageShared` across rows. Any figure that cannot be trusted reports `{"status": "unknown", "reason": ...}` rather than zero, and a workspace with no container reports a measured `0`.',
|
|
@@ -3273,6 +3277,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3273
3277
|
"- devrouter repo devcontainer write --repo <REPO_PATH> --yes",
|
|
3274
3278
|
"- devrouter repo devcontainer verify --repo <REPO_PATH> --json",
|
|
3275
3279
|
"- Normal primary or linked startup: devrouter ensure <REPO_PATH> --json",
|
|
3280
|
+
"- Degraded recovery: explicit devrouter ensure <REPO_PATH> --repair --json validates the recorded profile and unchanged retained configuration; stopped primary requires all project containers stopped and zero checkout routes. It starts only retained IDs, never provider bootstrap/recreation. Inspect failures before retrying.",
|
|
3276
3281
|
"- Managed selective startup: devrouter ensure <REPO_PATH> --profile <name> --json",
|
|
3277
3282
|
"- Container seed/migration: devrouter exec <REPO_PATH> -- <command...>",
|
|
3278
3283
|
"- Compatibility verification only after ensure: devrouter repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
|
|
@@ -3391,7 +3396,7 @@ var init_ai_prompt = __esm({
|
|
|
3391
3396
|
purpose: "Write/update devrouter section in the repo's AGENTS.md and install the devrouter skill."
|
|
3392
3397
|
},
|
|
3393
3398
|
{
|
|
3394
|
-
command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
|
|
3399
|
+
command: "devrouter ensure [path] [--profile <name>] [--repair] [--open] [--json]",
|
|
3395
3400
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
3396
3401
|
},
|
|
3397
3402
|
{
|
|
@@ -5691,19 +5696,12 @@ function assertSafeContainerId(containerId) {
|
|
|
5691
5696
|
throw new Error(`Container '${containerId}' is not a safe exact identifier.`);
|
|
5692
5697
|
}
|
|
5693
5698
|
}
|
|
5694
|
-
function
|
|
5695
|
-
if (options.services.length === 0) return;
|
|
5696
|
-
const project = safeComposeProject(options.composeProject);
|
|
5697
|
-
const services = Array.from(new Set(options.services));
|
|
5698
|
-
if (services.some((service) => !options.plan.composeServices.includes(service))) {
|
|
5699
|
-
throw new Error("Exact Compose startup contains a service outside the validated model.");
|
|
5700
|
-
}
|
|
5701
|
-
const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
5699
|
+
function managedComposeEnvironment(workspace) {
|
|
5702
5700
|
const env = { ...process.env };
|
|
5703
|
-
if (
|
|
5704
|
-
env.WORKSPACE =
|
|
5705
|
-
env.DEVROUTER_WORKSPACE =
|
|
5706
|
-
env.DEVROUTER_GIT_COMMON_DIR =
|
|
5701
|
+
if (workspace) {
|
|
5702
|
+
env.WORKSPACE = workspace.token;
|
|
5703
|
+
env.DEVROUTER_WORKSPACE = workspace.token;
|
|
5704
|
+
env.DEVROUTER_GIT_COMMON_DIR = workspace.gitCommonDir;
|
|
5707
5705
|
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
5708
5706
|
} else {
|
|
5709
5707
|
delete env.WORKSPACE;
|
|
@@ -5711,6 +5709,87 @@ function startExactManagedServices(options) {
|
|
|
5711
5709
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
5712
5710
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
5713
5711
|
}
|
|
5712
|
+
return env;
|
|
5713
|
+
}
|
|
5714
|
+
function assertReadableComposeFile(file) {
|
|
5715
|
+
try {
|
|
5716
|
+
if (!import_node_fs15.default.statSync(file).isFile()) throw new Error("not a regular file");
|
|
5717
|
+
import_node_fs15.default.accessSync(file, import_node_fs15.default.constants.R_OK);
|
|
5718
|
+
} catch {
|
|
5719
|
+
throw new Error("Managed container Compose configuration contains an unreadable file.");
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
function assertManagedContainerConfigUnchanged(options) {
|
|
5723
|
+
if (options.containers.length === 0) {
|
|
5724
|
+
throw new Error("Managed container configuration cannot be verified without containers.");
|
|
5725
|
+
}
|
|
5726
|
+
for (const container of options.containers) {
|
|
5727
|
+
const project = container.labels["com.docker.compose.project"];
|
|
5728
|
+
const service = container.labels["com.docker.compose.service"];
|
|
5729
|
+
const workingDirectory = container.labels["com.docker.compose.project.working_dir"];
|
|
5730
|
+
const recordedConfigFiles = container.labels["com.docker.compose.project.config_files"];
|
|
5731
|
+
const expectedHash = container.labels["com.docker.compose.config-hash"];
|
|
5732
|
+
if (!project || !service || !workingDirectory || !recordedConfigFiles || !expectedHash || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(service) || !/^[a-f0-9]{64}$/.test(expectedHash)) {
|
|
5733
|
+
throw new Error("Managed container is missing valid Compose identity and hash labels.");
|
|
5734
|
+
}
|
|
5735
|
+
const composeProject = safeComposeProject(project);
|
|
5736
|
+
const configFiles = recordedConfigFiles.split(",").map((file) => file.trim());
|
|
5737
|
+
if (configFiles.some((file) => file.length === 0)) {
|
|
5738
|
+
throw new Error("Managed container has an invalid recorded Compose file list.");
|
|
5739
|
+
}
|
|
5740
|
+
if (options.plan.composeFiles.some(
|
|
5741
|
+
(expected) => !configFiles.some((actual) => sameWorkspacePath(actual, expected))
|
|
5742
|
+
)) {
|
|
5743
|
+
throw new Error("Managed container Compose files do not include the managed plan files.");
|
|
5744
|
+
}
|
|
5745
|
+
for (const file of configFiles) assertReadableComposeFile(file);
|
|
5746
|
+
const fileArgs = configFiles.flatMap((file) => ["-f", file]);
|
|
5747
|
+
const result = (() => {
|
|
5748
|
+
try {
|
|
5749
|
+
return (0, import_node_child_process7.spawnSync)(
|
|
5750
|
+
"docker",
|
|
5751
|
+
[
|
|
5752
|
+
"compose",
|
|
5753
|
+
"--project-name",
|
|
5754
|
+
composeProject,
|
|
5755
|
+
"--project-directory",
|
|
5756
|
+
workingDirectory,
|
|
5757
|
+
...fileArgs,
|
|
5758
|
+
"config",
|
|
5759
|
+
"--hash",
|
|
5760
|
+
service
|
|
5761
|
+
],
|
|
5762
|
+
{
|
|
5763
|
+
cwd: workingDirectory,
|
|
5764
|
+
encoding: "utf-8",
|
|
5765
|
+
env: managedComposeEnvironment(options.workspace),
|
|
5766
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5767
|
+
timeout: COMPOSE_HASH_TIMEOUT_MS
|
|
5768
|
+
}
|
|
5769
|
+
);
|
|
5770
|
+
} catch {
|
|
5771
|
+
throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
|
|
5772
|
+
}
|
|
5773
|
+
})();
|
|
5774
|
+
if (result.status !== 0 || result.error) {
|
|
5775
|
+
throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
|
|
5776
|
+
}
|
|
5777
|
+
const output2 = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
5778
|
+
const match = /^(\S+)\s+([a-f0-9]{64})$/.exec(output2);
|
|
5779
|
+
if (!match || match[1] !== service || match[2] !== expectedHash) {
|
|
5780
|
+
throw new Error(`Managed Compose configuration changed for service '${service}'.`);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
function startExactManagedServices(options) {
|
|
5785
|
+
if (options.services.length === 0) return;
|
|
5786
|
+
const project = safeComposeProject(options.composeProject);
|
|
5787
|
+
const services = Array.from(new Set(options.services));
|
|
5788
|
+
if (services.some((service) => !options.plan.composeServices.includes(service))) {
|
|
5789
|
+
throw new Error("Exact Compose startup contains a service outside the validated model.");
|
|
5790
|
+
}
|
|
5791
|
+
const fileArgs = options.plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
5792
|
+
const env = managedComposeEnvironment(options.workspace);
|
|
5714
5793
|
const result = (0, import_node_child_process7.spawnSync)(
|
|
5715
5794
|
"docker",
|
|
5716
5795
|
[
|
|
@@ -5739,17 +5818,18 @@ function startExactManagedServices(options) {
|
|
|
5739
5818
|
);
|
|
5740
5819
|
}
|
|
5741
5820
|
}
|
|
5742
|
-
function stopExactManagedService(containerId, service) {
|
|
5821
|
+
function stopExactManagedService(containerId, service, options) {
|
|
5743
5822
|
assertSafeContainerId(containerId);
|
|
5744
5823
|
const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
|
|
5745
5824
|
encoding: "utf-8",
|
|
5746
|
-
stdio: "inherit"
|
|
5825
|
+
stdio: "inherit",
|
|
5826
|
+
...options ? { timeout: options.timeoutMs } : {}
|
|
5747
5827
|
});
|
|
5748
|
-
if (result.status !== 0) {
|
|
5828
|
+
if (result.status !== 0 || result.error) {
|
|
5749
5829
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5750
5830
|
}
|
|
5751
5831
|
}
|
|
5752
|
-
var import_node_child_process7, import_node_crypto7, import_node_fs15, import_node_path15, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
|
|
5832
|
+
var import_node_child_process7, import_node_crypto7, import_node_fs15, import_node_path15, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER, COMPOSE_HASH_TIMEOUT_MS;
|
|
5753
5833
|
var init_devcontainer_profile = __esm({
|
|
5754
5834
|
"src/core/devcontainer-profile.ts"() {
|
|
5755
5835
|
"use strict";
|
|
@@ -5761,12 +5841,206 @@ var init_devcontainer_profile = __esm({
|
|
|
5761
5841
|
init_atomic_file();
|
|
5762
5842
|
init_devcontainer_config();
|
|
5763
5843
|
init_paths();
|
|
5844
|
+
init_workspace();
|
|
5764
5845
|
MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
|
|
5765
5846
|
MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
|
|
5847
|
+
COMPOSE_HASH_TIMEOUT_MS = 1e4;
|
|
5766
5848
|
}
|
|
5767
5849
|
});
|
|
5768
5850
|
|
|
5769
5851
|
// src/core/devpod-environment.ts
|
|
5852
|
+
function isRecord2(value) {
|
|
5853
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5854
|
+
}
|
|
5855
|
+
function assertFullContainerId(value) {
|
|
5856
|
+
if (typeof value !== "string" || !FULL_CONTAINER_ID_PATTERN.test(value)) {
|
|
5857
|
+
throw new Error("Managed stop Docker inspection returned an invalid container id.");
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
function assertUniqueContainerIds(ids) {
|
|
5861
|
+
if (new Set(ids).size !== ids.length) {
|
|
5862
|
+
throw new Error("Managed stop Docker inspection returned duplicate container ids.");
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
function parseDockerLines(stdout) {
|
|
5866
|
+
const lines = stdout.split(/\r?\n/);
|
|
5867
|
+
while (lines.at(-1) === "") lines.pop();
|
|
5868
|
+
if (lines.some((line) => line === "")) {
|
|
5869
|
+
throw new Error("Managed stop Docker inspection returned an empty record.");
|
|
5870
|
+
}
|
|
5871
|
+
return lines;
|
|
5872
|
+
}
|
|
5873
|
+
function runManagedStopDocker(args) {
|
|
5874
|
+
const result = (0, import_node_child_process8.spawnSync)("docker", args, {
|
|
5875
|
+
encoding: "utf-8",
|
|
5876
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
5877
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
5878
|
+
});
|
|
5879
|
+
if (result.error || result.status !== 0) {
|
|
5880
|
+
throw new Error("Managed stop Docker inspection failed.");
|
|
5881
|
+
}
|
|
5882
|
+
if (typeof result.stdout !== "string") {
|
|
5883
|
+
throw new Error("Managed stop Docker inspection returned invalid output.");
|
|
5884
|
+
}
|
|
5885
|
+
return result.stdout;
|
|
5886
|
+
}
|
|
5887
|
+
function listManagedStopContainerIds(composeProject) {
|
|
5888
|
+
const stdout = runManagedStopDocker([
|
|
5889
|
+
"ps",
|
|
5890
|
+
"-a",
|
|
5891
|
+
"--no-trunc",
|
|
5892
|
+
"--filter",
|
|
5893
|
+
`label=${MANAGED_STOP_COMPOSE_LABEL}=${composeProject}`,
|
|
5894
|
+
"--format",
|
|
5895
|
+
"{{.ID}}"
|
|
5896
|
+
]);
|
|
5897
|
+
const ids = parseDockerLines(stdout);
|
|
5898
|
+
ids.forEach(assertFullContainerId);
|
|
5899
|
+
assertUniqueContainerIds(ids);
|
|
5900
|
+
return ids;
|
|
5901
|
+
}
|
|
5902
|
+
function assertString(value) {
|
|
5903
|
+
if (typeof value !== "string") {
|
|
5904
|
+
throw new Error("Managed stop Docker inspection returned an invalid field.");
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
function validateManagedStopSnapshot(value, composeProject) {
|
|
5908
|
+
if (!isRecord2(value)) {
|
|
5909
|
+
throw new Error("Managed stop Docker inspection returned an invalid record.");
|
|
5910
|
+
}
|
|
5911
|
+
assertFullContainerId(value.id);
|
|
5912
|
+
if (!isRecord2(value.state)) {
|
|
5913
|
+
throw new Error("Managed stop Docker inspection returned an invalid state.");
|
|
5914
|
+
}
|
|
5915
|
+
const status = value.state.Status;
|
|
5916
|
+
if (status !== "running" && status !== "exited" && status !== "created") {
|
|
5917
|
+
throw new Error("Managed stop Docker inspection returned an invalid state status.");
|
|
5918
|
+
}
|
|
5919
|
+
if (typeof value.state.Running !== "boolean") {
|
|
5920
|
+
throw new Error("Managed stop Docker inspection returned an invalid running flag.");
|
|
5921
|
+
}
|
|
5922
|
+
if (typeof value.state.Paused !== "boolean" || value.state.Paused) {
|
|
5923
|
+
throw new Error("Managed stop Docker inspection returned an invalid paused flag.");
|
|
5924
|
+
}
|
|
5925
|
+
if (typeof value.state.Restarting !== "boolean" || value.state.Restarting) {
|
|
5926
|
+
throw new Error("Managed stop Docker inspection returned an invalid restarting flag.");
|
|
5927
|
+
}
|
|
5928
|
+
if (typeof value.state.Dead !== "boolean" || value.state.Dead) {
|
|
5929
|
+
throw new Error("Managed stop Docker inspection returned an invalid dead flag.");
|
|
5930
|
+
}
|
|
5931
|
+
if (status === "running" !== value.state.Running) {
|
|
5932
|
+
throw new Error("Managed stop Docker inspection returned contradictory state.");
|
|
5933
|
+
}
|
|
5934
|
+
let health;
|
|
5935
|
+
if (value.state.Health !== void 0 && value.state.Health !== null) {
|
|
5936
|
+
if (!isRecord2(value.state.Health)) {
|
|
5937
|
+
throw new Error("Managed stop Docker inspection returned an invalid health field.");
|
|
5938
|
+
}
|
|
5939
|
+
assertString(value.state.Health.Status);
|
|
5940
|
+
health = { Status: value.state.Health.Status };
|
|
5941
|
+
}
|
|
5942
|
+
if (!isRecord2(value.labels)) {
|
|
5943
|
+
throw new Error("Managed stop Docker inspection returned invalid labels.");
|
|
5944
|
+
}
|
|
5945
|
+
for (const label of MANAGED_STOP_IDENTITY_LABELS) {
|
|
5946
|
+
assertString(value.labels[label]);
|
|
5947
|
+
}
|
|
5948
|
+
if (value.labels[MANAGED_STOP_COMPOSE_LABEL] !== composeProject) {
|
|
5949
|
+
throw new Error("Managed stop Docker inspection returned a foreign Compose project.");
|
|
5950
|
+
}
|
|
5951
|
+
if (!Array.isArray(value.mounts)) {
|
|
5952
|
+
throw new Error("Managed stop Docker inspection returned invalid mounts.");
|
|
5953
|
+
}
|
|
5954
|
+
const mounts = value.mounts.map((mount) => {
|
|
5955
|
+
if (!isRecord2(mount)) {
|
|
5956
|
+
throw new Error("Managed stop Docker inspection returned an invalid mount.");
|
|
5957
|
+
}
|
|
5958
|
+
assertString(mount.Type);
|
|
5959
|
+
assertString(mount.Source);
|
|
5960
|
+
assertString(mount.Destination);
|
|
5961
|
+
return {
|
|
5962
|
+
Type: mount.Type,
|
|
5963
|
+
Source: mount.Source,
|
|
5964
|
+
Destination: mount.Destination
|
|
5965
|
+
};
|
|
5966
|
+
});
|
|
5967
|
+
if (!isRecord2(value.networks)) {
|
|
5968
|
+
throw new Error("Managed stop Docker inspection returned invalid networks.");
|
|
5969
|
+
}
|
|
5970
|
+
return {
|
|
5971
|
+
id: value.id,
|
|
5972
|
+
state: {
|
|
5973
|
+
...health ? { Health: health } : {},
|
|
5974
|
+
Status: status,
|
|
5975
|
+
Running: value.state.Running,
|
|
5976
|
+
Paused: false,
|
|
5977
|
+
Restarting: false,
|
|
5978
|
+
Dead: false
|
|
5979
|
+
},
|
|
5980
|
+
labels: {
|
|
5981
|
+
"com.docker.compose.project": value.labels["com.docker.compose.project"],
|
|
5982
|
+
"com.docker.compose.service": value.labels["com.docker.compose.service"],
|
|
5983
|
+
"com.docker.compose.project.working_dir": value.labels["com.docker.compose.project.working_dir"],
|
|
5984
|
+
"com.docker.compose.project.config_files": value.labels["com.docker.compose.project.config_files"],
|
|
5985
|
+
"com.docker.compose.config-hash": value.labels["com.docker.compose.config-hash"]
|
|
5986
|
+
},
|
|
5987
|
+
mounts,
|
|
5988
|
+
networks: {}
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
function inspectManagedStopPopulation(ids, composeProject) {
|
|
5992
|
+
const stdout = runManagedStopDocker([
|
|
5993
|
+
"inspect",
|
|
5994
|
+
"--format",
|
|
5995
|
+
MANAGED_STOP_INSPECT_TEMPLATE,
|
|
5996
|
+
...ids
|
|
5997
|
+
]);
|
|
5998
|
+
const lines = parseDockerLines(stdout);
|
|
5999
|
+
if (lines.length !== ids.length) {
|
|
6000
|
+
throw new Error("Managed stop Docker inspection returned an incomplete population.");
|
|
6001
|
+
}
|
|
6002
|
+
const expected = new Set(ids);
|
|
6003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6004
|
+
const snapshots = lines.map((line) => {
|
|
6005
|
+
let parsed;
|
|
6006
|
+
try {
|
|
6007
|
+
parsed = JSON.parse(line);
|
|
6008
|
+
} catch {
|
|
6009
|
+
throw new Error("Managed stop Docker inspection returned malformed JSON.");
|
|
6010
|
+
}
|
|
6011
|
+
const snapshot = validateManagedStopSnapshot(parsed, composeProject);
|
|
6012
|
+
if (!expected.has(snapshot.id) || seen.has(snapshot.id)) {
|
|
6013
|
+
throw new Error("Managed stop Docker inspection returned an unexpected population.");
|
|
6014
|
+
}
|
|
6015
|
+
seen.add(snapshot.id);
|
|
6016
|
+
return snapshot;
|
|
6017
|
+
});
|
|
6018
|
+
return snapshots;
|
|
6019
|
+
}
|
|
6020
|
+
function requireSameContainerPopulation(expected, actual) {
|
|
6021
|
+
if (expected.length !== actual.length) {
|
|
6022
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6023
|
+
}
|
|
6024
|
+
const expectedSet = new Set(expected);
|
|
6025
|
+
if (actual.some((id) => !expectedSet.has(id))) {
|
|
6026
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
function inspectManagedStopContainers(composeProject) {
|
|
6030
|
+
if (typeof composeProject !== "string" || composeProject.length === 0 || composeProject !== composeProject.trim() || !MANAGED_STOP_PROJECT_PATTERN.test(composeProject)) {
|
|
6031
|
+
throw new Error("Managed stop Docker inspection requires a safe Compose project.");
|
|
6032
|
+
}
|
|
6033
|
+
const listedIds = listManagedStopContainerIds(composeProject);
|
|
6034
|
+
if (listedIds.length === 0) {
|
|
6035
|
+
const confirmedIds2 = listManagedStopContainerIds(composeProject);
|
|
6036
|
+
requireSameContainerPopulation(listedIds, confirmedIds2);
|
|
6037
|
+
return [];
|
|
6038
|
+
}
|
|
6039
|
+
const snapshots = inspectManagedStopPopulation(listedIds, composeProject);
|
|
6040
|
+
const confirmedIds = listManagedStopContainerIds(composeProject);
|
|
6041
|
+
requireSameContainerPopulation(listedIds, confirmedIds);
|
|
6042
|
+
return snapshots;
|
|
6043
|
+
}
|
|
5770
6044
|
function inspectWorkspaceContainers(options) {
|
|
5771
6045
|
let ids = options?.ids;
|
|
5772
6046
|
if (!ids) {
|
|
@@ -5850,18 +6124,31 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5850
6124
|
}
|
|
5851
6125
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5852
6126
|
}
|
|
5853
|
-
var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
|
|
6127
|
+
var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE, MANAGED_STOP_INSPECT_TEMPLATE, MANAGED_STOP_PROJECT_PATTERN, FULL_CONTAINER_ID_PATTERN, MANAGED_STOP_DOCKER_TIMEOUT_MS, MANAGED_STOP_DOCKER_MAX_BUFFER, MANAGED_STOP_COMPOSE_LABEL, MANAGED_STOP_IDENTITY_LABELS;
|
|
5854
6128
|
var init_devpod_environment = __esm({
|
|
5855
6129
|
"src/core/devpod-environment.ts"() {
|
|
5856
6130
|
"use strict";
|
|
5857
6131
|
import_node_child_process8 = require("child_process");
|
|
5858
6132
|
import_node_path16 = __toESM(require("path"));
|
|
5859
6133
|
init_workspace();
|
|
5860
|
-
SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
|
|
6134
|
+
SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}},"com.docker.compose.config-hash":{{json (index .Config.Labels "com.docker.compose.config-hash")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
|
|
5861
6135
|
SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
|
|
5862
6136
|
/}$/,
|
|
5863
6137
|
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
5864
6138
|
);
|
|
6139
|
+
MANAGED_STOP_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Status":{{json .State.Status}},"Running":{{json .State.Running}},"Paused":{{json .State.Paused}},"Restarting":{{json .State.Restarting}},"Dead":{{json .State.Dead}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}},"com.docker.compose.config-hash":{{json (index .Config.Labels "com.docker.compose.config-hash")}}},"mounts":[{{range $index, $mount := .Mounts}}{{if $index}},{{end}}{"Type":{{json $mount.Type}},"Source":{{json $mount.Source}},"Destination":{{json $mount.Destination}}}{{end}}],"networks":{}}';
|
|
6140
|
+
MANAGED_STOP_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6141
|
+
FULL_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
6142
|
+
MANAGED_STOP_DOCKER_TIMEOUT_MS = 5e3;
|
|
6143
|
+
MANAGED_STOP_DOCKER_MAX_BUFFER = 1024 * 1024;
|
|
6144
|
+
MANAGED_STOP_COMPOSE_LABEL = "com.docker.compose.project";
|
|
6145
|
+
MANAGED_STOP_IDENTITY_LABELS = [
|
|
6146
|
+
"com.docker.compose.project",
|
|
6147
|
+
"com.docker.compose.service",
|
|
6148
|
+
"com.docker.compose.project.working_dir",
|
|
6149
|
+
"com.docker.compose.project.config_files",
|
|
6150
|
+
"com.docker.compose.config-hash"
|
|
6151
|
+
];
|
|
5865
6152
|
}
|
|
5866
6153
|
});
|
|
5867
6154
|
|
|
@@ -6172,7 +6459,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
6172
6459
|
let state;
|
|
6173
6460
|
let stateReadFailed = false;
|
|
6174
6461
|
try {
|
|
6175
|
-
state = readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6462
|
+
state = options.candidateState ?? readManagedRuntimeState(options.repoPath, options.workspace);
|
|
6176
6463
|
} catch {
|
|
6177
6464
|
stateReadFailed = true;
|
|
6178
6465
|
}
|
|
@@ -6830,7 +7117,8 @@ function listDevsyWorkspaces() {
|
|
|
6830
7117
|
}
|
|
6831
7118
|
const workspace = {
|
|
6832
7119
|
id: candidate.id,
|
|
6833
|
-
source: { localFolder: source.localFolder }
|
|
7120
|
+
source: { localFolder: source.localFolder },
|
|
7121
|
+
...typeof candidate.context === "string" ? { context: candidate.context } : {}
|
|
6834
7122
|
};
|
|
6835
7123
|
if ("lastUsed" in candidate) {
|
|
6836
7124
|
if (typeof candidate.lastUsed === "string") {
|
|
@@ -7509,372 +7797,898 @@ var init_devpod_workspaces = __esm({
|
|
|
7509
7797
|
}
|
|
7510
7798
|
});
|
|
7511
7799
|
|
|
7512
|
-
// src/core/
|
|
7513
|
-
function
|
|
7514
|
-
|
|
7515
|
-
const attached = listDevsyWorkspaces();
|
|
7516
|
-
const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
|
|
7517
|
-
if (!attachedId) return false;
|
|
7518
|
-
return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
|
|
7519
|
-
} catch {
|
|
7520
|
-
return true;
|
|
7521
|
-
}
|
|
7522
|
-
}
|
|
7523
|
-
function withMutationLock(activity, target, operation) {
|
|
7524
|
-
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7525
|
-
return withFileLockSync(
|
|
7526
|
-
DEVSY_MUTATION_LOCK_FILE,
|
|
7527
|
-
{
|
|
7528
|
-
activity,
|
|
7529
|
-
target: `'${target}'`,
|
|
7530
|
-
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7531
|
-
fair: true,
|
|
7532
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7533
|
-
},
|
|
7534
|
-
operation
|
|
7535
|
-
);
|
|
7800
|
+
// src/core/route-publication.ts
|
|
7801
|
+
function routedAppsFromConfig(config) {
|
|
7802
|
+
return config.apps.filter((app) => app.kind !== "dependency");
|
|
7536
7803
|
}
|
|
7537
|
-
function
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
DEVSY_MUTATION_LOCK_FILE,
|
|
7541
|
-
{
|
|
7542
|
-
activity,
|
|
7543
|
-
target: `'${target}'`,
|
|
7544
|
-
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7545
|
-
fair: true,
|
|
7546
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7547
|
-
},
|
|
7548
|
-
operation
|
|
7804
|
+
function configuredProxyAppsFromConfig(config) {
|
|
7805
|
+
return routedAppsFromConfig(config).filter(
|
|
7806
|
+
(app) => app.runtime === "proxy"
|
|
7549
7807
|
);
|
|
7550
7808
|
}
|
|
7551
|
-
function
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
const child = (0, import_node_child_process15.spawn)("devsy", args, {
|
|
7557
|
-
stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
|
|
7558
|
-
env
|
|
7559
|
-
});
|
|
7560
|
-
const stderr = child.stderr;
|
|
7561
|
-
if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
|
|
7562
|
-
let stderrTail = Buffer.alloc(0);
|
|
7563
|
-
let spawnError;
|
|
7564
|
-
stderr.on("data", (chunk) => {
|
|
7565
|
-
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
7566
|
-
const writable = process.stderr.write(value);
|
|
7567
|
-
if (!writable) {
|
|
7568
|
-
stderr.pause();
|
|
7569
|
-
process.stderr.once("drain", () => stderr.resume());
|
|
7570
|
-
}
|
|
7571
|
-
if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
|
|
7572
|
-
stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
|
|
7573
|
-
} else {
|
|
7574
|
-
const combined = Buffer.concat([stderrTail, value]);
|
|
7575
|
-
stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
|
|
7576
|
-
}
|
|
7577
|
-
});
|
|
7578
|
-
child.once("error", (error) => {
|
|
7579
|
-
spawnError = error;
|
|
7580
|
-
});
|
|
7581
|
-
child.once("close", (status) => {
|
|
7582
|
-
resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
|
|
7583
|
-
});
|
|
7584
|
-
});
|
|
7585
|
-
}
|
|
7586
|
-
function runDevsyAction(action2, devsyId, force = false) {
|
|
7587
|
-
const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
|
|
7588
|
-
const result = (0, import_node_child_process15.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
|
|
7589
|
-
if (result.status !== 0) {
|
|
7809
|
+
function proxyAppsFromConfig(config) {
|
|
7810
|
+
const routedApps = routedAppsFromConfig(config);
|
|
7811
|
+
const proxyApps = configuredProxyAppsFromConfig(config);
|
|
7812
|
+
const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
|
|
7813
|
+
if (unsupported.length > 0) {
|
|
7590
7814
|
throw new Error(
|
|
7591
|
-
`
|
|
7815
|
+
`Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
|
|
7592
7816
|
);
|
|
7593
7817
|
}
|
|
7818
|
+
return proxyApps;
|
|
7594
7819
|
}
|
|
7595
|
-
function
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
if (
|
|
7604
|
-
|
|
7605
|
-
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
7606
|
-
if (action2 === "stop" && after.status !== "owned") {
|
|
7607
|
-
throw new Error(
|
|
7608
|
-
`Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
|
|
7609
|
-
);
|
|
7610
|
-
}
|
|
7611
|
-
if (action2 === "delete" && after.status === "owned") {
|
|
7612
|
-
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
7613
|
-
if (runtime !== "not-found") {
|
|
7614
|
-
throw new Error(
|
|
7615
|
-
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
7616
|
-
);
|
|
7617
|
-
}
|
|
7618
|
-
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7619
|
-
if (after.status === "owned") {
|
|
7620
|
-
runDevsyAction("delete", devsyId, true);
|
|
7621
|
-
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7622
|
-
}
|
|
7623
|
-
if (after.status !== "absent") {
|
|
7820
|
+
async function ensureRouteInfrastructure(apps, options = {}) {
|
|
7821
|
+
ensureRouterFiles();
|
|
7822
|
+
await ensureNetwork(DEVNET_NAME);
|
|
7823
|
+
const tlsCoverage = await ensureTLSHostsCovered(
|
|
7824
|
+
apps.map((app) => app.host),
|
|
7825
|
+
options
|
|
7826
|
+
);
|
|
7827
|
+
for (const app of apps) {
|
|
7828
|
+
if (app.protocol === "tcp") {
|
|
7829
|
+
if (!isTLSEnabled()) {
|
|
7624
7830
|
throw new Error(
|
|
7625
|
-
`
|
|
7831
|
+
`TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
7626
7832
|
);
|
|
7627
7833
|
}
|
|
7834
|
+
activateTcpProtocol(app.tcpProtocol);
|
|
7628
7835
|
}
|
|
7629
|
-
return { status: "changed" };
|
|
7630
|
-
});
|
|
7631
|
-
}
|
|
7632
|
-
function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
7633
|
-
return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
|
|
7634
|
-
}
|
|
7635
|
-
function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
7636
|
-
return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
|
|
7637
|
-
}
|
|
7638
|
-
function assertDevsyTarget(devsyId, repoPath) {
|
|
7639
|
-
const workspaces = listDevsyWorkspaces();
|
|
7640
|
-
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
7641
|
-
const id = devsyId ?? existing?.id;
|
|
7642
|
-
if (id) {
|
|
7643
|
-
const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
|
|
7644
|
-
if (before.status === "conflict") throw new Error(before.reason);
|
|
7645
7836
|
}
|
|
7646
|
-
|
|
7837
|
+
startRouterStack();
|
|
7838
|
+
return tlsCoverage;
|
|
7647
7839
|
}
|
|
7648
|
-
async function
|
|
7649
|
-
const
|
|
7650
|
-
|
|
7840
|
+
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
7841
|
+
const apps = proxyAppsFromConfig(config);
|
|
7842
|
+
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
7843
|
+
const routes = apps.map((app) => {
|
|
7844
|
+
const upstream = parseUpstream(app.upstream);
|
|
7845
|
+
return {
|
|
7846
|
+
name: app.name,
|
|
7847
|
+
host: app.host,
|
|
7848
|
+
protocol: app.protocol,
|
|
7849
|
+
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
7850
|
+
repoPath,
|
|
7851
|
+
port: upstream.port,
|
|
7852
|
+
mode: "proxy",
|
|
7853
|
+
upstreamHost: upstream.upstreamHost,
|
|
7854
|
+
workspace
|
|
7855
|
+
};
|
|
7856
|
+
});
|
|
7857
|
+
replaceHostRoutesForRepo(repoPath, routes);
|
|
7858
|
+
return { routes, tlsRefreshed: tlsCoverage.refreshed };
|
|
7859
|
+
}
|
|
7860
|
+
var init_route_publication = __esm({
|
|
7861
|
+
"src/core/route-publication.ts"() {
|
|
7862
|
+
"use strict";
|
|
7863
|
+
init_docker();
|
|
7864
|
+
init_host_routes();
|
|
7865
|
+
init_router();
|
|
7866
|
+
init_tls();
|
|
7867
|
+
}
|
|
7868
|
+
});
|
|
7869
|
+
|
|
7870
|
+
// src/core/workspace-ownership.ts
|
|
7871
|
+
function commandError(command, repoPath, stderr) {
|
|
7872
|
+
return new Error(
|
|
7873
|
+
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
7874
|
+
);
|
|
7875
|
+
}
|
|
7876
|
+
function resolveGitCommonDir(repoPath) {
|
|
7877
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
7878
|
+
encoding: "utf-8",
|
|
7879
|
+
env: READ_ONLY_GIT_ENV
|
|
7880
|
+
});
|
|
7881
|
+
const output2 = result.stdout.trim();
|
|
7882
|
+
if (result.status !== 0 || !output2) {
|
|
7883
|
+
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
7884
|
+
}
|
|
7885
|
+
return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
|
|
7886
|
+
}
|
|
7887
|
+
function resolveGitTopLevel(repoPath) {
|
|
7888
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
7889
|
+
encoding: "utf-8",
|
|
7890
|
+
env: READ_ONLY_GIT_ENV
|
|
7891
|
+
});
|
|
7892
|
+
const output2 = result.stdout.trim();
|
|
7893
|
+
if (result.status !== 0 || !output2) {
|
|
7894
|
+
throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
|
|
7895
|
+
}
|
|
7896
|
+
return comparableWorkspacePath(output2);
|
|
7897
|
+
}
|
|
7898
|
+
function listGitWorktrees(repoPath) {
|
|
7899
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
7900
|
+
encoding: "utf-8",
|
|
7901
|
+
env: READ_ONLY_GIT_ENV
|
|
7902
|
+
});
|
|
7903
|
+
if (result.status !== 0) {
|
|
7904
|
+
throw commandError("git worktree list", repoPath, result.stderr);
|
|
7905
|
+
}
|
|
7906
|
+
const worktrees = [];
|
|
7907
|
+
let current = {};
|
|
7908
|
+
const finish = () => {
|
|
7909
|
+
if (!current.path) return;
|
|
7910
|
+
worktrees.push({
|
|
7911
|
+
path: comparableWorkspacePath(current.path),
|
|
7912
|
+
branch: current.branch,
|
|
7913
|
+
locked: current.locked ?? false,
|
|
7914
|
+
prunable: current.prunable ?? false
|
|
7915
|
+
});
|
|
7916
|
+
current = {};
|
|
7917
|
+
};
|
|
7918
|
+
for (const line of `${result.stdout}
|
|
7919
|
+
`.split("\n")) {
|
|
7920
|
+
if (line.startsWith("worktree ")) {
|
|
7921
|
+
finish();
|
|
7922
|
+
current.path = line.slice("worktree ".length).trim();
|
|
7923
|
+
} else if (line.startsWith("branch ")) {
|
|
7924
|
+
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
7925
|
+
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
7926
|
+
current.locked = true;
|
|
7927
|
+
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
7928
|
+
current.prunable = true;
|
|
7929
|
+
} else if (line === "") {
|
|
7930
|
+
finish();
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
return worktrees;
|
|
7934
|
+
}
|
|
7935
|
+
function ownershipDirectory(repoPath) {
|
|
7936
|
+
return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
7937
|
+
}
|
|
7938
|
+
function validateWorkspace(value, label) {
|
|
7939
|
+
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
7940
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7941
|
+
}
|
|
7942
|
+
return value;
|
|
7943
|
+
}
|
|
7944
|
+
function validateTimestamp(value, label) {
|
|
7945
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
7946
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7947
|
+
}
|
|
7948
|
+
return value;
|
|
7949
|
+
}
|
|
7950
|
+
function validateRecord(value, expectedWorkspace) {
|
|
7951
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7952
|
+
throw new Error("invalid workspace ownership record");
|
|
7953
|
+
}
|
|
7954
|
+
const candidate = value;
|
|
7955
|
+
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
7956
|
+
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
7957
|
+
}
|
|
7958
|
+
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
7959
|
+
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
7960
|
+
throw new Error(
|
|
7961
|
+
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
7962
|
+
);
|
|
7963
|
+
}
|
|
7964
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
|
|
7965
|
+
throw new Error("invalid workspace ownership worktreePath");
|
|
7966
|
+
}
|
|
7967
|
+
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
7968
|
+
throw new Error("invalid workspace ownership branch");
|
|
7969
|
+
}
|
|
7970
|
+
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
7971
|
+
return {
|
|
7972
|
+
version: OWNERSHIP_VERSION,
|
|
7973
|
+
workspace,
|
|
7974
|
+
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
7975
|
+
branch: candidate.branch,
|
|
7976
|
+
devpodId,
|
|
7977
|
+
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
7978
|
+
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
7979
|
+
};
|
|
7980
|
+
}
|
|
7981
|
+
function recordPath(repoPath, workspace) {
|
|
7982
|
+
return import_node_path20.default.join(
|
|
7983
|
+
ownershipDirectory(repoPath),
|
|
7984
|
+
`${validateWorkspace(workspace, "workspace")}.json`
|
|
7985
|
+
);
|
|
7986
|
+
}
|
|
7987
|
+
function readRecordFile(filePath, expectedWorkspace) {
|
|
7988
|
+
let parsed;
|
|
7651
7989
|
try {
|
|
7652
|
-
|
|
7990
|
+
parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7653
7991
|
} catch (error) {
|
|
7654
|
-
if (
|
|
7655
|
-
|
|
7656
|
-
|
|
7992
|
+
if (error instanceof SyntaxError) {
|
|
7993
|
+
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
7994
|
+
}
|
|
7995
|
+
throw error;
|
|
7657
7996
|
}
|
|
7658
|
-
return
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7997
|
+
return validateRecord(parsed, expectedWorkspace);
|
|
7998
|
+
}
|
|
7999
|
+
function readWorkspaceOwnership(repoPath, workspace) {
|
|
8000
|
+
const filePath = recordPath(repoPath, workspace);
|
|
8001
|
+
try {
|
|
8002
|
+
return readRecordFile(filePath, workspace);
|
|
8003
|
+
} catch (error) {
|
|
8004
|
+
if (error.code === "ENOENT") return void 0;
|
|
8005
|
+
throw error;
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
8008
|
+
function listWorkspaceOwnership(repoPath) {
|
|
8009
|
+
const directory = ownershipDirectory(repoPath);
|
|
8010
|
+
return listWorkspaceOwnershipInDirectory(directory);
|
|
8011
|
+
}
|
|
8012
|
+
function listWorkspaceOwnershipInDirectory(directory) {
|
|
8013
|
+
let entries;
|
|
8014
|
+
try {
|
|
8015
|
+
entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
|
|
8016
|
+
} catch (error) {
|
|
8017
|
+
if (error.code === "ENOENT") return [];
|
|
8018
|
+
throw error;
|
|
8019
|
+
}
|
|
8020
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
8021
|
+
const workspace = entry.name.slice(0, -".json".length);
|
|
8022
|
+
validateWorkspace(workspace, "filename");
|
|
8023
|
+
return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
|
|
8024
|
+
});
|
|
8025
|
+
}
|
|
8026
|
+
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
8027
|
+
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
8028
|
+
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
8029
|
+
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
8030
|
+
const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
|
|
8031
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8032
|
+
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
8033
|
+
const existing = records.find((record2) => record2.workspace === workspace);
|
|
8034
|
+
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
8035
|
+
throw new Error(
|
|
8036
|
+
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
8037
|
+
);
|
|
8038
|
+
}
|
|
8039
|
+
const pathOwner = records.find(
|
|
8040
|
+
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
8041
|
+
);
|
|
8042
|
+
if (pathOwner) {
|
|
8043
|
+
throw new Error(
|
|
8044
|
+
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
8045
|
+
);
|
|
8046
|
+
}
|
|
8047
|
+
if (existing && existing.devpodId !== devpodId) {
|
|
8048
|
+
throw new Error(
|
|
8049
|
+
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
8050
|
+
);
|
|
8051
|
+
}
|
|
8052
|
+
const record = {
|
|
8053
|
+
version: OWNERSHIP_VERSION,
|
|
8054
|
+
workspace,
|
|
8055
|
+
worktreePath,
|
|
8056
|
+
branch: input2.branch ?? null,
|
|
8057
|
+
devpodId,
|
|
8058
|
+
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
8059
|
+
updatedAt: validateTimestamp(now, "updatedAt")
|
|
8060
|
+
};
|
|
8061
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
8062
|
+
`);
|
|
8063
|
+
return record;
|
|
8064
|
+
}
|
|
8065
|
+
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
8066
|
+
const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
8067
|
+
try {
|
|
8068
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8069
|
+
return true;
|
|
8070
|
+
} catch (error) {
|
|
8071
|
+
if (error.code === "ENOENT") return false;
|
|
8072
|
+
throw error;
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
function sameOwnershipRecord(left, right) {
|
|
8076
|
+
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;
|
|
8077
|
+
}
|
|
8078
|
+
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
8079
|
+
const filePath = import_node_path20.default.join(
|
|
8080
|
+
directory,
|
|
8081
|
+
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
8082
|
+
);
|
|
8083
|
+
let current;
|
|
8084
|
+
try {
|
|
8085
|
+
current = readRecordFile(filePath, expected.workspace);
|
|
8086
|
+
} catch (error) {
|
|
8087
|
+
if (error.code === "ENOENT") return "absent";
|
|
8088
|
+
throw error;
|
|
8089
|
+
}
|
|
8090
|
+
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
8091
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8092
|
+
return "removed";
|
|
8093
|
+
}
|
|
8094
|
+
function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
|
|
8095
|
+
const directory = ownershipDirectory(repoPath);
|
|
8096
|
+
import_node_fs20.default.mkdirSync(directory, { recursive: true });
|
|
8097
|
+
return withFileLockSync(
|
|
8098
|
+
import_node_path20.default.join(directory, ".lock"),
|
|
8099
|
+
{
|
|
8100
|
+
activity: "workspace ownership transaction",
|
|
8101
|
+
target: `'${repoPath}'`,
|
|
8102
|
+
waitMs: options.waitMs ?? 5e3
|
|
8103
|
+
},
|
|
8104
|
+
() => operation({
|
|
8105
|
+
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8106
|
+
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8107
|
+
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8108
|
+
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8109
|
+
})
|
|
8110
|
+
);
|
|
8111
|
+
}
|
|
8112
|
+
function providerPathOwner(providerWorkspaces, worktreePath) {
|
|
8113
|
+
const owners = providerWorkspaces.filter(
|
|
8114
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
8115
|
+
);
|
|
8116
|
+
if (owners.length > 1) {
|
|
8117
|
+
throw new Error(
|
|
8118
|
+
`Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
|
|
8119
|
+
);
|
|
8120
|
+
}
|
|
8121
|
+
return owners[0];
|
|
8122
|
+
}
|
|
8123
|
+
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
8124
|
+
const owners = /* @__PURE__ */ new Map();
|
|
8125
|
+
for (const worktree of listGitWorktrees(repoPath)) {
|
|
8126
|
+
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
8127
|
+
let workspace;
|
|
8128
|
+
try {
|
|
8129
|
+
workspace = readPersistedWorkspace(worktree.path);
|
|
8130
|
+
} catch (error) {
|
|
8131
|
+
if (!import_node_fs20.default.existsSync(worktree.path)) continue;
|
|
8132
|
+
throw error;
|
|
8133
|
+
}
|
|
8134
|
+
if (!workspace) continue;
|
|
8135
|
+
const existing = owners.get(workspace);
|
|
8136
|
+
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
8137
|
+
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
8138
|
+
}
|
|
8139
|
+
owners.set(workspace, worktree.path);
|
|
8140
|
+
}
|
|
8141
|
+
return owners;
|
|
8142
|
+
}
|
|
8143
|
+
function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
|
|
8144
|
+
const recordOwner = records.find(
|
|
8145
|
+
(record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
|
|
8146
|
+
);
|
|
8147
|
+
if (recordOwner) {
|
|
8148
|
+
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
8149
|
+
}
|
|
8150
|
+
const providerOwner = providerWorkspaces.find(
|
|
8151
|
+
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
8152
|
+
);
|
|
8153
|
+
if (providerOwner) {
|
|
8154
|
+
return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
|
|
8155
|
+
}
|
|
8156
|
+
const persistedOwner = persistedOwners.get(workspace);
|
|
8157
|
+
if (persistedOwner) {
|
|
8158
|
+
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
8159
|
+
}
|
|
8160
|
+
return void 0;
|
|
8161
|
+
}
|
|
8162
|
+
function claimWorkspaceIdentity(repoPath, input2) {
|
|
8163
|
+
const worktreePath = comparableWorkspacePath(repoPath);
|
|
8164
|
+
const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
|
|
8165
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
|
|
8166
|
+
const records = transaction.list();
|
|
8167
|
+
const exactRecords = records.filter(
|
|
8168
|
+
(record) => sameWorkspacePath(record.worktreePath, worktreePath)
|
|
8169
|
+
);
|
|
8170
|
+
if (exactRecords.length > 1) {
|
|
8171
|
+
throw new Error(
|
|
8172
|
+
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
8175
|
+
const exactRecord = exactRecords[0];
|
|
8176
|
+
const persisted = readPersistedWorkspace(worktreePath);
|
|
8177
|
+
const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
|
|
8178
|
+
if (exactRecord) {
|
|
8179
|
+
if (persisted && persisted !== exactRecord.workspace) {
|
|
8180
|
+
throw new Error(
|
|
8181
|
+
`Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
|
|
8182
|
+
);
|
|
8183
|
+
}
|
|
8184
|
+
if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
|
|
8185
|
+
throw new Error(
|
|
8186
|
+
`Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
|
|
8187
|
+
);
|
|
7665
8188
|
}
|
|
8189
|
+
const conflict2 = claimConflict(
|
|
8190
|
+
exactRecord.workspace,
|
|
8191
|
+
exactRecord.devpodId,
|
|
8192
|
+
worktreePath,
|
|
8193
|
+
records,
|
|
8194
|
+
input2.providerWorkspaces,
|
|
8195
|
+
persistedOwners,
|
|
8196
|
+
exactRecord
|
|
8197
|
+
);
|
|
8198
|
+
if (conflict2) {
|
|
8199
|
+
throw new Error(
|
|
8200
|
+
`Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
|
|
8201
|
+
);
|
|
8202
|
+
}
|
|
8203
|
+
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
8204
|
+
return transaction.write({
|
|
8205
|
+
workspace: exactRecord.workspace,
|
|
8206
|
+
worktreePath,
|
|
8207
|
+
branch: input2.branch ?? null,
|
|
8208
|
+
devpodId: exactRecord.devpodId
|
|
8209
|
+
});
|
|
7666
8210
|
}
|
|
7667
|
-
if (
|
|
7668
|
-
throw new Error(
|
|
8211
|
+
if (persisted && exactProvider && persisted !== exactProvider.id) {
|
|
8212
|
+
throw new Error(
|
|
8213
|
+
`Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
|
|
8214
|
+
);
|
|
7669
8215
|
}
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
if (
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
8216
|
+
let workspace = persisted ?? exactProvider?.id;
|
|
8217
|
+
let devpodId = exactProvider?.id ?? persisted;
|
|
8218
|
+
if (!workspace || !devpodId) {
|
|
8219
|
+
if (input2.unavailableRuntimes.length > 0) {
|
|
8220
|
+
throw new Error(
|
|
8221
|
+
`Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
const candidate = workspaceIdentityCandidates(input2.source).find(
|
|
8225
|
+
(next) => !claimConflict(
|
|
8226
|
+
next,
|
|
8227
|
+
next,
|
|
8228
|
+
worktreePath,
|
|
8229
|
+
records,
|
|
8230
|
+
input2.providerWorkspaces,
|
|
8231
|
+
persistedOwners
|
|
8232
|
+
)
|
|
7680
8233
|
);
|
|
8234
|
+
if (!candidate) {
|
|
8235
|
+
throw new Error(
|
|
8236
|
+
`Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
|
|
8237
|
+
);
|
|
8238
|
+
}
|
|
8239
|
+
workspace = candidate;
|
|
8240
|
+
devpodId = candidate;
|
|
8241
|
+
}
|
|
8242
|
+
const conflict = claimConflict(
|
|
8243
|
+
workspace,
|
|
8244
|
+
devpodId,
|
|
8245
|
+
worktreePath,
|
|
8246
|
+
records,
|
|
8247
|
+
input2.providerWorkspaces,
|
|
8248
|
+
persistedOwners
|
|
8249
|
+
);
|
|
8250
|
+
if (conflict) {
|
|
8251
|
+
throw new Error(
|
|
8252
|
+
`Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
|
|
8253
|
+
);
|
|
8254
|
+
}
|
|
8255
|
+
const written = transaction.write({
|
|
8256
|
+
workspace,
|
|
8257
|
+
worktreePath,
|
|
8258
|
+
branch: input2.branch ?? null,
|
|
8259
|
+
devpodId
|
|
8260
|
+
});
|
|
8261
|
+
try {
|
|
8262
|
+
persistWorkspace(worktreePath, workspace);
|
|
8263
|
+
} catch (error) {
|
|
8264
|
+
const cleanup = transaction.removeIfMatches(written);
|
|
8265
|
+
if (cleanup !== "removed") {
|
|
8266
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8267
|
+
throw new Error(
|
|
8268
|
+
`Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
|
|
8269
|
+
);
|
|
8270
|
+
}
|
|
8271
|
+
throw error;
|
|
8272
|
+
}
|
|
8273
|
+
return written;
|
|
8274
|
+
});
|
|
8275
|
+
}
|
|
8276
|
+
function removeWorkspaceOwnership(repoPath, workspace) {
|
|
8277
|
+
return withWorkspaceOwnershipTransaction(
|
|
8278
|
+
repoPath,
|
|
8279
|
+
(transaction) => transaction.remove(workspace)
|
|
8280
|
+
);
|
|
8281
|
+
}
|
|
8282
|
+
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
8283
|
+
const worktree = worktrees.find(
|
|
8284
|
+
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
8285
|
+
);
|
|
8286
|
+
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
8287
|
+
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
8288
|
+
if (devpodStatus === "conflict") {
|
|
8289
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8290
|
+
}
|
|
8291
|
+
if (worktree?.locked) {
|
|
8292
|
+
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
8293
|
+
}
|
|
8294
|
+
if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
|
|
8295
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8296
|
+
}
|
|
8297
|
+
if (!worktree || worktree.prunable) {
|
|
8298
|
+
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
8299
|
+
}
|
|
8300
|
+
let persisted;
|
|
8301
|
+
try {
|
|
8302
|
+
persisted = readPersistedWorkspace(worktree.path);
|
|
8303
|
+
} catch {
|
|
8304
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8305
|
+
}
|
|
8306
|
+
return {
|
|
8307
|
+
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
8308
|
+
devpodStatus,
|
|
8309
|
+
worktree
|
|
8310
|
+
};
|
|
8311
|
+
}
|
|
8312
|
+
function listMissingWorkspaceOwnership(repoPath) {
|
|
8313
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
8314
|
+
return listWorkspaceOwnership(repoPath).filter(
|
|
8315
|
+
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
8316
|
+
);
|
|
8317
|
+
}
|
|
8318
|
+
var import_node_child_process15, import_node_fs20, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
8319
|
+
var init_workspace_ownership = __esm({
|
|
8320
|
+
"src/core/workspace-ownership.ts"() {
|
|
8321
|
+
"use strict";
|
|
8322
|
+
import_node_child_process15 = require("child_process");
|
|
8323
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8324
|
+
import_node_path20 = __toESM(require("path"));
|
|
8325
|
+
init_atomic_file();
|
|
8326
|
+
init_devpod_workspaces();
|
|
8327
|
+
init_file_lock();
|
|
8328
|
+
init_workspace();
|
|
8329
|
+
READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
8330
|
+
OWNERSHIP_VERSION = 1;
|
|
8331
|
+
OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
|
|
8332
|
+
}
|
|
8333
|
+
});
|
|
8334
|
+
|
|
8335
|
+
// src/core/managed-devsy-stop.ts
|
|
8336
|
+
function sameSet(left, right) {
|
|
8337
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
8338
|
+
}
|
|
8339
|
+
function containerIdentity(container) {
|
|
8340
|
+
return JSON.stringify({ id: container.id, labels: container.labels, mounts: container.mounts });
|
|
8341
|
+
}
|
|
8342
|
+
function stopRetainedManagedDevsyWorkspace(options) {
|
|
8343
|
+
const { repoPath, devsyId } = options;
|
|
8344
|
+
const linked = isLinkedWorktree(repoPath);
|
|
8345
|
+
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
8346
|
+
const retainedState = readManagedRuntimeState(repoPath, workspace);
|
|
8347
|
+
if (!retainedState) return false;
|
|
8348
|
+
const state = retainedState;
|
|
8349
|
+
if (state.devpodId !== devsyId || linked && !workspace) {
|
|
8350
|
+
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
8351
|
+
}
|
|
8352
|
+
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
8353
|
+
function registration() {
|
|
8354
|
+
resetWorkspaceRuntimeCaches();
|
|
8355
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
8356
|
+
throw new Error("Managed stop provider selection changed.");
|
|
8357
|
+
}
|
|
8358
|
+
const owner = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
8359
|
+
if (owner.status !== "owned") {
|
|
8360
|
+
throw new Error("Managed stop requires one exact retained Devsy registration.");
|
|
8361
|
+
}
|
|
8362
|
+
if (workspace) {
|
|
8363
|
+
const record = readWorkspaceOwnership(repoPath, workspace);
|
|
8364
|
+
if (!record || record.devpodId !== devsyId || !sameWorkspacePath(record.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir) {
|
|
8365
|
+
throw new Error("Managed stop workspace ownership changed.");
|
|
8366
|
+
}
|
|
7681
8367
|
}
|
|
7682
|
-
|
|
7683
|
-
|
|
8368
|
+
return owner.workspace;
|
|
8369
|
+
}
|
|
8370
|
+
const context = registration().context;
|
|
8371
|
+
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
8372
|
+
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
8373
|
+
}
|
|
8374
|
+
const devsyRoot = import_node_path21.default.resolve(process.env.DEVSY_HOME || import_node_path21.default.join(import_node_os2.default.homedir(), ".devsy"));
|
|
8375
|
+
const featureDirectory = import_node_path21.default.join(
|
|
8376
|
+
devsyRoot,
|
|
8377
|
+
"contexts",
|
|
8378
|
+
context,
|
|
8379
|
+
"workspaces",
|
|
8380
|
+
devsyId,
|
|
8381
|
+
"agent",
|
|
8382
|
+
".docker-compose"
|
|
8383
|
+
);
|
|
8384
|
+
function prove(previous) {
|
|
8385
|
+
if (registration().context !== context || JSON.stringify(readManagedRuntimeState(repoPath, workspace)) !== JSON.stringify(state)) {
|
|
8386
|
+
throw new Error("Managed stop retained context or runtime record changed.");
|
|
7684
8387
|
}
|
|
7685
|
-
|
|
7686
|
-
const
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
env.WORKSPACE = options.workspace.token;
|
|
7690
|
-
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
7691
|
-
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
7692
|
-
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
7693
|
-
} else {
|
|
7694
|
-
delete env.WORKSPACE;
|
|
7695
|
-
delete env.DEVROUTER_WORKSPACE;
|
|
7696
|
-
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
7697
|
-
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
8388
|
+
const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
|
|
8389
|
+
const managed = runtime.config.managedRuntime;
|
|
8390
|
+
if (!managed || runtime.profile !== state.profile || runtime.workspace !== workspace) {
|
|
8391
|
+
throw new Error("Managed stop requires the recorded managed profile.");
|
|
7698
8392
|
}
|
|
7699
|
-
const
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
8393
|
+
const plan = inspectManagedDevcontainerConfig({
|
|
8394
|
+
repoPath,
|
|
8395
|
+
config: runtime.config,
|
|
8396
|
+
profile: runtime.resolvedProfile,
|
|
8397
|
+
linked
|
|
8398
|
+
});
|
|
8399
|
+
const selectedProcesses = runtime.resolvedProfile?.processes;
|
|
8400
|
+
const processes = !runtime.resolvedProfile || selectedProcesses?.length === 1 && selectedProcesses[0] === "*" ? managed.processes : selectedProcesses ?? [];
|
|
8401
|
+
if (state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || !sameSet(
|
|
8402
|
+
state.desired.apps,
|
|
8403
|
+
proxyAppsFromConfig(runtime.config).map((app) => app.name)
|
|
8404
|
+
) || !sameSet(state.desired.services, plan.desiredProfileServices) || !sameSet(state.desired.processes, processes) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
|
|
8405
|
+
throw new Error("Managed stop requires unchanged recorded resources and configuration.");
|
|
8406
|
+
}
|
|
8407
|
+
const containers = inspectManagedStopContainers(state.composeProject);
|
|
8408
|
+
const services = /* @__PURE__ */ new Set();
|
|
8409
|
+
for (const container of containers) {
|
|
8410
|
+
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
8411
|
+
if (!plan.nativeRunServices.includes(service) || services.has(service) || container.labels["com.docker.compose.project"] !== state.composeProject || !sameWorkspacePath(
|
|
8412
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
8413
|
+
plan.composeDirectory
|
|
8414
|
+
)) {
|
|
8415
|
+
throw new Error("Managed stop found unexpected or duplicate project membership.");
|
|
7705
8416
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
8417
|
+
services.add(service);
|
|
8418
|
+
const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
|
|
8419
|
+
if (files.some((file) => !file || !import_node_path21.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path21.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
|
|
8420
|
+
throw new Error("Managed stop Compose file identity changed.");
|
|
7708
8421
|
}
|
|
7709
|
-
|
|
8422
|
+
for (const file of files.slice(plan.composeFiles.length)) {
|
|
8423
|
+
if (import_node_path21.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
8424
|
+
import_node_path21.default.basename(file)
|
|
8425
|
+
) || import_node_fs21.default.realpathSync(file) !== import_node_path21.default.join(import_node_fs21.default.realpathSync(devsyRoot), import_node_path21.default.relative(devsyRoot, file))) {
|
|
8426
|
+
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
if (previous) {
|
|
8430
|
+
const retained = previous.find((entry) => entry.id === container.id);
|
|
8431
|
+
if (!retained || containerIdentity(retained) !== containerIdentity(container) || !retained.state.Running && container.state.Running) {
|
|
8432
|
+
throw new Error("Managed stop container identity or quiescent state changed.");
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
if (plan.desiredServices.some((service) => !services.has(service)) || previous && !sameSet(
|
|
8437
|
+
previous.map((c) => c.id),
|
|
8438
|
+
containers.map((c) => c.id)
|
|
8439
|
+
)) {
|
|
8440
|
+
throw new Error("Managed stop cannot prove the complete retained service population.");
|
|
8441
|
+
}
|
|
8442
|
+
const primary = containers.find(
|
|
8443
|
+
(c) => c.labels["com.docker.compose.service"] === plan.primaryService
|
|
8444
|
+
);
|
|
8445
|
+
if (primary?.mounts.filter(
|
|
8446
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
8447
|
+
).length !== 1) {
|
|
8448
|
+
throw new Error("Managed stop cannot prove the exact primary workspace mount.");
|
|
7710
8449
|
}
|
|
8450
|
+
assertManagedContainerConfigUnchanged({ plan, containers, workspace: workspaceEnv });
|
|
8451
|
+
if (registration().context !== context) {
|
|
8452
|
+
throw new Error("Managed stop provider context changed during inspection.");
|
|
8453
|
+
}
|
|
8454
|
+
const status = inspectDevsyRuntimeStatus(devsyId);
|
|
8455
|
+
if (status !== "running" && status !== "stopped" || status === "running" !== primary.state.Running) {
|
|
8456
|
+
throw new Error("Managed stop requires consistent provider and primary container state.");
|
|
8457
|
+
}
|
|
8458
|
+
return { containers, primary, status };
|
|
8459
|
+
}
|
|
8460
|
+
const initial = prove();
|
|
8461
|
+
let providerError;
|
|
8462
|
+
let providerFailed = false;
|
|
8463
|
+
if (initial.status === "running") {
|
|
7711
8464
|
try {
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
8465
|
+
options.stopProvider();
|
|
8466
|
+
} catch (error) {
|
|
8467
|
+
providerFailed = true;
|
|
8468
|
+
providerError = error;
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
try {
|
|
8472
|
+
const stopped = () => {
|
|
8473
|
+
const current = prove(initial.containers);
|
|
8474
|
+
if (current.status !== "stopped" || current.primary.state.Running) {
|
|
8475
|
+
throw new Error("Managed stop has not stopped the exact primary container.");
|
|
7716
8476
|
}
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
8477
|
+
return current.containers;
|
|
8478
|
+
};
|
|
8479
|
+
stopped();
|
|
8480
|
+
for (const retained of initial.containers) {
|
|
8481
|
+
if (!retained.state.Running || retained.id === initial.primary.id) continue;
|
|
8482
|
+
const current = stopped().find((entry) => entry.id === retained.id);
|
|
8483
|
+
if (current?.state.Running) {
|
|
8484
|
+
stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
|
|
8485
|
+
timeoutMs: 3e4
|
|
8486
|
+
});
|
|
7723
8487
|
}
|
|
7724
|
-
return devsyId;
|
|
7725
|
-
} catch (error) {
|
|
7726
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7727
|
-
throw new DevsyStartPostconditionError(message);
|
|
7728
8488
|
}
|
|
7729
|
-
|
|
8489
|
+
if (stopped().some((container) => container.state.Running)) {
|
|
8490
|
+
throw new Error("Managed stop left a retained service running.");
|
|
8491
|
+
}
|
|
8492
|
+
} catch (error) {
|
|
8493
|
+
if (providerFailed) {
|
|
8494
|
+
throw new AggregateError(
|
|
8495
|
+
[providerError, error],
|
|
8496
|
+
"Devsy provider stop failed; complete retained shutdown could not be verified.",
|
|
8497
|
+
{ cause: providerError }
|
|
8498
|
+
);
|
|
8499
|
+
}
|
|
8500
|
+
throw error;
|
|
8501
|
+
}
|
|
8502
|
+
if (providerFailed) throw providerError;
|
|
8503
|
+
return true;
|
|
7730
8504
|
}
|
|
7731
|
-
var
|
|
7732
|
-
var
|
|
7733
|
-
"src/core/devsy-
|
|
8505
|
+
var import_node_fs21, import_node_os2, import_node_path21;
|
|
8506
|
+
var init_managed_devsy_stop = __esm({
|
|
8507
|
+
"src/core/managed-devsy-stop.ts"() {
|
|
7734
8508
|
"use strict";
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8509
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8510
|
+
import_node_os2 = __toESM(require("os"));
|
|
8511
|
+
import_node_path21 = __toESM(require("path"));
|
|
8512
|
+
init_devcontainer_profile();
|
|
8513
|
+
init_devpod_environment();
|
|
7739
8514
|
init_devsy_workspaces();
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
|
|
7747
|
-
DEVSY_STDERR_TAIL_BYTES = 8192;
|
|
8515
|
+
init_managed_runtime_state();
|
|
8516
|
+
init_repo_config();
|
|
8517
|
+
init_route_publication();
|
|
8518
|
+
init_workspace();
|
|
8519
|
+
init_workspace_ownership();
|
|
8520
|
+
init_workspace_runtime();
|
|
7748
8521
|
}
|
|
7749
8522
|
});
|
|
7750
8523
|
|
|
7751
|
-
// src/core/
|
|
7752
|
-
function
|
|
7753
|
-
|
|
8524
|
+
// src/core/devsy-mutation.ts
|
|
8525
|
+
function failedStartMayHaveAttached(devsyId, repoPath) {
|
|
8526
|
+
try {
|
|
8527
|
+
const attached = listDevsyWorkspaces();
|
|
8528
|
+
const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
|
|
8529
|
+
if (!attachedId) return false;
|
|
8530
|
+
return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
|
|
8531
|
+
} catch {
|
|
8532
|
+
return true;
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
function withMutationLock(activity, target, operation) {
|
|
8536
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7754
8537
|
return withFileLockSync(
|
|
7755
|
-
|
|
8538
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
7756
8539
|
{
|
|
7757
8540
|
activity,
|
|
7758
8541
|
target: `'${target}'`,
|
|
7759
|
-
waitMs:
|
|
8542
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7760
8543
|
fair: true,
|
|
7761
8544
|
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7762
8545
|
},
|
|
7763
8546
|
operation
|
|
7764
8547
|
);
|
|
7765
8548
|
}
|
|
7766
|
-
function
|
|
8549
|
+
function withMutationLockAsync(activity, target, operation) {
|
|
8550
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8551
|
+
return withFileLock(
|
|
8552
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
8553
|
+
{
|
|
8554
|
+
activity,
|
|
8555
|
+
target: `'${target}'`,
|
|
8556
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
8557
|
+
fair: true,
|
|
8558
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8559
|
+
},
|
|
8560
|
+
operation
|
|
8561
|
+
);
|
|
8562
|
+
}
|
|
8563
|
+
function commandFailure2(result) {
|
|
7767
8564
|
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7768
8565
|
}
|
|
7769
|
-
function
|
|
7770
|
-
|
|
7771
|
-
|
|
8566
|
+
function runDevsyUp(args, env, quiet) {
|
|
8567
|
+
return new Promise((resolve) => {
|
|
8568
|
+
const child = (0, import_node_child_process16.spawn)("devsy", args, {
|
|
8569
|
+
stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
|
|
8570
|
+
env
|
|
8571
|
+
});
|
|
8572
|
+
const stderr = child.stderr;
|
|
8573
|
+
if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
|
|
8574
|
+
let stderrTail = Buffer.alloc(0);
|
|
8575
|
+
let spawnError;
|
|
8576
|
+
stderr.on("data", (chunk) => {
|
|
8577
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8578
|
+
const writable = process.stderr.write(value);
|
|
8579
|
+
if (!writable) {
|
|
8580
|
+
stderr.pause();
|
|
8581
|
+
process.stderr.once("drain", () => stderr.resume());
|
|
8582
|
+
}
|
|
8583
|
+
if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
|
|
8584
|
+
stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
|
|
8585
|
+
} else {
|
|
8586
|
+
const combined = Buffer.concat([stderrTail, value]);
|
|
8587
|
+
stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
|
|
8588
|
+
}
|
|
8589
|
+
});
|
|
8590
|
+
child.once("error", (error) => {
|
|
8591
|
+
spawnError = error;
|
|
8592
|
+
});
|
|
8593
|
+
child.once("close", (status) => {
|
|
8594
|
+
resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
|
|
8595
|
+
});
|
|
8596
|
+
});
|
|
8597
|
+
}
|
|
8598
|
+
function runDevsyAction(action2, devsyId, force = false) {
|
|
8599
|
+
const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
|
|
8600
|
+
const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
|
|
7772
8601
|
if (result.status !== 0) {
|
|
7773
8602
|
throw new Error(
|
|
7774
|
-
`
|
|
8603
|
+
`devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
|
|
7775
8604
|
);
|
|
7776
8605
|
}
|
|
7777
8606
|
}
|
|
7778
|
-
function
|
|
7779
|
-
const ownership =
|
|
7780
|
-
listDevpodWorkspaces(worktreePath),
|
|
7781
|
-
devpodId,
|
|
7782
|
-
worktreePath
|
|
7783
|
-
);
|
|
8607
|
+
function inspectExactOwnership(devsyId, worktreePath) {
|
|
8608
|
+
const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
|
|
7784
8609
|
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
7785
8610
|
return ownership;
|
|
7786
8611
|
}
|
|
7787
|
-
function
|
|
7788
|
-
return
|
|
7789
|
-
|
|
8612
|
+
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
8613
|
+
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
8614
|
+
if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
|
|
8615
|
+
repoPath: worktreePath,
|
|
8616
|
+
devsyId,
|
|
8617
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
8618
|
+
}))
|
|
8619
|
+
return { status: "changed" };
|
|
8620
|
+
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
7790
8621
|
if (before.status === "absent") return { status: "absent" };
|
|
7791
|
-
|
|
7792
|
-
let after =
|
|
8622
|
+
runDevsyAction(action2, devsyId);
|
|
8623
|
+
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
7793
8624
|
if (action2 === "stop" && after.status !== "owned") {
|
|
7794
|
-
throw new Error(
|
|
8625
|
+
throw new Error(
|
|
8626
|
+
`Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
|
|
8627
|
+
);
|
|
7795
8628
|
}
|
|
7796
8629
|
if (action2 === "delete" && after.status === "owned") {
|
|
7797
|
-
const runtime =
|
|
8630
|
+
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
7798
8631
|
if (runtime !== "not-found") {
|
|
7799
8632
|
throw new Error(
|
|
7800
|
-
`
|
|
8633
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
7801
8634
|
);
|
|
7802
8635
|
}
|
|
7803
|
-
after =
|
|
8636
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7804
8637
|
if (after.status === "owned") {
|
|
7805
|
-
|
|
7806
|
-
after =
|
|
8638
|
+
runDevsyAction("delete", devsyId, true);
|
|
8639
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7807
8640
|
}
|
|
7808
8641
|
if (after.status !== "absent") {
|
|
7809
8642
|
throw new Error(
|
|
7810
|
-
`
|
|
8643
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
|
|
7811
8644
|
);
|
|
7812
8645
|
}
|
|
7813
8646
|
}
|
|
7814
8647
|
return { status: "changed" };
|
|
7815
8648
|
});
|
|
7816
8649
|
}
|
|
7817
|
-
function
|
|
7818
|
-
|
|
7819
|
-
const result2 = stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
7820
|
-
resetWorkspaceRuntimeCaches();
|
|
7821
|
-
return result2;
|
|
7822
|
-
}
|
|
7823
|
-
const result = mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
7824
|
-
resetWorkspaceRuntimeCaches();
|
|
7825
|
-
return result;
|
|
8650
|
+
function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8651
|
+
return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
|
|
7826
8652
|
}
|
|
7827
|
-
function
|
|
7828
|
-
|
|
7829
|
-
const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
7830
|
-
resetWorkspaceRuntimeCaches();
|
|
7831
|
-
return result2;
|
|
7832
|
-
}
|
|
7833
|
-
const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
7834
|
-
resetWorkspaceRuntimeCaches();
|
|
7835
|
-
return result;
|
|
8653
|
+
function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8654
|
+
return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
|
|
7836
8655
|
}
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
recreate: options.recreate,
|
|
7845
|
-
quiet: options.quiet,
|
|
7846
|
-
workspace: options.workspace,
|
|
7847
|
-
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
7848
|
-
});
|
|
7849
|
-
resetWorkspaceRuntimeCaches();
|
|
7850
|
-
return result;
|
|
7851
|
-
} catch (error) {
|
|
7852
|
-
if (error instanceof DevsyStartPostconditionError) {
|
|
7853
|
-
resetWorkspaceRuntimeCaches();
|
|
7854
|
-
throw new DevpodStartPostconditionError(error.message);
|
|
7855
|
-
}
|
|
7856
|
-
throw error;
|
|
7857
|
-
}
|
|
8656
|
+
function assertDevsyTarget(devsyId, repoPath) {
|
|
8657
|
+
const workspaces = listDevsyWorkspaces();
|
|
8658
|
+
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
8659
|
+
const id = devsyId ?? existing?.id;
|
|
8660
|
+
if (id) {
|
|
8661
|
+
const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
|
|
8662
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
7858
8663
|
}
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
8664
|
+
return id;
|
|
8665
|
+
}
|
|
8666
|
+
async function startDevsyWorkspace(options) {
|
|
8667
|
+
const activity = options.recreate ? "Devsy recreate" : "Devsy start";
|
|
8668
|
+
let agent;
|
|
8669
|
+
try {
|
|
8670
|
+
agent = requireReadyDevsyAgent();
|
|
8671
|
+
} catch (error) {
|
|
8672
|
+
if (!(error instanceof DevsyAgentReadinessError)) throw error;
|
|
8673
|
+
const repair = devsyAgentRepairSuggestion(error.inspection);
|
|
8674
|
+
throw new Error(`${error.message}. ${repair}`);
|
|
8675
|
+
}
|
|
8676
|
+
return withMutationLockAsync(activity, options.repoPath, async () => {
|
|
8677
|
+
let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
|
|
8678
|
+
if (devsyId && options.recreate) {
|
|
8679
|
+
const attached = listDevsyWorkspaces();
|
|
8680
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8681
|
+
if (ownership.status !== "owned") {
|
|
8682
|
+
throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
|
|
7868
8683
|
}
|
|
7869
|
-
} else if (options.recreate) {
|
|
7870
|
-
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
7871
8684
|
}
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
if (options.devcontainerPath) {
|
|
7875
|
-
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8685
|
+
if (!devsyId && options.recreate) {
|
|
8686
|
+
throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
|
|
7876
8687
|
}
|
|
7877
|
-
args
|
|
8688
|
+
const args = ["workspace", "up", options.repoPath];
|
|
8689
|
+
if (devsyId) args.push("--id", devsyId);
|
|
8690
|
+
if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
|
|
8691
|
+
args.push("--ide-launch", "skip");
|
|
7878
8692
|
if (options.workspace) {
|
|
7879
8693
|
args.push(
|
|
7880
8694
|
"--workspace-env",
|
|
@@ -7883,8 +8697,12 @@ async function startDevpodWorkspace(options) {
|
|
|
7883
8697
|
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
7884
8698
|
);
|
|
7885
8699
|
}
|
|
8700
|
+
if (options.inactivityTimeout) {
|
|
8701
|
+
args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
|
|
8702
|
+
}
|
|
7886
8703
|
if (options.recreate) args.push("--recreate");
|
|
7887
8704
|
const env = { ...process.env };
|
|
8705
|
+
env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
7888
8706
|
if (options.workspace) {
|
|
7889
8707
|
env.WORKSPACE = options.workspace.token;
|
|
7890
8708
|
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
@@ -7896,500 +8714,252 @@ async function startDevpodWorkspace(options) {
|
|
|
7896
8714
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
7897
8715
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
7898
8716
|
}
|
|
7899
|
-
const result =
|
|
7900
|
-
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
7901
|
-
env
|
|
7902
|
-
});
|
|
8717
|
+
const result = await runDevsyUp(args, env, options.quiet ?? false);
|
|
7903
8718
|
if (result.status !== 0) {
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
if (
|
|
7910
|
-
throw new
|
|
7911
|
-
}
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
}
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
import_node_path21 = __toESM(require("path"));
|
|
7934
|
-
init_devpod_workspaces();
|
|
7935
|
-
init_devsy_mutation();
|
|
7936
|
-
init_file_lock();
|
|
7937
|
-
init_router();
|
|
7938
|
-
init_workspace_runtime();
|
|
7939
|
-
DEVPOD_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
7940
|
-
DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
7941
|
-
DevpodStartPostconditionError = class extends Error {
|
|
7942
|
-
};
|
|
7943
|
-
}
|
|
7944
|
-
});
|
|
7945
|
-
|
|
7946
|
-
// src/core/workspace-ownership.ts
|
|
7947
|
-
function commandError(command, repoPath, stderr) {
|
|
7948
|
-
return new Error(
|
|
7949
|
-
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
7950
|
-
);
|
|
7951
|
-
}
|
|
7952
|
-
function resolveGitCommonDir(repoPath) {
|
|
7953
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
7954
|
-
encoding: "utf-8",
|
|
7955
|
-
env: READ_ONLY_GIT_ENV
|
|
7956
|
-
});
|
|
7957
|
-
const output2 = result.stdout.trim();
|
|
7958
|
-
if (result.status !== 0 || !output2) {
|
|
7959
|
-
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
7960
|
-
}
|
|
7961
|
-
return comparableWorkspacePath(import_node_path22.default.isAbsolute(output2) ? output2 : import_node_path22.default.resolve(repoPath, output2));
|
|
7962
|
-
}
|
|
7963
|
-
function resolveGitTopLevel(repoPath) {
|
|
7964
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
7965
|
-
encoding: "utf-8",
|
|
7966
|
-
env: READ_ONLY_GIT_ENV
|
|
7967
|
-
});
|
|
7968
|
-
const output2 = result.stdout.trim();
|
|
7969
|
-
if (result.status !== 0 || !output2) {
|
|
7970
|
-
throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
|
|
7971
|
-
}
|
|
7972
|
-
return comparableWorkspacePath(output2);
|
|
7973
|
-
}
|
|
7974
|
-
function listGitWorktrees(repoPath) {
|
|
7975
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
7976
|
-
encoding: "utf-8",
|
|
7977
|
-
env: READ_ONLY_GIT_ENV
|
|
7978
|
-
});
|
|
7979
|
-
if (result.status !== 0) {
|
|
7980
|
-
throw commandError("git worktree list", repoPath, result.stderr);
|
|
7981
|
-
}
|
|
7982
|
-
const worktrees = [];
|
|
7983
|
-
let current = {};
|
|
7984
|
-
const finish = () => {
|
|
7985
|
-
if (!current.path) return;
|
|
7986
|
-
worktrees.push({
|
|
7987
|
-
path: comparableWorkspacePath(current.path),
|
|
7988
|
-
branch: current.branch,
|
|
7989
|
-
locked: current.locked ?? false,
|
|
7990
|
-
prunable: current.prunable ?? false
|
|
7991
|
-
});
|
|
7992
|
-
current = {};
|
|
7993
|
-
};
|
|
7994
|
-
for (const line of `${result.stdout}
|
|
7995
|
-
`.split("\n")) {
|
|
7996
|
-
if (line.startsWith("worktree ")) {
|
|
7997
|
-
finish();
|
|
7998
|
-
current.path = line.slice("worktree ".length).trim();
|
|
7999
|
-
} else if (line.startsWith("branch ")) {
|
|
8000
|
-
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
8001
|
-
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
8002
|
-
current.locked = true;
|
|
8003
|
-
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
8004
|
-
current.prunable = true;
|
|
8005
|
-
} else if (line === "") {
|
|
8006
|
-
finish();
|
|
8007
|
-
}
|
|
8008
|
-
}
|
|
8009
|
-
return worktrees;
|
|
8010
|
-
}
|
|
8011
|
-
function ownershipDirectory(repoPath) {
|
|
8012
|
-
return import_node_path22.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
8013
|
-
}
|
|
8014
|
-
function validateWorkspace(value, label) {
|
|
8015
|
-
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
8016
|
-
throw new Error(`invalid workspace ownership ${label}`);
|
|
8017
|
-
}
|
|
8018
|
-
return value;
|
|
8019
|
-
}
|
|
8020
|
-
function validateTimestamp(value, label) {
|
|
8021
|
-
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
8022
|
-
throw new Error(`invalid workspace ownership ${label}`);
|
|
8023
|
-
}
|
|
8024
|
-
return value;
|
|
8025
|
-
}
|
|
8026
|
-
function validateRecord(value, expectedWorkspace) {
|
|
8027
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8028
|
-
throw new Error("invalid workspace ownership record");
|
|
8029
|
-
}
|
|
8030
|
-
const candidate = value;
|
|
8031
|
-
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
8032
|
-
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
8033
|
-
}
|
|
8034
|
-
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
8035
|
-
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
8036
|
-
throw new Error(
|
|
8037
|
-
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
8038
|
-
);
|
|
8039
|
-
}
|
|
8040
|
-
if (typeof candidate.worktreePath !== "string" || !import_node_path22.default.isAbsolute(candidate.worktreePath)) {
|
|
8041
|
-
throw new Error("invalid workspace ownership worktreePath");
|
|
8042
|
-
}
|
|
8043
|
-
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
8044
|
-
throw new Error("invalid workspace ownership branch");
|
|
8045
|
-
}
|
|
8046
|
-
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
8047
|
-
return {
|
|
8048
|
-
version: OWNERSHIP_VERSION,
|
|
8049
|
-
workspace,
|
|
8050
|
-
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
8051
|
-
branch: candidate.branch,
|
|
8052
|
-
devpodId,
|
|
8053
|
-
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
8054
|
-
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
8055
|
-
};
|
|
8056
|
-
}
|
|
8057
|
-
function readRecordFile(filePath, expectedWorkspace) {
|
|
8058
|
-
let parsed;
|
|
8059
|
-
try {
|
|
8060
|
-
parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
|
|
8061
|
-
} catch (error) {
|
|
8062
|
-
if (error instanceof SyntaxError) {
|
|
8063
|
-
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
8064
|
-
}
|
|
8065
|
-
throw error;
|
|
8066
|
-
}
|
|
8067
|
-
return validateRecord(parsed, expectedWorkspace);
|
|
8068
|
-
}
|
|
8069
|
-
function listWorkspaceOwnership(repoPath) {
|
|
8070
|
-
const directory = ownershipDirectory(repoPath);
|
|
8071
|
-
return listWorkspaceOwnershipInDirectory(directory);
|
|
8072
|
-
}
|
|
8073
|
-
function listWorkspaceOwnershipInDirectory(directory) {
|
|
8074
|
-
let entries;
|
|
8075
|
-
try {
|
|
8076
|
-
entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
|
|
8077
|
-
} catch (error) {
|
|
8078
|
-
if (error.code === "ENOENT") return [];
|
|
8079
|
-
throw error;
|
|
8080
|
-
}
|
|
8081
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
8082
|
-
const workspace = entry.name.slice(0, -".json".length);
|
|
8083
|
-
validateWorkspace(workspace, "filename");
|
|
8084
|
-
return readRecordFile(import_node_path22.default.join(directory, entry.name), workspace);
|
|
8085
|
-
});
|
|
8086
|
-
}
|
|
8087
|
-
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
8088
|
-
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
8089
|
-
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
8090
|
-
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
8091
|
-
const filePath = import_node_path22.default.join(directory, `${workspace}.json`);
|
|
8092
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8093
|
-
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
8094
|
-
const existing = records.find((record2) => record2.workspace === workspace);
|
|
8095
|
-
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
8096
|
-
throw new Error(
|
|
8097
|
-
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
8098
|
-
);
|
|
8099
|
-
}
|
|
8100
|
-
const pathOwner = records.find(
|
|
8101
|
-
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
8102
|
-
);
|
|
8103
|
-
if (pathOwner) {
|
|
8104
|
-
throw new Error(
|
|
8105
|
-
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
8106
|
-
);
|
|
8107
|
-
}
|
|
8108
|
-
if (existing && existing.devpodId !== devpodId) {
|
|
8109
|
-
throw new Error(
|
|
8110
|
-
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
8111
|
-
);
|
|
8112
|
-
}
|
|
8113
|
-
const record = {
|
|
8114
|
-
version: OWNERSHIP_VERSION,
|
|
8115
|
-
workspace,
|
|
8116
|
-
worktreePath,
|
|
8117
|
-
branch: input2.branch ?? null,
|
|
8118
|
-
devpodId,
|
|
8119
|
-
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
8120
|
-
updatedAt: validateTimestamp(now, "updatedAt")
|
|
8121
|
-
};
|
|
8122
|
-
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
8123
|
-
`);
|
|
8124
|
-
return record;
|
|
8125
|
-
}
|
|
8126
|
-
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
8127
|
-
const filePath = import_node_path22.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
8128
|
-
try {
|
|
8129
|
-
import_node_fs22.default.rmSync(filePath);
|
|
8130
|
-
return true;
|
|
8131
|
-
} catch (error) {
|
|
8132
|
-
if (error.code === "ENOENT") return false;
|
|
8133
|
-
throw error;
|
|
8134
|
-
}
|
|
8135
|
-
}
|
|
8136
|
-
function sameOwnershipRecord(left, right) {
|
|
8137
|
-
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;
|
|
8719
|
+
let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
|
|
8720
|
+
if (result.error?.message) message += ` ${result.error.message}`;
|
|
8721
|
+
if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
|
|
8722
|
+
message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
8723
|
+
}
|
|
8724
|
+
if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
|
|
8725
|
+
throw new DevsyStartPostconditionError(message);
|
|
8726
|
+
}
|
|
8727
|
+
throw new Error(message);
|
|
8728
|
+
}
|
|
8729
|
+
try {
|
|
8730
|
+
const attached = listDevsyWorkspaces();
|
|
8731
|
+
devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
|
|
8732
|
+
if (!devsyId) {
|
|
8733
|
+
throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
|
|
8734
|
+
}
|
|
8735
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8736
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8737
|
+
if (ownership.status !== "owned") {
|
|
8738
|
+
throw new Error(
|
|
8739
|
+
`Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
|
|
8740
|
+
);
|
|
8741
|
+
}
|
|
8742
|
+
return devsyId;
|
|
8743
|
+
} catch (error) {
|
|
8744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8745
|
+
throw new DevsyStartPostconditionError(message);
|
|
8746
|
+
}
|
|
8747
|
+
});
|
|
8138
8748
|
}
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8749
|
+
var import_node_child_process16, import_node_fs22, import_node_path22, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
|
|
8750
|
+
var init_devsy_mutation = __esm({
|
|
8751
|
+
"src/core/devsy-mutation.ts"() {
|
|
8752
|
+
"use strict";
|
|
8753
|
+
import_node_child_process16 = require("child_process");
|
|
8754
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
8755
|
+
import_node_path22 = __toESM(require("path"));
|
|
8756
|
+
init_devsy_agent();
|
|
8757
|
+
init_devsy_workspaces();
|
|
8758
|
+
init_file_lock();
|
|
8759
|
+
init_managed_devsy_stop();
|
|
8760
|
+
init_router();
|
|
8761
|
+
DEVSY_MUTATION_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
|
|
8762
|
+
DEVSY_MUTATION_WAIT_MS = 18e5;
|
|
8763
|
+
DevsyStartPostconditionError = class extends Error {
|
|
8764
|
+
};
|
|
8765
|
+
AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
|
|
8766
|
+
DEVSY_STDERR_TAIL_BYTES = 8192;
|
|
8150
8767
|
}
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
const directory = ownershipDirectory(repoPath);
|
|
8157
|
-
import_node_fs22.default.mkdirSync(directory, { recursive: true });
|
|
8768
|
+
});
|
|
8769
|
+
|
|
8770
|
+
// src/core/devpod-mutation.ts
|
|
8771
|
+
function withMutationLock2(activity, target, operation) {
|
|
8772
|
+
import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8158
8773
|
return withFileLockSync(
|
|
8159
|
-
|
|
8774
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
8160
8775
|
{
|
|
8161
|
-
activity
|
|
8162
|
-
target: `'${
|
|
8163
|
-
waitMs:
|
|
8776
|
+
activity,
|
|
8777
|
+
target: `'${target}'`,
|
|
8778
|
+
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
8779
|
+
fair: true,
|
|
8780
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8164
8781
|
},
|
|
8165
|
-
|
|
8166
|
-
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8167
|
-
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8168
|
-
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8169
|
-
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8170
|
-
})
|
|
8782
|
+
operation
|
|
8171
8783
|
);
|
|
8172
8784
|
}
|
|
8173
|
-
function
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8785
|
+
function commandFailure3(result) {
|
|
8786
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
8787
|
+
}
|
|
8788
|
+
function runDevpodAction(action2, devpodId, force = false) {
|
|
8789
|
+
const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
|
|
8790
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
8791
|
+
if (result.status !== 0) {
|
|
8178
8792
|
throw new Error(
|
|
8179
|
-
`
|
|
8793
|
+
`devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
|
|
8180
8794
|
);
|
|
8181
8795
|
}
|
|
8182
|
-
return owners[0];
|
|
8183
|
-
}
|
|
8184
|
-
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
8185
|
-
const owners = /* @__PURE__ */ new Map();
|
|
8186
|
-
for (const worktree of listGitWorktrees(repoPath)) {
|
|
8187
|
-
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
8188
|
-
let workspace;
|
|
8189
|
-
try {
|
|
8190
|
-
workspace = readPersistedWorkspace(worktree.path);
|
|
8191
|
-
} catch (error) {
|
|
8192
|
-
if (!import_node_fs22.default.existsSync(worktree.path)) continue;
|
|
8193
|
-
throw error;
|
|
8194
|
-
}
|
|
8195
|
-
if (!workspace) continue;
|
|
8196
|
-
const existing = owners.get(workspace);
|
|
8197
|
-
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
8198
|
-
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
8199
|
-
}
|
|
8200
|
-
owners.set(workspace, worktree.path);
|
|
8201
|
-
}
|
|
8202
|
-
return owners;
|
|
8203
8796
|
}
|
|
8204
|
-
function
|
|
8205
|
-
const
|
|
8206
|
-
(
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
8210
|
-
}
|
|
8211
|
-
const providerOwner = providerWorkspaces.find(
|
|
8212
|
-
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
8797
|
+
function inspectExactOwnership2(devpodId, worktreePath) {
|
|
8798
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
8799
|
+
listDevpodWorkspaces(worktreePath),
|
|
8800
|
+
devpodId,
|
|
8801
|
+
worktreePath
|
|
8213
8802
|
);
|
|
8214
|
-
if (
|
|
8215
|
-
|
|
8216
|
-
}
|
|
8217
|
-
const persistedOwner = persistedOwners.get(workspace);
|
|
8218
|
-
if (persistedOwner) {
|
|
8219
|
-
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
8220
|
-
}
|
|
8221
|
-
return void 0;
|
|
8803
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8804
|
+
return ownership;
|
|
8222
8805
|
}
|
|
8223
|
-
function
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
if (exactRecords.length > 1) {
|
|
8232
|
-
throw new Error(
|
|
8233
|
-
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
8234
|
-
);
|
|
8806
|
+
function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
|
|
8807
|
+
return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
|
|
8808
|
+
const before = inspectExactOwnership2(devpodId, worktreePath);
|
|
8809
|
+
if (before.status === "absent") return { status: "absent" };
|
|
8810
|
+
runDevpodAction(action2, devpodId);
|
|
8811
|
+
let after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8812
|
+
if (action2 === "stop" && after.status !== "owned") {
|
|
8813
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
8235
8814
|
}
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
if (exactRecord) {
|
|
8240
|
-
if (persisted && persisted !== exactRecord.workspace) {
|
|
8815
|
+
if (action2 === "delete" && after.status === "owned") {
|
|
8816
|
+
const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
|
|
8817
|
+
if (runtime !== "not-found") {
|
|
8241
8818
|
throw new Error(
|
|
8242
|
-
`
|
|
8819
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
8243
8820
|
);
|
|
8244
8821
|
}
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
);
|
|
8822
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8823
|
+
if (after.status === "owned") {
|
|
8824
|
+
runDevpodAction("delete", devpodId, true);
|
|
8825
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8249
8826
|
}
|
|
8250
|
-
|
|
8251
|
-
exactRecord.workspace,
|
|
8252
|
-
exactRecord.devpodId,
|
|
8253
|
-
worktreePath,
|
|
8254
|
-
records,
|
|
8255
|
-
input2.providerWorkspaces,
|
|
8256
|
-
persistedOwners,
|
|
8257
|
-
exactRecord
|
|
8258
|
-
);
|
|
8259
|
-
if (conflict2) {
|
|
8827
|
+
if (after.status !== "absent") {
|
|
8260
8828
|
throw new Error(
|
|
8261
|
-
`
|
|
8829
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
8262
8830
|
);
|
|
8263
8831
|
}
|
|
8264
|
-
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
8265
|
-
return transaction.write({
|
|
8266
|
-
workspace: exactRecord.workspace,
|
|
8267
|
-
worktreePath,
|
|
8268
|
-
branch: input2.branch ?? null,
|
|
8269
|
-
devpodId: exactRecord.devpodId
|
|
8270
|
-
});
|
|
8271
8832
|
}
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8833
|
+
return { status: "changed" };
|
|
8834
|
+
});
|
|
8835
|
+
}
|
|
8836
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8837
|
+
try {
|
|
8838
|
+
resetWorkspaceRuntimeCaches();
|
|
8839
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8840
|
+
return stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8276
8841
|
}
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8842
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
8843
|
+
} finally {
|
|
8844
|
+
resetWorkspaceRuntimeCaches();
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8848
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8849
|
+
const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8850
|
+
resetWorkspaceRuntimeCaches();
|
|
8851
|
+
return result2;
|
|
8852
|
+
}
|
|
8853
|
+
const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
8854
|
+
resetWorkspaceRuntimeCaches();
|
|
8855
|
+
return result;
|
|
8856
|
+
}
|
|
8857
|
+
async function startDevpodWorkspace(options) {
|
|
8858
|
+
if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
|
|
8859
|
+
try {
|
|
8860
|
+
const result = await startDevsyWorkspace({
|
|
8861
|
+
repoPath: options.repoPath,
|
|
8862
|
+
devsyId: options.devpodId,
|
|
8863
|
+
devcontainerPath: options.devcontainerPath,
|
|
8864
|
+
recreate: options.recreate,
|
|
8865
|
+
quiet: options.quiet,
|
|
8866
|
+
workspace: options.workspace,
|
|
8867
|
+
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
8868
|
+
});
|
|
8869
|
+
resetWorkspaceRuntimeCaches();
|
|
8870
|
+
return result;
|
|
8871
|
+
} catch (error) {
|
|
8872
|
+
if (error instanceof DevsyStartPostconditionError) {
|
|
8873
|
+
resetWorkspaceRuntimeCaches();
|
|
8874
|
+
throw new DevpodStartPostconditionError(error.message);
|
|
8299
8875
|
}
|
|
8300
|
-
|
|
8301
|
-
devpodId = candidate;
|
|
8876
|
+
throw error;
|
|
8302
8877
|
}
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8878
|
+
}
|
|
8879
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
8880
|
+
return withMutationLock2(activity, options.repoPath, () => {
|
|
8881
|
+
const workspaces = listDevpodWorkspaces(options.repoPath);
|
|
8882
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
8883
|
+
if (devpodId) {
|
|
8884
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
8885
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
8886
|
+
if (options.recreate && before.status !== "owned") {
|
|
8887
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
8888
|
+
}
|
|
8889
|
+
} else if (options.recreate) {
|
|
8890
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
8891
|
+
}
|
|
8892
|
+
const args = ["up", options.repoPath];
|
|
8893
|
+
if (devpodId) args.push("--id", devpodId);
|
|
8894
|
+
if (options.devcontainerPath) {
|
|
8895
|
+
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8896
|
+
}
|
|
8897
|
+
args.push("--open-ide=false");
|
|
8898
|
+
if (options.workspace) {
|
|
8899
|
+
args.push(
|
|
8900
|
+
"--workspace-env",
|
|
8901
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
8902
|
+
"--workspace-env",
|
|
8903
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
8314
8904
|
);
|
|
8315
8905
|
}
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8906
|
+
if (options.recreate) args.push("--recreate");
|
|
8907
|
+
const env = { ...process.env };
|
|
8908
|
+
if (options.workspace) {
|
|
8909
|
+
env.WORKSPACE = options.workspace.token;
|
|
8910
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
8911
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
8912
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
8913
|
+
} else {
|
|
8914
|
+
delete env.WORKSPACE;
|
|
8915
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
8916
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
8917
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
8918
|
+
}
|
|
8919
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
|
|
8920
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
8921
|
+
env
|
|
8321
8922
|
});
|
|
8923
|
+
if (result.status !== 0) {
|
|
8924
|
+
throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
|
|
8925
|
+
}
|
|
8322
8926
|
try {
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8927
|
+
const attached = listDevpodWorkspaces(options.repoPath);
|
|
8928
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
8929
|
+
if (!devpodId) {
|
|
8930
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
8931
|
+
}
|
|
8932
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
8933
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8934
|
+
if (ownership.status !== "owned") {
|
|
8328
8935
|
throw new Error(
|
|
8329
|
-
`
|
|
8936
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
8330
8937
|
);
|
|
8331
8938
|
}
|
|
8332
|
-
|
|
8939
|
+
resetWorkspaceRuntimeCaches();
|
|
8940
|
+
return devpodId;
|
|
8941
|
+
} catch (error) {
|
|
8942
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8943
|
+
throw new DevpodStartPostconditionError(message);
|
|
8333
8944
|
}
|
|
8334
|
-
return written;
|
|
8335
8945
|
});
|
|
8336
8946
|
}
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
(transaction) => transaction.remove(workspace)
|
|
8341
|
-
);
|
|
8342
|
-
}
|
|
8343
|
-
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
8344
|
-
const worktree = worktrees.find(
|
|
8345
|
-
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
8346
|
-
);
|
|
8347
|
-
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
8348
|
-
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
8349
|
-
if (devpodStatus === "conflict") {
|
|
8350
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8351
|
-
}
|
|
8352
|
-
if (worktree?.locked) {
|
|
8353
|
-
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
8354
|
-
}
|
|
8355
|
-
if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record.worktreePath)) {
|
|
8356
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8357
|
-
}
|
|
8358
|
-
if (!worktree || worktree.prunable) {
|
|
8359
|
-
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
8360
|
-
}
|
|
8361
|
-
let persisted;
|
|
8362
|
-
try {
|
|
8363
|
-
persisted = readPersistedWorkspace(worktree.path);
|
|
8364
|
-
} catch {
|
|
8365
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8366
|
-
}
|
|
8367
|
-
return {
|
|
8368
|
-
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
8369
|
-
devpodStatus,
|
|
8370
|
-
worktree
|
|
8371
|
-
};
|
|
8372
|
-
}
|
|
8373
|
-
function listMissingWorkspaceOwnership(repoPath) {
|
|
8374
|
-
const worktrees = listGitWorktrees(repoPath);
|
|
8375
|
-
return listWorkspaceOwnership(repoPath).filter(
|
|
8376
|
-
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
8377
|
-
);
|
|
8378
|
-
}
|
|
8379
|
-
var import_node_child_process17, import_node_fs22, import_node_path22, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
8380
|
-
var init_workspace_ownership = __esm({
|
|
8381
|
-
"src/core/workspace-ownership.ts"() {
|
|
8947
|
+
var import_node_child_process17, import_node_fs23, import_node_path23, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
8948
|
+
var init_devpod_mutation = __esm({
|
|
8949
|
+
"src/core/devpod-mutation.ts"() {
|
|
8382
8950
|
"use strict";
|
|
8383
8951
|
import_node_child_process17 = require("child_process");
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
init_atomic_file();
|
|
8952
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
8953
|
+
import_node_path23 = __toESM(require("path"));
|
|
8387
8954
|
init_devpod_workspaces();
|
|
8955
|
+
init_devsy_mutation();
|
|
8388
8956
|
init_file_lock();
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8957
|
+
init_router();
|
|
8958
|
+
init_workspace_runtime();
|
|
8959
|
+
DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8960
|
+
DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8961
|
+
DevpodStartPostconditionError = class extends Error {
|
|
8962
|
+
};
|
|
8393
8963
|
}
|
|
8394
8964
|
});
|
|
8395
8965
|
|
|
@@ -8403,10 +8973,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
8403
8973
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
8404
8974
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
8405
8975
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
8406
|
-
const localRoot =
|
|
8976
|
+
const localRoot = import_node_path24.default.join(comparableRepo, "trees") + import_node_path24.default.sep;
|
|
8407
8977
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
8408
|
-
const legacyPrefix = `${
|
|
8409
|
-
return
|
|
8978
|
+
const legacyPrefix = `${import_node_path24.default.basename(comparableRepo)}-`;
|
|
8979
|
+
return import_node_path24.default.dirname(comparableWorktree) === import_node_path24.default.dirname(comparableRepo) && import_node_path24.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
8410
8980
|
}
|
|
8411
8981
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
8412
8982
|
const actions = [
|
|
@@ -8658,11 +9228,11 @@ function applyWorkspaceGc(plan) {
|
|
|
8658
9228
|
candidates
|
|
8659
9229
|
};
|
|
8660
9230
|
}
|
|
8661
|
-
var
|
|
9231
|
+
var import_node_path24;
|
|
8662
9232
|
var init_workspace_gc = __esm({
|
|
8663
9233
|
"src/core/workspace-gc.ts"() {
|
|
8664
9234
|
"use strict";
|
|
8665
|
-
|
|
9235
|
+
import_node_path24 = __toESM(require("path"));
|
|
8666
9236
|
init_devpod_mutation();
|
|
8667
9237
|
init_devpod_workspaces();
|
|
8668
9238
|
init_host_routes();
|
|
@@ -8732,11 +9302,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
8732
9302
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
8733
9303
|
continue;
|
|
8734
9304
|
}
|
|
8735
|
-
if (!
|
|
9305
|
+
if (!import_node_fs24.default.existsSync(absolutePath)) {
|
|
8736
9306
|
continue;
|
|
8737
9307
|
}
|
|
8738
9308
|
try {
|
|
8739
|
-
const raw =
|
|
9309
|
+
const raw = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
|
|
8740
9310
|
const parsed = import_yaml5.default.parse(raw);
|
|
8741
9311
|
const root = asRecord2(parsed);
|
|
8742
9312
|
const services = asRecord2(root?.services);
|
|
@@ -9015,7 +9585,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
9015
9585
|
const config = runtimeConfig.config;
|
|
9016
9586
|
loadedConfig = config;
|
|
9017
9587
|
loadedWorkspace = runtimeConfig.workspace;
|
|
9018
|
-
const cliVersion = true ? "0.0.
|
|
9588
|
+
const cliVersion = true ? "0.0.53" : "0.0.0-dev";
|
|
9019
9589
|
const configVersion = config.devrouter?.version;
|
|
9020
9590
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
9021
9591
|
addCheck(checks, {
|
|
@@ -9045,9 +9615,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
9045
9615
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
9046
9616
|
app: app.name,
|
|
9047
9617
|
filePath,
|
|
9048
|
-
absolutePath:
|
|
9618
|
+
absolutePath: import_node_path25.default.resolve(repo.path, filePath)
|
|
9049
9619
|
}))
|
|
9050
|
-
).filter((entry) => !
|
|
9620
|
+
).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9051
9621
|
addCheck(checks, {
|
|
9052
9622
|
id: "repo.compose-files",
|
|
9053
9623
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -9090,8 +9660,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
9090
9660
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
9091
9661
|
app: app.name,
|
|
9092
9662
|
cwd: app.hostRun.cwd,
|
|
9093
|
-
absolutePath:
|
|
9094
|
-
})).filter((entry) => !
|
|
9663
|
+
absolutePath: import_node_path25.default.resolve(repo.path, app.hostRun.cwd)
|
|
9664
|
+
})).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9095
9665
|
addCheck(checks, {
|
|
9096
9666
|
id: "repo.host-cwd",
|
|
9097
9667
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -9235,12 +9805,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
9235
9805
|
nextSteps
|
|
9236
9806
|
};
|
|
9237
9807
|
}
|
|
9238
|
-
var
|
|
9808
|
+
var import_node_fs24, import_node_path25, import_yaml5, POSTGRES_DEFAULTS;
|
|
9239
9809
|
var init_doctor = __esm({
|
|
9240
9810
|
"src/core/doctor.ts"() {
|
|
9241
9811
|
"use strict";
|
|
9242
|
-
|
|
9243
|
-
|
|
9812
|
+
import_node_fs24 = __toESM(require("fs"));
|
|
9813
|
+
import_node_path25 = __toESM(require("path"));
|
|
9244
9814
|
import_yaml5 = __toESM(require("yaml"));
|
|
9245
9815
|
init_docker();
|
|
9246
9816
|
init_host_routes();
|
|
@@ -9687,81 +10257,54 @@ var init_http_route_probe = __esm({
|
|
|
9687
10257
|
}
|
|
9688
10258
|
});
|
|
9689
10259
|
|
|
9690
|
-
// src/core/route-
|
|
9691
|
-
function
|
|
9692
|
-
return
|
|
9693
|
-
}
|
|
9694
|
-
function configuredProxyAppsFromConfig(config) {
|
|
9695
|
-
return routedAppsFromConfig(config).filter(
|
|
9696
|
-
(app) => app.runtime === "proxy"
|
|
9697
|
-
);
|
|
9698
|
-
}
|
|
9699
|
-
function proxyAppsFromConfig(config) {
|
|
9700
|
-
const routedApps = routedAppsFromConfig(config);
|
|
9701
|
-
const proxyApps = configuredProxyAppsFromConfig(config);
|
|
9702
|
-
const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
|
|
9703
|
-
if (unsupported.length > 0) {
|
|
9704
|
-
throw new Error(
|
|
9705
|
-
`Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
|
|
9706
|
-
);
|
|
9707
|
-
}
|
|
9708
|
-
return proxyApps;
|
|
10260
|
+
// src/core/traefik-route-health.ts
|
|
10261
|
+
function sleep(ms) {
|
|
10262
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9709
10263
|
}
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
10264
|
+
function inspectRouterApi(protocol) {
|
|
10265
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
10266
|
+
"curl",
|
|
10267
|
+
[
|
|
10268
|
+
"--silent",
|
|
10269
|
+
"--show-error",
|
|
10270
|
+
"--fail",
|
|
10271
|
+
"--max-time",
|
|
10272
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
10273
|
+
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
10274
|
+
],
|
|
10275
|
+
{ encoding: "utf-8" }
|
|
9716
10276
|
);
|
|
9717
|
-
|
|
9718
|
-
if (app.protocol === "tcp") {
|
|
9719
|
-
if (!isTLSEnabled()) {
|
|
9720
|
-
throw new Error(
|
|
9721
|
-
`TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
9722
|
-
);
|
|
9723
|
-
}
|
|
9724
|
-
activateTcpProtocol(app.tcpProtocol);
|
|
9725
|
-
}
|
|
9726
|
-
}
|
|
9727
|
-
startRouterStack();
|
|
9728
|
-
return tlsCoverage;
|
|
9729
|
-
}
|
|
9730
|
-
async function replacePublishedProxyRoutes(repoPath, config, workspace) {
|
|
9731
|
-
const apps = proxyAppsFromConfig(config);
|
|
9732
|
-
const tlsCoverage = await ensureRouteInfrastructure(apps, { repoPath });
|
|
9733
|
-
const routes = apps.map((app) => {
|
|
9734
|
-
const upstream = parseUpstream(app.upstream);
|
|
10277
|
+
if (result.status !== 0) {
|
|
9735
10278
|
return {
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
protocol: app.protocol,
|
|
9739
|
-
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
9740
|
-
repoPath,
|
|
9741
|
-
port: upstream.port,
|
|
9742
|
-
mode: "proxy",
|
|
9743
|
-
upstreamHost: upstream.upstreamHost,
|
|
9744
|
-
workspace
|
|
10279
|
+
ok: false,
|
|
10280
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
9745
10281
|
};
|
|
9746
|
-
});
|
|
9747
|
-
replaceHostRoutesForRepo(repoPath, routes);
|
|
9748
|
-
return { routes, tlsRefreshed: tlsCoverage.refreshed };
|
|
9749
|
-
}
|
|
9750
|
-
var init_route_publication = __esm({
|
|
9751
|
-
"src/core/route-publication.ts"() {
|
|
9752
|
-
"use strict";
|
|
9753
|
-
init_docker();
|
|
9754
|
-
init_host_routes();
|
|
9755
|
-
init_router();
|
|
9756
|
-
init_tls();
|
|
9757
10282
|
}
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
10283
|
+
let value;
|
|
10284
|
+
try {
|
|
10285
|
+
value = JSON.parse(result.stdout);
|
|
10286
|
+
} catch {
|
|
10287
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
|
|
10288
|
+
}
|
|
10289
|
+
if (!Array.isArray(value)) {
|
|
10290
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
10291
|
+
}
|
|
10292
|
+
return {
|
|
10293
|
+
ok: true,
|
|
10294
|
+
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
10295
|
+
names: new Set(
|
|
10296
|
+
value.flatMap((item) => {
|
|
10297
|
+
if (!item || typeof item !== "object") return [];
|
|
10298
|
+
const name = item.name;
|
|
10299
|
+
return typeof name === "string" ? [name] : [];
|
|
10300
|
+
})
|
|
10301
|
+
)
|
|
10302
|
+
};
|
|
10303
|
+
}
|
|
10304
|
+
function isRecord3(value) {
|
|
10305
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9763
10306
|
}
|
|
9764
|
-
function
|
|
10307
|
+
function inspectNamedRouterApi(protocol, resource, name) {
|
|
9765
10308
|
const result = (0, import_node_child_process20.spawnSync)(
|
|
9766
10309
|
"curl",
|
|
9767
10310
|
[
|
|
@@ -9769,8 +10312,8 @@ function inspectRouterApi(protocol) {
|
|
|
9769
10312
|
"--show-error",
|
|
9770
10313
|
"--fail",
|
|
9771
10314
|
"--max-time",
|
|
9772
|
-
|
|
9773
|
-
`${ROUTER_API_BASE}/${protocol}
|
|
10315
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
10316
|
+
`${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
|
|
9774
10317
|
],
|
|
9775
10318
|
{ encoding: "utf-8" }
|
|
9776
10319
|
);
|
|
@@ -9784,23 +10327,135 @@ function inspectRouterApi(protocol) {
|
|
|
9784
10327
|
try {
|
|
9785
10328
|
value = JSON.parse(result.stdout);
|
|
9786
10329
|
} catch {
|
|
9787
|
-
return {
|
|
10330
|
+
return {
|
|
10331
|
+
ok: false,
|
|
10332
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
10333
|
+
};
|
|
9788
10334
|
}
|
|
9789
|
-
if (!
|
|
9790
|
-
return {
|
|
10335
|
+
if (!isRecord3(value)) {
|
|
10336
|
+
return {
|
|
10337
|
+
ok: false,
|
|
10338
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
10339
|
+
};
|
|
10340
|
+
}
|
|
10341
|
+
return { ok: true, value };
|
|
10342
|
+
}
|
|
10343
|
+
function renderExpectedRoute(route) {
|
|
10344
|
+
const protocol = route.protocol ?? "http";
|
|
10345
|
+
const routerName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
10346
|
+
const routeState = {
|
|
10347
|
+
id: buildHostRouteId(route.repoPath, route.name),
|
|
10348
|
+
name: route.name,
|
|
10349
|
+
host: route.host,
|
|
10350
|
+
protocol,
|
|
10351
|
+
tcpProtocol: route.tcpProtocol,
|
|
10352
|
+
repoPath: route.repoPath,
|
|
10353
|
+
port: route.port,
|
|
10354
|
+
mode: route.mode,
|
|
10355
|
+
upstreamHost: route.upstreamHost,
|
|
10356
|
+
pid: route.pid,
|
|
10357
|
+
command: route.command,
|
|
10358
|
+
workspace: route.workspace,
|
|
10359
|
+
createdAt: "",
|
|
10360
|
+
updatedAt: ""
|
|
10361
|
+
};
|
|
10362
|
+
const document = buildHostRoutesDocument([routeState], false);
|
|
10363
|
+
const protocolDocument = document[protocol];
|
|
10364
|
+
if (!isRecord3(protocolDocument)) return void 0;
|
|
10365
|
+
const routers = protocolDocument.routers;
|
|
10366
|
+
const services = protocolDocument.services;
|
|
10367
|
+
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
10368
|
+
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
10369
|
+
const router = routers[routerKey];
|
|
10370
|
+
const service = services[routerKey];
|
|
10371
|
+
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
10372
|
+
const rule = router.rule;
|
|
10373
|
+
const serviceReference = router.service;
|
|
10374
|
+
const loadBalancer = service.loadBalancer;
|
|
10375
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10376
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
10377
|
+
return void 0;
|
|
9791
10378
|
}
|
|
10379
|
+
const serverField = protocol === "http" ? "url" : "address";
|
|
10380
|
+
const server = servers[0][serverField];
|
|
10381
|
+
if (typeof server !== "string") return void 0;
|
|
10382
|
+
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
10383
|
+
if (!provider.startsWith("@")) return void 0;
|
|
9792
10384
|
return {
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
10385
|
+
protocol,
|
|
10386
|
+
routerName,
|
|
10387
|
+
serviceKey: routerKey,
|
|
10388
|
+
serviceName: `${routerKey}${provider}`,
|
|
10389
|
+
rule,
|
|
10390
|
+
serverField,
|
|
10391
|
+
server
|
|
10392
|
+
};
|
|
10393
|
+
}
|
|
10394
|
+
function inspectExpectedRouteMatch(route) {
|
|
10395
|
+
const expected = renderExpectedRoute(route);
|
|
10396
|
+
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
10397
|
+
if (!expected) {
|
|
10398
|
+
return {
|
|
10399
|
+
ok: false,
|
|
10400
|
+
routes: [routeName],
|
|
10401
|
+
details: "host-route renderer returned an incomplete route configuration"
|
|
10402
|
+
};
|
|
10403
|
+
}
|
|
10404
|
+
const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
|
|
10405
|
+
if (!routerResult.ok) {
|
|
10406
|
+
return { ok: false, routes: [routeName], details: routerResult.details };
|
|
10407
|
+
}
|
|
10408
|
+
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) {
|
|
10409
|
+
return {
|
|
10410
|
+
ok: false,
|
|
10411
|
+
routes: [routeName],
|
|
10412
|
+
details: "active Traefik router does not match the rendered route"
|
|
10413
|
+
};
|
|
10414
|
+
}
|
|
10415
|
+
const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
|
|
10416
|
+
if (!serviceResult.ok) {
|
|
10417
|
+
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
10418
|
+
}
|
|
10419
|
+
const loadBalancer = serviceResult.value.loadBalancer;
|
|
10420
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10421
|
+
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
10422
|
+
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) {
|
|
10423
|
+
return {
|
|
10424
|
+
ok: false,
|
|
10425
|
+
routes: [routeName],
|
|
10426
|
+
details: "active Traefik service does not match the rendered route"
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
10429
|
+
return { ok: true };
|
|
10430
|
+
}
|
|
10431
|
+
function inspectExpectedRouteMatches(routes) {
|
|
10432
|
+
const mismatched = [];
|
|
10433
|
+
const failures = [];
|
|
10434
|
+
for (const route of routes) {
|
|
10435
|
+
const result = inspectExpectedRouteMatch(route);
|
|
10436
|
+
if (!result.ok) {
|
|
10437
|
+
mismatched.push(...result.routes);
|
|
10438
|
+
failures.push(result.details);
|
|
10439
|
+
}
|
|
10440
|
+
}
|
|
10441
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
10442
|
+
ok: false,
|
|
10443
|
+
routes: mismatched,
|
|
10444
|
+
details: failures.join("; ")
|
|
9802
10445
|
};
|
|
9803
10446
|
}
|
|
10447
|
+
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
10448
|
+
const deadline = Date.now() + timeoutMs;
|
|
10449
|
+
let result;
|
|
10450
|
+
do {
|
|
10451
|
+
result = inspectExpectedRouteMatches(routes);
|
|
10452
|
+
if (result.ok) return result;
|
|
10453
|
+
if (Date.now() < deadline) {
|
|
10454
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
10455
|
+
}
|
|
10456
|
+
} while (Date.now() < deadline);
|
|
10457
|
+
return result;
|
|
10458
|
+
}
|
|
9804
10459
|
function inspectExpectedRoutes(routes, expectation) {
|
|
9805
10460
|
const mismatched = [];
|
|
9806
10461
|
const failures = [];
|
|
@@ -9857,7 +10512,11 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9857
10512
|
pollIntervalMs
|
|
9858
10513
|
);
|
|
9859
10514
|
if (initial.ok) return { restarted: false };
|
|
9860
|
-
|
|
10515
|
+
if (options.allowRestart === false)
|
|
10516
|
+
throw new Error(
|
|
10517
|
+
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
10518
|
+
);
|
|
10519
|
+
import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
9861
10520
|
return withFileLock(
|
|
9862
10521
|
ROUTER_RELOAD_LOCK_FILE,
|
|
9863
10522
|
{
|
|
@@ -9887,21 +10546,33 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
9887
10546
|
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
9888
10547
|
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
9889
10548
|
}
|
|
10549
|
+
async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
10550
|
+
if (routes.length === 0) return;
|
|
10551
|
+
const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
10552
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
10553
|
+
const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
|
|
10554
|
+
if (!result.ok) {
|
|
10555
|
+
throw new Error(
|
|
10556
|
+
`Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
|
|
10557
|
+
);
|
|
10558
|
+
}
|
|
10559
|
+
}
|
|
9890
10560
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
9891
10561
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
9892
10562
|
}
|
|
9893
|
-
var import_node_child_process20,
|
|
10563
|
+
var import_node_child_process20, import_node_fs25, import_node_path26, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
|
|
9894
10564
|
var init_traefik_route_health = __esm({
|
|
9895
10565
|
"src/core/traefik-route-health.ts"() {
|
|
9896
10566
|
"use strict";
|
|
9897
10567
|
import_node_child_process20 = require("child_process");
|
|
9898
|
-
|
|
9899
|
-
|
|
10568
|
+
import_node_fs25 = __toESM(require("fs"));
|
|
10569
|
+
import_node_path26 = __toESM(require("path"));
|
|
9900
10570
|
init_file_lock();
|
|
9901
10571
|
init_host_routes();
|
|
9902
10572
|
init_router();
|
|
9903
10573
|
ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
9904
|
-
ROUTER_RELOAD_LOCK_FILE =
|
|
10574
|
+
ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
10575
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
9905
10576
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
9906
10577
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
9907
10578
|
DEFAULT_POLL_INTERVAL_MS = 250;
|
|
@@ -9912,11 +10583,11 @@ var init_traefik_route_health = __esm({
|
|
|
9912
10583
|
// src/core/workspace-ensure.ts
|
|
9913
10584
|
function assertOverlay(container, repoPath) {
|
|
9914
10585
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
9915
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
10586
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
|
|
9916
10587
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
9917
10588
|
}
|
|
9918
10589
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
9919
|
-
const expectedOverlay =
|
|
10590
|
+
const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
9920
10591
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
9921
10592
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
9922
10593
|
}
|
|
@@ -10106,7 +10777,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
10106
10777
|
}).some((container) => {
|
|
10107
10778
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
10108
10779
|
return Boolean(
|
|
10109
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
10780
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
|
|
10110
10781
|
);
|
|
10111
10782
|
});
|
|
10112
10783
|
} catch {
|
|
@@ -10165,7 +10836,7 @@ function validateWorkspaceContainers(containers, options) {
|
|
|
10165
10836
|
if (options.target.kind === "linked") {
|
|
10166
10837
|
assertOverlay(appContainer, options.repoPath);
|
|
10167
10838
|
}
|
|
10168
|
-
assertReady(appContainer, "Workspace app");
|
|
10839
|
+
if (!options.allowStopped) assertReady(appContainer, "Workspace app");
|
|
10169
10840
|
if (options.target.kind === "linked") {
|
|
10170
10841
|
const gitCommonDir = options.target.gitCommonDir;
|
|
10171
10842
|
const gitMount = appContainer.mounts.find(
|
|
@@ -10317,6 +10988,142 @@ function resolveLinkedTarget(repoPath) {
|
|
|
10317
10988
|
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
10318
10989
|
};
|
|
10319
10990
|
}
|
|
10991
|
+
function resolveRepairTarget(repoPath, linked) {
|
|
10992
|
+
const workspace = linked ? readPersistedWorkspace(repoPath) : void 0;
|
|
10993
|
+
if (linked && !workspace) throw new Error("Repair requires an existing workspace identity.");
|
|
10994
|
+
const state = readManagedRuntimeState(repoPath, workspace);
|
|
10995
|
+
if (state?.status !== "degraded") {
|
|
10996
|
+
throw new Error("Repair requires a persisted degraded managed runtime.");
|
|
10997
|
+
}
|
|
10998
|
+
resetWorkspaceRuntimeCaches();
|
|
10999
|
+
const snapshots = getWorkspaceRegistrySnapshots();
|
|
11000
|
+
if (snapshots.unavailable.length) throw new Error("Repair cannot read every provider registry.");
|
|
11001
|
+
const matches = [...snapshots.devpod ?? [], ...snapshots.devsy ?? []].filter(
|
|
11002
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
11003
|
+
);
|
|
11004
|
+
if (matches.length !== 1 || matches[0].id !== state.devpodId || !sameWorkspacePath(matches[0].source.localFolder, repoPath)) {
|
|
11005
|
+
throw new Error("Repair requires one exact retained provider registration.");
|
|
11006
|
+
}
|
|
11007
|
+
const selected = snapshots[resolveWorkspaceRuntimeOrDefault(repoPath)] ?? [];
|
|
11008
|
+
if (!selected.some(
|
|
11009
|
+
(entry) => entry.id === state.devpodId && sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
11010
|
+
)) {
|
|
11011
|
+
throw new Error("Repair provider selection does not own the retained workspace.");
|
|
11012
|
+
}
|
|
11013
|
+
if (workspace) {
|
|
11014
|
+
const owner = readWorkspaceOwnership(repoPath, workspace);
|
|
11015
|
+
if (!owner || owner.devpodId !== state.devpodId || !sameWorkspacePath(owner.worktreePath, repoPath)) {
|
|
11016
|
+
throw new Error("Repair requires the existing exact workspace owner record.");
|
|
11017
|
+
}
|
|
11018
|
+
return {
|
|
11019
|
+
kind: "linked",
|
|
11020
|
+
workspace,
|
|
11021
|
+
devpodId: state.devpodId,
|
|
11022
|
+
hadExactDevpod: true,
|
|
11023
|
+
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
11024
|
+
};
|
|
11025
|
+
}
|
|
11026
|
+
return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
|
|
11027
|
+
}
|
|
11028
|
+
function assertRepairBaseline(options) {
|
|
11029
|
+
const { repoPath, target, state, plan, config, profile, processes } = options;
|
|
11030
|
+
const desired = {
|
|
11031
|
+
apps: proxyAppsFromConfig(config).map((app) => app.name),
|
|
11032
|
+
services: plan.desiredProfileServices,
|
|
11033
|
+
processes
|
|
11034
|
+
};
|
|
11035
|
+
if (state.profile !== profile || state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || Object.keys(desired).some((key) => {
|
|
11036
|
+
const resource = key;
|
|
11037
|
+
return JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort());
|
|
11038
|
+
}) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
|
|
11039
|
+
throw new Error(
|
|
11040
|
+
"Repair requires the recorded profile, resource sets, and unchanged managed configuration."
|
|
11041
|
+
);
|
|
11042
|
+
}
|
|
11043
|
+
if (options.routes.some(
|
|
11044
|
+
(route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
|
|
11045
|
+
)) {
|
|
11046
|
+
throw new Error("Repair refuses unexpected active routes.");
|
|
11047
|
+
}
|
|
11048
|
+
const containers = inspectWorkspaceContainers();
|
|
11049
|
+
const retained = plan.desiredServices.map((service) => {
|
|
11050
|
+
const matches = exactWorkspaceServiceContainers(
|
|
11051
|
+
containers,
|
|
11052
|
+
repoPath,
|
|
11053
|
+
state.composeProject,
|
|
11054
|
+
service,
|
|
11055
|
+
plan.composeFiles
|
|
11056
|
+
);
|
|
11057
|
+
if (matches.length !== 1)
|
|
11058
|
+
throw new Error(`Repair requires one retained container for '${service}'.`);
|
|
11059
|
+
return matches[0];
|
|
11060
|
+
});
|
|
11061
|
+
const primary = retained.find(
|
|
11062
|
+
(c) => c.labels["com.docker.compose.service"] === plan.primaryService
|
|
11063
|
+
);
|
|
11064
|
+
if (!primary) throw new Error("Repair cannot identify the retained primary container.");
|
|
11065
|
+
if (!primary.state.Running && (options.routes.length > 0 || containers.some(
|
|
11066
|
+
(container) => container.labels["com.docker.compose.project"] === state.composeProject && container.state.Running !== false
|
|
11067
|
+
))) {
|
|
11068
|
+
throw new Error(
|
|
11069
|
+
"Repair of a stopped primary requires all exact project containers stopped and no checkout routes."
|
|
11070
|
+
);
|
|
11071
|
+
}
|
|
11072
|
+
validateWorkspaceContainers(containers, {
|
|
11073
|
+
repoPath,
|
|
11074
|
+
target,
|
|
11075
|
+
upstreamHosts: [],
|
|
11076
|
+
allowStopped: !primary.state.Running
|
|
11077
|
+
});
|
|
11078
|
+
for (const container of containers) {
|
|
11079
|
+
if (container.labels["com.docker.compose.project"] !== state.composeProject) continue;
|
|
11080
|
+
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
11081
|
+
if (typeof container.state.Running !== "boolean" || !hasExactComposeIdentity(container, {
|
|
11082
|
+
repoPath,
|
|
11083
|
+
service,
|
|
11084
|
+
composeProject: state.composeProject,
|
|
11085
|
+
composeFiles: plan.composeFiles
|
|
11086
|
+
}) || container.state.Running && !plan.desiredServices.includes(service)) {
|
|
11087
|
+
throw new Error("Repair refuses foreign or unexpected active Compose resources.");
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
const workspacePath = primary.mounts.find(
|
|
11091
|
+
(m) => m.Type === "bind" && sameWorkspacePath(m.Source, repoPath)
|
|
11092
|
+
)?.Destination;
|
|
11093
|
+
if (!workspacePath) throw new Error("Repair cannot prove the primary workspace mount.");
|
|
11094
|
+
for (const name of primary.state.Running ? config.managedRuntime?.processes ?? [] : []) {
|
|
11095
|
+
if (!processes.includes(name) && runManagedProcessAction({
|
|
11096
|
+
container: { id: primary.id, workspacePath },
|
|
11097
|
+
name,
|
|
11098
|
+
action: "status",
|
|
11099
|
+
quiet: true
|
|
11100
|
+
}) !== "stopped") {
|
|
11101
|
+
throw new Error(`Repair refuses unexpected or unproven process '${name}'.`);
|
|
11102
|
+
}
|
|
11103
|
+
}
|
|
11104
|
+
assertManagedContainerConfigUnchanged({
|
|
11105
|
+
plan,
|
|
11106
|
+
containers: retained,
|
|
11107
|
+
workspace: options.workspace
|
|
11108
|
+
});
|
|
11109
|
+
return retained;
|
|
11110
|
+
}
|
|
11111
|
+
function assertRetainedRepairContainers(repoPath, plan, retained) {
|
|
11112
|
+
const current = inspectWorkspaceContainers();
|
|
11113
|
+
for (const previous of retained) {
|
|
11114
|
+
const service = previous.labels["com.docker.compose.service"] ?? "";
|
|
11115
|
+
const matches = exactWorkspaceServiceContainers(
|
|
11116
|
+
current,
|
|
11117
|
+
repoPath,
|
|
11118
|
+
previous.labels["com.docker.compose.project"] ?? "",
|
|
11119
|
+
service,
|
|
11120
|
+
plan.composeFiles
|
|
11121
|
+
);
|
|
11122
|
+
if (matches.length !== 1 || matches[0].id !== previous.id) {
|
|
11123
|
+
throw new Error(`Repair lost retained container identity for '${service}'.`);
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
}
|
|
10320
11127
|
function resolvePrimaryTarget(repoPath) {
|
|
10321
11128
|
const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
10322
11129
|
return {
|
|
@@ -10327,7 +11134,7 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
10327
11134
|
}
|
|
10328
11135
|
function isPrimaryCheckout(repoPath) {
|
|
10329
11136
|
try {
|
|
10330
|
-
return
|
|
11137
|
+
return import_node_fs26.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
|
|
10331
11138
|
} catch {
|
|
10332
11139
|
return false;
|
|
10333
11140
|
}
|
|
@@ -10358,7 +11165,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10358
11165
|
);
|
|
10359
11166
|
}
|
|
10360
11167
|
}
|
|
10361
|
-
|
|
11168
|
+
const ensureLocked = async () => {
|
|
11169
|
+
let repairMutationStarted = false;
|
|
11170
|
+
let retainedRepairContainers = [];
|
|
10362
11171
|
let environmentStarted = false;
|
|
10363
11172
|
let managedPlan;
|
|
10364
11173
|
let previousManagedState;
|
|
@@ -10376,25 +11185,30 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10376
11185
|
let firstTransitionBaseline;
|
|
10377
11186
|
const routeLoadOptions = {
|
|
10378
11187
|
initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
|
|
10379
|
-
recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4)
|
|
11188
|
+
recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4),
|
|
11189
|
+
...options.repair ? { allowRestart: false } : {}
|
|
10380
11190
|
};
|
|
10381
11191
|
try {
|
|
10382
|
-
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
11192
|
+
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10383
11193
|
let devpodId = target.devpodId;
|
|
10384
11194
|
if (target.kind === "linked") {
|
|
10385
|
-
const overlayPath =
|
|
10386
|
-
if (!
|
|
11195
|
+
const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
11196
|
+
if (!import_node_fs26.default.existsSync(overlayPath)) {
|
|
10387
11197
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
10388
11198
|
}
|
|
10389
11199
|
}
|
|
10390
11200
|
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
10391
11201
|
managedPostStartPlan = managedPostStart;
|
|
11202
|
+
if (options.repair)
|
|
11203
|
+
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10392
11204
|
const runtime = loadRuntimeConfig(
|
|
10393
11205
|
repoPath,
|
|
10394
11206
|
target.kind === "primary" ? "" : target.workspace,
|
|
10395
|
-
options.profile
|
|
11207
|
+
options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
|
|
10396
11208
|
);
|
|
10397
11209
|
const managedRuntime = runtime.config.managedRuntime;
|
|
11210
|
+
if (options.repair && !managedRuntime)
|
|
11211
|
+
throw new Error("Repair requires managedRuntime configuration.");
|
|
10398
11212
|
previousRoutes = listHostRouteState().filter(
|
|
10399
11213
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
10400
11214
|
);
|
|
@@ -10403,12 +11217,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10403
11217
|
const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
|
|
10404
11218
|
if (managedRuntime) {
|
|
10405
11219
|
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
10406
|
-
if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
11220
|
+
if (!options.repair && previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
10407
11221
|
previousManagedState = void 0;
|
|
10408
11222
|
}
|
|
10409
|
-
if (previousManagedState?.status === "degraded") {
|
|
11223
|
+
if (!options.repair && previousManagedState?.status === "degraded") {
|
|
10410
11224
|
throw new Error(
|
|
10411
|
-
"Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
|
|
11225
|
+
"Managed runtime state is degraded; refusing a new profile transition until drift is repaired. Inspect status, then use ensure --repair for the recorded profile."
|
|
10412
11226
|
);
|
|
10413
11227
|
}
|
|
10414
11228
|
if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
|
|
@@ -10453,7 +11267,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10453
11267
|
}
|
|
10454
11268
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
10455
11269
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
10456
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
11270
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
|
|
10457
11271
|
for (const [index, app] of apps.entries()) {
|
|
10458
11272
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
10459
11273
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -10462,7 +11276,20 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10462
11276
|
);
|
|
10463
11277
|
}
|
|
10464
11278
|
}
|
|
10465
|
-
if (managedPlan) {
|
|
11279
|
+
if (options.repair && managedPlan && previousManagedState) {
|
|
11280
|
+
retainedRepairContainers = assertRepairBaseline({
|
|
11281
|
+
repoPath,
|
|
11282
|
+
target,
|
|
11283
|
+
state: previousManagedState,
|
|
11284
|
+
plan: managedPlan,
|
|
11285
|
+
config: runtime.config,
|
|
11286
|
+
profile: runtime.profile,
|
|
11287
|
+
processes: desiredProcesses,
|
|
11288
|
+
routes: previousRoutes,
|
|
11289
|
+
workspace: managedWorkspaceEnv
|
|
11290
|
+
});
|
|
11291
|
+
}
|
|
11292
|
+
if (managedPlan && !options.repair) {
|
|
10466
11293
|
writeManagedDevcontainerConfig(managedPlan);
|
|
10467
11294
|
managedConfigWritten = true;
|
|
10468
11295
|
}
|
|
@@ -10498,7 +11325,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10498
11325
|
let recreated = false;
|
|
10499
11326
|
let container;
|
|
10500
11327
|
try {
|
|
10501
|
-
await startAndProveAttachment();
|
|
11328
|
+
if (!options.repair) await startAndProveAttachment();
|
|
10502
11329
|
} catch (error) {
|
|
10503
11330
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10504
11331
|
throw error;
|
|
@@ -10508,7 +11335,45 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10508
11335
|
}
|
|
10509
11336
|
if (!container) {
|
|
10510
11337
|
try {
|
|
10511
|
-
|
|
11338
|
+
if (options.repair && managedPlan && previousManagedState) {
|
|
11339
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11340
|
+
const primaryService = managedPlan.primaryService;
|
|
11341
|
+
const primary = retainedRepairContainers.find(
|
|
11342
|
+
(entry) => entry.labels["com.docker.compose.service"] === primaryService
|
|
11343
|
+
);
|
|
11344
|
+
if (!primary) throw new Error("Repair lost its retained primary container.");
|
|
11345
|
+
const stopped = retainedRepairContainers.filter((entry) => !entry.state.Running);
|
|
11346
|
+
for (const entry of stopped) {
|
|
11347
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(entry.id))
|
|
11348
|
+
throw new Error("Repair found an invalid retained container ID.");
|
|
11349
|
+
}
|
|
11350
|
+
if (stopped.length > 0) {
|
|
11351
|
+
repairMutationStarted = true;
|
|
11352
|
+
transitionPhase = "service-start";
|
|
11353
|
+
const result = (0, import_node_child_process21.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
11354
|
+
encoding: "utf-8",
|
|
11355
|
+
timeout: DEFAULT_READINESS_TIMEOUT_MS,
|
|
11356
|
+
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit"
|
|
11357
|
+
});
|
|
11358
|
+
if (result.status !== 0)
|
|
11359
|
+
throw new Error("Could not start retained repair containers.");
|
|
11360
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11361
|
+
}
|
|
11362
|
+
await waitForManagedServices(
|
|
11363
|
+
managedPlan,
|
|
11364
|
+
repoPath,
|
|
11365
|
+
previousManagedState.composeProject,
|
|
11366
|
+
managedPlan.desiredServices,
|
|
11367
|
+
primary.id,
|
|
11368
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
11369
|
+
);
|
|
11370
|
+
}
|
|
11371
|
+
container = options.repair ? await waitForContainerPreflight(
|
|
11372
|
+
repoPath,
|
|
11373
|
+
currentTarget(),
|
|
11374
|
+
[],
|
|
11375
|
+
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
11376
|
+
) : await preflight(0);
|
|
10512
11377
|
} catch (error) {
|
|
10513
11378
|
if (managedPlan || !target.hadExactDevpod) {
|
|
10514
11379
|
throw error;
|
|
@@ -10549,13 +11414,18 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10549
11414
|
}
|
|
10550
11415
|
if (!matches[0]?.state.Running) missingServices.push(service);
|
|
10551
11416
|
}
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
11417
|
+
if (options.repair) {
|
|
11418
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11419
|
+
repairMutationStarted = true;
|
|
11420
|
+
}
|
|
11421
|
+
if (!options.repair)
|
|
11422
|
+
startExactManagedServices({
|
|
11423
|
+
plan: managedPlan,
|
|
11424
|
+
composeProject: managedComposeProject,
|
|
11425
|
+
services: missingServices,
|
|
11426
|
+
quiet: options.quiet,
|
|
11427
|
+
workspace: managedWorkspaceEnv
|
|
11428
|
+
});
|
|
10559
11429
|
await waitForManagedServices(
|
|
10560
11430
|
managedPlan,
|
|
10561
11431
|
repoPath,
|
|
@@ -10567,6 +11437,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10567
11437
|
container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
10568
11438
|
managedContainer = container;
|
|
10569
11439
|
}
|
|
11440
|
+
if (options.repair && managedPlan)
|
|
11441
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
10570
11442
|
transitionPhase = "process-start";
|
|
10571
11443
|
runManagedPostStart({
|
|
10572
11444
|
plan: managedPostStart,
|
|
@@ -10580,6 +11452,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10580
11452
|
const desiredProcessSet = new Set(desiredProcesses);
|
|
10581
11453
|
for (const process2 of processRegistry) {
|
|
10582
11454
|
if (desiredProcessSet.has(process2)) continue;
|
|
11455
|
+
if (options.repair) {
|
|
11456
|
+
if (runManagedProcessAction({
|
|
11457
|
+
container,
|
|
11458
|
+
name: process2,
|
|
11459
|
+
action: "status",
|
|
11460
|
+
quiet: options.quiet
|
|
11461
|
+
}) !== "stopped") {
|
|
11462
|
+
throw new Error(`Repair found unexpected process '${process2}' after adapter replay.`);
|
|
11463
|
+
}
|
|
11464
|
+
continue;
|
|
11465
|
+
}
|
|
10583
11466
|
runManagedProcessAction({
|
|
10584
11467
|
container,
|
|
10585
11468
|
name: process2,
|
|
@@ -10599,7 +11482,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10599
11482
|
}
|
|
10600
11483
|
}
|
|
10601
11484
|
transitionPhase = "service-stop";
|
|
10602
|
-
|
|
11485
|
+
if (!options.repair)
|
|
11486
|
+
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
10603
11487
|
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
10604
11488
|
await waitForManagedServices(
|
|
10605
11489
|
managedPlan,
|
|
@@ -10614,11 +11498,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10614
11498
|
const publication = await replacePublishedProxyRoutes(
|
|
10615
11499
|
repoPath,
|
|
10616
11500
|
runtime.config,
|
|
10617
|
-
target.workspace
|
|
11501
|
+
target.workspace,
|
|
11502
|
+
options.repair ? { prepareInfrastructure: false } : void 0
|
|
10618
11503
|
);
|
|
10619
11504
|
candidateRoutes = publication.routes;
|
|
10620
11505
|
candidateRoutesPublished = true;
|
|
10621
|
-
await
|
|
11506
|
+
if (options.repair) await ensureTraefikRoutesMatch(publication.routes, routeLoadOptions);
|
|
11507
|
+
else await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
|
|
10622
11508
|
await ensureTraefikRoutesRemoved(
|
|
10623
11509
|
removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
|
|
10624
11510
|
routeLoadOptions
|
|
@@ -10665,8 +11551,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10665
11551
|
if (!devpodId) {
|
|
10666
11552
|
throw new Error(`DevPod id for '${repoPath}' was not resolved after startup.`);
|
|
10667
11553
|
}
|
|
11554
|
+
let candidateState;
|
|
10668
11555
|
if (managedPlan && managedComposeProject) {
|
|
10669
|
-
|
|
11556
|
+
candidateState = {
|
|
10670
11557
|
version: 1,
|
|
10671
11558
|
repoPath,
|
|
10672
11559
|
...target.workspace !== void 0 ? { workspace: target.workspace } : {},
|
|
@@ -10682,15 +11569,24 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10682
11569
|
effectiveConfigSha256: managedPlan.effectiveConfigSha256,
|
|
10683
11570
|
status: "ready",
|
|
10684
11571
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10685
|
-
}
|
|
11572
|
+
};
|
|
11573
|
+
if (!options.repair) writeManagedRuntimeState(candidateState);
|
|
10686
11574
|
}
|
|
10687
11575
|
const managedRuntimeStatus = managedPlan ? collectManagedRuntimeStatus({
|
|
10688
11576
|
repoPath,
|
|
10689
11577
|
workspace: target.workspace,
|
|
10690
11578
|
config: runtime.config,
|
|
10691
11579
|
profile: runtime.profile,
|
|
10692
|
-
resolvedProfile: runtime.resolvedProfile
|
|
11580
|
+
resolvedProfile: runtime.resolvedProfile,
|
|
11581
|
+
...options.repair ? { candidateState } : {}
|
|
10693
11582
|
}) : void 0;
|
|
11583
|
+
if (options.repair && candidateState && managedPlan) {
|
|
11584
|
+
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
11585
|
+
resolveRepairTarget(repoPath, linked);
|
|
11586
|
+
if (managedRuntimeStatus?.status !== "ready")
|
|
11587
|
+
throw new Error("Repaired candidate runtime did not pass final readiness validation.");
|
|
11588
|
+
writeManagedRuntimeState(candidateState);
|
|
11589
|
+
}
|
|
10694
11590
|
return {
|
|
10695
11591
|
kind: target.kind,
|
|
10696
11592
|
repoPath,
|
|
@@ -10703,6 +11599,37 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10703
11599
|
...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
|
|
10704
11600
|
};
|
|
10705
11601
|
} catch (error) {
|
|
11602
|
+
if (options.repair && !repairMutationStarted) throw error;
|
|
11603
|
+
if (options.repair && repairMutationStarted && previousManagedState && managedPlan) {
|
|
11604
|
+
const failures = [];
|
|
11605
|
+
if (transitionPhase === "route-publication") {
|
|
11606
|
+
try {
|
|
11607
|
+
const currentRoutes = listHostRouteState().filter((route) => sameWorkspacePath(route.repoPath, repoPath)).map(routeInputFromState);
|
|
11608
|
+
const rollbackRoutes = previousRoutes.map(routeInputFromState);
|
|
11609
|
+
replaceHostRoutesForRepo(repoPath, rollbackRoutes);
|
|
11610
|
+
await ensureTraefikRoutesMatch(rollbackRoutes, routeLoadOptions);
|
|
11611
|
+
await ensureTraefikRoutesRemoved(
|
|
11612
|
+
removedRoutesForReplacement(currentRoutes, rollbackRoutes),
|
|
11613
|
+
routeLoadOptions
|
|
11614
|
+
);
|
|
11615
|
+
} catch (rollbackError) {
|
|
11616
|
+
failures.push(
|
|
11617
|
+
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
11618
|
+
);
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
try {
|
|
11622
|
+
markManagedRuntimeDegraded(previousManagedState, transitionPhase);
|
|
11623
|
+
} catch (stateError) {
|
|
11624
|
+
failures.push(
|
|
11625
|
+
`state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
|
|
11626
|
+
);
|
|
11627
|
+
}
|
|
11628
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
11629
|
+
throw new Error(
|
|
11630
|
+
`${original} Repair failed; owned resources may remain running.${failures.length ? ` Recovery errors: ${failures.join("; ")}.` : " State remains degraded."}`
|
|
11631
|
+
);
|
|
11632
|
+
}
|
|
10706
11633
|
if (managedPlan && managedContainer && managedComposeProject) {
|
|
10707
11634
|
const rollbackErrors = [];
|
|
10708
11635
|
const failedPhase = transitionPhase;
|
|
@@ -10904,19 +11831,36 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10904
11831
|
}
|
|
10905
11832
|
throw error;
|
|
10906
11833
|
}
|
|
11834
|
+
};
|
|
11835
|
+
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11836
|
+
if (!options.repair) return ensureLocked();
|
|
11837
|
+
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
11838
|
+
import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11839
|
+
return withFileLock(
|
|
11840
|
+
import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
11841
|
+
{
|
|
11842
|
+
activity: "Managed runtime repair",
|
|
11843
|
+
target: repoPath,
|
|
11844
|
+
fair: true,
|
|
11845
|
+
waitMs: 18e5,
|
|
11846
|
+
onWait: createStderrWaitReporter("Managed runtime repair", repoPath)
|
|
11847
|
+
},
|
|
11848
|
+
ensureLocked
|
|
11849
|
+
);
|
|
10907
11850
|
});
|
|
10908
11851
|
}
|
|
10909
|
-
var import_node_child_process21,
|
|
11852
|
+
var import_node_child_process21, import_node_fs26, import_node_path27, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
10910
11853
|
var init_workspace_ensure = __esm({
|
|
10911
11854
|
"src/core/workspace-ensure.ts"() {
|
|
10912
11855
|
"use strict";
|
|
10913
11856
|
import_node_child_process21 = require("child_process");
|
|
10914
|
-
|
|
10915
|
-
|
|
11857
|
+
import_node_fs26 = __toESM(require("fs"));
|
|
11858
|
+
import_node_path27 = __toESM(require("path"));
|
|
10916
11859
|
init_devcontainer_profile();
|
|
10917
11860
|
init_devpod_environment();
|
|
10918
11861
|
init_devpod_mutation();
|
|
10919
11862
|
init_devpod_workspaces();
|
|
11863
|
+
init_file_lock();
|
|
10920
11864
|
init_host_routes();
|
|
10921
11865
|
init_http_route_probe();
|
|
10922
11866
|
init_managed_post_start();
|
|
@@ -10964,7 +11908,8 @@ async function runEnsureCommand(options) {
|
|
|
10964
11908
|
const result = await workspaceEnsure(repoPath, {
|
|
10965
11909
|
open: options.open,
|
|
10966
11910
|
quiet: Boolean(options.json),
|
|
10967
|
-
profile: options.profile
|
|
11911
|
+
profile: options.profile,
|
|
11912
|
+
...options.repair ? { repair: true } : {}
|
|
10968
11913
|
});
|
|
10969
11914
|
if (options.json) {
|
|
10970
11915
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -10999,7 +11944,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
10999
11944
|
);
|
|
11000
11945
|
}
|
|
11001
11946
|
function defaultWorktreePath(mainRepo, ws) {
|
|
11002
|
-
return
|
|
11947
|
+
return import_node_path28.default.join(mainRepo, "trees", ws);
|
|
11003
11948
|
}
|
|
11004
11949
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
11005
11950
|
const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
@@ -11007,12 +11952,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
|
11007
11952
|
});
|
|
11008
11953
|
if (ignored.status !== 0) {
|
|
11009
11954
|
throw new Error(
|
|
11010
|
-
`Default worktree root '${
|
|
11955
|
+
`Default worktree root '${import_node_path28.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path28.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
11011
11956
|
);
|
|
11012
11957
|
}
|
|
11013
11958
|
}
|
|
11014
11959
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
11015
|
-
return
|
|
11960
|
+
return import_node_path28.default.join(import_node_path28.default.dirname(mainRepo), `${import_node_path28.default.basename(mainRepo)}-${ws}`);
|
|
11016
11961
|
}
|
|
11017
11962
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
11018
11963
|
const candidates = [
|
|
@@ -11124,7 +12069,7 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
11124
12069
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
11125
12070
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
11126
12071
|
}
|
|
11127
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
12072
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs27.default.existsSync(target.worktreePath)) return;
|
|
11128
12073
|
if (target.worktree.locked) {
|
|
11129
12074
|
throw new Error(
|
|
11130
12075
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
@@ -11164,9 +12109,9 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11164
12109
|
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
11165
12110
|
);
|
|
11166
12111
|
const generatedPath = candidatePaths.find(
|
|
11167
|
-
(candidate) => !
|
|
12112
|
+
(candidate) => !import_node_fs27.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
11168
12113
|
);
|
|
11169
|
-
const selectedPath = opts.path ?
|
|
12114
|
+
const selectedPath = opts.path ? import_node_path28.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
11170
12115
|
if (!selectedPath) {
|
|
11171
12116
|
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
11172
12117
|
}
|
|
@@ -11175,7 +12120,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11175
12120
|
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
11176
12121
|
);
|
|
11177
12122
|
}
|
|
11178
|
-
if (
|
|
12123
|
+
if (import_node_fs27.default.existsSync(selectedPath)) {
|
|
11179
12124
|
const registered = worktrees.find(
|
|
11180
12125
|
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
11181
12126
|
);
|
|
@@ -11269,6 +12214,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
11269
12214
|
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
11270
12215
|
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
11271
12216
|
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
12217
|
+
if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
|
|
12218
|
+
throw new Error(
|
|
12219
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12220
|
+
);
|
|
12221
|
+
}
|
|
11272
12222
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
11273
12223
|
await ensureTraefikRoutesRemoved(routes);
|
|
11274
12224
|
if (!quiet) {
|
|
@@ -11301,7 +12251,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11301
12251
|
opts.quiet
|
|
11302
12252
|
);
|
|
11303
12253
|
if (removeWorktree) {
|
|
11304
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
12254
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath)) {
|
|
11305
12255
|
const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
11306
12256
|
encoding: "utf-8"
|
|
11307
12257
|
});
|
|
@@ -11320,7 +12270,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11320
12270
|
}
|
|
11321
12271
|
return result;
|
|
11322
12272
|
};
|
|
11323
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
12273
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
11324
12274
|
}
|
|
11325
12275
|
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
11326
12276
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
@@ -11355,15 +12305,16 @@ async function workspaceStop(target, opts = {}) {
|
|
|
11355
12305
|
async function workspaceDown(target, opts = {}) {
|
|
11356
12306
|
return runWorkspaceLifecycle("down", target, opts);
|
|
11357
12307
|
}
|
|
11358
|
-
var import_node_child_process22,
|
|
12308
|
+
var import_node_child_process22, import_node_fs27, import_node_path28, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
11359
12309
|
var init_workspace_lifecycle = __esm({
|
|
11360
12310
|
"src/core/workspace-lifecycle.ts"() {
|
|
11361
12311
|
"use strict";
|
|
11362
12312
|
import_node_child_process22 = require("child_process");
|
|
11363
|
-
|
|
11364
|
-
|
|
12313
|
+
import_node_fs27 = __toESM(require("fs"));
|
|
12314
|
+
import_node_path28 = __toESM(require("path"));
|
|
11365
12315
|
init_devpod_mutation();
|
|
11366
12316
|
init_devpod_workspaces();
|
|
12317
|
+
init_managed_runtime_state();
|
|
11367
12318
|
init_repo_config();
|
|
11368
12319
|
init_route_state();
|
|
11369
12320
|
init_traefik_route_health();
|
|
@@ -11415,6 +12366,11 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
11415
12366
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11416
12367
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
11417
12368
|
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
12369
|
+
if (!options.delete && mutation.status === "absent" && readManagedRuntimeState(repoPath, workspace)) {
|
|
12370
|
+
throw new Error(
|
|
12371
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12372
|
+
);
|
|
12373
|
+
}
|
|
11418
12374
|
const removedRoutes = removeHostRoutesWhere(
|
|
11419
12375
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
11420
12376
|
);
|
|
@@ -11436,6 +12392,7 @@ var init_environment_stop = __esm({
|
|
|
11436
12392
|
init_devpod_mutation();
|
|
11437
12393
|
init_devpod_workspaces();
|
|
11438
12394
|
init_host_routes();
|
|
12395
|
+
init_managed_runtime_state();
|
|
11439
12396
|
init_traefik_route_health();
|
|
11440
12397
|
init_workspace();
|
|
11441
12398
|
init_workspace_lifecycle();
|
|
@@ -11933,17 +12890,17 @@ function asRecord3(value) {
|
|
|
11933
12890
|
return value;
|
|
11934
12891
|
}
|
|
11935
12892
|
function readJson(filePath) {
|
|
11936
|
-
if (!
|
|
12893
|
+
if (!import_node_fs28.default.existsSync(filePath)) {
|
|
11937
12894
|
return void 0;
|
|
11938
12895
|
}
|
|
11939
12896
|
try {
|
|
11940
|
-
return JSON.parse(
|
|
12897
|
+
return JSON.parse(import_node_fs28.default.readFileSync(filePath, "utf-8"));
|
|
11941
12898
|
} catch {
|
|
11942
12899
|
return void 0;
|
|
11943
12900
|
}
|
|
11944
12901
|
}
|
|
11945
12902
|
function relative(repoPath, filePath) {
|
|
11946
|
-
return
|
|
12903
|
+
return import_node_path29.default.relative(repoPath, filePath) || ".";
|
|
11947
12904
|
}
|
|
11948
12905
|
function redactEnvAssignments(value) {
|
|
11949
12906
|
return value.replace(
|
|
@@ -11984,7 +12941,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
11984
12941
|
["bun.lock", "bun"]
|
|
11985
12942
|
];
|
|
11986
12943
|
for (const [fileName, name] of lockfiles) {
|
|
11987
|
-
if (
|
|
12944
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
11988
12945
|
return { name, source: fileName };
|
|
11989
12946
|
}
|
|
11990
12947
|
}
|
|
@@ -11999,9 +12956,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
11999
12956
|
if (typeof engines?.node === "string") {
|
|
12000
12957
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
12001
12958
|
}
|
|
12002
|
-
const nvmrc =
|
|
12003
|
-
if (
|
|
12004
|
-
const version =
|
|
12959
|
+
const nvmrc = import_node_path29.default.join(repoPath, ".nvmrc");
|
|
12960
|
+
if (import_node_fs28.default.existsSync(nvmrc)) {
|
|
12961
|
+
const version = import_node_fs28.default.readFileSync(nvmrc, "utf-8").trim();
|
|
12005
12962
|
return { version, source: ".nvmrc" };
|
|
12006
12963
|
}
|
|
12007
12964
|
return void 0;
|
|
@@ -12062,7 +13019,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
12062
13019
|
const files = config.apps.filter(
|
|
12063
13020
|
(app) => app.runtime === "docker"
|
|
12064
13021
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
12065
|
-
(fileName) => !
|
|
13022
|
+
(fileName) => !import_node_path29.default.isAbsolute(fileName) && !import_node_path29.default.normalize(fileName).startsWith("..")
|
|
12066
13023
|
);
|
|
12067
13024
|
return Array.from(new Set(files));
|
|
12068
13025
|
} catch {
|
|
@@ -12080,7 +13037,7 @@ function composeFiles(repoPath) {
|
|
|
12080
13037
|
...configuredComposeFiles(repoPath)
|
|
12081
13038
|
];
|
|
12082
13039
|
return Array.from(new Set(candidates)).filter(
|
|
12083
|
-
(fileName) =>
|
|
13040
|
+
(fileName) => import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))
|
|
12084
13041
|
);
|
|
12085
13042
|
}
|
|
12086
13043
|
function stringArray2(value) {
|
|
@@ -12118,7 +13075,7 @@ function inspectServices(repoPath) {
|
|
|
12118
13075
|
const services = [];
|
|
12119
13076
|
for (const fileName of composeFiles(repoPath)) {
|
|
12120
13077
|
try {
|
|
12121
|
-
const parsed = import_yaml6.default.parse(
|
|
13078
|
+
const parsed = import_yaml6.default.parse(import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8"));
|
|
12122
13079
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
12123
13080
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
12124
13081
|
const service = asRecord3(value);
|
|
@@ -12153,8 +13110,8 @@ function inspectServices(repoPath) {
|
|
|
12153
13110
|
return services;
|
|
12154
13111
|
}
|
|
12155
13112
|
function inspectEnvFiles(repoPath) {
|
|
12156
|
-
const files =
|
|
12157
|
-
const content =
|
|
13113
|
+
const files = import_node_fs28.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
13114
|
+
const content = import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8");
|
|
12158
13115
|
const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
|
|
12159
13116
|
return { path: fileName, names };
|
|
12160
13117
|
});
|
|
@@ -12168,18 +13125,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
12168
13125
|
};
|
|
12169
13126
|
}
|
|
12170
13127
|
function inspectDevcontainer(repoPath) {
|
|
12171
|
-
const dir =
|
|
12172
|
-
if (!
|
|
13128
|
+
const dir = import_node_path29.default.join(repoPath, ".devcontainer");
|
|
13129
|
+
if (!import_node_fs28.default.existsSync(dir)) {
|
|
12173
13130
|
return { exists: false, files: [] };
|
|
12174
13131
|
}
|
|
12175
13132
|
return {
|
|
12176
13133
|
exists: true,
|
|
12177
|
-
files:
|
|
13134
|
+
files: import_node_fs28.default.readdirSync(dir).filter((fileName) => import_node_fs28.default.statSync(import_node_path29.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
12178
13135
|
};
|
|
12179
13136
|
}
|
|
12180
13137
|
function inspectDevrouter(repoPath) {
|
|
12181
13138
|
const configPath = getRepoConfigPath(repoPath);
|
|
12182
|
-
if (!
|
|
13139
|
+
if (!import_node_fs28.default.existsSync(configPath)) {
|
|
12183
13140
|
return {
|
|
12184
13141
|
exists: false,
|
|
12185
13142
|
configPath,
|
|
@@ -12223,15 +13180,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
12223
13180
|
["AGENTS.md", "agents"],
|
|
12224
13181
|
["CLAUDE.md", "claude"]
|
|
12225
13182
|
]) {
|
|
12226
|
-
if (
|
|
13183
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
12227
13184
|
results.push({ path: fileName, kind });
|
|
12228
13185
|
}
|
|
12229
13186
|
}
|
|
12230
|
-
const skillsDir =
|
|
12231
|
-
if (
|
|
12232
|
-
for (const name of
|
|
12233
|
-
const skillPath =
|
|
12234
|
-
if (
|
|
13187
|
+
const skillsDir = import_node_path29.default.join(repoPath, ".agents", "skills");
|
|
13188
|
+
if (import_node_fs28.default.existsSync(skillsDir)) {
|
|
13189
|
+
for (const name of import_node_fs28.default.readdirSync(skillsDir).sort()) {
|
|
13190
|
+
const skillPath = import_node_path29.default.join(skillsDir, name, "SKILL.md");
|
|
13191
|
+
if (import_node_fs28.default.existsSync(skillPath)) {
|
|
12235
13192
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
12236
13193
|
}
|
|
12237
13194
|
}
|
|
@@ -12274,7 +13231,7 @@ function buildIssues(report) {
|
|
|
12274
13231
|
}
|
|
12275
13232
|
function inspectRepo(options = {}) {
|
|
12276
13233
|
const repoPath = resolveRepoPath(options.repo);
|
|
12277
|
-
const pkg = readJson(
|
|
13234
|
+
const pkg = readJson(import_node_path29.default.join(repoPath, "package.json"));
|
|
12278
13235
|
const scripts = inspectScripts(pkg);
|
|
12279
13236
|
const reportWithoutIssues = {
|
|
12280
13237
|
repoPath,
|
|
@@ -12293,12 +13250,12 @@ function inspectRepo(options = {}) {
|
|
|
12293
13250
|
issues: buildIssues(reportWithoutIssues)
|
|
12294
13251
|
};
|
|
12295
13252
|
}
|
|
12296
|
-
var
|
|
13253
|
+
var import_node_fs28, import_node_path29, import_yaml6;
|
|
12297
13254
|
var init_repo_inspect = __esm({
|
|
12298
13255
|
"src/core/repo-inspect.ts"() {
|
|
12299
13256
|
"use strict";
|
|
12300
|
-
|
|
12301
|
-
|
|
13257
|
+
import_node_fs28 = __toESM(require("fs"));
|
|
13258
|
+
import_node_path29 = __toESM(require("path"));
|
|
12302
13259
|
import_yaml6 = __toESM(require("yaml"));
|
|
12303
13260
|
init_repo_config();
|
|
12304
13261
|
}
|
|
@@ -12403,7 +13360,7 @@ function requiredFileChecks(repoPath) {
|
|
|
12403
13360
|
".devcontainer/docker-compose.yml",
|
|
12404
13361
|
".devrouter.yml"
|
|
12405
13362
|
];
|
|
12406
|
-
const missing = required.filter((fileName) => !
|
|
13363
|
+
const missing = required.filter((fileName) => !import_node_fs29.default.existsSync(import_node_path30.default.join(repoPath, fileName)));
|
|
12407
13364
|
return {
|
|
12408
13365
|
id: "repo.devcontainer.verify-files",
|
|
12409
13366
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -12622,12 +13579,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
12622
13579
|
nextSteps: collectNextSteps3(checks)
|
|
12623
13580
|
};
|
|
12624
13581
|
}
|
|
12625
|
-
var
|
|
13582
|
+
var import_node_fs29, import_node_path30;
|
|
12626
13583
|
var init_devcontainer_verify = __esm({
|
|
12627
13584
|
"src/core/devcontainer-verify.ts"() {
|
|
12628
13585
|
"use strict";
|
|
12629
|
-
|
|
12630
|
-
|
|
13586
|
+
import_node_fs29 = __toESM(require("fs"));
|
|
13587
|
+
import_node_path30 = __toESM(require("path"));
|
|
12631
13588
|
init_capabilities();
|
|
12632
13589
|
init_doctor();
|
|
12633
13590
|
init_host_routes();
|
|
@@ -12924,7 +13881,7 @@ function packageManagerIssues(repo) {
|
|
|
12924
13881
|
}
|
|
12925
13882
|
function plannedFiles(repoPath, version) {
|
|
12926
13883
|
const repo = inspectRepo({ repo: repoPath });
|
|
12927
|
-
const projectName = sanitizeProjectName(
|
|
13884
|
+
const projectName = sanitizeProjectName(import_node_path31.default.basename(repo.repoPath));
|
|
12928
13885
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
12929
13886
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
12930
13887
|
const port = inferPort2(repo);
|
|
@@ -12969,8 +13926,8 @@ function plannedFiles(repoPath, version) {
|
|
|
12969
13926
|
};
|
|
12970
13927
|
}
|
|
12971
13928
|
function classifyFile(repoPath, file) {
|
|
12972
|
-
const absolutePath =
|
|
12973
|
-
if (!
|
|
13929
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
13930
|
+
if (!import_node_fs30.default.existsSync(absolutePath)) {
|
|
12974
13931
|
return {
|
|
12975
13932
|
path: file.relativePath,
|
|
12976
13933
|
action: "create",
|
|
@@ -12978,7 +13935,7 @@ function classifyFile(repoPath, file) {
|
|
|
12978
13935
|
bytes: Buffer.byteLength(file.content)
|
|
12979
13936
|
};
|
|
12980
13937
|
}
|
|
12981
|
-
const current =
|
|
13938
|
+
const current = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
|
|
12982
13939
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
12983
13940
|
return {
|
|
12984
13941
|
path: file.relativePath,
|
|
@@ -13035,11 +13992,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
13035
13992
|
};
|
|
13036
13993
|
}
|
|
13037
13994
|
function writeFile(repoPath, file) {
|
|
13038
|
-
const absolutePath =
|
|
13039
|
-
|
|
13040
|
-
|
|
13995
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
13996
|
+
import_node_fs30.default.mkdirSync(import_node_path31.default.dirname(absolutePath), { recursive: true });
|
|
13997
|
+
import_node_fs30.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
13041
13998
|
if (file.executable) {
|
|
13042
|
-
|
|
13999
|
+
import_node_fs30.default.chmodSync(absolutePath, 493);
|
|
13043
14000
|
}
|
|
13044
14001
|
}
|
|
13045
14002
|
function writeDevcontainer(options = {}) {
|
|
@@ -13077,12 +14034,12 @@ function writeDevcontainer(options = {}) {
|
|
|
13077
14034
|
nextSteps: postWriteNextSteps(repoPath)
|
|
13078
14035
|
};
|
|
13079
14036
|
}
|
|
13080
|
-
var
|
|
14037
|
+
var import_node_fs30, import_node_path31, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
13081
14038
|
var init_devcontainer_write = __esm({
|
|
13082
14039
|
"src/core/devcontainer-write.ts"() {
|
|
13083
14040
|
"use strict";
|
|
13084
|
-
|
|
13085
|
-
|
|
14041
|
+
import_node_fs30 = __toESM(require("fs"));
|
|
14042
|
+
import_node_path31 = __toESM(require("path"));
|
|
13086
14043
|
init_repo_config();
|
|
13087
14044
|
init_repo_inspect();
|
|
13088
14045
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -13268,11 +14225,11 @@ var init_profile_resolution = __esm({
|
|
|
13268
14225
|
function fail(message) {
|
|
13269
14226
|
throw new Error(message);
|
|
13270
14227
|
}
|
|
13271
|
-
function
|
|
14228
|
+
function isRecord4(value) {
|
|
13272
14229
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13273
14230
|
}
|
|
13274
14231
|
function requireRecord(value, label) {
|
|
13275
|
-
if (!
|
|
14232
|
+
if (!isRecord4(value)) fail(`${label} must be a mapping.`);
|
|
13276
14233
|
return value;
|
|
13277
14234
|
}
|
|
13278
14235
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -13489,13 +14446,13 @@ function buildProfilePlanReport(options) {
|
|
|
13489
14446
|
return { ...report, contractPath, bindings };
|
|
13490
14447
|
}
|
|
13491
14448
|
function loadContract(repoPath, contractPath) {
|
|
13492
|
-
if (
|
|
14449
|
+
if (import_node_path32.default.isAbsolute(contractPath)) {
|
|
13493
14450
|
fail("profile plan contract path must be relative to the repository.");
|
|
13494
14451
|
}
|
|
13495
14452
|
const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
|
|
13496
14453
|
let metadata;
|
|
13497
14454
|
try {
|
|
13498
|
-
metadata =
|
|
14455
|
+
metadata = import_node_fs31.default.lstatSync(absolutePath);
|
|
13499
14456
|
} catch (error) {
|
|
13500
14457
|
fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
|
|
13501
14458
|
}
|
|
@@ -13505,16 +14462,16 @@ function loadContract(repoPath, contractPath) {
|
|
|
13505
14462
|
if (metadata.size > MAX_CONTRACT_BYTES) {
|
|
13506
14463
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
13507
14464
|
}
|
|
13508
|
-
const realRepo =
|
|
13509
|
-
const realContract =
|
|
14465
|
+
const realRepo = import_node_fs31.default.realpathSync(repoPath);
|
|
14466
|
+
const realContract = import_node_fs31.default.realpathSync(absolutePath);
|
|
13510
14467
|
assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
|
|
13511
|
-
const source =
|
|
14468
|
+
const source = import_node_fs31.default.readFileSync(absolutePath, "utf-8");
|
|
13512
14469
|
if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
|
|
13513
14470
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
13514
14471
|
}
|
|
13515
14472
|
return {
|
|
13516
14473
|
contract: parseProfilePlanContract(source),
|
|
13517
|
-
contractPath:
|
|
14474
|
+
contractPath: import_node_path32.default.relative(repoPath, absolutePath).split(import_node_path32.default.sep).join("/")
|
|
13518
14475
|
};
|
|
13519
14476
|
}
|
|
13520
14477
|
function resolveProfilePlan(options) {
|
|
@@ -13528,12 +14485,12 @@ function resolveProfilePlan(options) {
|
|
|
13528
14485
|
contractPath: loaded.contractPath
|
|
13529
14486
|
});
|
|
13530
14487
|
}
|
|
13531
|
-
var
|
|
14488
|
+
var import_node_fs31, import_node_path32, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
|
|
13532
14489
|
var init_profile_plan = __esm({
|
|
13533
14490
|
"src/core/profile-plan.ts"() {
|
|
13534
14491
|
"use strict";
|
|
13535
|
-
|
|
13536
|
-
|
|
14492
|
+
import_node_fs31 = __toESM(require("fs"));
|
|
14493
|
+
import_node_path32 = __toESM(require("path"));
|
|
13537
14494
|
import_yaml7 = require("yaml");
|
|
13538
14495
|
init_paths();
|
|
13539
14496
|
init_profile_resolution();
|
|
@@ -13565,7 +14522,7 @@ async function runProfileResolveCommand(options) {
|
|
|
13565
14522
|
async function runProfilePlanCommand(options) {
|
|
13566
14523
|
const report = resolveProfilePlan(options);
|
|
13567
14524
|
if (options.output) {
|
|
13568
|
-
writeFileAtomically(
|
|
14525
|
+
writeFileAtomically(import_node_path33.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
|
|
13569
14526
|
`);
|
|
13570
14527
|
}
|
|
13571
14528
|
if (options.json) {
|
|
@@ -13601,14 +14558,14 @@ function printProfilePlanSummary(report, output2) {
|
|
|
13601
14558
|
process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
|
|
13602
14559
|
`);
|
|
13603
14560
|
}
|
|
13604
|
-
if (output2) process.stdout.write(`Output: ${
|
|
14561
|
+
if (output2) process.stdout.write(`Output: ${import_node_path33.default.resolve(output2)}
|
|
13605
14562
|
`);
|
|
13606
14563
|
}
|
|
13607
|
-
var
|
|
14564
|
+
var import_node_path33;
|
|
13608
14565
|
var init_profile = __esm({
|
|
13609
14566
|
"src/commands/profile.ts"() {
|
|
13610
14567
|
"use strict";
|
|
13611
|
-
|
|
14568
|
+
import_node_path33 = __toESM(require("path"));
|
|
13612
14569
|
init_atomic_file();
|
|
13613
14570
|
init_output();
|
|
13614
14571
|
init_profile_plan();
|
|
@@ -13898,7 +14855,7 @@ function sanitizeRouterId(value) {
|
|
|
13898
14855
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
13899
14856
|
}
|
|
13900
14857
|
function repoHash(repoPath) {
|
|
13901
|
-
return (0, import_node_crypto10.createHash)("sha1").update(
|
|
14858
|
+
return (0, import_node_crypto10.createHash)("sha1").update(import_node_path34.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
13902
14859
|
}
|
|
13903
14860
|
function asDockerApp(app) {
|
|
13904
14861
|
return app.runtime === "docker";
|
|
@@ -13977,11 +14934,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
13977
14934
|
if (dockerApps.length === 0) {
|
|
13978
14935
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
13979
14936
|
}
|
|
13980
|
-
const cachePath =
|
|
13981
|
-
|
|
13982
|
-
const overlayPath =
|
|
14937
|
+
const cachePath = import_node_path34.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
14938
|
+
import_node_fs32.default.mkdirSync(cachePath, { recursive: true });
|
|
14939
|
+
const overlayPath = import_node_path34.default.join(cachePath, "compose.devrouter.yml");
|
|
13983
14940
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
13984
|
-
|
|
14941
|
+
import_node_fs32.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
13985
14942
|
return {
|
|
13986
14943
|
overlayPath,
|
|
13987
14944
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -14101,14 +15058,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
14101
15058
|
const port = Number(match[1]);
|
|
14102
15059
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
14103
15060
|
}
|
|
14104
|
-
var import_node_child_process27, import_node_crypto10,
|
|
15061
|
+
var import_node_child_process27, import_node_crypto10, import_node_fs32, import_node_path34, import_yaml8;
|
|
14105
15062
|
var init_docker_run = __esm({
|
|
14106
15063
|
"src/core/docker-run.ts"() {
|
|
14107
15064
|
"use strict";
|
|
14108
15065
|
import_node_child_process27 = require("child_process");
|
|
14109
15066
|
import_node_crypto10 = require("crypto");
|
|
14110
|
-
|
|
14111
|
-
|
|
15067
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
15068
|
+
import_node_path34 = __toESM(require("path"));
|
|
14112
15069
|
import_yaml8 = __toESM(require("yaml"));
|
|
14113
15070
|
init_docker_error_guidance();
|
|
14114
15071
|
init_paths();
|
|
@@ -14930,13 +15887,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14930
15887
|
const startedAt = Date.now();
|
|
14931
15888
|
let rootStat;
|
|
14932
15889
|
try {
|
|
14933
|
-
rootStat =
|
|
15890
|
+
rootStat = import_node_fs33.default.lstatSync(worktreePath);
|
|
14934
15891
|
} catch (error) {
|
|
14935
15892
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
14936
15893
|
}
|
|
14937
15894
|
let rootEntries;
|
|
14938
15895
|
try {
|
|
14939
|
-
rootEntries =
|
|
15896
|
+
rootEntries = import_node_fs33.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
14940
15897
|
} catch (error) {
|
|
14941
15898
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
14942
15899
|
}
|
|
@@ -14962,10 +15919,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14962
15919
|
timedOut = true;
|
|
14963
15920
|
break;
|
|
14964
15921
|
}
|
|
14965
|
-
const entryPath =
|
|
15922
|
+
const entryPath = import_node_path35.default.join(dirPath, entry.name);
|
|
14966
15923
|
let entryStat;
|
|
14967
15924
|
try {
|
|
14968
|
-
entryStat =
|
|
15925
|
+
entryStat = import_node_fs33.default.lstatSync(entryPath);
|
|
14969
15926
|
} catch (error) {
|
|
14970
15927
|
if (error?.code === "ENOENT") continue;
|
|
14971
15928
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -14975,7 +15932,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
14975
15932
|
if (!entryStat.isDirectory()) continue;
|
|
14976
15933
|
let childEntries;
|
|
14977
15934
|
try {
|
|
14978
|
-
childEntries =
|
|
15935
|
+
childEntries = import_node_fs33.default.readdirSync(entryPath, { withFileTypes: true });
|
|
14979
15936
|
} catch (error) {
|
|
14980
15937
|
unreadableReason = describeIncompleteWalk(error);
|
|
14981
15938
|
break;
|
|
@@ -15039,12 +15996,12 @@ function describeError(error, worktreePath) {
|
|
|
15039
15996
|
}
|
|
15040
15997
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
15041
15998
|
}
|
|
15042
|
-
var
|
|
15999
|
+
var import_node_fs33, import_node_path35, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
15043
16000
|
var init_workspace_consumption = __esm({
|
|
15044
16001
|
"src/core/workspace-consumption.ts"() {
|
|
15045
16002
|
"use strict";
|
|
15046
|
-
|
|
15047
|
-
|
|
16003
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
16004
|
+
import_node_path35 = __toESM(require("path"));
|
|
15048
16005
|
init_devpod_environment();
|
|
15049
16006
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
15050
16007
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -15052,7 +16009,7 @@ var init_workspace_consumption = __esm({
|
|
|
15052
16009
|
});
|
|
15053
16010
|
|
|
15054
16011
|
// src/core/workspace-cleanup.ts
|
|
15055
|
-
function
|
|
16012
|
+
function isRecord5(value) {
|
|
15056
16013
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15057
16014
|
}
|
|
15058
16015
|
function isSha(value) {
|
|
@@ -15145,7 +16102,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
15145
16102
|
}
|
|
15146
16103
|
function readGitSnapshot(worktree, commandRunner) {
|
|
15147
16104
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
15148
|
-
if (worktree.prunable || !
|
|
16105
|
+
if (worktree.prunable || !import_node_fs34.default.existsSync(comparablePath)) {
|
|
15149
16106
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
15150
16107
|
}
|
|
15151
16108
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -15196,14 +16153,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
15196
16153
|
if (!Array.isArray(value)) return void 0;
|
|
15197
16154
|
const changes = [];
|
|
15198
16155
|
for (const item of value) {
|
|
15199
|
-
if (!
|
|
15200
|
-
const repository =
|
|
15201
|
-
const headRepository =
|
|
16156
|
+
if (!isRecord5(item)) return void 0;
|
|
16157
|
+
const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
16158
|
+
const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
15202
16159
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
15203
16160
|
continue;
|
|
15204
16161
|
}
|
|
15205
16162
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
15206
|
-
const mergeCommit =
|
|
16163
|
+
const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
15207
16164
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
15208
16165
|
changes.push({
|
|
15209
16166
|
sourceBranch: branch,
|
|
@@ -15220,14 +16177,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
15220
16177
|
if (!Array.isArray(value)) return void 0;
|
|
15221
16178
|
const changes = [];
|
|
15222
16179
|
for (const item of value) {
|
|
15223
|
-
if (!
|
|
16180
|
+
if (!isRecord5(item)) return void 0;
|
|
15224
16181
|
const sourceProjectId = item.source_project_id;
|
|
15225
16182
|
const targetProjectId = item.target_project_id;
|
|
15226
16183
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
15227
16184
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
15228
16185
|
continue;
|
|
15229
16186
|
}
|
|
15230
|
-
const diffRefs =
|
|
16187
|
+
const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
|
|
15231
16188
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
15232
16189
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
15233
16190
|
changes.push({
|
|
@@ -15670,7 +16627,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
15670
16627
|
containers = measureContainersFn(worktreePaths);
|
|
15671
16628
|
} catch (error) {
|
|
15672
16629
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
15673
|
-
containers = new Map(worktreePaths.map((
|
|
16630
|
+
containers = new Map(worktreePaths.map((path35) => [path35, unknownContainers(reason)]));
|
|
15674
16631
|
}
|
|
15675
16632
|
}
|
|
15676
16633
|
const rows = records.map((record) => {
|
|
@@ -15719,12 +16676,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
15719
16676
|
workspaces: rows
|
|
15720
16677
|
};
|
|
15721
16678
|
}
|
|
15722
|
-
var import_node_child_process29,
|
|
16679
|
+
var import_node_child_process29, import_node_fs34, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
15723
16680
|
var init_workspace_cleanup = __esm({
|
|
15724
16681
|
"src/core/workspace-cleanup.ts"() {
|
|
15725
16682
|
"use strict";
|
|
15726
16683
|
import_node_child_process29 = require("child_process");
|
|
15727
|
-
|
|
16684
|
+
import_node_fs34 = __toESM(require("fs"));
|
|
15728
16685
|
init_devpod_workspaces();
|
|
15729
16686
|
init_host_routes();
|
|
15730
16687
|
init_repo_config();
|
|
@@ -15901,7 +16858,7 @@ var init_version = __esm({
|
|
|
15901
16858
|
|
|
15902
16859
|
// src/cli.ts
|
|
15903
16860
|
var import_commander = require("commander");
|
|
15904
|
-
var CLI_VERSION = true ? "0.0.
|
|
16861
|
+
var CLI_VERSION = true ? "0.0.53" : "0.0.0-dev";
|
|
15905
16862
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
15906
16863
|
function withErrorHandling(action2) {
|
|
15907
16864
|
return async (...args) => {
|
|
@@ -15956,13 +16913,14 @@ program.command("up").description("Ensure devnet and start shared Traefik (reser
|
|
|
15956
16913
|
await runUpCommand2();
|
|
15957
16914
|
})
|
|
15958
16915
|
);
|
|
15959
|
-
program.command("ensure").description("Start and prove a primary or linked checkout's environment and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
16916
|
+
program.command("ensure").description("Start and prove a primary or linked checkout's environment and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--repair", "Repair a degraded managed runtime from its recorded profile").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
15960
16917
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
15961
16918
|
const options = command.opts();
|
|
15962
16919
|
const { runEnsureCommand: runEnsureCommand2 } = await Promise.resolve().then(() => (init_ensure(), ensure_exports));
|
|
15963
16920
|
await runEnsureCommand2({
|
|
15964
16921
|
path: repoPath,
|
|
15965
16922
|
profile: options.profile,
|
|
16923
|
+
repair: options.repair,
|
|
15966
16924
|
open: Boolean(options.open),
|
|
15967
16925
|
json: Boolean(options.json)
|
|
15968
16926
|
});
|
|
@@ -16184,7 +17142,7 @@ workspaceCommand2.command("up").description(
|
|
|
16184
17142
|
);
|
|
16185
17143
|
workspaceCommand2.command("ensure").description(
|
|
16186
17144
|
"Start and prove a primary or linked checkout's workspace runtime, upstreams, and routes"
|
|
16187
|
-
).argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
17145
|
+
).argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--repair", "Repair a degraded managed runtime from its recorded profile").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
16188
17146
|
withErrorHandling(
|
|
16189
17147
|
async (worktreePath, _options, command) => {
|
|
16190
17148
|
const options = command.opts();
|
|
@@ -16192,6 +17150,7 @@ workspaceCommand2.command("ensure").description(
|
|
|
16192
17150
|
await runEnsureCommand2({
|
|
16193
17151
|
path: worktreePath,
|
|
16194
17152
|
profile: options.profile,
|
|
17153
|
+
repair: options.repair,
|
|
16195
17154
|
open: Boolean(options.open),
|
|
16196
17155
|
json: Boolean(options.json)
|
|
16197
17156
|
});
|