@devrouter/cli 0.0.52 → 0.0.54
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 +87 -0
- package/dist/devrouter.js +1412 -977
- package/package.json +1 -1
- package/upgrade-prompts/0.0.53.md +42 -0
- package/upgrade-prompts/0.0.54.md +26 -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
|
|
|
@@ -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.54" : "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`.',
|
|
@@ -5742,25 +5746,40 @@ function assertManagedContainerConfigUnchanged(options) {
|
|
|
5742
5746
|
const fileArgs = configFiles.flatMap((file) => ["-f", file]);
|
|
5743
5747
|
const result = (() => {
|
|
5744
5748
|
try {
|
|
5745
|
-
|
|
5749
|
+
const composeArgs = [
|
|
5750
|
+
"compose",
|
|
5751
|
+
"--project-name",
|
|
5752
|
+
composeProject,
|
|
5753
|
+
"--project-directory",
|
|
5754
|
+
workingDirectory
|
|
5755
|
+
];
|
|
5756
|
+
const env = managedComposeEnvironment(options.workspace);
|
|
5757
|
+
const rendered = (0, import_node_child_process7.spawnSync)(
|
|
5746
5758
|
"docker",
|
|
5747
|
-
[
|
|
5748
|
-
"compose",
|
|
5749
|
-
"--project-name",
|
|
5750
|
-
composeProject,
|
|
5751
|
-
"--project-directory",
|
|
5752
|
-
workingDirectory,
|
|
5753
|
-
...fileArgs,
|
|
5754
|
-
"config",
|
|
5755
|
-
"--hash",
|
|
5756
|
-
service
|
|
5757
|
-
],
|
|
5759
|
+
[...composeArgs, ...fileArgs, "config", "--format", "json"],
|
|
5758
5760
|
{
|
|
5759
5761
|
cwd: workingDirectory,
|
|
5760
5762
|
encoding: "utf-8",
|
|
5761
|
-
env
|
|
5763
|
+
env,
|
|
5762
5764
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5763
|
-
timeout: COMPOSE_HASH_TIMEOUT_MS
|
|
5765
|
+
timeout: COMPOSE_HASH_TIMEOUT_MS,
|
|
5766
|
+
maxBuffer: 1024 * 1024
|
|
5767
|
+
}
|
|
5768
|
+
);
|
|
5769
|
+
if (rendered.status !== 0 || rendered.error || !rendered.stdout?.trim()) {
|
|
5770
|
+
throw new Error("Compose configuration rendering failed.");
|
|
5771
|
+
}
|
|
5772
|
+
return (0, import_node_child_process7.spawnSync)(
|
|
5773
|
+
"docker",
|
|
5774
|
+
[...composeArgs, "-f", "-", "config", "--no-interpolate", "--hash", service],
|
|
5775
|
+
{
|
|
5776
|
+
cwd: workingDirectory,
|
|
5777
|
+
encoding: "utf-8",
|
|
5778
|
+
env,
|
|
5779
|
+
input: rendered.stdout,
|
|
5780
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
5781
|
+
timeout: COMPOSE_HASH_TIMEOUT_MS,
|
|
5782
|
+
maxBuffer: 1024 * 1024
|
|
5764
5783
|
}
|
|
5765
5784
|
);
|
|
5766
5785
|
} catch {
|
|
@@ -5814,13 +5833,14 @@ function startExactManagedServices(options) {
|
|
|
5814
5833
|
);
|
|
5815
5834
|
}
|
|
5816
5835
|
}
|
|
5817
|
-
function stopExactManagedService(containerId, service) {
|
|
5836
|
+
function stopExactManagedService(containerId, service, options) {
|
|
5818
5837
|
assertSafeContainerId(containerId);
|
|
5819
5838
|
const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
|
|
5820
5839
|
encoding: "utf-8",
|
|
5821
|
-
stdio: "inherit"
|
|
5840
|
+
stdio: "inherit",
|
|
5841
|
+
...options ? { timeout: options.timeoutMs } : {}
|
|
5822
5842
|
});
|
|
5823
|
-
if (result.status !== 0) {
|
|
5843
|
+
if (result.status !== 0 || result.error) {
|
|
5824
5844
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5825
5845
|
}
|
|
5826
5846
|
}
|
|
@@ -5844,6 +5864,198 @@ var init_devcontainer_profile = __esm({
|
|
|
5844
5864
|
});
|
|
5845
5865
|
|
|
5846
5866
|
// src/core/devpod-environment.ts
|
|
5867
|
+
function isRecord2(value) {
|
|
5868
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5869
|
+
}
|
|
5870
|
+
function assertFullContainerId(value) {
|
|
5871
|
+
if (typeof value !== "string" || !FULL_CONTAINER_ID_PATTERN.test(value)) {
|
|
5872
|
+
throw new Error("Managed stop Docker inspection returned an invalid container id.");
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
function assertUniqueContainerIds(ids) {
|
|
5876
|
+
if (new Set(ids).size !== ids.length) {
|
|
5877
|
+
throw new Error("Managed stop Docker inspection returned duplicate container ids.");
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
function parseDockerLines(stdout) {
|
|
5881
|
+
const lines = stdout.split(/\r?\n/);
|
|
5882
|
+
while (lines.at(-1) === "") lines.pop();
|
|
5883
|
+
if (lines.some((line) => line === "")) {
|
|
5884
|
+
throw new Error("Managed stop Docker inspection returned an empty record.");
|
|
5885
|
+
}
|
|
5886
|
+
return lines;
|
|
5887
|
+
}
|
|
5888
|
+
function runManagedStopDocker(args) {
|
|
5889
|
+
const result = (0, import_node_child_process8.spawnSync)("docker", args, {
|
|
5890
|
+
encoding: "utf-8",
|
|
5891
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
5892
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
5893
|
+
});
|
|
5894
|
+
if (result.error || result.status !== 0) {
|
|
5895
|
+
throw new Error("Managed stop Docker inspection failed.");
|
|
5896
|
+
}
|
|
5897
|
+
if (typeof result.stdout !== "string") {
|
|
5898
|
+
throw new Error("Managed stop Docker inspection returned invalid output.");
|
|
5899
|
+
}
|
|
5900
|
+
return result.stdout;
|
|
5901
|
+
}
|
|
5902
|
+
function listManagedStopContainerIds(composeProject) {
|
|
5903
|
+
const stdout = runManagedStopDocker([
|
|
5904
|
+
"ps",
|
|
5905
|
+
"-a",
|
|
5906
|
+
"--no-trunc",
|
|
5907
|
+
"--filter",
|
|
5908
|
+
`label=${MANAGED_STOP_COMPOSE_LABEL}=${composeProject}`,
|
|
5909
|
+
"--format",
|
|
5910
|
+
"{{.ID}}"
|
|
5911
|
+
]);
|
|
5912
|
+
const ids = parseDockerLines(stdout);
|
|
5913
|
+
ids.forEach(assertFullContainerId);
|
|
5914
|
+
assertUniqueContainerIds(ids);
|
|
5915
|
+
return ids;
|
|
5916
|
+
}
|
|
5917
|
+
function assertString(value) {
|
|
5918
|
+
if (typeof value !== "string") {
|
|
5919
|
+
throw new Error("Managed stop Docker inspection returned an invalid field.");
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
function validateManagedStopSnapshot(value, composeProject) {
|
|
5923
|
+
if (!isRecord2(value)) {
|
|
5924
|
+
throw new Error("Managed stop Docker inspection returned an invalid record.");
|
|
5925
|
+
}
|
|
5926
|
+
assertFullContainerId(value.id);
|
|
5927
|
+
if (!isRecord2(value.state)) {
|
|
5928
|
+
throw new Error("Managed stop Docker inspection returned an invalid state.");
|
|
5929
|
+
}
|
|
5930
|
+
const status = value.state.Status;
|
|
5931
|
+
if (status !== "running" && status !== "exited" && status !== "created") {
|
|
5932
|
+
throw new Error("Managed stop Docker inspection returned an invalid state status.");
|
|
5933
|
+
}
|
|
5934
|
+
if (typeof value.state.Running !== "boolean") {
|
|
5935
|
+
throw new Error("Managed stop Docker inspection returned an invalid running flag.");
|
|
5936
|
+
}
|
|
5937
|
+
if (typeof value.state.Paused !== "boolean" || value.state.Paused) {
|
|
5938
|
+
throw new Error("Managed stop Docker inspection returned an invalid paused flag.");
|
|
5939
|
+
}
|
|
5940
|
+
if (typeof value.state.Restarting !== "boolean" || value.state.Restarting) {
|
|
5941
|
+
throw new Error("Managed stop Docker inspection returned an invalid restarting flag.");
|
|
5942
|
+
}
|
|
5943
|
+
if (typeof value.state.Dead !== "boolean" || value.state.Dead) {
|
|
5944
|
+
throw new Error("Managed stop Docker inspection returned an invalid dead flag.");
|
|
5945
|
+
}
|
|
5946
|
+
if (status === "running" !== value.state.Running) {
|
|
5947
|
+
throw new Error("Managed stop Docker inspection returned contradictory state.");
|
|
5948
|
+
}
|
|
5949
|
+
let health;
|
|
5950
|
+
if (value.state.Health !== void 0 && value.state.Health !== null) {
|
|
5951
|
+
if (!isRecord2(value.state.Health)) {
|
|
5952
|
+
throw new Error("Managed stop Docker inspection returned an invalid health field.");
|
|
5953
|
+
}
|
|
5954
|
+
assertString(value.state.Health.Status);
|
|
5955
|
+
health = { Status: value.state.Health.Status };
|
|
5956
|
+
}
|
|
5957
|
+
if (!isRecord2(value.labels)) {
|
|
5958
|
+
throw new Error("Managed stop Docker inspection returned invalid labels.");
|
|
5959
|
+
}
|
|
5960
|
+
for (const label of MANAGED_STOP_IDENTITY_LABELS) {
|
|
5961
|
+
assertString(value.labels[label]);
|
|
5962
|
+
}
|
|
5963
|
+
if (value.labels[MANAGED_STOP_COMPOSE_LABEL] !== composeProject) {
|
|
5964
|
+
throw new Error("Managed stop Docker inspection returned a foreign Compose project.");
|
|
5965
|
+
}
|
|
5966
|
+
if (!Array.isArray(value.mounts)) {
|
|
5967
|
+
throw new Error("Managed stop Docker inspection returned invalid mounts.");
|
|
5968
|
+
}
|
|
5969
|
+
const mounts = value.mounts.map((mount) => {
|
|
5970
|
+
if (!isRecord2(mount)) {
|
|
5971
|
+
throw new Error("Managed stop Docker inspection returned an invalid mount.");
|
|
5972
|
+
}
|
|
5973
|
+
assertString(mount.Type);
|
|
5974
|
+
assertString(mount.Source);
|
|
5975
|
+
assertString(mount.Destination);
|
|
5976
|
+
return {
|
|
5977
|
+
Type: mount.Type,
|
|
5978
|
+
Source: mount.Source,
|
|
5979
|
+
Destination: mount.Destination
|
|
5980
|
+
};
|
|
5981
|
+
});
|
|
5982
|
+
if (!isRecord2(value.networks)) {
|
|
5983
|
+
throw new Error("Managed stop Docker inspection returned invalid networks.");
|
|
5984
|
+
}
|
|
5985
|
+
return {
|
|
5986
|
+
id: value.id,
|
|
5987
|
+
state: {
|
|
5988
|
+
...health ? { Health: health } : {},
|
|
5989
|
+
Status: status,
|
|
5990
|
+
Running: value.state.Running,
|
|
5991
|
+
Paused: false,
|
|
5992
|
+
Restarting: false,
|
|
5993
|
+
Dead: false
|
|
5994
|
+
},
|
|
5995
|
+
labels: {
|
|
5996
|
+
"com.docker.compose.project": value.labels["com.docker.compose.project"],
|
|
5997
|
+
"com.docker.compose.service": value.labels["com.docker.compose.service"],
|
|
5998
|
+
"com.docker.compose.project.working_dir": value.labels["com.docker.compose.project.working_dir"],
|
|
5999
|
+
"com.docker.compose.project.config_files": value.labels["com.docker.compose.project.config_files"],
|
|
6000
|
+
"com.docker.compose.config-hash": value.labels["com.docker.compose.config-hash"]
|
|
6001
|
+
},
|
|
6002
|
+
mounts,
|
|
6003
|
+
networks: {}
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
function inspectManagedStopPopulation(ids, composeProject) {
|
|
6007
|
+
const stdout = runManagedStopDocker([
|
|
6008
|
+
"inspect",
|
|
6009
|
+
"--format",
|
|
6010
|
+
MANAGED_STOP_INSPECT_TEMPLATE,
|
|
6011
|
+
...ids
|
|
6012
|
+
]);
|
|
6013
|
+
const lines = parseDockerLines(stdout);
|
|
6014
|
+
if (lines.length !== ids.length) {
|
|
6015
|
+
throw new Error("Managed stop Docker inspection returned an incomplete population.");
|
|
6016
|
+
}
|
|
6017
|
+
const expected = new Set(ids);
|
|
6018
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6019
|
+
const snapshots = lines.map((line) => {
|
|
6020
|
+
let parsed;
|
|
6021
|
+
try {
|
|
6022
|
+
parsed = JSON.parse(line);
|
|
6023
|
+
} catch {
|
|
6024
|
+
throw new Error("Managed stop Docker inspection returned malformed JSON.");
|
|
6025
|
+
}
|
|
6026
|
+
const snapshot = validateManagedStopSnapshot(parsed, composeProject);
|
|
6027
|
+
if (!expected.has(snapshot.id) || seen.has(snapshot.id)) {
|
|
6028
|
+
throw new Error("Managed stop Docker inspection returned an unexpected population.");
|
|
6029
|
+
}
|
|
6030
|
+
seen.add(snapshot.id);
|
|
6031
|
+
return snapshot;
|
|
6032
|
+
});
|
|
6033
|
+
return snapshots;
|
|
6034
|
+
}
|
|
6035
|
+
function requireSameContainerPopulation(expected, actual) {
|
|
6036
|
+
if (expected.length !== actual.length) {
|
|
6037
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6038
|
+
}
|
|
6039
|
+
const expectedSet = new Set(expected);
|
|
6040
|
+
if (actual.some((id) => !expectedSet.has(id))) {
|
|
6041
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
function inspectManagedStopContainers(composeProject) {
|
|
6045
|
+
if (typeof composeProject !== "string" || composeProject.length === 0 || composeProject !== composeProject.trim() || !MANAGED_STOP_PROJECT_PATTERN.test(composeProject)) {
|
|
6046
|
+
throw new Error("Managed stop Docker inspection requires a safe Compose project.");
|
|
6047
|
+
}
|
|
6048
|
+
const listedIds = listManagedStopContainerIds(composeProject);
|
|
6049
|
+
if (listedIds.length === 0) {
|
|
6050
|
+
const confirmedIds2 = listManagedStopContainerIds(composeProject);
|
|
6051
|
+
requireSameContainerPopulation(listedIds, confirmedIds2);
|
|
6052
|
+
return [];
|
|
6053
|
+
}
|
|
6054
|
+
const snapshots = inspectManagedStopPopulation(listedIds, composeProject);
|
|
6055
|
+
const confirmedIds = listManagedStopContainerIds(composeProject);
|
|
6056
|
+
requireSameContainerPopulation(listedIds, confirmedIds);
|
|
6057
|
+
return snapshots;
|
|
6058
|
+
}
|
|
5847
6059
|
function inspectWorkspaceContainers(options) {
|
|
5848
6060
|
let ids = options?.ids;
|
|
5849
6061
|
if (!ids) {
|
|
@@ -5927,7 +6139,7 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5927
6139
|
}
|
|
5928
6140
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5929
6141
|
}
|
|
5930
|
-
var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
|
|
6142
|
+
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;
|
|
5931
6143
|
var init_devpod_environment = __esm({
|
|
5932
6144
|
"src/core/devpod-environment.ts"() {
|
|
5933
6145
|
"use strict";
|
|
@@ -5939,6 +6151,19 @@ var init_devpod_environment = __esm({
|
|
|
5939
6151
|
/}$/,
|
|
5940
6152
|
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
5941
6153
|
);
|
|
6154
|
+
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":{}}';
|
|
6155
|
+
MANAGED_STOP_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6156
|
+
FULL_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
6157
|
+
MANAGED_STOP_DOCKER_TIMEOUT_MS = 5e3;
|
|
6158
|
+
MANAGED_STOP_DOCKER_MAX_BUFFER = 1024 * 1024;
|
|
6159
|
+
MANAGED_STOP_COMPOSE_LABEL = "com.docker.compose.project";
|
|
6160
|
+
MANAGED_STOP_IDENTITY_LABELS = [
|
|
6161
|
+
"com.docker.compose.project",
|
|
6162
|
+
"com.docker.compose.service",
|
|
6163
|
+
"com.docker.compose.project.working_dir",
|
|
6164
|
+
"com.docker.compose.project.config_files",
|
|
6165
|
+
"com.docker.compose.config-hash"
|
|
6166
|
+
];
|
|
5942
6167
|
}
|
|
5943
6168
|
});
|
|
5944
6169
|
|
|
@@ -6907,7 +7132,8 @@ function listDevsyWorkspaces() {
|
|
|
6907
7132
|
}
|
|
6908
7133
|
const workspace = {
|
|
6909
7134
|
id: candidate.id,
|
|
6910
|
-
source: { localFolder: source.localFolder }
|
|
7135
|
+
source: { localFolder: source.localFolder },
|
|
7136
|
+
...typeof candidate.context === "string" ? { context: candidate.context } : {}
|
|
6911
7137
|
};
|
|
6912
7138
|
if ("lastUsed" in candidate) {
|
|
6913
7139
|
if (typeof candidate.lastUsed === "string") {
|
|
@@ -7586,372 +7812,898 @@ var init_devpod_workspaces = __esm({
|
|
|
7586
7812
|
}
|
|
7587
7813
|
});
|
|
7588
7814
|
|
|
7589
|
-
// src/core/
|
|
7590
|
-
function
|
|
7591
|
-
|
|
7592
|
-
const attached = listDevsyWorkspaces();
|
|
7593
|
-
const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
|
|
7594
|
-
if (!attachedId) return false;
|
|
7595
|
-
return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
|
|
7596
|
-
} catch {
|
|
7597
|
-
return true;
|
|
7598
|
-
}
|
|
7599
|
-
}
|
|
7600
|
-
function withMutationLock(activity, target, operation) {
|
|
7601
|
-
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7602
|
-
return withFileLockSync(
|
|
7603
|
-
DEVSY_MUTATION_LOCK_FILE,
|
|
7604
|
-
{
|
|
7605
|
-
activity,
|
|
7606
|
-
target: `'${target}'`,
|
|
7607
|
-
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7608
|
-
fair: true,
|
|
7609
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7610
|
-
},
|
|
7611
|
-
operation
|
|
7612
|
-
);
|
|
7815
|
+
// src/core/route-publication.ts
|
|
7816
|
+
function routedAppsFromConfig(config) {
|
|
7817
|
+
return config.apps.filter((app) => app.kind !== "dependency");
|
|
7613
7818
|
}
|
|
7614
|
-
function
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
DEVSY_MUTATION_LOCK_FILE,
|
|
7618
|
-
{
|
|
7619
|
-
activity,
|
|
7620
|
-
target: `'${target}'`,
|
|
7621
|
-
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7622
|
-
fair: true,
|
|
7623
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7624
|
-
},
|
|
7625
|
-
operation
|
|
7819
|
+
function configuredProxyAppsFromConfig(config) {
|
|
7820
|
+
return routedAppsFromConfig(config).filter(
|
|
7821
|
+
(app) => app.runtime === "proxy"
|
|
7626
7822
|
);
|
|
7627
7823
|
}
|
|
7628
|
-
function
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
const child = (0, import_node_child_process15.spawn)("devsy", args, {
|
|
7634
|
-
stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
|
|
7635
|
-
env
|
|
7636
|
-
});
|
|
7637
|
-
const stderr = child.stderr;
|
|
7638
|
-
if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
|
|
7639
|
-
let stderrTail = Buffer.alloc(0);
|
|
7640
|
-
let spawnError;
|
|
7641
|
-
stderr.on("data", (chunk) => {
|
|
7642
|
-
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
7643
|
-
const writable = process.stderr.write(value);
|
|
7644
|
-
if (!writable) {
|
|
7645
|
-
stderr.pause();
|
|
7646
|
-
process.stderr.once("drain", () => stderr.resume());
|
|
7647
|
-
}
|
|
7648
|
-
if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
|
|
7649
|
-
stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
|
|
7650
|
-
} else {
|
|
7651
|
-
const combined = Buffer.concat([stderrTail, value]);
|
|
7652
|
-
stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
|
|
7653
|
-
}
|
|
7654
|
-
});
|
|
7655
|
-
child.once("error", (error) => {
|
|
7656
|
-
spawnError = error;
|
|
7657
|
-
});
|
|
7658
|
-
child.once("close", (status) => {
|
|
7659
|
-
resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
|
|
7660
|
-
});
|
|
7661
|
-
});
|
|
7662
|
-
}
|
|
7663
|
-
function runDevsyAction(action2, devsyId, force = false) {
|
|
7664
|
-
const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
|
|
7665
|
-
const result = (0, import_node_child_process15.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
|
|
7666
|
-
if (result.status !== 0) {
|
|
7824
|
+
function proxyAppsFromConfig(config) {
|
|
7825
|
+
const routedApps = routedAppsFromConfig(config);
|
|
7826
|
+
const proxyApps = configuredProxyAppsFromConfig(config);
|
|
7827
|
+
const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
|
|
7828
|
+
if (unsupported.length > 0) {
|
|
7667
7829
|
throw new Error(
|
|
7668
|
-
`
|
|
7830
|
+
`Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
|
|
7669
7831
|
);
|
|
7670
7832
|
}
|
|
7833
|
+
return proxyApps;
|
|
7671
7834
|
}
|
|
7672
|
-
function
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
if (
|
|
7681
|
-
|
|
7682
|
-
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
7683
|
-
if (action2 === "stop" && after.status !== "owned") {
|
|
7684
|
-
throw new Error(
|
|
7685
|
-
`Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
|
|
7686
|
-
);
|
|
7687
|
-
}
|
|
7688
|
-
if (action2 === "delete" && after.status === "owned") {
|
|
7689
|
-
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
7690
|
-
if (runtime !== "not-found") {
|
|
7691
|
-
throw new Error(
|
|
7692
|
-
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
7693
|
-
);
|
|
7694
|
-
}
|
|
7695
|
-
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7696
|
-
if (after.status === "owned") {
|
|
7697
|
-
runDevsyAction("delete", devsyId, true);
|
|
7698
|
-
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7699
|
-
}
|
|
7700
|
-
if (after.status !== "absent") {
|
|
7835
|
+
async function ensureRouteInfrastructure(apps, options = {}) {
|
|
7836
|
+
ensureRouterFiles();
|
|
7837
|
+
await ensureNetwork(DEVNET_NAME);
|
|
7838
|
+
const tlsCoverage = await ensureTLSHostsCovered(
|
|
7839
|
+
apps.map((app) => app.host),
|
|
7840
|
+
options
|
|
7841
|
+
);
|
|
7842
|
+
for (const app of apps) {
|
|
7843
|
+
if (app.protocol === "tcp") {
|
|
7844
|
+
if (!isTLSEnabled()) {
|
|
7701
7845
|
throw new Error(
|
|
7702
|
-
`
|
|
7846
|
+
`TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
7703
7847
|
);
|
|
7704
7848
|
}
|
|
7849
|
+
activateTcpProtocol(app.tcpProtocol);
|
|
7705
7850
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
7710
|
-
return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
|
|
7851
|
+
}
|
|
7852
|
+
startRouterStack();
|
|
7853
|
+
return tlsCoverage;
|
|
7711
7854
|
}
|
|
7712
|
-
function
|
|
7713
|
-
|
|
7855
|
+
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
7856
|
+
const apps = proxyAppsFromConfig(config);
|
|
7857
|
+
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
7858
|
+
const routes = apps.map((app) => {
|
|
7859
|
+
const upstream = parseUpstream(app.upstream);
|
|
7860
|
+
return {
|
|
7861
|
+
name: app.name,
|
|
7862
|
+
host: app.host,
|
|
7863
|
+
protocol: app.protocol,
|
|
7864
|
+
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
7865
|
+
repoPath,
|
|
7866
|
+
port: upstream.port,
|
|
7867
|
+
mode: "proxy",
|
|
7868
|
+
upstreamHost: upstream.upstreamHost,
|
|
7869
|
+
workspace
|
|
7870
|
+
};
|
|
7871
|
+
});
|
|
7872
|
+
replaceHostRoutesForRepo(repoPath, routes);
|
|
7873
|
+
return { routes, tlsRefreshed: tlsCoverage.refreshed };
|
|
7714
7874
|
}
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7875
|
+
var init_route_publication = __esm({
|
|
7876
|
+
"src/core/route-publication.ts"() {
|
|
7877
|
+
"use strict";
|
|
7878
|
+
init_docker();
|
|
7879
|
+
init_host_routes();
|
|
7880
|
+
init_router();
|
|
7881
|
+
init_tls();
|
|
7722
7882
|
}
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7883
|
+
});
|
|
7884
|
+
|
|
7885
|
+
// src/core/workspace-ownership.ts
|
|
7886
|
+
function commandError(command, repoPath, stderr) {
|
|
7887
|
+
return new Error(
|
|
7888
|
+
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
7889
|
+
);
|
|
7890
|
+
}
|
|
7891
|
+
function resolveGitCommonDir(repoPath) {
|
|
7892
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
7893
|
+
encoding: "utf-8",
|
|
7894
|
+
env: READ_ONLY_GIT_ENV
|
|
7895
|
+
});
|
|
7896
|
+
const output2 = result.stdout.trim();
|
|
7897
|
+
if (result.status !== 0 || !output2) {
|
|
7898
|
+
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
7899
|
+
}
|
|
7900
|
+
return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
|
|
7901
|
+
}
|
|
7902
|
+
function resolveGitTopLevel(repoPath) {
|
|
7903
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
7904
|
+
encoding: "utf-8",
|
|
7905
|
+
env: READ_ONLY_GIT_ENV
|
|
7906
|
+
});
|
|
7907
|
+
const output2 = result.stdout.trim();
|
|
7908
|
+
if (result.status !== 0 || !output2) {
|
|
7909
|
+
throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
|
|
7910
|
+
}
|
|
7911
|
+
return comparableWorkspacePath(output2);
|
|
7912
|
+
}
|
|
7913
|
+
function listGitWorktrees(repoPath) {
|
|
7914
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
7915
|
+
encoding: "utf-8",
|
|
7916
|
+
env: READ_ONLY_GIT_ENV
|
|
7917
|
+
});
|
|
7918
|
+
if (result.status !== 0) {
|
|
7919
|
+
throw commandError("git worktree list", repoPath, result.stderr);
|
|
7920
|
+
}
|
|
7921
|
+
const worktrees = [];
|
|
7922
|
+
let current = {};
|
|
7923
|
+
const finish = () => {
|
|
7924
|
+
if (!current.path) return;
|
|
7925
|
+
worktrees.push({
|
|
7926
|
+
path: comparableWorkspacePath(current.path),
|
|
7927
|
+
branch: current.branch,
|
|
7928
|
+
locked: current.locked ?? false,
|
|
7929
|
+
prunable: current.prunable ?? false
|
|
7930
|
+
});
|
|
7931
|
+
current = {};
|
|
7932
|
+
};
|
|
7933
|
+
for (const line of `${result.stdout}
|
|
7934
|
+
`.split("\n")) {
|
|
7935
|
+
if (line.startsWith("worktree ")) {
|
|
7936
|
+
finish();
|
|
7937
|
+
current.path = line.slice("worktree ".length).trim();
|
|
7938
|
+
} else if (line.startsWith("branch ")) {
|
|
7939
|
+
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
7940
|
+
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
7941
|
+
current.locked = true;
|
|
7942
|
+
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
7943
|
+
current.prunable = true;
|
|
7944
|
+
} else if (line === "") {
|
|
7945
|
+
finish();
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
return worktrees;
|
|
7949
|
+
}
|
|
7950
|
+
function ownershipDirectory(repoPath) {
|
|
7951
|
+
return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
7952
|
+
}
|
|
7953
|
+
function validateWorkspace(value, label) {
|
|
7954
|
+
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
7955
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7956
|
+
}
|
|
7957
|
+
return value;
|
|
7958
|
+
}
|
|
7959
|
+
function validateTimestamp(value, label) {
|
|
7960
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
7961
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7962
|
+
}
|
|
7963
|
+
return value;
|
|
7964
|
+
}
|
|
7965
|
+
function validateRecord(value, expectedWorkspace) {
|
|
7966
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7967
|
+
throw new Error("invalid workspace ownership record");
|
|
7968
|
+
}
|
|
7969
|
+
const candidate = value;
|
|
7970
|
+
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
7971
|
+
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
7972
|
+
}
|
|
7973
|
+
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
7974
|
+
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
7975
|
+
throw new Error(
|
|
7976
|
+
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
7977
|
+
);
|
|
7978
|
+
}
|
|
7979
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
|
|
7980
|
+
throw new Error("invalid workspace ownership worktreePath");
|
|
7981
|
+
}
|
|
7982
|
+
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
7983
|
+
throw new Error("invalid workspace ownership branch");
|
|
7984
|
+
}
|
|
7985
|
+
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
7986
|
+
return {
|
|
7987
|
+
version: OWNERSHIP_VERSION,
|
|
7988
|
+
workspace,
|
|
7989
|
+
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
7990
|
+
branch: candidate.branch,
|
|
7991
|
+
devpodId,
|
|
7992
|
+
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
7993
|
+
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
7994
|
+
};
|
|
7995
|
+
}
|
|
7996
|
+
function recordPath(repoPath, workspace) {
|
|
7997
|
+
return import_node_path20.default.join(
|
|
7998
|
+
ownershipDirectory(repoPath),
|
|
7999
|
+
`${validateWorkspace(workspace, "workspace")}.json`
|
|
8000
|
+
);
|
|
8001
|
+
}
|
|
8002
|
+
function readRecordFile(filePath, expectedWorkspace) {
|
|
8003
|
+
let parsed;
|
|
8004
|
+
try {
|
|
8005
|
+
parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7730
8006
|
} catch (error) {
|
|
7731
|
-
if (
|
|
7732
|
-
|
|
7733
|
-
|
|
8007
|
+
if (error instanceof SyntaxError) {
|
|
8008
|
+
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
8009
|
+
}
|
|
8010
|
+
throw error;
|
|
7734
8011
|
}
|
|
7735
|
-
return
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
8012
|
+
return validateRecord(parsed, expectedWorkspace);
|
|
8013
|
+
}
|
|
8014
|
+
function readWorkspaceOwnership(repoPath, workspace) {
|
|
8015
|
+
const filePath = recordPath(repoPath, workspace);
|
|
8016
|
+
try {
|
|
8017
|
+
return readRecordFile(filePath, workspace);
|
|
8018
|
+
} catch (error) {
|
|
8019
|
+
if (error.code === "ENOENT") return void 0;
|
|
8020
|
+
throw error;
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
function listWorkspaceOwnership(repoPath) {
|
|
8024
|
+
const directory = ownershipDirectory(repoPath);
|
|
8025
|
+
return listWorkspaceOwnershipInDirectory(directory);
|
|
8026
|
+
}
|
|
8027
|
+
function listWorkspaceOwnershipInDirectory(directory) {
|
|
8028
|
+
let entries;
|
|
8029
|
+
try {
|
|
8030
|
+
entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
|
|
8031
|
+
} catch (error) {
|
|
8032
|
+
if (error.code === "ENOENT") return [];
|
|
8033
|
+
throw error;
|
|
8034
|
+
}
|
|
8035
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
8036
|
+
const workspace = entry.name.slice(0, -".json".length);
|
|
8037
|
+
validateWorkspace(workspace, "filename");
|
|
8038
|
+
return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
|
|
8039
|
+
});
|
|
8040
|
+
}
|
|
8041
|
+
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
8042
|
+
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
8043
|
+
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
8044
|
+
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
8045
|
+
const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
|
|
8046
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8047
|
+
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
8048
|
+
const existing = records.find((record2) => record2.workspace === workspace);
|
|
8049
|
+
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
8050
|
+
throw new Error(
|
|
8051
|
+
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
8052
|
+
);
|
|
8053
|
+
}
|
|
8054
|
+
const pathOwner = records.find(
|
|
8055
|
+
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
8056
|
+
);
|
|
8057
|
+
if (pathOwner) {
|
|
8058
|
+
throw new Error(
|
|
8059
|
+
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
8060
|
+
);
|
|
8061
|
+
}
|
|
8062
|
+
if (existing && existing.devpodId !== devpodId) {
|
|
8063
|
+
throw new Error(
|
|
8064
|
+
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
8065
|
+
);
|
|
8066
|
+
}
|
|
8067
|
+
const record = {
|
|
8068
|
+
version: OWNERSHIP_VERSION,
|
|
8069
|
+
workspace,
|
|
8070
|
+
worktreePath,
|
|
8071
|
+
branch: input2.branch ?? null,
|
|
8072
|
+
devpodId,
|
|
8073
|
+
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
8074
|
+
updatedAt: validateTimestamp(now, "updatedAt")
|
|
8075
|
+
};
|
|
8076
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
8077
|
+
`);
|
|
8078
|
+
return record;
|
|
8079
|
+
}
|
|
8080
|
+
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
8081
|
+
const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
8082
|
+
try {
|
|
8083
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8084
|
+
return true;
|
|
8085
|
+
} catch (error) {
|
|
8086
|
+
if (error.code === "ENOENT") return false;
|
|
8087
|
+
throw error;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
function sameOwnershipRecord(left, right) {
|
|
8091
|
+
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;
|
|
8092
|
+
}
|
|
8093
|
+
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
8094
|
+
const filePath = import_node_path20.default.join(
|
|
8095
|
+
directory,
|
|
8096
|
+
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
8097
|
+
);
|
|
8098
|
+
let current;
|
|
8099
|
+
try {
|
|
8100
|
+
current = readRecordFile(filePath, expected.workspace);
|
|
8101
|
+
} catch (error) {
|
|
8102
|
+
if (error.code === "ENOENT") return "absent";
|
|
8103
|
+
throw error;
|
|
8104
|
+
}
|
|
8105
|
+
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
8106
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8107
|
+
return "removed";
|
|
8108
|
+
}
|
|
8109
|
+
function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
|
|
8110
|
+
const directory = ownershipDirectory(repoPath);
|
|
8111
|
+
import_node_fs20.default.mkdirSync(directory, { recursive: true });
|
|
8112
|
+
return withFileLockSync(
|
|
8113
|
+
import_node_path20.default.join(directory, ".lock"),
|
|
8114
|
+
{
|
|
8115
|
+
activity: "workspace ownership transaction",
|
|
8116
|
+
target: `'${repoPath}'`,
|
|
8117
|
+
waitMs: options.waitMs ?? 5e3
|
|
8118
|
+
},
|
|
8119
|
+
() => operation({
|
|
8120
|
+
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8121
|
+
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8122
|
+
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8123
|
+
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8124
|
+
})
|
|
8125
|
+
);
|
|
8126
|
+
}
|
|
8127
|
+
function providerPathOwner(providerWorkspaces, worktreePath) {
|
|
8128
|
+
const owners = providerWorkspaces.filter(
|
|
8129
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
8130
|
+
);
|
|
8131
|
+
if (owners.length > 1) {
|
|
8132
|
+
throw new Error(
|
|
8133
|
+
`Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
|
|
8134
|
+
);
|
|
8135
|
+
}
|
|
8136
|
+
return owners[0];
|
|
8137
|
+
}
|
|
8138
|
+
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
8139
|
+
const owners = /* @__PURE__ */ new Map();
|
|
8140
|
+
for (const worktree of listGitWorktrees(repoPath)) {
|
|
8141
|
+
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
8142
|
+
let workspace;
|
|
8143
|
+
try {
|
|
8144
|
+
workspace = readPersistedWorkspace(worktree.path);
|
|
8145
|
+
} catch (error) {
|
|
8146
|
+
if (!import_node_fs20.default.existsSync(worktree.path)) continue;
|
|
8147
|
+
throw error;
|
|
8148
|
+
}
|
|
8149
|
+
if (!workspace) continue;
|
|
8150
|
+
const existing = owners.get(workspace);
|
|
8151
|
+
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
8152
|
+
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
8153
|
+
}
|
|
8154
|
+
owners.set(workspace, worktree.path);
|
|
8155
|
+
}
|
|
8156
|
+
return owners;
|
|
8157
|
+
}
|
|
8158
|
+
function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
|
|
8159
|
+
const recordOwner = records.find(
|
|
8160
|
+
(record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
|
|
8161
|
+
);
|
|
8162
|
+
if (recordOwner) {
|
|
8163
|
+
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
8164
|
+
}
|
|
8165
|
+
const providerOwner = providerWorkspaces.find(
|
|
8166
|
+
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
8167
|
+
);
|
|
8168
|
+
if (providerOwner) {
|
|
8169
|
+
return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
|
|
8170
|
+
}
|
|
8171
|
+
const persistedOwner = persistedOwners.get(workspace);
|
|
8172
|
+
if (persistedOwner) {
|
|
8173
|
+
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
8174
|
+
}
|
|
8175
|
+
return void 0;
|
|
8176
|
+
}
|
|
8177
|
+
function claimWorkspaceIdentity(repoPath, input2) {
|
|
8178
|
+
const worktreePath = comparableWorkspacePath(repoPath);
|
|
8179
|
+
const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
|
|
8180
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
|
|
8181
|
+
const records = transaction.list();
|
|
8182
|
+
const exactRecords = records.filter(
|
|
8183
|
+
(record) => sameWorkspacePath(record.worktreePath, worktreePath)
|
|
8184
|
+
);
|
|
8185
|
+
if (exactRecords.length > 1) {
|
|
8186
|
+
throw new Error(
|
|
8187
|
+
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
8188
|
+
);
|
|
8189
|
+
}
|
|
8190
|
+
const exactRecord = exactRecords[0];
|
|
8191
|
+
const persisted = readPersistedWorkspace(worktreePath);
|
|
8192
|
+
const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
|
|
8193
|
+
if (exactRecord) {
|
|
8194
|
+
if (persisted && persisted !== exactRecord.workspace) {
|
|
8195
|
+
throw new Error(
|
|
8196
|
+
`Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
|
|
8197
|
+
);
|
|
7742
8198
|
}
|
|
8199
|
+
if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
|
|
8200
|
+
throw new Error(
|
|
8201
|
+
`Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
|
|
8202
|
+
);
|
|
8203
|
+
}
|
|
8204
|
+
const conflict2 = claimConflict(
|
|
8205
|
+
exactRecord.workspace,
|
|
8206
|
+
exactRecord.devpodId,
|
|
8207
|
+
worktreePath,
|
|
8208
|
+
records,
|
|
8209
|
+
input2.providerWorkspaces,
|
|
8210
|
+
persistedOwners,
|
|
8211
|
+
exactRecord
|
|
8212
|
+
);
|
|
8213
|
+
if (conflict2) {
|
|
8214
|
+
throw new Error(
|
|
8215
|
+
`Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
|
|
8216
|
+
);
|
|
8217
|
+
}
|
|
8218
|
+
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
8219
|
+
return transaction.write({
|
|
8220
|
+
workspace: exactRecord.workspace,
|
|
8221
|
+
worktreePath,
|
|
8222
|
+
branch: input2.branch ?? null,
|
|
8223
|
+
devpodId: exactRecord.devpodId
|
|
8224
|
+
});
|
|
7743
8225
|
}
|
|
7744
|
-
if (
|
|
7745
|
-
throw new Error(
|
|
8226
|
+
if (persisted && exactProvider && persisted !== exactProvider.id) {
|
|
8227
|
+
throw new Error(
|
|
8228
|
+
`Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
|
|
8229
|
+
);
|
|
7746
8230
|
}
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
if (
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
8231
|
+
let workspace = persisted ?? exactProvider?.id;
|
|
8232
|
+
let devpodId = exactProvider?.id ?? persisted;
|
|
8233
|
+
if (!workspace || !devpodId) {
|
|
8234
|
+
if (input2.unavailableRuntimes.length > 0) {
|
|
8235
|
+
throw new Error(
|
|
8236
|
+
`Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
|
|
8237
|
+
);
|
|
8238
|
+
}
|
|
8239
|
+
const candidate = workspaceIdentityCandidates(input2.source).find(
|
|
8240
|
+
(next) => !claimConflict(
|
|
8241
|
+
next,
|
|
8242
|
+
next,
|
|
8243
|
+
worktreePath,
|
|
8244
|
+
records,
|
|
8245
|
+
input2.providerWorkspaces,
|
|
8246
|
+
persistedOwners
|
|
8247
|
+
)
|
|
8248
|
+
);
|
|
8249
|
+
if (!candidate) {
|
|
8250
|
+
throw new Error(
|
|
8251
|
+
`Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
|
|
8252
|
+
);
|
|
8253
|
+
}
|
|
8254
|
+
workspace = candidate;
|
|
8255
|
+
devpodId = candidate;
|
|
8256
|
+
}
|
|
8257
|
+
const conflict = claimConflict(
|
|
8258
|
+
workspace,
|
|
8259
|
+
devpodId,
|
|
8260
|
+
worktreePath,
|
|
8261
|
+
records,
|
|
8262
|
+
input2.providerWorkspaces,
|
|
8263
|
+
persistedOwners
|
|
8264
|
+
);
|
|
8265
|
+
if (conflict) {
|
|
8266
|
+
throw new Error(
|
|
8267
|
+
`Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
|
|
7757
8268
|
);
|
|
7758
8269
|
}
|
|
7759
|
-
|
|
7760
|
-
|
|
8270
|
+
const written = transaction.write({
|
|
8271
|
+
workspace,
|
|
8272
|
+
worktreePath,
|
|
8273
|
+
branch: input2.branch ?? null,
|
|
8274
|
+
devpodId
|
|
8275
|
+
});
|
|
8276
|
+
try {
|
|
8277
|
+
persistWorkspace(worktreePath, workspace);
|
|
8278
|
+
} catch (error) {
|
|
8279
|
+
const cleanup = transaction.removeIfMatches(written);
|
|
8280
|
+
if (cleanup !== "removed") {
|
|
8281
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8282
|
+
throw new Error(
|
|
8283
|
+
`Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
|
|
8284
|
+
);
|
|
8285
|
+
}
|
|
8286
|
+
throw error;
|
|
7761
8287
|
}
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
8288
|
+
return written;
|
|
8289
|
+
});
|
|
8290
|
+
}
|
|
8291
|
+
function removeWorkspaceOwnership(repoPath, workspace) {
|
|
8292
|
+
return withWorkspaceOwnershipTransaction(
|
|
8293
|
+
repoPath,
|
|
8294
|
+
(transaction) => transaction.remove(workspace)
|
|
8295
|
+
);
|
|
8296
|
+
}
|
|
8297
|
+
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
8298
|
+
const worktree = worktrees.find(
|
|
8299
|
+
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
8300
|
+
);
|
|
8301
|
+
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
8302
|
+
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
8303
|
+
if (devpodStatus === "conflict") {
|
|
8304
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8305
|
+
}
|
|
8306
|
+
if (worktree?.locked) {
|
|
8307
|
+
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
8308
|
+
}
|
|
8309
|
+
if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
|
|
8310
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8311
|
+
}
|
|
8312
|
+
if (!worktree || worktree.prunable) {
|
|
8313
|
+
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
8314
|
+
}
|
|
8315
|
+
let persisted;
|
|
8316
|
+
try {
|
|
8317
|
+
persisted = readPersistedWorkspace(worktree.path);
|
|
8318
|
+
} catch {
|
|
8319
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8320
|
+
}
|
|
8321
|
+
return {
|
|
8322
|
+
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
8323
|
+
devpodStatus,
|
|
8324
|
+
worktree
|
|
8325
|
+
};
|
|
8326
|
+
}
|
|
8327
|
+
function listMissingWorkspaceOwnership(repoPath) {
|
|
8328
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
8329
|
+
return listWorkspaceOwnership(repoPath).filter(
|
|
8330
|
+
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
8331
|
+
);
|
|
8332
|
+
}
|
|
8333
|
+
var import_node_child_process15, import_node_fs20, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
8334
|
+
var init_workspace_ownership = __esm({
|
|
8335
|
+
"src/core/workspace-ownership.ts"() {
|
|
8336
|
+
"use strict";
|
|
8337
|
+
import_node_child_process15 = require("child_process");
|
|
8338
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8339
|
+
import_node_path20 = __toESM(require("path"));
|
|
8340
|
+
init_atomic_file();
|
|
8341
|
+
init_devpod_workspaces();
|
|
8342
|
+
init_file_lock();
|
|
8343
|
+
init_workspace();
|
|
8344
|
+
READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
8345
|
+
OWNERSHIP_VERSION = 1;
|
|
8346
|
+
OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
|
|
8347
|
+
}
|
|
8348
|
+
});
|
|
8349
|
+
|
|
8350
|
+
// src/core/managed-devsy-stop.ts
|
|
8351
|
+
function sameSet(left, right) {
|
|
8352
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
8353
|
+
}
|
|
8354
|
+
function containerIdentity(container) {
|
|
8355
|
+
return JSON.stringify({ id: container.id, labels: container.labels, mounts: container.mounts });
|
|
8356
|
+
}
|
|
8357
|
+
function stopRetainedManagedDevsyWorkspace(options) {
|
|
8358
|
+
const { repoPath, devsyId } = options;
|
|
8359
|
+
const linked = isLinkedWorktree(repoPath);
|
|
8360
|
+
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
8361
|
+
const retainedState = readManagedRuntimeState(repoPath, workspace);
|
|
8362
|
+
if (!retainedState) return false;
|
|
8363
|
+
const state = retainedState;
|
|
8364
|
+
if (state.devpodId !== devsyId || linked && !workspace) {
|
|
8365
|
+
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
8366
|
+
}
|
|
8367
|
+
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
8368
|
+
function registration() {
|
|
8369
|
+
resetWorkspaceRuntimeCaches();
|
|
8370
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
8371
|
+
throw new Error("Managed stop provider selection changed.");
|
|
7775
8372
|
}
|
|
7776
|
-
const
|
|
7777
|
-
if (
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8373
|
+
const owner = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
8374
|
+
if (owner.status !== "owned") {
|
|
8375
|
+
throw new Error("Managed stop requires one exact retained Devsy registration.");
|
|
8376
|
+
}
|
|
8377
|
+
if (workspace) {
|
|
8378
|
+
const record = readWorkspaceOwnership(repoPath, workspace);
|
|
8379
|
+
if (!record || record.devpodId !== devsyId || !sameWorkspacePath(record.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir) {
|
|
8380
|
+
throw new Error("Managed stop workspace ownership changed.");
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
return owner.workspace;
|
|
8384
|
+
}
|
|
8385
|
+
const context = registration().context;
|
|
8386
|
+
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
8387
|
+
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
8388
|
+
}
|
|
8389
|
+
const devsyRoot = import_node_path21.default.resolve(process.env.DEVSY_HOME || import_node_path21.default.join(import_node_os2.default.homedir(), ".devsy"));
|
|
8390
|
+
const featureDirectory = import_node_path21.default.join(
|
|
8391
|
+
devsyRoot,
|
|
8392
|
+
"contexts",
|
|
8393
|
+
context,
|
|
8394
|
+
"workspaces",
|
|
8395
|
+
devsyId,
|
|
8396
|
+
"agent",
|
|
8397
|
+
".docker-compose"
|
|
8398
|
+
);
|
|
8399
|
+
function prove(previous) {
|
|
8400
|
+
if (registration().context !== context || JSON.stringify(readManagedRuntimeState(repoPath, workspace)) !== JSON.stringify(state)) {
|
|
8401
|
+
throw new Error("Managed stop retained context or runtime record changed.");
|
|
8402
|
+
}
|
|
8403
|
+
const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
|
|
8404
|
+
const managed = runtime.config.managedRuntime;
|
|
8405
|
+
if (!managed || runtime.profile !== state.profile || runtime.workspace !== workspace) {
|
|
8406
|
+
throw new Error("Managed stop requires the recorded managed profile.");
|
|
8407
|
+
}
|
|
8408
|
+
const plan = inspectManagedDevcontainerConfig({
|
|
8409
|
+
repoPath,
|
|
8410
|
+
config: runtime.config,
|
|
8411
|
+
profile: runtime.resolvedProfile,
|
|
8412
|
+
linked
|
|
8413
|
+
});
|
|
8414
|
+
const selectedProcesses = runtime.resolvedProfile?.processes;
|
|
8415
|
+
const processes = !runtime.resolvedProfile || selectedProcesses?.length === 1 && selectedProcesses[0] === "*" ? managed.processes : selectedProcesses ?? [];
|
|
8416
|
+
if (state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || !sameSet(
|
|
8417
|
+
state.desired.apps,
|
|
8418
|
+
proxyAppsFromConfig(runtime.config).map((app) => app.name)
|
|
8419
|
+
) || !sameSet(state.desired.services, plan.desiredProfileServices) || !sameSet(state.desired.processes, processes) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
|
|
8420
|
+
throw new Error("Managed stop requires unchanged recorded resources and configuration.");
|
|
8421
|
+
}
|
|
8422
|
+
const containers = inspectManagedStopContainers(state.composeProject);
|
|
8423
|
+
const services = /* @__PURE__ */ new Set();
|
|
8424
|
+
for (const container of containers) {
|
|
8425
|
+
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
8426
|
+
if (!plan.nativeRunServices.includes(service) || services.has(service) || container.labels["com.docker.compose.project"] !== state.composeProject || !sameWorkspacePath(
|
|
8427
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
8428
|
+
plan.composeDirectory
|
|
8429
|
+
)) {
|
|
8430
|
+
throw new Error("Managed stop found unexpected or duplicate project membership.");
|
|
7782
8431
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
8432
|
+
services.add(service);
|
|
8433
|
+
const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
|
|
8434
|
+
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]))) {
|
|
8435
|
+
throw new Error("Managed stop Compose file identity changed.");
|
|
7785
8436
|
}
|
|
7786
|
-
|
|
8437
|
+
for (const file of files.slice(plan.composeFiles.length)) {
|
|
8438
|
+
if (import_node_path21.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
8439
|
+
import_node_path21.default.basename(file)
|
|
8440
|
+
) || import_node_fs21.default.realpathSync(file) !== import_node_path21.default.join(import_node_fs21.default.realpathSync(devsyRoot), import_node_path21.default.relative(devsyRoot, file))) {
|
|
8441
|
+
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
if (previous) {
|
|
8445
|
+
const retained = previous.find((entry) => entry.id === container.id);
|
|
8446
|
+
if (!retained || containerIdentity(retained) !== containerIdentity(container) || !retained.state.Running && container.state.Running) {
|
|
8447
|
+
throw new Error("Managed stop container identity or quiescent state changed.");
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
if (plan.desiredServices.some((service) => !services.has(service)) || previous && !sameSet(
|
|
8452
|
+
previous.map((c) => c.id),
|
|
8453
|
+
containers.map((c) => c.id)
|
|
8454
|
+
)) {
|
|
8455
|
+
throw new Error("Managed stop cannot prove the complete retained service population.");
|
|
8456
|
+
}
|
|
8457
|
+
const primary = containers.find(
|
|
8458
|
+
(c) => c.labels["com.docker.compose.service"] === plan.primaryService
|
|
8459
|
+
);
|
|
8460
|
+
if (primary?.mounts.filter(
|
|
8461
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
8462
|
+
).length !== 1) {
|
|
8463
|
+
throw new Error("Managed stop cannot prove the exact primary workspace mount.");
|
|
8464
|
+
}
|
|
8465
|
+
assertManagedContainerConfigUnchanged({ plan, containers, workspace: workspaceEnv });
|
|
8466
|
+
if (registration().context !== context) {
|
|
8467
|
+
throw new Error("Managed stop provider context changed during inspection.");
|
|
7787
8468
|
}
|
|
8469
|
+
const status = inspectDevsyRuntimeStatus(devsyId);
|
|
8470
|
+
if (status !== "running" && status !== "stopped" || status === "running" !== primary.state.Running) {
|
|
8471
|
+
throw new Error("Managed stop requires consistent provider and primary container state.");
|
|
8472
|
+
}
|
|
8473
|
+
return { containers, primary, status };
|
|
8474
|
+
}
|
|
8475
|
+
const initial = prove();
|
|
8476
|
+
let providerError;
|
|
8477
|
+
let providerFailed = false;
|
|
8478
|
+
if (initial.status === "running") {
|
|
7788
8479
|
try {
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
8480
|
+
options.stopProvider();
|
|
8481
|
+
} catch (error) {
|
|
8482
|
+
providerFailed = true;
|
|
8483
|
+
providerError = error;
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
try {
|
|
8487
|
+
const stopped = () => {
|
|
8488
|
+
const current = prove(initial.containers);
|
|
8489
|
+
if (current.status !== "stopped" || current.primary.state.Running) {
|
|
8490
|
+
throw new Error("Managed stop has not stopped the exact primary container.");
|
|
7793
8491
|
}
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
8492
|
+
return current.containers;
|
|
8493
|
+
};
|
|
8494
|
+
stopped();
|
|
8495
|
+
for (const retained of initial.containers) {
|
|
8496
|
+
if (!retained.state.Running || retained.id === initial.primary.id) continue;
|
|
8497
|
+
const current = stopped().find((entry) => entry.id === retained.id);
|
|
8498
|
+
if (current?.state.Running) {
|
|
8499
|
+
stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
|
|
8500
|
+
timeoutMs: 3e4
|
|
8501
|
+
});
|
|
7800
8502
|
}
|
|
7801
|
-
return devsyId;
|
|
7802
|
-
} catch (error) {
|
|
7803
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7804
|
-
throw new DevsyStartPostconditionError(message);
|
|
7805
8503
|
}
|
|
7806
|
-
|
|
8504
|
+
if (stopped().some((container) => container.state.Running)) {
|
|
8505
|
+
throw new Error("Managed stop left a retained service running.");
|
|
8506
|
+
}
|
|
8507
|
+
} catch (error) {
|
|
8508
|
+
if (providerFailed) {
|
|
8509
|
+
throw new AggregateError(
|
|
8510
|
+
[providerError, error],
|
|
8511
|
+
"Devsy provider stop failed; complete retained shutdown could not be verified.",
|
|
8512
|
+
{ cause: providerError }
|
|
8513
|
+
);
|
|
8514
|
+
}
|
|
8515
|
+
throw error;
|
|
8516
|
+
}
|
|
8517
|
+
if (providerFailed) throw providerError;
|
|
8518
|
+
return true;
|
|
7807
8519
|
}
|
|
7808
|
-
var
|
|
7809
|
-
var
|
|
7810
|
-
"src/core/devsy-
|
|
8520
|
+
var import_node_fs21, import_node_os2, import_node_path21;
|
|
8521
|
+
var init_managed_devsy_stop = __esm({
|
|
8522
|
+
"src/core/managed-devsy-stop.ts"() {
|
|
7811
8523
|
"use strict";
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
8524
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8525
|
+
import_node_os2 = __toESM(require("os"));
|
|
8526
|
+
import_node_path21 = __toESM(require("path"));
|
|
8527
|
+
init_devcontainer_profile();
|
|
8528
|
+
init_devpod_environment();
|
|
7816
8529
|
init_devsy_workspaces();
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
|
|
7824
|
-
DEVSY_STDERR_TAIL_BYTES = 8192;
|
|
8530
|
+
init_managed_runtime_state();
|
|
8531
|
+
init_repo_config();
|
|
8532
|
+
init_route_publication();
|
|
8533
|
+
init_workspace();
|
|
8534
|
+
init_workspace_ownership();
|
|
8535
|
+
init_workspace_runtime();
|
|
7825
8536
|
}
|
|
7826
8537
|
});
|
|
7827
8538
|
|
|
7828
|
-
// src/core/
|
|
7829
|
-
function
|
|
7830
|
-
|
|
8539
|
+
// src/core/devsy-mutation.ts
|
|
8540
|
+
function failedStartMayHaveAttached(devsyId, repoPath) {
|
|
8541
|
+
try {
|
|
8542
|
+
const attached = listDevsyWorkspaces();
|
|
8543
|
+
const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
|
|
8544
|
+
if (!attachedId) return false;
|
|
8545
|
+
return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
|
|
8546
|
+
} catch {
|
|
8547
|
+
return true;
|
|
8548
|
+
}
|
|
8549
|
+
}
|
|
8550
|
+
function withMutationLock(activity, target, operation) {
|
|
8551
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7831
8552
|
return withFileLockSync(
|
|
7832
|
-
|
|
8553
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
7833
8554
|
{
|
|
7834
8555
|
activity,
|
|
7835
8556
|
target: `'${target}'`,
|
|
7836
|
-
waitMs:
|
|
8557
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7837
8558
|
fair: true,
|
|
7838
8559
|
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7839
8560
|
},
|
|
7840
8561
|
operation
|
|
7841
8562
|
);
|
|
7842
8563
|
}
|
|
7843
|
-
function
|
|
8564
|
+
function withMutationLockAsync(activity, target, operation) {
|
|
8565
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8566
|
+
return withFileLock(
|
|
8567
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
8568
|
+
{
|
|
8569
|
+
activity,
|
|
8570
|
+
target: `'${target}'`,
|
|
8571
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
8572
|
+
fair: true,
|
|
8573
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8574
|
+
},
|
|
8575
|
+
operation
|
|
8576
|
+
);
|
|
8577
|
+
}
|
|
8578
|
+
function commandFailure2(result) {
|
|
7844
8579
|
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7845
8580
|
}
|
|
7846
|
-
function
|
|
7847
|
-
|
|
7848
|
-
|
|
8581
|
+
function runDevsyUp(args, env, quiet) {
|
|
8582
|
+
return new Promise((resolve) => {
|
|
8583
|
+
const child = (0, import_node_child_process16.spawn)("devsy", args, {
|
|
8584
|
+
stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
|
|
8585
|
+
env
|
|
8586
|
+
});
|
|
8587
|
+
const stderr = child.stderr;
|
|
8588
|
+
if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
|
|
8589
|
+
let stderrTail = Buffer.alloc(0);
|
|
8590
|
+
let spawnError;
|
|
8591
|
+
stderr.on("data", (chunk) => {
|
|
8592
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8593
|
+
const writable = process.stderr.write(value);
|
|
8594
|
+
if (!writable) {
|
|
8595
|
+
stderr.pause();
|
|
8596
|
+
process.stderr.once("drain", () => stderr.resume());
|
|
8597
|
+
}
|
|
8598
|
+
if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
|
|
8599
|
+
stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
|
|
8600
|
+
} else {
|
|
8601
|
+
const combined = Buffer.concat([stderrTail, value]);
|
|
8602
|
+
stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
|
|
8603
|
+
}
|
|
8604
|
+
});
|
|
8605
|
+
child.once("error", (error) => {
|
|
8606
|
+
spawnError = error;
|
|
8607
|
+
});
|
|
8608
|
+
child.once("close", (status) => {
|
|
8609
|
+
resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
|
|
8610
|
+
});
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8613
|
+
function runDevsyAction(action2, devsyId, force = false) {
|
|
8614
|
+
const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
|
|
8615
|
+
const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
|
|
7849
8616
|
if (result.status !== 0) {
|
|
7850
8617
|
throw new Error(
|
|
7851
|
-
`
|
|
8618
|
+
`devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
|
|
7852
8619
|
);
|
|
7853
8620
|
}
|
|
7854
8621
|
}
|
|
7855
|
-
function
|
|
7856
|
-
const ownership =
|
|
7857
|
-
listDevpodWorkspaces(worktreePath),
|
|
7858
|
-
devpodId,
|
|
7859
|
-
worktreePath
|
|
7860
|
-
);
|
|
8622
|
+
function inspectExactOwnership(devsyId, worktreePath) {
|
|
8623
|
+
const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
|
|
7861
8624
|
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
7862
8625
|
return ownership;
|
|
7863
8626
|
}
|
|
7864
|
-
function
|
|
7865
|
-
return
|
|
7866
|
-
|
|
8627
|
+
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
8628
|
+
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
8629
|
+
if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
|
|
8630
|
+
repoPath: worktreePath,
|
|
8631
|
+
devsyId,
|
|
8632
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
8633
|
+
}))
|
|
8634
|
+
return { status: "changed" };
|
|
8635
|
+
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
7867
8636
|
if (before.status === "absent") return { status: "absent" };
|
|
7868
|
-
|
|
7869
|
-
let after =
|
|
8637
|
+
runDevsyAction(action2, devsyId);
|
|
8638
|
+
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
7870
8639
|
if (action2 === "stop" && after.status !== "owned") {
|
|
7871
|
-
throw new Error(
|
|
8640
|
+
throw new Error(
|
|
8641
|
+
`Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
|
|
8642
|
+
);
|
|
7872
8643
|
}
|
|
7873
8644
|
if (action2 === "delete" && after.status === "owned") {
|
|
7874
|
-
const runtime =
|
|
8645
|
+
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
7875
8646
|
if (runtime !== "not-found") {
|
|
7876
8647
|
throw new Error(
|
|
7877
|
-
`
|
|
8648
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
7878
8649
|
);
|
|
7879
8650
|
}
|
|
7880
|
-
after =
|
|
8651
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7881
8652
|
if (after.status === "owned") {
|
|
7882
|
-
|
|
7883
|
-
after =
|
|
8653
|
+
runDevsyAction("delete", devsyId, true);
|
|
8654
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7884
8655
|
}
|
|
7885
8656
|
if (after.status !== "absent") {
|
|
7886
8657
|
throw new Error(
|
|
7887
|
-
`
|
|
8658
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
|
|
7888
8659
|
);
|
|
7889
8660
|
}
|
|
7890
8661
|
}
|
|
7891
8662
|
return { status: "changed" };
|
|
7892
8663
|
});
|
|
7893
8664
|
}
|
|
7894
|
-
function
|
|
7895
|
-
|
|
7896
|
-
const result2 = stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
7897
|
-
resetWorkspaceRuntimeCaches();
|
|
7898
|
-
return result2;
|
|
7899
|
-
}
|
|
7900
|
-
const result = mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
7901
|
-
resetWorkspaceRuntimeCaches();
|
|
7902
|
-
return result;
|
|
8665
|
+
function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8666
|
+
return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
|
|
7903
8667
|
}
|
|
7904
|
-
function
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
8668
|
+
function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8669
|
+
return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
|
|
8670
|
+
}
|
|
8671
|
+
function assertDevsyTarget(devsyId, repoPath) {
|
|
8672
|
+
const workspaces = listDevsyWorkspaces();
|
|
8673
|
+
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
8674
|
+
const id = devsyId ?? existing?.id;
|
|
8675
|
+
if (id) {
|
|
8676
|
+
const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
|
|
8677
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
7909
8678
|
}
|
|
7910
|
-
|
|
7911
|
-
resetWorkspaceRuntimeCaches();
|
|
7912
|
-
return result;
|
|
8679
|
+
return id;
|
|
7913
8680
|
}
|
|
7914
|
-
async function
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
workspace: options.workspace,
|
|
7924
|
-
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
7925
|
-
});
|
|
7926
|
-
resetWorkspaceRuntimeCaches();
|
|
7927
|
-
return result;
|
|
7928
|
-
} catch (error) {
|
|
7929
|
-
if (error instanceof DevsyStartPostconditionError) {
|
|
7930
|
-
resetWorkspaceRuntimeCaches();
|
|
7931
|
-
throw new DevpodStartPostconditionError(error.message);
|
|
7932
|
-
}
|
|
7933
|
-
throw error;
|
|
7934
|
-
}
|
|
8681
|
+
async function startDevsyWorkspace(options) {
|
|
8682
|
+
const activity = options.recreate ? "Devsy recreate" : "Devsy start";
|
|
8683
|
+
let agent;
|
|
8684
|
+
try {
|
|
8685
|
+
agent = requireReadyDevsyAgent();
|
|
8686
|
+
} catch (error) {
|
|
8687
|
+
if (!(error instanceof DevsyAgentReadinessError)) throw error;
|
|
8688
|
+
const repair = devsyAgentRepairSuggestion(error.inspection);
|
|
8689
|
+
throw new Error(`${error.message}. ${repair}`);
|
|
7935
8690
|
}
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
if (options.recreate && before.status !== "owned") {
|
|
7944
|
-
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
8691
|
+
return withMutationLockAsync(activity, options.repoPath, async () => {
|
|
8692
|
+
let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
|
|
8693
|
+
if (devsyId && options.recreate) {
|
|
8694
|
+
const attached = listDevsyWorkspaces();
|
|
8695
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8696
|
+
if (ownership.status !== "owned") {
|
|
8697
|
+
throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
|
|
7945
8698
|
}
|
|
7946
|
-
} else if (options.recreate) {
|
|
7947
|
-
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
7948
8699
|
}
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
if (options.devcontainerPath) {
|
|
7952
|
-
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8700
|
+
if (!devsyId && options.recreate) {
|
|
8701
|
+
throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
|
|
7953
8702
|
}
|
|
7954
|
-
args
|
|
8703
|
+
const args = ["workspace", "up", options.repoPath];
|
|
8704
|
+
if (devsyId) args.push("--id", devsyId);
|
|
8705
|
+
if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
|
|
8706
|
+
args.push("--ide-launch", "skip");
|
|
7955
8707
|
if (options.workspace) {
|
|
7956
8708
|
args.push(
|
|
7957
8709
|
"--workspace-env",
|
|
@@ -7960,8 +8712,12 @@ async function startDevpodWorkspace(options) {
|
|
|
7960
8712
|
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
7961
8713
|
);
|
|
7962
8714
|
}
|
|
8715
|
+
if (options.inactivityTimeout) {
|
|
8716
|
+
args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
|
|
8717
|
+
}
|
|
7963
8718
|
if (options.recreate) args.push("--recreate");
|
|
7964
8719
|
const env = { ...process.env };
|
|
8720
|
+
env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
7965
8721
|
if (options.workspace) {
|
|
7966
8722
|
env.WORKSPACE = options.workspace.token;
|
|
7967
8723
|
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
@@ -7973,515 +8729,252 @@ async function startDevpodWorkspace(options) {
|
|
|
7973
8729
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
7974
8730
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
7975
8731
|
}
|
|
7976
|
-
const result =
|
|
7977
|
-
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
7978
|
-
env
|
|
7979
|
-
});
|
|
8732
|
+
const result = await runDevsyUp(args, env, options.quiet ?? false);
|
|
7980
8733
|
if (result.status !== 0) {
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
7986
|
-
if (!devpodId) {
|
|
7987
|
-
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
8734
|
+
let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
|
|
8735
|
+
if (result.error?.message) message += ` ${result.error.message}`;
|
|
8736
|
+
if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
|
|
8737
|
+
message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
7988
8738
|
}
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
if (ownership.status !== "owned") {
|
|
7992
|
-
throw new Error(
|
|
7993
|
-
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
7994
|
-
);
|
|
8739
|
+
if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
|
|
8740
|
+
throw new DevsyStartPostconditionError(message);
|
|
7995
8741
|
}
|
|
7996
|
-
|
|
7997
|
-
return devpodId;
|
|
7998
|
-
} catch (error) {
|
|
7999
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
8000
|
-
throw new DevpodStartPostconditionError(message);
|
|
8001
|
-
}
|
|
8002
|
-
});
|
|
8003
|
-
}
|
|
8004
|
-
var import_node_child_process16, import_node_fs21, import_node_path21, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
8005
|
-
var init_devpod_mutation = __esm({
|
|
8006
|
-
"src/core/devpod-mutation.ts"() {
|
|
8007
|
-
"use strict";
|
|
8008
|
-
import_node_child_process16 = require("child_process");
|
|
8009
|
-
import_node_fs21 = __toESM(require("fs"));
|
|
8010
|
-
import_node_path21 = __toESM(require("path"));
|
|
8011
|
-
init_devpod_workspaces();
|
|
8012
|
-
init_devsy_mutation();
|
|
8013
|
-
init_file_lock();
|
|
8014
|
-
init_router();
|
|
8015
|
-
init_workspace_runtime();
|
|
8016
|
-
DEVPOD_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8017
|
-
DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8018
|
-
DevpodStartPostconditionError = class extends Error {
|
|
8019
|
-
};
|
|
8020
|
-
}
|
|
8021
|
-
});
|
|
8022
|
-
|
|
8023
|
-
// src/core/workspace-ownership.ts
|
|
8024
|
-
function commandError(command, repoPath, stderr) {
|
|
8025
|
-
return new Error(
|
|
8026
|
-
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
8027
|
-
);
|
|
8028
|
-
}
|
|
8029
|
-
function resolveGitCommonDir(repoPath) {
|
|
8030
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
8031
|
-
encoding: "utf-8",
|
|
8032
|
-
env: READ_ONLY_GIT_ENV
|
|
8033
|
-
});
|
|
8034
|
-
const output2 = result.stdout.trim();
|
|
8035
|
-
if (result.status !== 0 || !output2) {
|
|
8036
|
-
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
8037
|
-
}
|
|
8038
|
-
return comparableWorkspacePath(import_node_path22.default.isAbsolute(output2) ? output2 : import_node_path22.default.resolve(repoPath, output2));
|
|
8039
|
-
}
|
|
8040
|
-
function resolveGitTopLevel(repoPath) {
|
|
8041
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
8042
|
-
encoding: "utf-8",
|
|
8043
|
-
env: READ_ONLY_GIT_ENV
|
|
8044
|
-
});
|
|
8045
|
-
const output2 = result.stdout.trim();
|
|
8046
|
-
if (result.status !== 0 || !output2) {
|
|
8047
|
-
throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
|
|
8048
|
-
}
|
|
8049
|
-
return comparableWorkspacePath(output2);
|
|
8050
|
-
}
|
|
8051
|
-
function listGitWorktrees(repoPath) {
|
|
8052
|
-
const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
8053
|
-
encoding: "utf-8",
|
|
8054
|
-
env: READ_ONLY_GIT_ENV
|
|
8055
|
-
});
|
|
8056
|
-
if (result.status !== 0) {
|
|
8057
|
-
throw commandError("git worktree list", repoPath, result.stderr);
|
|
8058
|
-
}
|
|
8059
|
-
const worktrees = [];
|
|
8060
|
-
let current = {};
|
|
8061
|
-
const finish = () => {
|
|
8062
|
-
if (!current.path) return;
|
|
8063
|
-
worktrees.push({
|
|
8064
|
-
path: comparableWorkspacePath(current.path),
|
|
8065
|
-
branch: current.branch,
|
|
8066
|
-
locked: current.locked ?? false,
|
|
8067
|
-
prunable: current.prunable ?? false
|
|
8068
|
-
});
|
|
8069
|
-
current = {};
|
|
8070
|
-
};
|
|
8071
|
-
for (const line of `${result.stdout}
|
|
8072
|
-
`.split("\n")) {
|
|
8073
|
-
if (line.startsWith("worktree ")) {
|
|
8074
|
-
finish();
|
|
8075
|
-
current.path = line.slice("worktree ".length).trim();
|
|
8076
|
-
} else if (line.startsWith("branch ")) {
|
|
8077
|
-
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
8078
|
-
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
8079
|
-
current.locked = true;
|
|
8080
|
-
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
8081
|
-
current.prunable = true;
|
|
8082
|
-
} else if (line === "") {
|
|
8083
|
-
finish();
|
|
8084
|
-
}
|
|
8085
|
-
}
|
|
8086
|
-
return worktrees;
|
|
8087
|
-
}
|
|
8088
|
-
function ownershipDirectory(repoPath) {
|
|
8089
|
-
return import_node_path22.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
8090
|
-
}
|
|
8091
|
-
function validateWorkspace(value, label) {
|
|
8092
|
-
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
8093
|
-
throw new Error(`invalid workspace ownership ${label}`);
|
|
8094
|
-
}
|
|
8095
|
-
return value;
|
|
8096
|
-
}
|
|
8097
|
-
function validateTimestamp(value, label) {
|
|
8098
|
-
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
8099
|
-
throw new Error(`invalid workspace ownership ${label}`);
|
|
8100
|
-
}
|
|
8101
|
-
return value;
|
|
8102
|
-
}
|
|
8103
|
-
function validateRecord(value, expectedWorkspace) {
|
|
8104
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8105
|
-
throw new Error("invalid workspace ownership record");
|
|
8106
|
-
}
|
|
8107
|
-
const candidate = value;
|
|
8108
|
-
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
8109
|
-
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
8110
|
-
}
|
|
8111
|
-
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
8112
|
-
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
8113
|
-
throw new Error(
|
|
8114
|
-
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
8115
|
-
);
|
|
8116
|
-
}
|
|
8117
|
-
if (typeof candidate.worktreePath !== "string" || !import_node_path22.default.isAbsolute(candidate.worktreePath)) {
|
|
8118
|
-
throw new Error("invalid workspace ownership worktreePath");
|
|
8119
|
-
}
|
|
8120
|
-
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
8121
|
-
throw new Error("invalid workspace ownership branch");
|
|
8122
|
-
}
|
|
8123
|
-
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
8124
|
-
return {
|
|
8125
|
-
version: OWNERSHIP_VERSION,
|
|
8126
|
-
workspace,
|
|
8127
|
-
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
8128
|
-
branch: candidate.branch,
|
|
8129
|
-
devpodId,
|
|
8130
|
-
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
8131
|
-
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
8132
|
-
};
|
|
8133
|
-
}
|
|
8134
|
-
function recordPath(repoPath, workspace) {
|
|
8135
|
-
return import_node_path22.default.join(
|
|
8136
|
-
ownershipDirectory(repoPath),
|
|
8137
|
-
`${validateWorkspace(workspace, "workspace")}.json`
|
|
8138
|
-
);
|
|
8139
|
-
}
|
|
8140
|
-
function readRecordFile(filePath, expectedWorkspace) {
|
|
8141
|
-
let parsed;
|
|
8142
|
-
try {
|
|
8143
|
-
parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
|
|
8144
|
-
} catch (error) {
|
|
8145
|
-
if (error instanceof SyntaxError) {
|
|
8146
|
-
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
8742
|
+
throw new Error(message);
|
|
8147
8743
|
}
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
}
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
try {
|
|
8168
|
-
entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
|
|
8169
|
-
} catch (error) {
|
|
8170
|
-
if (error.code === "ENOENT") return [];
|
|
8171
|
-
throw error;
|
|
8172
|
-
}
|
|
8173
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
8174
|
-
const workspace = entry.name.slice(0, -".json".length);
|
|
8175
|
-
validateWorkspace(workspace, "filename");
|
|
8176
|
-
return readRecordFile(import_node_path22.default.join(directory, entry.name), workspace);
|
|
8177
|
-
});
|
|
8178
|
-
}
|
|
8179
|
-
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
8180
|
-
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
8181
|
-
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
8182
|
-
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
8183
|
-
const filePath = import_node_path22.default.join(directory, `${workspace}.json`);
|
|
8184
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8185
|
-
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
8186
|
-
const existing = records.find((record2) => record2.workspace === workspace);
|
|
8187
|
-
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
8188
|
-
throw new Error(
|
|
8189
|
-
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
8190
|
-
);
|
|
8191
|
-
}
|
|
8192
|
-
const pathOwner = records.find(
|
|
8193
|
-
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
8194
|
-
);
|
|
8195
|
-
if (pathOwner) {
|
|
8196
|
-
throw new Error(
|
|
8197
|
-
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
8198
|
-
);
|
|
8199
|
-
}
|
|
8200
|
-
if (existing && existing.devpodId !== devpodId) {
|
|
8201
|
-
throw new Error(
|
|
8202
|
-
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
8203
|
-
);
|
|
8204
|
-
}
|
|
8205
|
-
const record = {
|
|
8206
|
-
version: OWNERSHIP_VERSION,
|
|
8207
|
-
workspace,
|
|
8208
|
-
worktreePath,
|
|
8209
|
-
branch: input2.branch ?? null,
|
|
8210
|
-
devpodId,
|
|
8211
|
-
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
8212
|
-
updatedAt: validateTimestamp(now, "updatedAt")
|
|
8213
|
-
};
|
|
8214
|
-
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
8215
|
-
`);
|
|
8216
|
-
return record;
|
|
8217
|
-
}
|
|
8218
|
-
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
8219
|
-
const filePath = import_node_path22.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
8220
|
-
try {
|
|
8221
|
-
import_node_fs22.default.rmSync(filePath);
|
|
8222
|
-
return true;
|
|
8223
|
-
} catch (error) {
|
|
8224
|
-
if (error.code === "ENOENT") return false;
|
|
8225
|
-
throw error;
|
|
8226
|
-
}
|
|
8227
|
-
}
|
|
8228
|
-
function sameOwnershipRecord(left, right) {
|
|
8229
|
-
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;
|
|
8744
|
+
try {
|
|
8745
|
+
const attached = listDevsyWorkspaces();
|
|
8746
|
+
devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
|
|
8747
|
+
if (!devsyId) {
|
|
8748
|
+
throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
|
|
8749
|
+
}
|
|
8750
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8751
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8752
|
+
if (ownership.status !== "owned") {
|
|
8753
|
+
throw new Error(
|
|
8754
|
+
`Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
|
|
8755
|
+
);
|
|
8756
|
+
}
|
|
8757
|
+
return devsyId;
|
|
8758
|
+
} catch (error) {
|
|
8759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8760
|
+
throw new DevsyStartPostconditionError(message);
|
|
8761
|
+
}
|
|
8762
|
+
});
|
|
8230
8763
|
}
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8764
|
+
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;
|
|
8765
|
+
var init_devsy_mutation = __esm({
|
|
8766
|
+
"src/core/devsy-mutation.ts"() {
|
|
8767
|
+
"use strict";
|
|
8768
|
+
import_node_child_process16 = require("child_process");
|
|
8769
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
8770
|
+
import_node_path22 = __toESM(require("path"));
|
|
8771
|
+
init_devsy_agent();
|
|
8772
|
+
init_devsy_workspaces();
|
|
8773
|
+
init_file_lock();
|
|
8774
|
+
init_managed_devsy_stop();
|
|
8775
|
+
init_router();
|
|
8776
|
+
DEVSY_MUTATION_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
|
|
8777
|
+
DEVSY_MUTATION_WAIT_MS = 18e5;
|
|
8778
|
+
DevsyStartPostconditionError = class extends Error {
|
|
8779
|
+
};
|
|
8780
|
+
AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
|
|
8781
|
+
DEVSY_STDERR_TAIL_BYTES = 8192;
|
|
8242
8782
|
}
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
const directory = ownershipDirectory(repoPath);
|
|
8249
|
-
import_node_fs22.default.mkdirSync(directory, { recursive: true });
|
|
8783
|
+
});
|
|
8784
|
+
|
|
8785
|
+
// src/core/devpod-mutation.ts
|
|
8786
|
+
function withMutationLock2(activity, target, operation) {
|
|
8787
|
+
import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8250
8788
|
return withFileLockSync(
|
|
8251
|
-
|
|
8789
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
8252
8790
|
{
|
|
8253
|
-
activity
|
|
8254
|
-
target: `'${
|
|
8255
|
-
waitMs:
|
|
8791
|
+
activity,
|
|
8792
|
+
target: `'${target}'`,
|
|
8793
|
+
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
8794
|
+
fair: true,
|
|
8795
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8256
8796
|
},
|
|
8257
|
-
|
|
8258
|
-
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8259
|
-
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8260
|
-
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8261
|
-
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8262
|
-
})
|
|
8797
|
+
operation
|
|
8263
8798
|
);
|
|
8264
8799
|
}
|
|
8265
|
-
function
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8800
|
+
function commandFailure3(result) {
|
|
8801
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
8802
|
+
}
|
|
8803
|
+
function runDevpodAction(action2, devpodId, force = false) {
|
|
8804
|
+
const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
|
|
8805
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
8806
|
+
if (result.status !== 0) {
|
|
8270
8807
|
throw new Error(
|
|
8271
|
-
`
|
|
8808
|
+
`devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
|
|
8272
8809
|
);
|
|
8273
8810
|
}
|
|
8274
|
-
return owners[0];
|
|
8275
|
-
}
|
|
8276
|
-
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
8277
|
-
const owners = /* @__PURE__ */ new Map();
|
|
8278
|
-
for (const worktree of listGitWorktrees(repoPath)) {
|
|
8279
|
-
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
8280
|
-
let workspace;
|
|
8281
|
-
try {
|
|
8282
|
-
workspace = readPersistedWorkspace(worktree.path);
|
|
8283
|
-
} catch (error) {
|
|
8284
|
-
if (!import_node_fs22.default.existsSync(worktree.path)) continue;
|
|
8285
|
-
throw error;
|
|
8286
|
-
}
|
|
8287
|
-
if (!workspace) continue;
|
|
8288
|
-
const existing = owners.get(workspace);
|
|
8289
|
-
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
8290
|
-
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
8291
|
-
}
|
|
8292
|
-
owners.set(workspace, worktree.path);
|
|
8293
|
-
}
|
|
8294
|
-
return owners;
|
|
8295
8811
|
}
|
|
8296
|
-
function
|
|
8297
|
-
const
|
|
8298
|
-
(
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
8302
|
-
}
|
|
8303
|
-
const providerOwner = providerWorkspaces.find(
|
|
8304
|
-
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
8812
|
+
function inspectExactOwnership2(devpodId, worktreePath) {
|
|
8813
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
8814
|
+
listDevpodWorkspaces(worktreePath),
|
|
8815
|
+
devpodId,
|
|
8816
|
+
worktreePath
|
|
8305
8817
|
);
|
|
8306
|
-
if (
|
|
8307
|
-
|
|
8308
|
-
}
|
|
8309
|
-
const persistedOwner = persistedOwners.get(workspace);
|
|
8310
|
-
if (persistedOwner) {
|
|
8311
|
-
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
8312
|
-
}
|
|
8313
|
-
return void 0;
|
|
8818
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8819
|
+
return ownership;
|
|
8314
8820
|
}
|
|
8315
|
-
function
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
if (exactRecords.length > 1) {
|
|
8324
|
-
throw new Error(
|
|
8325
|
-
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
8326
|
-
);
|
|
8821
|
+
function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
|
|
8822
|
+
return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
|
|
8823
|
+
const before = inspectExactOwnership2(devpodId, worktreePath);
|
|
8824
|
+
if (before.status === "absent") return { status: "absent" };
|
|
8825
|
+
runDevpodAction(action2, devpodId);
|
|
8826
|
+
let after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8827
|
+
if (action2 === "stop" && after.status !== "owned") {
|
|
8828
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
8327
8829
|
}
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
if (exactRecord) {
|
|
8332
|
-
if (persisted && persisted !== exactRecord.workspace) {
|
|
8830
|
+
if (action2 === "delete" && after.status === "owned") {
|
|
8831
|
+
const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
|
|
8832
|
+
if (runtime !== "not-found") {
|
|
8333
8833
|
throw new Error(
|
|
8334
|
-
`
|
|
8834
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
8335
8835
|
);
|
|
8336
8836
|
}
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
);
|
|
8837
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8838
|
+
if (after.status === "owned") {
|
|
8839
|
+
runDevpodAction("delete", devpodId, true);
|
|
8840
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8341
8841
|
}
|
|
8342
|
-
|
|
8343
|
-
exactRecord.workspace,
|
|
8344
|
-
exactRecord.devpodId,
|
|
8345
|
-
worktreePath,
|
|
8346
|
-
records,
|
|
8347
|
-
input2.providerWorkspaces,
|
|
8348
|
-
persistedOwners,
|
|
8349
|
-
exactRecord
|
|
8350
|
-
);
|
|
8351
|
-
if (conflict2) {
|
|
8842
|
+
if (after.status !== "absent") {
|
|
8352
8843
|
throw new Error(
|
|
8353
|
-
`
|
|
8844
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
8354
8845
|
);
|
|
8355
8846
|
}
|
|
8356
|
-
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
8357
|
-
return transaction.write({
|
|
8358
|
-
workspace: exactRecord.workspace,
|
|
8359
|
-
worktreePath,
|
|
8360
|
-
branch: input2.branch ?? null,
|
|
8361
|
-
devpodId: exactRecord.devpodId
|
|
8362
|
-
});
|
|
8363
8847
|
}
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8848
|
+
return { status: "changed" };
|
|
8849
|
+
});
|
|
8850
|
+
}
|
|
8851
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8852
|
+
try {
|
|
8853
|
+
resetWorkspaceRuntimeCaches();
|
|
8854
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8855
|
+
return stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8368
8856
|
}
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8857
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
8858
|
+
} finally {
|
|
8859
|
+
resetWorkspaceRuntimeCaches();
|
|
8860
|
+
}
|
|
8861
|
+
}
|
|
8862
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8863
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8864
|
+
const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8865
|
+
resetWorkspaceRuntimeCaches();
|
|
8866
|
+
return result2;
|
|
8867
|
+
}
|
|
8868
|
+
const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
8869
|
+
resetWorkspaceRuntimeCaches();
|
|
8870
|
+
return result;
|
|
8871
|
+
}
|
|
8872
|
+
async function startDevpodWorkspace(options) {
|
|
8873
|
+
if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
|
|
8874
|
+
try {
|
|
8875
|
+
const result = await startDevsyWorkspace({
|
|
8876
|
+
repoPath: options.repoPath,
|
|
8877
|
+
devsyId: options.devpodId,
|
|
8878
|
+
devcontainerPath: options.devcontainerPath,
|
|
8879
|
+
recreate: options.recreate,
|
|
8880
|
+
quiet: options.quiet,
|
|
8881
|
+
workspace: options.workspace,
|
|
8882
|
+
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
8883
|
+
});
|
|
8884
|
+
resetWorkspaceRuntimeCaches();
|
|
8885
|
+
return result;
|
|
8886
|
+
} catch (error) {
|
|
8887
|
+
if (error instanceof DevsyStartPostconditionError) {
|
|
8888
|
+
resetWorkspaceRuntimeCaches();
|
|
8889
|
+
throw new DevpodStartPostconditionError(error.message);
|
|
8376
8890
|
}
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
);
|
|
8387
|
-
if (
|
|
8388
|
-
throw new Error(
|
|
8389
|
-
`Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
|
|
8390
|
-
);
|
|
8891
|
+
throw error;
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
8895
|
+
return withMutationLock2(activity, options.repoPath, () => {
|
|
8896
|
+
const workspaces = listDevpodWorkspaces(options.repoPath);
|
|
8897
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
8898
|
+
if (devpodId) {
|
|
8899
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
8900
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
8901
|
+
if (options.recreate && before.status !== "owned") {
|
|
8902
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
8391
8903
|
}
|
|
8392
|
-
|
|
8393
|
-
|
|
8904
|
+
} else if (options.recreate) {
|
|
8905
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
8394
8906
|
}
|
|
8395
|
-
const
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8907
|
+
const args = ["up", options.repoPath];
|
|
8908
|
+
if (devpodId) args.push("--id", devpodId);
|
|
8909
|
+
if (options.devcontainerPath) {
|
|
8910
|
+
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8911
|
+
}
|
|
8912
|
+
args.push("--open-ide=false");
|
|
8913
|
+
if (options.workspace) {
|
|
8914
|
+
args.push(
|
|
8915
|
+
"--workspace-env",
|
|
8916
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
8917
|
+
"--workspace-env",
|
|
8918
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
8406
8919
|
);
|
|
8407
8920
|
}
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8921
|
+
if (options.recreate) args.push("--recreate");
|
|
8922
|
+
const env = { ...process.env };
|
|
8923
|
+
if (options.workspace) {
|
|
8924
|
+
env.WORKSPACE = options.workspace.token;
|
|
8925
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
8926
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
8927
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
8928
|
+
} else {
|
|
8929
|
+
delete env.WORKSPACE;
|
|
8930
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
8931
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
8932
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
8933
|
+
}
|
|
8934
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
|
|
8935
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
8936
|
+
env
|
|
8413
8937
|
});
|
|
8938
|
+
if (result.status !== 0) {
|
|
8939
|
+
throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
|
|
8940
|
+
}
|
|
8414
8941
|
try {
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8942
|
+
const attached = listDevpodWorkspaces(options.repoPath);
|
|
8943
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
8944
|
+
if (!devpodId) {
|
|
8945
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
8946
|
+
}
|
|
8947
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
8948
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8949
|
+
if (ownership.status !== "owned") {
|
|
8420
8950
|
throw new Error(
|
|
8421
|
-
`
|
|
8951
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
8422
8952
|
);
|
|
8423
8953
|
}
|
|
8424
|
-
|
|
8954
|
+
resetWorkspaceRuntimeCaches();
|
|
8955
|
+
return devpodId;
|
|
8956
|
+
} catch (error) {
|
|
8957
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8958
|
+
throw new DevpodStartPostconditionError(message);
|
|
8425
8959
|
}
|
|
8426
|
-
return written;
|
|
8427
8960
|
});
|
|
8428
8961
|
}
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
(transaction) => transaction.remove(workspace)
|
|
8433
|
-
);
|
|
8434
|
-
}
|
|
8435
|
-
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
8436
|
-
const worktree = worktrees.find(
|
|
8437
|
-
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
8438
|
-
);
|
|
8439
|
-
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
8440
|
-
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
8441
|
-
if (devpodStatus === "conflict") {
|
|
8442
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8443
|
-
}
|
|
8444
|
-
if (worktree?.locked) {
|
|
8445
|
-
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
8446
|
-
}
|
|
8447
|
-
if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record.worktreePath)) {
|
|
8448
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8449
|
-
}
|
|
8450
|
-
if (!worktree || worktree.prunable) {
|
|
8451
|
-
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
8452
|
-
}
|
|
8453
|
-
let persisted;
|
|
8454
|
-
try {
|
|
8455
|
-
persisted = readPersistedWorkspace(worktree.path);
|
|
8456
|
-
} catch {
|
|
8457
|
-
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8458
|
-
}
|
|
8459
|
-
return {
|
|
8460
|
-
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
8461
|
-
devpodStatus,
|
|
8462
|
-
worktree
|
|
8463
|
-
};
|
|
8464
|
-
}
|
|
8465
|
-
function listMissingWorkspaceOwnership(repoPath) {
|
|
8466
|
-
const worktrees = listGitWorktrees(repoPath);
|
|
8467
|
-
return listWorkspaceOwnership(repoPath).filter(
|
|
8468
|
-
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
8469
|
-
);
|
|
8470
|
-
}
|
|
8471
|
-
var import_node_child_process17, import_node_fs22, import_node_path22, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
8472
|
-
var init_workspace_ownership = __esm({
|
|
8473
|
-
"src/core/workspace-ownership.ts"() {
|
|
8962
|
+
var import_node_child_process17, import_node_fs23, import_node_path23, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
8963
|
+
var init_devpod_mutation = __esm({
|
|
8964
|
+
"src/core/devpod-mutation.ts"() {
|
|
8474
8965
|
"use strict";
|
|
8475
8966
|
import_node_child_process17 = require("child_process");
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
init_atomic_file();
|
|
8967
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
8968
|
+
import_node_path23 = __toESM(require("path"));
|
|
8479
8969
|
init_devpod_workspaces();
|
|
8970
|
+
init_devsy_mutation();
|
|
8480
8971
|
init_file_lock();
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8972
|
+
init_router();
|
|
8973
|
+
init_workspace_runtime();
|
|
8974
|
+
DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8975
|
+
DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8976
|
+
DevpodStartPostconditionError = class extends Error {
|
|
8977
|
+
};
|
|
8485
8978
|
}
|
|
8486
8979
|
});
|
|
8487
8980
|
|
|
@@ -8495,10 +8988,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
8495
8988
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
8496
8989
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
8497
8990
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
8498
|
-
const localRoot =
|
|
8991
|
+
const localRoot = import_node_path24.default.join(comparableRepo, "trees") + import_node_path24.default.sep;
|
|
8499
8992
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
8500
|
-
const legacyPrefix = `${
|
|
8501
|
-
return
|
|
8993
|
+
const legacyPrefix = `${import_node_path24.default.basename(comparableRepo)}-`;
|
|
8994
|
+
return import_node_path24.default.dirname(comparableWorktree) === import_node_path24.default.dirname(comparableRepo) && import_node_path24.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
8502
8995
|
}
|
|
8503
8996
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
8504
8997
|
const actions = [
|
|
@@ -8750,11 +9243,11 @@ function applyWorkspaceGc(plan) {
|
|
|
8750
9243
|
candidates
|
|
8751
9244
|
};
|
|
8752
9245
|
}
|
|
8753
|
-
var
|
|
9246
|
+
var import_node_path24;
|
|
8754
9247
|
var init_workspace_gc = __esm({
|
|
8755
9248
|
"src/core/workspace-gc.ts"() {
|
|
8756
9249
|
"use strict";
|
|
8757
|
-
|
|
9250
|
+
import_node_path24 = __toESM(require("path"));
|
|
8758
9251
|
init_devpod_mutation();
|
|
8759
9252
|
init_devpod_workspaces();
|
|
8760
9253
|
init_host_routes();
|
|
@@ -8824,11 +9317,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
8824
9317
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
8825
9318
|
continue;
|
|
8826
9319
|
}
|
|
8827
|
-
if (!
|
|
9320
|
+
if (!import_node_fs24.default.existsSync(absolutePath)) {
|
|
8828
9321
|
continue;
|
|
8829
9322
|
}
|
|
8830
9323
|
try {
|
|
8831
|
-
const raw =
|
|
9324
|
+
const raw = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
|
|
8832
9325
|
const parsed = import_yaml5.default.parse(raw);
|
|
8833
9326
|
const root = asRecord2(parsed);
|
|
8834
9327
|
const services = asRecord2(root?.services);
|
|
@@ -9107,7 +9600,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
9107
9600
|
const config = runtimeConfig.config;
|
|
9108
9601
|
loadedConfig = config;
|
|
9109
9602
|
loadedWorkspace = runtimeConfig.workspace;
|
|
9110
|
-
const cliVersion = true ? "0.0.
|
|
9603
|
+
const cliVersion = true ? "0.0.54" : "0.0.0-dev";
|
|
9111
9604
|
const configVersion = config.devrouter?.version;
|
|
9112
9605
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
9113
9606
|
addCheck(checks, {
|
|
@@ -9137,9 +9630,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
9137
9630
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
9138
9631
|
app: app.name,
|
|
9139
9632
|
filePath,
|
|
9140
|
-
absolutePath:
|
|
9633
|
+
absolutePath: import_node_path25.default.resolve(repo.path, filePath)
|
|
9141
9634
|
}))
|
|
9142
|
-
).filter((entry) => !
|
|
9635
|
+
).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9143
9636
|
addCheck(checks, {
|
|
9144
9637
|
id: "repo.compose-files",
|
|
9145
9638
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -9182,8 +9675,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
9182
9675
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
9183
9676
|
app: app.name,
|
|
9184
9677
|
cwd: app.hostRun.cwd,
|
|
9185
|
-
absolutePath:
|
|
9186
|
-
})).filter((entry) => !
|
|
9678
|
+
absolutePath: import_node_path25.default.resolve(repo.path, app.hostRun.cwd)
|
|
9679
|
+
})).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9187
9680
|
addCheck(checks, {
|
|
9188
9681
|
id: "repo.host-cwd",
|
|
9189
9682
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -9327,12 +9820,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
9327
9820
|
nextSteps
|
|
9328
9821
|
};
|
|
9329
9822
|
}
|
|
9330
|
-
var
|
|
9823
|
+
var import_node_fs24, import_node_path25, import_yaml5, POSTGRES_DEFAULTS;
|
|
9331
9824
|
var init_doctor = __esm({
|
|
9332
9825
|
"src/core/doctor.ts"() {
|
|
9333
9826
|
"use strict";
|
|
9334
|
-
|
|
9335
|
-
|
|
9827
|
+
import_node_fs24 = __toESM(require("fs"));
|
|
9828
|
+
import_node_path25 = __toESM(require("path"));
|
|
9336
9829
|
import_yaml5 = __toESM(require("yaml"));
|
|
9337
9830
|
init_docker();
|
|
9338
9831
|
init_host_routes();
|
|
@@ -9779,76 +10272,6 @@ var init_http_route_probe = __esm({
|
|
|
9779
10272
|
}
|
|
9780
10273
|
});
|
|
9781
10274
|
|
|
9782
|
-
// src/core/route-publication.ts
|
|
9783
|
-
function routedAppsFromConfig(config) {
|
|
9784
|
-
return config.apps.filter((app) => app.kind !== "dependency");
|
|
9785
|
-
}
|
|
9786
|
-
function configuredProxyAppsFromConfig(config) {
|
|
9787
|
-
return routedAppsFromConfig(config).filter(
|
|
9788
|
-
(app) => app.runtime === "proxy"
|
|
9789
|
-
);
|
|
9790
|
-
}
|
|
9791
|
-
function proxyAppsFromConfig(config) {
|
|
9792
|
-
const routedApps = routedAppsFromConfig(config);
|
|
9793
|
-
const proxyApps = configuredProxyAppsFromConfig(config);
|
|
9794
|
-
const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
|
|
9795
|
-
if (unsupported.length > 0) {
|
|
9796
|
-
throw new Error(
|
|
9797
|
-
`Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
|
|
9798
|
-
);
|
|
9799
|
-
}
|
|
9800
|
-
return proxyApps;
|
|
9801
|
-
}
|
|
9802
|
-
async function ensureRouteInfrastructure(apps, options = {}) {
|
|
9803
|
-
ensureRouterFiles();
|
|
9804
|
-
await ensureNetwork(DEVNET_NAME);
|
|
9805
|
-
const tlsCoverage = await ensureTLSHostsCovered(
|
|
9806
|
-
apps.map((app) => app.host),
|
|
9807
|
-
options
|
|
9808
|
-
);
|
|
9809
|
-
for (const app of apps) {
|
|
9810
|
-
if (app.protocol === "tcp") {
|
|
9811
|
-
if (!isTLSEnabled()) {
|
|
9812
|
-
throw new Error(
|
|
9813
|
-
`TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
9814
|
-
);
|
|
9815
|
-
}
|
|
9816
|
-
activateTcpProtocol(app.tcpProtocol);
|
|
9817
|
-
}
|
|
9818
|
-
}
|
|
9819
|
-
startRouterStack();
|
|
9820
|
-
return tlsCoverage;
|
|
9821
|
-
}
|
|
9822
|
-
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
9823
|
-
const apps = proxyAppsFromConfig(config);
|
|
9824
|
-
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
9825
|
-
const routes = apps.map((app) => {
|
|
9826
|
-
const upstream = parseUpstream(app.upstream);
|
|
9827
|
-
return {
|
|
9828
|
-
name: app.name,
|
|
9829
|
-
host: app.host,
|
|
9830
|
-
protocol: app.protocol,
|
|
9831
|
-
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
9832
|
-
repoPath,
|
|
9833
|
-
port: upstream.port,
|
|
9834
|
-
mode: "proxy",
|
|
9835
|
-
upstreamHost: upstream.upstreamHost,
|
|
9836
|
-
workspace
|
|
9837
|
-
};
|
|
9838
|
-
});
|
|
9839
|
-
replaceHostRoutesForRepo(repoPath, routes);
|
|
9840
|
-
return { routes, tlsRefreshed: tlsCoverage.refreshed };
|
|
9841
|
-
}
|
|
9842
|
-
var init_route_publication = __esm({
|
|
9843
|
-
"src/core/route-publication.ts"() {
|
|
9844
|
-
"use strict";
|
|
9845
|
-
init_docker();
|
|
9846
|
-
init_host_routes();
|
|
9847
|
-
init_router();
|
|
9848
|
-
init_tls();
|
|
9849
|
-
}
|
|
9850
|
-
});
|
|
9851
|
-
|
|
9852
10275
|
// src/core/traefik-route-health.ts
|
|
9853
10276
|
function sleep(ms) {
|
|
9854
10277
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -9893,7 +10316,7 @@ function inspectRouterApi(protocol) {
|
|
|
9893
10316
|
)
|
|
9894
10317
|
};
|
|
9895
10318
|
}
|
|
9896
|
-
function
|
|
10319
|
+
function isRecord3(value) {
|
|
9897
10320
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9898
10321
|
}
|
|
9899
10322
|
function inspectNamedRouterApi(protocol, resource, name) {
|
|
@@ -9924,7 +10347,7 @@ function inspectNamedRouterApi(protocol, resource, name) {
|
|
|
9924
10347
|
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
9925
10348
|
};
|
|
9926
10349
|
}
|
|
9927
|
-
if (!
|
|
10350
|
+
if (!isRecord3(value)) {
|
|
9928
10351
|
return {
|
|
9929
10352
|
ok: false,
|
|
9930
10353
|
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
@@ -9953,19 +10376,19 @@ function renderExpectedRoute(route) {
|
|
|
9953
10376
|
};
|
|
9954
10377
|
const document = buildHostRoutesDocument([routeState], false);
|
|
9955
10378
|
const protocolDocument = document[protocol];
|
|
9956
|
-
if (!
|
|
10379
|
+
if (!isRecord3(protocolDocument)) return void 0;
|
|
9957
10380
|
const routers = protocolDocument.routers;
|
|
9958
10381
|
const services = protocolDocument.services;
|
|
9959
|
-
if (!
|
|
10382
|
+
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
9960
10383
|
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
9961
10384
|
const router = routers[routerKey];
|
|
9962
10385
|
const service = services[routerKey];
|
|
9963
|
-
if (!
|
|
10386
|
+
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
9964
10387
|
const rule = router.rule;
|
|
9965
10388
|
const serviceReference = router.service;
|
|
9966
10389
|
const loadBalancer = service.loadBalancer;
|
|
9967
|
-
const servers =
|
|
9968
|
-
if (typeof rule !== "string" || serviceReference !== routerKey || !
|
|
10390
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10391
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
9969
10392
|
return void 0;
|
|
9970
10393
|
}
|
|
9971
10394
|
const serverField = protocol === "http" ? "url" : "address";
|
|
@@ -10009,9 +10432,9 @@ function inspectExpectedRouteMatch(route) {
|
|
|
10009
10432
|
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
10010
10433
|
}
|
|
10011
10434
|
const loadBalancer = serviceResult.value.loadBalancer;
|
|
10012
|
-
const servers =
|
|
10435
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10013
10436
|
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
10014
|
-
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !
|
|
10437
|
+
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) {
|
|
10015
10438
|
return {
|
|
10016
10439
|
ok: false,
|
|
10017
10440
|
routes: [routeName],
|
|
@@ -10108,7 +10531,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
10108
10531
|
throw new Error(
|
|
10109
10532
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
10110
10533
|
);
|
|
10111
|
-
|
|
10534
|
+
import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
10112
10535
|
return withFileLock(
|
|
10113
10536
|
ROUTER_RELOAD_LOCK_FILE,
|
|
10114
10537
|
{
|
|
@@ -10152,18 +10575,18 @@ async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
|
10152
10575
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
10153
10576
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
10154
10577
|
}
|
|
10155
|
-
var import_node_child_process20,
|
|
10578
|
+
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;
|
|
10156
10579
|
var init_traefik_route_health = __esm({
|
|
10157
10580
|
"src/core/traefik-route-health.ts"() {
|
|
10158
10581
|
"use strict";
|
|
10159
10582
|
import_node_child_process20 = require("child_process");
|
|
10160
|
-
|
|
10161
|
-
|
|
10583
|
+
import_node_fs25 = __toESM(require("fs"));
|
|
10584
|
+
import_node_path26 = __toESM(require("path"));
|
|
10162
10585
|
init_file_lock();
|
|
10163
10586
|
init_host_routes();
|
|
10164
10587
|
init_router();
|
|
10165
10588
|
ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
10166
|
-
ROUTER_RELOAD_LOCK_FILE =
|
|
10589
|
+
ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
10167
10590
|
ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
10168
10591
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
10169
10592
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
@@ -10175,11 +10598,11 @@ var init_traefik_route_health = __esm({
|
|
|
10175
10598
|
// src/core/workspace-ensure.ts
|
|
10176
10599
|
function assertOverlay(container, repoPath) {
|
|
10177
10600
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
10178
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
10601
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
|
|
10179
10602
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
10180
10603
|
}
|
|
10181
10604
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
10182
|
-
const expectedOverlay =
|
|
10605
|
+
const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
10183
10606
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
10184
10607
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
10185
10608
|
}
|
|
@@ -10369,7 +10792,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
10369
10792
|
}).some((container) => {
|
|
10370
10793
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
10371
10794
|
return Boolean(
|
|
10372
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
10795
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
|
|
10373
10796
|
);
|
|
10374
10797
|
});
|
|
10375
10798
|
} catch {
|
|
@@ -10726,7 +11149,7 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
10726
11149
|
}
|
|
10727
11150
|
function isPrimaryCheckout(repoPath) {
|
|
10728
11151
|
try {
|
|
10729
|
-
return
|
|
11152
|
+
return import_node_fs26.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
|
|
10730
11153
|
} catch {
|
|
10731
11154
|
return false;
|
|
10732
11155
|
}
|
|
@@ -10784,8 +11207,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10784
11207
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10785
11208
|
let devpodId = target.devpodId;
|
|
10786
11209
|
if (target.kind === "linked") {
|
|
10787
|
-
const overlayPath =
|
|
10788
|
-
if (!
|
|
11210
|
+
const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
11211
|
+
if (!import_node_fs26.default.existsSync(overlayPath)) {
|
|
10789
11212
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
10790
11213
|
}
|
|
10791
11214
|
}
|
|
@@ -10859,7 +11282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10859
11282
|
}
|
|
10860
11283
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
10861
11284
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
10862
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
11285
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
|
|
10863
11286
|
for (const [index, app] of apps.entries()) {
|
|
10864
11287
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
10865
11288
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -11427,9 +11850,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11427
11850
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11428
11851
|
if (!options.repair) return ensureLocked();
|
|
11429
11852
|
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
11430
|
-
|
|
11853
|
+
import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11431
11854
|
return withFileLock(
|
|
11432
|
-
|
|
11855
|
+
import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
11433
11856
|
{
|
|
11434
11857
|
activity: "Managed runtime repair",
|
|
11435
11858
|
target: repoPath,
|
|
@@ -11441,13 +11864,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11441
11864
|
);
|
|
11442
11865
|
});
|
|
11443
11866
|
}
|
|
11444
|
-
var import_node_child_process21,
|
|
11867
|
+
var import_node_child_process21, import_node_fs26, import_node_path27, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
11445
11868
|
var init_workspace_ensure = __esm({
|
|
11446
11869
|
"src/core/workspace-ensure.ts"() {
|
|
11447
11870
|
"use strict";
|
|
11448
11871
|
import_node_child_process21 = require("child_process");
|
|
11449
|
-
|
|
11450
|
-
|
|
11872
|
+
import_node_fs26 = __toESM(require("fs"));
|
|
11873
|
+
import_node_path27 = __toESM(require("path"));
|
|
11451
11874
|
init_devcontainer_profile();
|
|
11452
11875
|
init_devpod_environment();
|
|
11453
11876
|
init_devpod_mutation();
|
|
@@ -11536,7 +11959,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
11536
11959
|
);
|
|
11537
11960
|
}
|
|
11538
11961
|
function defaultWorktreePath(mainRepo, ws) {
|
|
11539
|
-
return
|
|
11962
|
+
return import_node_path28.default.join(mainRepo, "trees", ws);
|
|
11540
11963
|
}
|
|
11541
11964
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
11542
11965
|
const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
@@ -11544,12 +11967,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
|
11544
11967
|
});
|
|
11545
11968
|
if (ignored.status !== 0) {
|
|
11546
11969
|
throw new Error(
|
|
11547
|
-
`Default worktree root '${
|
|
11970
|
+
`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.`
|
|
11548
11971
|
);
|
|
11549
11972
|
}
|
|
11550
11973
|
}
|
|
11551
11974
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
11552
|
-
return
|
|
11975
|
+
return import_node_path28.default.join(import_node_path28.default.dirname(mainRepo), `${import_node_path28.default.basename(mainRepo)}-${ws}`);
|
|
11553
11976
|
}
|
|
11554
11977
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
11555
11978
|
const candidates = [
|
|
@@ -11661,7 +12084,7 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
11661
12084
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
11662
12085
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
11663
12086
|
}
|
|
11664
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
12087
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs27.default.existsSync(target.worktreePath)) return;
|
|
11665
12088
|
if (target.worktree.locked) {
|
|
11666
12089
|
throw new Error(
|
|
11667
12090
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
@@ -11701,9 +12124,9 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11701
12124
|
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
11702
12125
|
);
|
|
11703
12126
|
const generatedPath = candidatePaths.find(
|
|
11704
|
-
(candidate) => !
|
|
12127
|
+
(candidate) => !import_node_fs27.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
11705
12128
|
);
|
|
11706
|
-
const selectedPath = opts.path ?
|
|
12129
|
+
const selectedPath = opts.path ? import_node_path28.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
11707
12130
|
if (!selectedPath) {
|
|
11708
12131
|
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
11709
12132
|
}
|
|
@@ -11712,7 +12135,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11712
12135
|
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
11713
12136
|
);
|
|
11714
12137
|
}
|
|
11715
|
-
if (
|
|
12138
|
+
if (import_node_fs27.default.existsSync(selectedPath)) {
|
|
11716
12139
|
const registered = worktrees.find(
|
|
11717
12140
|
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
11718
12141
|
);
|
|
@@ -11806,6 +12229,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
11806
12229
|
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
11807
12230
|
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
11808
12231
|
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
12232
|
+
if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
|
|
12233
|
+
throw new Error(
|
|
12234
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12235
|
+
);
|
|
12236
|
+
}
|
|
11809
12237
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
11810
12238
|
await ensureTraefikRoutesRemoved(routes);
|
|
11811
12239
|
if (!quiet) {
|
|
@@ -11838,7 +12266,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11838
12266
|
opts.quiet
|
|
11839
12267
|
);
|
|
11840
12268
|
if (removeWorktree) {
|
|
11841
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
12269
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath)) {
|
|
11842
12270
|
const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
11843
12271
|
encoding: "utf-8"
|
|
11844
12272
|
});
|
|
@@ -11857,7 +12285,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11857
12285
|
}
|
|
11858
12286
|
return result;
|
|
11859
12287
|
};
|
|
11860
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
12288
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
11861
12289
|
}
|
|
11862
12290
|
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
11863
12291
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
@@ -11892,15 +12320,16 @@ async function workspaceStop(target, opts = {}) {
|
|
|
11892
12320
|
async function workspaceDown(target, opts = {}) {
|
|
11893
12321
|
return runWorkspaceLifecycle("down", target, opts);
|
|
11894
12322
|
}
|
|
11895
|
-
var import_node_child_process22,
|
|
12323
|
+
var import_node_child_process22, import_node_fs27, import_node_path28, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
11896
12324
|
var init_workspace_lifecycle = __esm({
|
|
11897
12325
|
"src/core/workspace-lifecycle.ts"() {
|
|
11898
12326
|
"use strict";
|
|
11899
12327
|
import_node_child_process22 = require("child_process");
|
|
11900
|
-
|
|
11901
|
-
|
|
12328
|
+
import_node_fs27 = __toESM(require("fs"));
|
|
12329
|
+
import_node_path28 = __toESM(require("path"));
|
|
11902
12330
|
init_devpod_mutation();
|
|
11903
12331
|
init_devpod_workspaces();
|
|
12332
|
+
init_managed_runtime_state();
|
|
11904
12333
|
init_repo_config();
|
|
11905
12334
|
init_route_state();
|
|
11906
12335
|
init_traefik_route_health();
|
|
@@ -11952,6 +12381,11 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
11952
12381
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11953
12382
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
11954
12383
|
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
12384
|
+
if (!options.delete && mutation.status === "absent" && readManagedRuntimeState(repoPath, workspace)) {
|
|
12385
|
+
throw new Error(
|
|
12386
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12387
|
+
);
|
|
12388
|
+
}
|
|
11955
12389
|
const removedRoutes = removeHostRoutesWhere(
|
|
11956
12390
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
11957
12391
|
);
|
|
@@ -11973,6 +12407,7 @@ var init_environment_stop = __esm({
|
|
|
11973
12407
|
init_devpod_mutation();
|
|
11974
12408
|
init_devpod_workspaces();
|
|
11975
12409
|
init_host_routes();
|
|
12410
|
+
init_managed_runtime_state();
|
|
11976
12411
|
init_traefik_route_health();
|
|
11977
12412
|
init_workspace();
|
|
11978
12413
|
init_workspace_lifecycle();
|
|
@@ -12470,17 +12905,17 @@ function asRecord3(value) {
|
|
|
12470
12905
|
return value;
|
|
12471
12906
|
}
|
|
12472
12907
|
function readJson(filePath) {
|
|
12473
|
-
if (!
|
|
12908
|
+
if (!import_node_fs28.default.existsSync(filePath)) {
|
|
12474
12909
|
return void 0;
|
|
12475
12910
|
}
|
|
12476
12911
|
try {
|
|
12477
|
-
return JSON.parse(
|
|
12912
|
+
return JSON.parse(import_node_fs28.default.readFileSync(filePath, "utf-8"));
|
|
12478
12913
|
} catch {
|
|
12479
12914
|
return void 0;
|
|
12480
12915
|
}
|
|
12481
12916
|
}
|
|
12482
12917
|
function relative(repoPath, filePath) {
|
|
12483
|
-
return
|
|
12918
|
+
return import_node_path29.default.relative(repoPath, filePath) || ".";
|
|
12484
12919
|
}
|
|
12485
12920
|
function redactEnvAssignments(value) {
|
|
12486
12921
|
return value.replace(
|
|
@@ -12521,7 +12956,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
12521
12956
|
["bun.lock", "bun"]
|
|
12522
12957
|
];
|
|
12523
12958
|
for (const [fileName, name] of lockfiles) {
|
|
12524
|
-
if (
|
|
12959
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
12525
12960
|
return { name, source: fileName };
|
|
12526
12961
|
}
|
|
12527
12962
|
}
|
|
@@ -12536,9 +12971,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
12536
12971
|
if (typeof engines?.node === "string") {
|
|
12537
12972
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
12538
12973
|
}
|
|
12539
|
-
const nvmrc =
|
|
12540
|
-
if (
|
|
12541
|
-
const version =
|
|
12974
|
+
const nvmrc = import_node_path29.default.join(repoPath, ".nvmrc");
|
|
12975
|
+
if (import_node_fs28.default.existsSync(nvmrc)) {
|
|
12976
|
+
const version = import_node_fs28.default.readFileSync(nvmrc, "utf-8").trim();
|
|
12542
12977
|
return { version, source: ".nvmrc" };
|
|
12543
12978
|
}
|
|
12544
12979
|
return void 0;
|
|
@@ -12599,7 +13034,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
12599
13034
|
const files = config.apps.filter(
|
|
12600
13035
|
(app) => app.runtime === "docker"
|
|
12601
13036
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
12602
|
-
(fileName) => !
|
|
13037
|
+
(fileName) => !import_node_path29.default.isAbsolute(fileName) && !import_node_path29.default.normalize(fileName).startsWith("..")
|
|
12603
13038
|
);
|
|
12604
13039
|
return Array.from(new Set(files));
|
|
12605
13040
|
} catch {
|
|
@@ -12617,7 +13052,7 @@ function composeFiles(repoPath) {
|
|
|
12617
13052
|
...configuredComposeFiles(repoPath)
|
|
12618
13053
|
];
|
|
12619
13054
|
return Array.from(new Set(candidates)).filter(
|
|
12620
|
-
(fileName) =>
|
|
13055
|
+
(fileName) => import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))
|
|
12621
13056
|
);
|
|
12622
13057
|
}
|
|
12623
13058
|
function stringArray2(value) {
|
|
@@ -12655,7 +13090,7 @@ function inspectServices(repoPath) {
|
|
|
12655
13090
|
const services = [];
|
|
12656
13091
|
for (const fileName of composeFiles(repoPath)) {
|
|
12657
13092
|
try {
|
|
12658
|
-
const parsed = import_yaml6.default.parse(
|
|
13093
|
+
const parsed = import_yaml6.default.parse(import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8"));
|
|
12659
13094
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
12660
13095
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
12661
13096
|
const service = asRecord3(value);
|
|
@@ -12690,8 +13125,8 @@ function inspectServices(repoPath) {
|
|
|
12690
13125
|
return services;
|
|
12691
13126
|
}
|
|
12692
13127
|
function inspectEnvFiles(repoPath) {
|
|
12693
|
-
const files =
|
|
12694
|
-
const content =
|
|
13128
|
+
const files = import_node_fs28.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
13129
|
+
const content = import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8");
|
|
12695
13130
|
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();
|
|
12696
13131
|
return { path: fileName, names };
|
|
12697
13132
|
});
|
|
@@ -12705,18 +13140,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
12705
13140
|
};
|
|
12706
13141
|
}
|
|
12707
13142
|
function inspectDevcontainer(repoPath) {
|
|
12708
|
-
const dir =
|
|
12709
|
-
if (!
|
|
13143
|
+
const dir = import_node_path29.default.join(repoPath, ".devcontainer");
|
|
13144
|
+
if (!import_node_fs28.default.existsSync(dir)) {
|
|
12710
13145
|
return { exists: false, files: [] };
|
|
12711
13146
|
}
|
|
12712
13147
|
return {
|
|
12713
13148
|
exists: true,
|
|
12714
|
-
files:
|
|
13149
|
+
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}`)
|
|
12715
13150
|
};
|
|
12716
13151
|
}
|
|
12717
13152
|
function inspectDevrouter(repoPath) {
|
|
12718
13153
|
const configPath = getRepoConfigPath(repoPath);
|
|
12719
|
-
if (!
|
|
13154
|
+
if (!import_node_fs28.default.existsSync(configPath)) {
|
|
12720
13155
|
return {
|
|
12721
13156
|
exists: false,
|
|
12722
13157
|
configPath,
|
|
@@ -12760,15 +13195,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
12760
13195
|
["AGENTS.md", "agents"],
|
|
12761
13196
|
["CLAUDE.md", "claude"]
|
|
12762
13197
|
]) {
|
|
12763
|
-
if (
|
|
13198
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
12764
13199
|
results.push({ path: fileName, kind });
|
|
12765
13200
|
}
|
|
12766
13201
|
}
|
|
12767
|
-
const skillsDir =
|
|
12768
|
-
if (
|
|
12769
|
-
for (const name of
|
|
12770
|
-
const skillPath =
|
|
12771
|
-
if (
|
|
13202
|
+
const skillsDir = import_node_path29.default.join(repoPath, ".agents", "skills");
|
|
13203
|
+
if (import_node_fs28.default.existsSync(skillsDir)) {
|
|
13204
|
+
for (const name of import_node_fs28.default.readdirSync(skillsDir).sort()) {
|
|
13205
|
+
const skillPath = import_node_path29.default.join(skillsDir, name, "SKILL.md");
|
|
13206
|
+
if (import_node_fs28.default.existsSync(skillPath)) {
|
|
12772
13207
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
12773
13208
|
}
|
|
12774
13209
|
}
|
|
@@ -12811,7 +13246,7 @@ function buildIssues(report) {
|
|
|
12811
13246
|
}
|
|
12812
13247
|
function inspectRepo(options = {}) {
|
|
12813
13248
|
const repoPath = resolveRepoPath(options.repo);
|
|
12814
|
-
const pkg = readJson(
|
|
13249
|
+
const pkg = readJson(import_node_path29.default.join(repoPath, "package.json"));
|
|
12815
13250
|
const scripts = inspectScripts(pkg);
|
|
12816
13251
|
const reportWithoutIssues = {
|
|
12817
13252
|
repoPath,
|
|
@@ -12830,12 +13265,12 @@ function inspectRepo(options = {}) {
|
|
|
12830
13265
|
issues: buildIssues(reportWithoutIssues)
|
|
12831
13266
|
};
|
|
12832
13267
|
}
|
|
12833
|
-
var
|
|
13268
|
+
var import_node_fs28, import_node_path29, import_yaml6;
|
|
12834
13269
|
var init_repo_inspect = __esm({
|
|
12835
13270
|
"src/core/repo-inspect.ts"() {
|
|
12836
13271
|
"use strict";
|
|
12837
|
-
|
|
12838
|
-
|
|
13272
|
+
import_node_fs28 = __toESM(require("fs"));
|
|
13273
|
+
import_node_path29 = __toESM(require("path"));
|
|
12839
13274
|
import_yaml6 = __toESM(require("yaml"));
|
|
12840
13275
|
init_repo_config();
|
|
12841
13276
|
}
|
|
@@ -12940,7 +13375,7 @@ function requiredFileChecks(repoPath) {
|
|
|
12940
13375
|
".devcontainer/docker-compose.yml",
|
|
12941
13376
|
".devrouter.yml"
|
|
12942
13377
|
];
|
|
12943
|
-
const missing = required.filter((fileName) => !
|
|
13378
|
+
const missing = required.filter((fileName) => !import_node_fs29.default.existsSync(import_node_path30.default.join(repoPath, fileName)));
|
|
12944
13379
|
return {
|
|
12945
13380
|
id: "repo.devcontainer.verify-files",
|
|
12946
13381
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -13159,12 +13594,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
13159
13594
|
nextSteps: collectNextSteps3(checks)
|
|
13160
13595
|
};
|
|
13161
13596
|
}
|
|
13162
|
-
var
|
|
13597
|
+
var import_node_fs29, import_node_path30;
|
|
13163
13598
|
var init_devcontainer_verify = __esm({
|
|
13164
13599
|
"src/core/devcontainer-verify.ts"() {
|
|
13165
13600
|
"use strict";
|
|
13166
|
-
|
|
13167
|
-
|
|
13601
|
+
import_node_fs29 = __toESM(require("fs"));
|
|
13602
|
+
import_node_path30 = __toESM(require("path"));
|
|
13168
13603
|
init_capabilities();
|
|
13169
13604
|
init_doctor();
|
|
13170
13605
|
init_host_routes();
|
|
@@ -13461,7 +13896,7 @@ function packageManagerIssues(repo) {
|
|
|
13461
13896
|
}
|
|
13462
13897
|
function plannedFiles(repoPath, version) {
|
|
13463
13898
|
const repo = inspectRepo({ repo: repoPath });
|
|
13464
|
-
const projectName = sanitizeProjectName(
|
|
13899
|
+
const projectName = sanitizeProjectName(import_node_path31.default.basename(repo.repoPath));
|
|
13465
13900
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
13466
13901
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
13467
13902
|
const port = inferPort2(repo);
|
|
@@ -13506,8 +13941,8 @@ function plannedFiles(repoPath, version) {
|
|
|
13506
13941
|
};
|
|
13507
13942
|
}
|
|
13508
13943
|
function classifyFile(repoPath, file) {
|
|
13509
|
-
const absolutePath =
|
|
13510
|
-
if (!
|
|
13944
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
13945
|
+
if (!import_node_fs30.default.existsSync(absolutePath)) {
|
|
13511
13946
|
return {
|
|
13512
13947
|
path: file.relativePath,
|
|
13513
13948
|
action: "create",
|
|
@@ -13515,7 +13950,7 @@ function classifyFile(repoPath, file) {
|
|
|
13515
13950
|
bytes: Buffer.byteLength(file.content)
|
|
13516
13951
|
};
|
|
13517
13952
|
}
|
|
13518
|
-
const current =
|
|
13953
|
+
const current = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
|
|
13519
13954
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
13520
13955
|
return {
|
|
13521
13956
|
path: file.relativePath,
|
|
@@ -13572,11 +14007,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
13572
14007
|
};
|
|
13573
14008
|
}
|
|
13574
14009
|
function writeFile(repoPath, file) {
|
|
13575
|
-
const absolutePath =
|
|
13576
|
-
|
|
13577
|
-
|
|
14010
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
14011
|
+
import_node_fs30.default.mkdirSync(import_node_path31.default.dirname(absolutePath), { recursive: true });
|
|
14012
|
+
import_node_fs30.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
13578
14013
|
if (file.executable) {
|
|
13579
|
-
|
|
14014
|
+
import_node_fs30.default.chmodSync(absolutePath, 493);
|
|
13580
14015
|
}
|
|
13581
14016
|
}
|
|
13582
14017
|
function writeDevcontainer(options = {}) {
|
|
@@ -13614,12 +14049,12 @@ function writeDevcontainer(options = {}) {
|
|
|
13614
14049
|
nextSteps: postWriteNextSteps(repoPath)
|
|
13615
14050
|
};
|
|
13616
14051
|
}
|
|
13617
|
-
var
|
|
14052
|
+
var import_node_fs30, import_node_path31, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
13618
14053
|
var init_devcontainer_write = __esm({
|
|
13619
14054
|
"src/core/devcontainer-write.ts"() {
|
|
13620
14055
|
"use strict";
|
|
13621
|
-
|
|
13622
|
-
|
|
14056
|
+
import_node_fs30 = __toESM(require("fs"));
|
|
14057
|
+
import_node_path31 = __toESM(require("path"));
|
|
13623
14058
|
init_repo_config();
|
|
13624
14059
|
init_repo_inspect();
|
|
13625
14060
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -13805,11 +14240,11 @@ var init_profile_resolution = __esm({
|
|
|
13805
14240
|
function fail(message) {
|
|
13806
14241
|
throw new Error(message);
|
|
13807
14242
|
}
|
|
13808
|
-
function
|
|
14243
|
+
function isRecord4(value) {
|
|
13809
14244
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13810
14245
|
}
|
|
13811
14246
|
function requireRecord(value, label) {
|
|
13812
|
-
if (!
|
|
14247
|
+
if (!isRecord4(value)) fail(`${label} must be a mapping.`);
|
|
13813
14248
|
return value;
|
|
13814
14249
|
}
|
|
13815
14250
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -14026,13 +14461,13 @@ function buildProfilePlanReport(options) {
|
|
|
14026
14461
|
return { ...report, contractPath, bindings };
|
|
14027
14462
|
}
|
|
14028
14463
|
function loadContract(repoPath, contractPath) {
|
|
14029
|
-
if (
|
|
14464
|
+
if (import_node_path32.default.isAbsolute(contractPath)) {
|
|
14030
14465
|
fail("profile plan contract path must be relative to the repository.");
|
|
14031
14466
|
}
|
|
14032
14467
|
const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
|
|
14033
14468
|
let metadata;
|
|
14034
14469
|
try {
|
|
14035
|
-
metadata =
|
|
14470
|
+
metadata = import_node_fs31.default.lstatSync(absolutePath);
|
|
14036
14471
|
} catch (error) {
|
|
14037
14472
|
fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
|
|
14038
14473
|
}
|
|
@@ -14042,16 +14477,16 @@ function loadContract(repoPath, contractPath) {
|
|
|
14042
14477
|
if (metadata.size > MAX_CONTRACT_BYTES) {
|
|
14043
14478
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14044
14479
|
}
|
|
14045
|
-
const realRepo =
|
|
14046
|
-
const realContract =
|
|
14480
|
+
const realRepo = import_node_fs31.default.realpathSync(repoPath);
|
|
14481
|
+
const realContract = import_node_fs31.default.realpathSync(absolutePath);
|
|
14047
14482
|
assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
|
|
14048
|
-
const source =
|
|
14483
|
+
const source = import_node_fs31.default.readFileSync(absolutePath, "utf-8");
|
|
14049
14484
|
if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
|
|
14050
14485
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14051
14486
|
}
|
|
14052
14487
|
return {
|
|
14053
14488
|
contract: parseProfilePlanContract(source),
|
|
14054
|
-
contractPath:
|
|
14489
|
+
contractPath: import_node_path32.default.relative(repoPath, absolutePath).split(import_node_path32.default.sep).join("/")
|
|
14055
14490
|
};
|
|
14056
14491
|
}
|
|
14057
14492
|
function resolveProfilePlan(options) {
|
|
@@ -14065,12 +14500,12 @@ function resolveProfilePlan(options) {
|
|
|
14065
14500
|
contractPath: loaded.contractPath
|
|
14066
14501
|
});
|
|
14067
14502
|
}
|
|
14068
|
-
var
|
|
14503
|
+
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;
|
|
14069
14504
|
var init_profile_plan = __esm({
|
|
14070
14505
|
"src/core/profile-plan.ts"() {
|
|
14071
14506
|
"use strict";
|
|
14072
|
-
|
|
14073
|
-
|
|
14507
|
+
import_node_fs31 = __toESM(require("fs"));
|
|
14508
|
+
import_node_path32 = __toESM(require("path"));
|
|
14074
14509
|
import_yaml7 = require("yaml");
|
|
14075
14510
|
init_paths();
|
|
14076
14511
|
init_profile_resolution();
|
|
@@ -14102,7 +14537,7 @@ async function runProfileResolveCommand(options) {
|
|
|
14102
14537
|
async function runProfilePlanCommand(options) {
|
|
14103
14538
|
const report = resolveProfilePlan(options);
|
|
14104
14539
|
if (options.output) {
|
|
14105
|
-
writeFileAtomically(
|
|
14540
|
+
writeFileAtomically(import_node_path33.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
|
|
14106
14541
|
`);
|
|
14107
14542
|
}
|
|
14108
14543
|
if (options.json) {
|
|
@@ -14138,14 +14573,14 @@ function printProfilePlanSummary(report, output2) {
|
|
|
14138
14573
|
process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
|
|
14139
14574
|
`);
|
|
14140
14575
|
}
|
|
14141
|
-
if (output2) process.stdout.write(`Output: ${
|
|
14576
|
+
if (output2) process.stdout.write(`Output: ${import_node_path33.default.resolve(output2)}
|
|
14142
14577
|
`);
|
|
14143
14578
|
}
|
|
14144
|
-
var
|
|
14579
|
+
var import_node_path33;
|
|
14145
14580
|
var init_profile = __esm({
|
|
14146
14581
|
"src/commands/profile.ts"() {
|
|
14147
14582
|
"use strict";
|
|
14148
|
-
|
|
14583
|
+
import_node_path33 = __toESM(require("path"));
|
|
14149
14584
|
init_atomic_file();
|
|
14150
14585
|
init_output();
|
|
14151
14586
|
init_profile_plan();
|
|
@@ -14435,7 +14870,7 @@ function sanitizeRouterId(value) {
|
|
|
14435
14870
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
14436
14871
|
}
|
|
14437
14872
|
function repoHash(repoPath) {
|
|
14438
|
-
return (0, import_node_crypto10.createHash)("sha1").update(
|
|
14873
|
+
return (0, import_node_crypto10.createHash)("sha1").update(import_node_path34.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
14439
14874
|
}
|
|
14440
14875
|
function asDockerApp(app) {
|
|
14441
14876
|
return app.runtime === "docker";
|
|
@@ -14514,11 +14949,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
14514
14949
|
if (dockerApps.length === 0) {
|
|
14515
14950
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
14516
14951
|
}
|
|
14517
|
-
const cachePath =
|
|
14518
|
-
|
|
14519
|
-
const overlayPath =
|
|
14952
|
+
const cachePath = import_node_path34.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
14953
|
+
import_node_fs32.default.mkdirSync(cachePath, { recursive: true });
|
|
14954
|
+
const overlayPath = import_node_path34.default.join(cachePath, "compose.devrouter.yml");
|
|
14520
14955
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
14521
|
-
|
|
14956
|
+
import_node_fs32.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
14522
14957
|
return {
|
|
14523
14958
|
overlayPath,
|
|
14524
14959
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -14638,14 +15073,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
14638
15073
|
const port = Number(match[1]);
|
|
14639
15074
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
14640
15075
|
}
|
|
14641
|
-
var import_node_child_process27, import_node_crypto10,
|
|
15076
|
+
var import_node_child_process27, import_node_crypto10, import_node_fs32, import_node_path34, import_yaml8;
|
|
14642
15077
|
var init_docker_run = __esm({
|
|
14643
15078
|
"src/core/docker-run.ts"() {
|
|
14644
15079
|
"use strict";
|
|
14645
15080
|
import_node_child_process27 = require("child_process");
|
|
14646
15081
|
import_node_crypto10 = require("crypto");
|
|
14647
|
-
|
|
14648
|
-
|
|
15082
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
15083
|
+
import_node_path34 = __toESM(require("path"));
|
|
14649
15084
|
import_yaml8 = __toESM(require("yaml"));
|
|
14650
15085
|
init_docker_error_guidance();
|
|
14651
15086
|
init_paths();
|
|
@@ -15467,13 +15902,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15467
15902
|
const startedAt = Date.now();
|
|
15468
15903
|
let rootStat;
|
|
15469
15904
|
try {
|
|
15470
|
-
rootStat =
|
|
15905
|
+
rootStat = import_node_fs33.default.lstatSync(worktreePath);
|
|
15471
15906
|
} catch (error) {
|
|
15472
15907
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
15473
15908
|
}
|
|
15474
15909
|
let rootEntries;
|
|
15475
15910
|
try {
|
|
15476
|
-
rootEntries =
|
|
15911
|
+
rootEntries = import_node_fs33.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
15477
15912
|
} catch (error) {
|
|
15478
15913
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
15479
15914
|
}
|
|
@@ -15499,10 +15934,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15499
15934
|
timedOut = true;
|
|
15500
15935
|
break;
|
|
15501
15936
|
}
|
|
15502
|
-
const entryPath =
|
|
15937
|
+
const entryPath = import_node_path35.default.join(dirPath, entry.name);
|
|
15503
15938
|
let entryStat;
|
|
15504
15939
|
try {
|
|
15505
|
-
entryStat =
|
|
15940
|
+
entryStat = import_node_fs33.default.lstatSync(entryPath);
|
|
15506
15941
|
} catch (error) {
|
|
15507
15942
|
if (error?.code === "ENOENT") continue;
|
|
15508
15943
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -15512,7 +15947,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15512
15947
|
if (!entryStat.isDirectory()) continue;
|
|
15513
15948
|
let childEntries;
|
|
15514
15949
|
try {
|
|
15515
|
-
childEntries =
|
|
15950
|
+
childEntries = import_node_fs33.default.readdirSync(entryPath, { withFileTypes: true });
|
|
15516
15951
|
} catch (error) {
|
|
15517
15952
|
unreadableReason = describeIncompleteWalk(error);
|
|
15518
15953
|
break;
|
|
@@ -15576,12 +16011,12 @@ function describeError(error, worktreePath) {
|
|
|
15576
16011
|
}
|
|
15577
16012
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
15578
16013
|
}
|
|
15579
|
-
var
|
|
16014
|
+
var import_node_fs33, import_node_path35, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
15580
16015
|
var init_workspace_consumption = __esm({
|
|
15581
16016
|
"src/core/workspace-consumption.ts"() {
|
|
15582
16017
|
"use strict";
|
|
15583
|
-
|
|
15584
|
-
|
|
16018
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
16019
|
+
import_node_path35 = __toESM(require("path"));
|
|
15585
16020
|
init_devpod_environment();
|
|
15586
16021
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
15587
16022
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -15589,7 +16024,7 @@ var init_workspace_consumption = __esm({
|
|
|
15589
16024
|
});
|
|
15590
16025
|
|
|
15591
16026
|
// src/core/workspace-cleanup.ts
|
|
15592
|
-
function
|
|
16027
|
+
function isRecord5(value) {
|
|
15593
16028
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15594
16029
|
}
|
|
15595
16030
|
function isSha(value) {
|
|
@@ -15682,7 +16117,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
15682
16117
|
}
|
|
15683
16118
|
function readGitSnapshot(worktree, commandRunner) {
|
|
15684
16119
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
15685
|
-
if (worktree.prunable || !
|
|
16120
|
+
if (worktree.prunable || !import_node_fs34.default.existsSync(comparablePath)) {
|
|
15686
16121
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
15687
16122
|
}
|
|
15688
16123
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -15733,14 +16168,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
15733
16168
|
if (!Array.isArray(value)) return void 0;
|
|
15734
16169
|
const changes = [];
|
|
15735
16170
|
for (const item of value) {
|
|
15736
|
-
if (!
|
|
15737
|
-
const repository =
|
|
15738
|
-
const headRepository =
|
|
16171
|
+
if (!isRecord5(item)) return void 0;
|
|
16172
|
+
const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
16173
|
+
const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
15739
16174
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
15740
16175
|
continue;
|
|
15741
16176
|
}
|
|
15742
16177
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
15743
|
-
const mergeCommit =
|
|
16178
|
+
const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
15744
16179
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
15745
16180
|
changes.push({
|
|
15746
16181
|
sourceBranch: branch,
|
|
@@ -15757,14 +16192,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
15757
16192
|
if (!Array.isArray(value)) return void 0;
|
|
15758
16193
|
const changes = [];
|
|
15759
16194
|
for (const item of value) {
|
|
15760
|
-
if (!
|
|
16195
|
+
if (!isRecord5(item)) return void 0;
|
|
15761
16196
|
const sourceProjectId = item.source_project_id;
|
|
15762
16197
|
const targetProjectId = item.target_project_id;
|
|
15763
16198
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
15764
16199
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
15765
16200
|
continue;
|
|
15766
16201
|
}
|
|
15767
|
-
const diffRefs =
|
|
16202
|
+
const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
|
|
15768
16203
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
15769
16204
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
15770
16205
|
changes.push({
|
|
@@ -16207,7 +16642,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
16207
16642
|
containers = measureContainersFn(worktreePaths);
|
|
16208
16643
|
} catch (error) {
|
|
16209
16644
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
16210
|
-
containers = new Map(worktreePaths.map((
|
|
16645
|
+
containers = new Map(worktreePaths.map((path35) => [path35, unknownContainers(reason)]));
|
|
16211
16646
|
}
|
|
16212
16647
|
}
|
|
16213
16648
|
const rows = records.map((record) => {
|
|
@@ -16256,12 +16691,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
16256
16691
|
workspaces: rows
|
|
16257
16692
|
};
|
|
16258
16693
|
}
|
|
16259
|
-
var import_node_child_process29,
|
|
16694
|
+
var import_node_child_process29, import_node_fs34, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
16260
16695
|
var init_workspace_cleanup = __esm({
|
|
16261
16696
|
"src/core/workspace-cleanup.ts"() {
|
|
16262
16697
|
"use strict";
|
|
16263
16698
|
import_node_child_process29 = require("child_process");
|
|
16264
|
-
|
|
16699
|
+
import_node_fs34 = __toESM(require("fs"));
|
|
16265
16700
|
init_devpod_workspaces();
|
|
16266
16701
|
init_host_routes();
|
|
16267
16702
|
init_repo_config();
|
|
@@ -16438,7 +16873,7 @@ var init_version = __esm({
|
|
|
16438
16873
|
|
|
16439
16874
|
// src/cli.ts
|
|
16440
16875
|
var import_commander = require("commander");
|
|
16441
|
-
var CLI_VERSION = true ? "0.0.
|
|
16876
|
+
var CLI_VERSION = true ? "0.0.54" : "0.0.0-dev";
|
|
16442
16877
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
16443
16878
|
function withErrorHandling(action2) {
|
|
16444
16879
|
return async (...args) => {
|