@devrouter/cli 0.0.52 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devrouter-process +87 -0
- package/dist/devrouter.js +1380 -960
- package/package.json +1 -1
- package/upgrade-prompts/0.0.53.md +42 -0
package/dist/devrouter.js
CHANGED
|
@@ -249,6 +249,8 @@ profiles:
|
|
|
249
249
|
- Validation is strict at config load: unknown keys, non-routed \`apps\`, non-dependency \`dependencies\`, \`readiness\` outside the profile's apps, and multiple defaults are rejected.
|
|
250
250
|
- Selection: \`devrouter ensure <path> --profile <name>\`. Comma-separated selections (\`--profile manage,pwa\`) merge with deduplication; the canonical name is sorted-unique so order never affects identity or fingerprints. A wildcard member collapses to everything.
|
|
251
251
|
- Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
|
|
252
|
+
- Adapters may pass \`--prepare-command <command>\` to \`devrouter-process ensure\` for synchronous dependency preparation under the process lock before application launch. Unchanged owned processes skip preparation; preparation participates in default fingerprints and must not daemonize or detach.
|
|
253
|
+
- Exact managed Devsy stop proves the complete retained container population, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup, and an original provider failure remains an error even if residual cleanup succeeds.
|
|
252
254
|
|
|
253
255
|
### Managed devcontainer resources
|
|
254
256
|
|
|
@@ -2691,7 +2693,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2691
2693
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2692
2694
|
const requiredVersion = config.devrouter?.version;
|
|
2693
2695
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2694
|
-
const cliVersion = true ? "0.0.
|
|
2696
|
+
const cliVersion = true ? "0.0.53" : "0.0.0-dev";
|
|
2695
2697
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2696
2698
|
hasWarnedVersionMismatch = true;
|
|
2697
2699
|
process.stderr.write(
|
|
@@ -3227,6 +3229,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3227
3229
|
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
|
|
3228
3230
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
3229
3231
|
"- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
|
|
3232
|
+
"- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
|
|
3233
|
+
"- Exact managed Devsy stop validates complete retained container membership, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup; original provider failures remain errors even after successful residual cleanup.",
|
|
3230
3234
|
"- Owner status is `present`, `missing`, `locked`, or `conflict`. Dirty or locked full down fails before side effects. `workspace gc` is a dry run; only `--yes` deletes exact eligible missing resources and records, never Git worktrees, branches, or prune state.",
|
|
3231
3235
|
"- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, workspace runtime registration, runtime state (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. Local DevPod/Devsy list/status checks always run; `--check-merged` alone enables read-only origin and matching GitHub/GitLab checks. Treat `not-found` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit `gc`/`down` can remove exact stale registration only after expected-ID `NotFound` proof and ownership revalidation. Runtime `lastUsed` remains advisory.",
|
|
3232
3236
|
'- `--measure-size` adds per-workspace storage consumption and stays read-only, but walks each worktree and runs `docker ps` / `docker inspect --size`, so leave it off for a quick report. Each row reports `worktree` and `containerWritable` bytes, which are reclaimable; `imageShared` bytes, which are not, because those image layers are shared with other containers and overlap across rows; and `reclaimable`, the sum of the reclaimable fields. Never add `imageShared` across rows. Any figure that cannot be trusted reports `{"status": "unknown", "reason": ...}` rather than zero, and a workspace with no container reports a measured `0`.',
|
|
@@ -5814,13 +5818,14 @@ function startExactManagedServices(options) {
|
|
|
5814
5818
|
);
|
|
5815
5819
|
}
|
|
5816
5820
|
}
|
|
5817
|
-
function stopExactManagedService(containerId, service) {
|
|
5821
|
+
function stopExactManagedService(containerId, service, options) {
|
|
5818
5822
|
assertSafeContainerId(containerId);
|
|
5819
5823
|
const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
|
|
5820
5824
|
encoding: "utf-8",
|
|
5821
|
-
stdio: "inherit"
|
|
5825
|
+
stdio: "inherit",
|
|
5826
|
+
...options ? { timeout: options.timeoutMs } : {}
|
|
5822
5827
|
});
|
|
5823
|
-
if (result.status !== 0) {
|
|
5828
|
+
if (result.status !== 0 || result.error) {
|
|
5824
5829
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5825
5830
|
}
|
|
5826
5831
|
}
|
|
@@ -5844,6 +5849,198 @@ var init_devcontainer_profile = __esm({
|
|
|
5844
5849
|
});
|
|
5845
5850
|
|
|
5846
5851
|
// src/core/devpod-environment.ts
|
|
5852
|
+
function isRecord2(value) {
|
|
5853
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5854
|
+
}
|
|
5855
|
+
function assertFullContainerId(value) {
|
|
5856
|
+
if (typeof value !== "string" || !FULL_CONTAINER_ID_PATTERN.test(value)) {
|
|
5857
|
+
throw new Error("Managed stop Docker inspection returned an invalid container id.");
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
function assertUniqueContainerIds(ids) {
|
|
5861
|
+
if (new Set(ids).size !== ids.length) {
|
|
5862
|
+
throw new Error("Managed stop Docker inspection returned duplicate container ids.");
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
function parseDockerLines(stdout) {
|
|
5866
|
+
const lines = stdout.split(/\r?\n/);
|
|
5867
|
+
while (lines.at(-1) === "") lines.pop();
|
|
5868
|
+
if (lines.some((line) => line === "")) {
|
|
5869
|
+
throw new Error("Managed stop Docker inspection returned an empty record.");
|
|
5870
|
+
}
|
|
5871
|
+
return lines;
|
|
5872
|
+
}
|
|
5873
|
+
function runManagedStopDocker(args) {
|
|
5874
|
+
const result = (0, import_node_child_process8.spawnSync)("docker", args, {
|
|
5875
|
+
encoding: "utf-8",
|
|
5876
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
5877
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
5878
|
+
});
|
|
5879
|
+
if (result.error || result.status !== 0) {
|
|
5880
|
+
throw new Error("Managed stop Docker inspection failed.");
|
|
5881
|
+
}
|
|
5882
|
+
if (typeof result.stdout !== "string") {
|
|
5883
|
+
throw new Error("Managed stop Docker inspection returned invalid output.");
|
|
5884
|
+
}
|
|
5885
|
+
return result.stdout;
|
|
5886
|
+
}
|
|
5887
|
+
function listManagedStopContainerIds(composeProject) {
|
|
5888
|
+
const stdout = runManagedStopDocker([
|
|
5889
|
+
"ps",
|
|
5890
|
+
"-a",
|
|
5891
|
+
"--no-trunc",
|
|
5892
|
+
"--filter",
|
|
5893
|
+
`label=${MANAGED_STOP_COMPOSE_LABEL}=${composeProject}`,
|
|
5894
|
+
"--format",
|
|
5895
|
+
"{{.ID}}"
|
|
5896
|
+
]);
|
|
5897
|
+
const ids = parseDockerLines(stdout);
|
|
5898
|
+
ids.forEach(assertFullContainerId);
|
|
5899
|
+
assertUniqueContainerIds(ids);
|
|
5900
|
+
return ids;
|
|
5901
|
+
}
|
|
5902
|
+
function assertString(value) {
|
|
5903
|
+
if (typeof value !== "string") {
|
|
5904
|
+
throw new Error("Managed stop Docker inspection returned an invalid field.");
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
function validateManagedStopSnapshot(value, composeProject) {
|
|
5908
|
+
if (!isRecord2(value)) {
|
|
5909
|
+
throw new Error("Managed stop Docker inspection returned an invalid record.");
|
|
5910
|
+
}
|
|
5911
|
+
assertFullContainerId(value.id);
|
|
5912
|
+
if (!isRecord2(value.state)) {
|
|
5913
|
+
throw new Error("Managed stop Docker inspection returned an invalid state.");
|
|
5914
|
+
}
|
|
5915
|
+
const status = value.state.Status;
|
|
5916
|
+
if (status !== "running" && status !== "exited" && status !== "created") {
|
|
5917
|
+
throw new Error("Managed stop Docker inspection returned an invalid state status.");
|
|
5918
|
+
}
|
|
5919
|
+
if (typeof value.state.Running !== "boolean") {
|
|
5920
|
+
throw new Error("Managed stop Docker inspection returned an invalid running flag.");
|
|
5921
|
+
}
|
|
5922
|
+
if (typeof value.state.Paused !== "boolean" || value.state.Paused) {
|
|
5923
|
+
throw new Error("Managed stop Docker inspection returned an invalid paused flag.");
|
|
5924
|
+
}
|
|
5925
|
+
if (typeof value.state.Restarting !== "boolean" || value.state.Restarting) {
|
|
5926
|
+
throw new Error("Managed stop Docker inspection returned an invalid restarting flag.");
|
|
5927
|
+
}
|
|
5928
|
+
if (typeof value.state.Dead !== "boolean" || value.state.Dead) {
|
|
5929
|
+
throw new Error("Managed stop Docker inspection returned an invalid dead flag.");
|
|
5930
|
+
}
|
|
5931
|
+
if (status === "running" !== value.state.Running) {
|
|
5932
|
+
throw new Error("Managed stop Docker inspection returned contradictory state.");
|
|
5933
|
+
}
|
|
5934
|
+
let health;
|
|
5935
|
+
if (value.state.Health !== void 0 && value.state.Health !== null) {
|
|
5936
|
+
if (!isRecord2(value.state.Health)) {
|
|
5937
|
+
throw new Error("Managed stop Docker inspection returned an invalid health field.");
|
|
5938
|
+
}
|
|
5939
|
+
assertString(value.state.Health.Status);
|
|
5940
|
+
health = { Status: value.state.Health.Status };
|
|
5941
|
+
}
|
|
5942
|
+
if (!isRecord2(value.labels)) {
|
|
5943
|
+
throw new Error("Managed stop Docker inspection returned invalid labels.");
|
|
5944
|
+
}
|
|
5945
|
+
for (const label of MANAGED_STOP_IDENTITY_LABELS) {
|
|
5946
|
+
assertString(value.labels[label]);
|
|
5947
|
+
}
|
|
5948
|
+
if (value.labels[MANAGED_STOP_COMPOSE_LABEL] !== composeProject) {
|
|
5949
|
+
throw new Error("Managed stop Docker inspection returned a foreign Compose project.");
|
|
5950
|
+
}
|
|
5951
|
+
if (!Array.isArray(value.mounts)) {
|
|
5952
|
+
throw new Error("Managed stop Docker inspection returned invalid mounts.");
|
|
5953
|
+
}
|
|
5954
|
+
const mounts = value.mounts.map((mount) => {
|
|
5955
|
+
if (!isRecord2(mount)) {
|
|
5956
|
+
throw new Error("Managed stop Docker inspection returned an invalid mount.");
|
|
5957
|
+
}
|
|
5958
|
+
assertString(mount.Type);
|
|
5959
|
+
assertString(mount.Source);
|
|
5960
|
+
assertString(mount.Destination);
|
|
5961
|
+
return {
|
|
5962
|
+
Type: mount.Type,
|
|
5963
|
+
Source: mount.Source,
|
|
5964
|
+
Destination: mount.Destination
|
|
5965
|
+
};
|
|
5966
|
+
});
|
|
5967
|
+
if (!isRecord2(value.networks)) {
|
|
5968
|
+
throw new Error("Managed stop Docker inspection returned invalid networks.");
|
|
5969
|
+
}
|
|
5970
|
+
return {
|
|
5971
|
+
id: value.id,
|
|
5972
|
+
state: {
|
|
5973
|
+
...health ? { Health: health } : {},
|
|
5974
|
+
Status: status,
|
|
5975
|
+
Running: value.state.Running,
|
|
5976
|
+
Paused: false,
|
|
5977
|
+
Restarting: false,
|
|
5978
|
+
Dead: false
|
|
5979
|
+
},
|
|
5980
|
+
labels: {
|
|
5981
|
+
"com.docker.compose.project": value.labels["com.docker.compose.project"],
|
|
5982
|
+
"com.docker.compose.service": value.labels["com.docker.compose.service"],
|
|
5983
|
+
"com.docker.compose.project.working_dir": value.labels["com.docker.compose.project.working_dir"],
|
|
5984
|
+
"com.docker.compose.project.config_files": value.labels["com.docker.compose.project.config_files"],
|
|
5985
|
+
"com.docker.compose.config-hash": value.labels["com.docker.compose.config-hash"]
|
|
5986
|
+
},
|
|
5987
|
+
mounts,
|
|
5988
|
+
networks: {}
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
function inspectManagedStopPopulation(ids, composeProject) {
|
|
5992
|
+
const stdout = runManagedStopDocker([
|
|
5993
|
+
"inspect",
|
|
5994
|
+
"--format",
|
|
5995
|
+
MANAGED_STOP_INSPECT_TEMPLATE,
|
|
5996
|
+
...ids
|
|
5997
|
+
]);
|
|
5998
|
+
const lines = parseDockerLines(stdout);
|
|
5999
|
+
if (lines.length !== ids.length) {
|
|
6000
|
+
throw new Error("Managed stop Docker inspection returned an incomplete population.");
|
|
6001
|
+
}
|
|
6002
|
+
const expected = new Set(ids);
|
|
6003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6004
|
+
const snapshots = lines.map((line) => {
|
|
6005
|
+
let parsed;
|
|
6006
|
+
try {
|
|
6007
|
+
parsed = JSON.parse(line);
|
|
6008
|
+
} catch {
|
|
6009
|
+
throw new Error("Managed stop Docker inspection returned malformed JSON.");
|
|
6010
|
+
}
|
|
6011
|
+
const snapshot = validateManagedStopSnapshot(parsed, composeProject);
|
|
6012
|
+
if (!expected.has(snapshot.id) || seen.has(snapshot.id)) {
|
|
6013
|
+
throw new Error("Managed stop Docker inspection returned an unexpected population.");
|
|
6014
|
+
}
|
|
6015
|
+
seen.add(snapshot.id);
|
|
6016
|
+
return snapshot;
|
|
6017
|
+
});
|
|
6018
|
+
return snapshots;
|
|
6019
|
+
}
|
|
6020
|
+
function requireSameContainerPopulation(expected, actual) {
|
|
6021
|
+
if (expected.length !== actual.length) {
|
|
6022
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6023
|
+
}
|
|
6024
|
+
const expectedSet = new Set(expected);
|
|
6025
|
+
if (actual.some((id) => !expectedSet.has(id))) {
|
|
6026
|
+
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
function inspectManagedStopContainers(composeProject) {
|
|
6030
|
+
if (typeof composeProject !== "string" || composeProject.length === 0 || composeProject !== composeProject.trim() || !MANAGED_STOP_PROJECT_PATTERN.test(composeProject)) {
|
|
6031
|
+
throw new Error("Managed stop Docker inspection requires a safe Compose project.");
|
|
6032
|
+
}
|
|
6033
|
+
const listedIds = listManagedStopContainerIds(composeProject);
|
|
6034
|
+
if (listedIds.length === 0) {
|
|
6035
|
+
const confirmedIds2 = listManagedStopContainerIds(composeProject);
|
|
6036
|
+
requireSameContainerPopulation(listedIds, confirmedIds2);
|
|
6037
|
+
return [];
|
|
6038
|
+
}
|
|
6039
|
+
const snapshots = inspectManagedStopPopulation(listedIds, composeProject);
|
|
6040
|
+
const confirmedIds = listManagedStopContainerIds(composeProject);
|
|
6041
|
+
requireSameContainerPopulation(listedIds, confirmedIds);
|
|
6042
|
+
return snapshots;
|
|
6043
|
+
}
|
|
5847
6044
|
function inspectWorkspaceContainers(options) {
|
|
5848
6045
|
let ids = options?.ids;
|
|
5849
6046
|
if (!ids) {
|
|
@@ -5927,7 +6124,7 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5927
6124
|
}
|
|
5928
6125
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5929
6126
|
}
|
|
5930
|
-
var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
|
|
6127
|
+
var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE, MANAGED_STOP_INSPECT_TEMPLATE, MANAGED_STOP_PROJECT_PATTERN, FULL_CONTAINER_ID_PATTERN, MANAGED_STOP_DOCKER_TIMEOUT_MS, MANAGED_STOP_DOCKER_MAX_BUFFER, MANAGED_STOP_COMPOSE_LABEL, MANAGED_STOP_IDENTITY_LABELS;
|
|
5931
6128
|
var init_devpod_environment = __esm({
|
|
5932
6129
|
"src/core/devpod-environment.ts"() {
|
|
5933
6130
|
"use strict";
|
|
@@ -5939,6 +6136,19 @@ var init_devpod_environment = __esm({
|
|
|
5939
6136
|
/}$/,
|
|
5940
6137
|
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
5941
6138
|
);
|
|
6139
|
+
MANAGED_STOP_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Status":{{json .State.Status}},"Running":{{json .State.Running}},"Paused":{{json .State.Paused}},"Restarting":{{json .State.Restarting}},"Dead":{{json .State.Dead}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}},"com.docker.compose.config-hash":{{json (index .Config.Labels "com.docker.compose.config-hash")}}},"mounts":[{{range $index, $mount := .Mounts}}{{if $index}},{{end}}{"Type":{{json $mount.Type}},"Source":{{json $mount.Source}},"Destination":{{json $mount.Destination}}}{{end}}],"networks":{}}';
|
|
6140
|
+
MANAGED_STOP_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6141
|
+
FULL_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
6142
|
+
MANAGED_STOP_DOCKER_TIMEOUT_MS = 5e3;
|
|
6143
|
+
MANAGED_STOP_DOCKER_MAX_BUFFER = 1024 * 1024;
|
|
6144
|
+
MANAGED_STOP_COMPOSE_LABEL = "com.docker.compose.project";
|
|
6145
|
+
MANAGED_STOP_IDENTITY_LABELS = [
|
|
6146
|
+
"com.docker.compose.project",
|
|
6147
|
+
"com.docker.compose.service",
|
|
6148
|
+
"com.docker.compose.project.working_dir",
|
|
6149
|
+
"com.docker.compose.project.config_files",
|
|
6150
|
+
"com.docker.compose.config-hash"
|
|
6151
|
+
];
|
|
5942
6152
|
}
|
|
5943
6153
|
});
|
|
5944
6154
|
|
|
@@ -6907,7 +7117,8 @@ function listDevsyWorkspaces() {
|
|
|
6907
7117
|
}
|
|
6908
7118
|
const workspace = {
|
|
6909
7119
|
id: candidate.id,
|
|
6910
|
-
source: { localFolder: source.localFolder }
|
|
7120
|
+
source: { localFolder: source.localFolder },
|
|
7121
|
+
...typeof candidate.context === "string" ? { context: candidate.context } : {}
|
|
6911
7122
|
};
|
|
6912
7123
|
if ("lastUsed" in candidate) {
|
|
6913
7124
|
if (typeof candidate.lastUsed === "string") {
|
|
@@ -7586,372 +7797,898 @@ var init_devpod_workspaces = __esm({
|
|
|
7586
7797
|
}
|
|
7587
7798
|
});
|
|
7588
7799
|
|
|
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
|
-
);
|
|
7800
|
+
// src/core/route-publication.ts
|
|
7801
|
+
function routedAppsFromConfig(config) {
|
|
7802
|
+
return config.apps.filter((app) => app.kind !== "dependency");
|
|
7613
7803
|
}
|
|
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
|
|
7804
|
+
function configuredProxyAppsFromConfig(config) {
|
|
7805
|
+
return routedAppsFromConfig(config).filter(
|
|
7806
|
+
(app) => app.runtime === "proxy"
|
|
7626
7807
|
);
|
|
7627
7808
|
}
|
|
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) {
|
|
7809
|
+
function proxyAppsFromConfig(config) {
|
|
7810
|
+
const routedApps = routedAppsFromConfig(config);
|
|
7811
|
+
const proxyApps = configuredProxyAppsFromConfig(config);
|
|
7812
|
+
const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
|
|
7813
|
+
if (unsupported.length > 0) {
|
|
7667
7814
|
throw new Error(
|
|
7668
|
-
`
|
|
7815
|
+
`Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
|
|
7669
7816
|
);
|
|
7670
7817
|
}
|
|
7818
|
+
return proxyApps;
|
|
7671
7819
|
}
|
|
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") {
|
|
7820
|
+
async function ensureRouteInfrastructure(apps, options = {}) {
|
|
7821
|
+
ensureRouterFiles();
|
|
7822
|
+
await ensureNetwork(DEVNET_NAME);
|
|
7823
|
+
const tlsCoverage = await ensureTLSHostsCovered(
|
|
7824
|
+
apps.map((app) => app.host),
|
|
7825
|
+
options
|
|
7826
|
+
);
|
|
7827
|
+
for (const app of apps) {
|
|
7828
|
+
if (app.protocol === "tcp") {
|
|
7829
|
+
if (!isTLSEnabled()) {
|
|
7701
7830
|
throw new Error(
|
|
7702
|
-
`
|
|
7831
|
+
`TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
7703
7832
|
);
|
|
7704
7833
|
}
|
|
7834
|
+
activateTcpProtocol(app.tcpProtocol);
|
|
7705
7835
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
7836
|
+
}
|
|
7837
|
+
startRouterStack();
|
|
7838
|
+
return tlsCoverage;
|
|
7708
7839
|
}
|
|
7709
|
-
function
|
|
7710
|
-
|
|
7840
|
+
async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
|
|
7841
|
+
const apps = proxyAppsFromConfig(config);
|
|
7842
|
+
const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
|
|
7843
|
+
const routes = apps.map((app) => {
|
|
7844
|
+
const upstream = parseUpstream(app.upstream);
|
|
7845
|
+
return {
|
|
7846
|
+
name: app.name,
|
|
7847
|
+
host: app.host,
|
|
7848
|
+
protocol: app.protocol,
|
|
7849
|
+
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
7850
|
+
repoPath,
|
|
7851
|
+
port: upstream.port,
|
|
7852
|
+
mode: "proxy",
|
|
7853
|
+
upstreamHost: upstream.upstreamHost,
|
|
7854
|
+
workspace
|
|
7855
|
+
};
|
|
7856
|
+
});
|
|
7857
|
+
replaceHostRoutesForRepo(repoPath, routes);
|
|
7858
|
+
return { routes, tlsRefreshed: tlsCoverage.refreshed };
|
|
7711
7859
|
}
|
|
7712
|
-
|
|
7713
|
-
|
|
7860
|
+
var init_route_publication = __esm({
|
|
7861
|
+
"src/core/route-publication.ts"() {
|
|
7862
|
+
"use strict";
|
|
7863
|
+
init_docker();
|
|
7864
|
+
init_host_routes();
|
|
7865
|
+
init_router();
|
|
7866
|
+
init_tls();
|
|
7867
|
+
}
|
|
7868
|
+
});
|
|
7869
|
+
|
|
7870
|
+
// src/core/workspace-ownership.ts
|
|
7871
|
+
function commandError(command, repoPath, stderr) {
|
|
7872
|
+
return new Error(
|
|
7873
|
+
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
7874
|
+
);
|
|
7714
7875
|
}
|
|
7715
|
-
function
|
|
7716
|
-
const
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7876
|
+
function resolveGitCommonDir(repoPath) {
|
|
7877
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
7878
|
+
encoding: "utf-8",
|
|
7879
|
+
env: READ_ONLY_GIT_ENV
|
|
7880
|
+
});
|
|
7881
|
+
const output2 = result.stdout.trim();
|
|
7882
|
+
if (result.status !== 0 || !output2) {
|
|
7883
|
+
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
7722
7884
|
}
|
|
7723
|
-
return
|
|
7885
|
+
return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
|
|
7724
7886
|
}
|
|
7725
|
-
|
|
7726
|
-
const
|
|
7727
|
-
|
|
7887
|
+
function resolveGitTopLevel(repoPath) {
|
|
7888
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
7889
|
+
encoding: "utf-8",
|
|
7890
|
+
env: READ_ONLY_GIT_ENV
|
|
7891
|
+
});
|
|
7892
|
+
const output2 = result.stdout.trim();
|
|
7893
|
+
if (result.status !== 0 || !output2) {
|
|
7894
|
+
throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
|
|
7895
|
+
}
|
|
7896
|
+
return comparableWorkspacePath(output2);
|
|
7897
|
+
}
|
|
7898
|
+
function listGitWorktrees(repoPath) {
|
|
7899
|
+
const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
7900
|
+
encoding: "utf-8",
|
|
7901
|
+
env: READ_ONLY_GIT_ENV
|
|
7902
|
+
});
|
|
7903
|
+
if (result.status !== 0) {
|
|
7904
|
+
throw commandError("git worktree list", repoPath, result.stderr);
|
|
7905
|
+
}
|
|
7906
|
+
const worktrees = [];
|
|
7907
|
+
let current = {};
|
|
7908
|
+
const finish = () => {
|
|
7909
|
+
if (!current.path) return;
|
|
7910
|
+
worktrees.push({
|
|
7911
|
+
path: comparableWorkspacePath(current.path),
|
|
7912
|
+
branch: current.branch,
|
|
7913
|
+
locked: current.locked ?? false,
|
|
7914
|
+
prunable: current.prunable ?? false
|
|
7915
|
+
});
|
|
7916
|
+
current = {};
|
|
7917
|
+
};
|
|
7918
|
+
for (const line of `${result.stdout}
|
|
7919
|
+
`.split("\n")) {
|
|
7920
|
+
if (line.startsWith("worktree ")) {
|
|
7921
|
+
finish();
|
|
7922
|
+
current.path = line.slice("worktree ".length).trim();
|
|
7923
|
+
} else if (line.startsWith("branch ")) {
|
|
7924
|
+
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
7925
|
+
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
7926
|
+
current.locked = true;
|
|
7927
|
+
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
7928
|
+
current.prunable = true;
|
|
7929
|
+
} else if (line === "") {
|
|
7930
|
+
finish();
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
return worktrees;
|
|
7934
|
+
}
|
|
7935
|
+
function ownershipDirectory(repoPath) {
|
|
7936
|
+
return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
7937
|
+
}
|
|
7938
|
+
function validateWorkspace(value, label) {
|
|
7939
|
+
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
7940
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7941
|
+
}
|
|
7942
|
+
return value;
|
|
7943
|
+
}
|
|
7944
|
+
function validateTimestamp(value, label) {
|
|
7945
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
7946
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
7947
|
+
}
|
|
7948
|
+
return value;
|
|
7949
|
+
}
|
|
7950
|
+
function validateRecord(value, expectedWorkspace) {
|
|
7951
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7952
|
+
throw new Error("invalid workspace ownership record");
|
|
7953
|
+
}
|
|
7954
|
+
const candidate = value;
|
|
7955
|
+
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
7956
|
+
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
7957
|
+
}
|
|
7958
|
+
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
7959
|
+
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
7960
|
+
throw new Error(
|
|
7961
|
+
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
7962
|
+
);
|
|
7963
|
+
}
|
|
7964
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
|
|
7965
|
+
throw new Error("invalid workspace ownership worktreePath");
|
|
7966
|
+
}
|
|
7967
|
+
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
7968
|
+
throw new Error("invalid workspace ownership branch");
|
|
7969
|
+
}
|
|
7970
|
+
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
7971
|
+
return {
|
|
7972
|
+
version: OWNERSHIP_VERSION,
|
|
7973
|
+
workspace,
|
|
7974
|
+
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
7975
|
+
branch: candidate.branch,
|
|
7976
|
+
devpodId,
|
|
7977
|
+
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
7978
|
+
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
7979
|
+
};
|
|
7980
|
+
}
|
|
7981
|
+
function recordPath(repoPath, workspace) {
|
|
7982
|
+
return import_node_path20.default.join(
|
|
7983
|
+
ownershipDirectory(repoPath),
|
|
7984
|
+
`${validateWorkspace(workspace, "workspace")}.json`
|
|
7985
|
+
);
|
|
7986
|
+
}
|
|
7987
|
+
function readRecordFile(filePath, expectedWorkspace) {
|
|
7988
|
+
let parsed;
|
|
7728
7989
|
try {
|
|
7729
|
-
|
|
7990
|
+
parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7730
7991
|
} catch (error) {
|
|
7731
|
-
if (
|
|
7732
|
-
|
|
7733
|
-
|
|
7992
|
+
if (error instanceof SyntaxError) {
|
|
7993
|
+
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
7994
|
+
}
|
|
7995
|
+
throw error;
|
|
7734
7996
|
}
|
|
7735
|
-
return
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7997
|
+
return validateRecord(parsed, expectedWorkspace);
|
|
7998
|
+
}
|
|
7999
|
+
function readWorkspaceOwnership(repoPath, workspace) {
|
|
8000
|
+
const filePath = recordPath(repoPath, workspace);
|
|
8001
|
+
try {
|
|
8002
|
+
return readRecordFile(filePath, workspace);
|
|
8003
|
+
} catch (error) {
|
|
8004
|
+
if (error.code === "ENOENT") return void 0;
|
|
8005
|
+
throw error;
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
8008
|
+
function listWorkspaceOwnership(repoPath) {
|
|
8009
|
+
const directory = ownershipDirectory(repoPath);
|
|
8010
|
+
return listWorkspaceOwnershipInDirectory(directory);
|
|
8011
|
+
}
|
|
8012
|
+
function listWorkspaceOwnershipInDirectory(directory) {
|
|
8013
|
+
let entries;
|
|
8014
|
+
try {
|
|
8015
|
+
entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
|
|
8016
|
+
} catch (error) {
|
|
8017
|
+
if (error.code === "ENOENT") return [];
|
|
8018
|
+
throw error;
|
|
8019
|
+
}
|
|
8020
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
8021
|
+
const workspace = entry.name.slice(0, -".json".length);
|
|
8022
|
+
validateWorkspace(workspace, "filename");
|
|
8023
|
+
return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
|
|
8024
|
+
});
|
|
8025
|
+
}
|
|
8026
|
+
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
8027
|
+
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
8028
|
+
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
8029
|
+
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
8030
|
+
const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
|
|
8031
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8032
|
+
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
8033
|
+
const existing = records.find((record2) => record2.workspace === workspace);
|
|
8034
|
+
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
8035
|
+
throw new Error(
|
|
8036
|
+
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
8037
|
+
);
|
|
8038
|
+
}
|
|
8039
|
+
const pathOwner = records.find(
|
|
8040
|
+
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
8041
|
+
);
|
|
8042
|
+
if (pathOwner) {
|
|
8043
|
+
throw new Error(
|
|
8044
|
+
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
8045
|
+
);
|
|
8046
|
+
}
|
|
8047
|
+
if (existing && existing.devpodId !== devpodId) {
|
|
8048
|
+
throw new Error(
|
|
8049
|
+
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
8050
|
+
);
|
|
8051
|
+
}
|
|
8052
|
+
const record = {
|
|
8053
|
+
version: OWNERSHIP_VERSION,
|
|
8054
|
+
workspace,
|
|
8055
|
+
worktreePath,
|
|
8056
|
+
branch: input2.branch ?? null,
|
|
8057
|
+
devpodId,
|
|
8058
|
+
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
8059
|
+
updatedAt: validateTimestamp(now, "updatedAt")
|
|
8060
|
+
};
|
|
8061
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
8062
|
+
`);
|
|
8063
|
+
return record;
|
|
8064
|
+
}
|
|
8065
|
+
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
8066
|
+
const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
8067
|
+
try {
|
|
8068
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8069
|
+
return true;
|
|
8070
|
+
} catch (error) {
|
|
8071
|
+
if (error.code === "ENOENT") return false;
|
|
8072
|
+
throw error;
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
function sameOwnershipRecord(left, right) {
|
|
8076
|
+
return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
|
|
8077
|
+
}
|
|
8078
|
+
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
8079
|
+
const filePath = import_node_path20.default.join(
|
|
8080
|
+
directory,
|
|
8081
|
+
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
8082
|
+
);
|
|
8083
|
+
let current;
|
|
8084
|
+
try {
|
|
8085
|
+
current = readRecordFile(filePath, expected.workspace);
|
|
8086
|
+
} catch (error) {
|
|
8087
|
+
if (error.code === "ENOENT") return "absent";
|
|
8088
|
+
throw error;
|
|
8089
|
+
}
|
|
8090
|
+
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
8091
|
+
import_node_fs20.default.rmSync(filePath);
|
|
8092
|
+
return "removed";
|
|
8093
|
+
}
|
|
8094
|
+
function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
|
|
8095
|
+
const directory = ownershipDirectory(repoPath);
|
|
8096
|
+
import_node_fs20.default.mkdirSync(directory, { recursive: true });
|
|
8097
|
+
return withFileLockSync(
|
|
8098
|
+
import_node_path20.default.join(directory, ".lock"),
|
|
8099
|
+
{
|
|
8100
|
+
activity: "workspace ownership transaction",
|
|
8101
|
+
target: `'${repoPath}'`,
|
|
8102
|
+
waitMs: options.waitMs ?? 5e3
|
|
8103
|
+
},
|
|
8104
|
+
() => operation({
|
|
8105
|
+
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8106
|
+
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8107
|
+
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8108
|
+
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8109
|
+
})
|
|
8110
|
+
);
|
|
8111
|
+
}
|
|
8112
|
+
function providerPathOwner(providerWorkspaces, worktreePath) {
|
|
8113
|
+
const owners = providerWorkspaces.filter(
|
|
8114
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
8115
|
+
);
|
|
8116
|
+
if (owners.length > 1) {
|
|
8117
|
+
throw new Error(
|
|
8118
|
+
`Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
|
|
8119
|
+
);
|
|
8120
|
+
}
|
|
8121
|
+
return owners[0];
|
|
8122
|
+
}
|
|
8123
|
+
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
8124
|
+
const owners = /* @__PURE__ */ new Map();
|
|
8125
|
+
for (const worktree of listGitWorktrees(repoPath)) {
|
|
8126
|
+
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
8127
|
+
let workspace;
|
|
8128
|
+
try {
|
|
8129
|
+
workspace = readPersistedWorkspace(worktree.path);
|
|
8130
|
+
} catch (error) {
|
|
8131
|
+
if (!import_node_fs20.default.existsSync(worktree.path)) continue;
|
|
8132
|
+
throw error;
|
|
8133
|
+
}
|
|
8134
|
+
if (!workspace) continue;
|
|
8135
|
+
const existing = owners.get(workspace);
|
|
8136
|
+
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
8137
|
+
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
8138
|
+
}
|
|
8139
|
+
owners.set(workspace, worktree.path);
|
|
8140
|
+
}
|
|
8141
|
+
return owners;
|
|
8142
|
+
}
|
|
8143
|
+
function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
|
|
8144
|
+
const recordOwner = records.find(
|
|
8145
|
+
(record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
|
|
8146
|
+
);
|
|
8147
|
+
if (recordOwner) {
|
|
8148
|
+
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
8149
|
+
}
|
|
8150
|
+
const providerOwner = providerWorkspaces.find(
|
|
8151
|
+
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
8152
|
+
);
|
|
8153
|
+
if (providerOwner) {
|
|
8154
|
+
return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
|
|
8155
|
+
}
|
|
8156
|
+
const persistedOwner = persistedOwners.get(workspace);
|
|
8157
|
+
if (persistedOwner) {
|
|
8158
|
+
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
8159
|
+
}
|
|
8160
|
+
return void 0;
|
|
8161
|
+
}
|
|
8162
|
+
function claimWorkspaceIdentity(repoPath, input2) {
|
|
8163
|
+
const worktreePath = comparableWorkspacePath(repoPath);
|
|
8164
|
+
const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
|
|
8165
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
|
|
8166
|
+
const records = transaction.list();
|
|
8167
|
+
const exactRecords = records.filter(
|
|
8168
|
+
(record) => sameWorkspacePath(record.worktreePath, worktreePath)
|
|
8169
|
+
);
|
|
8170
|
+
if (exactRecords.length > 1) {
|
|
8171
|
+
throw new Error(
|
|
8172
|
+
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
8175
|
+
const exactRecord = exactRecords[0];
|
|
8176
|
+
const persisted = readPersistedWorkspace(worktreePath);
|
|
8177
|
+
const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
|
|
8178
|
+
if (exactRecord) {
|
|
8179
|
+
if (persisted && persisted !== exactRecord.workspace) {
|
|
8180
|
+
throw new Error(
|
|
8181
|
+
`Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
|
|
8182
|
+
);
|
|
7742
8183
|
}
|
|
8184
|
+
if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
|
|
8185
|
+
throw new Error(
|
|
8186
|
+
`Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
|
|
8187
|
+
);
|
|
8188
|
+
}
|
|
8189
|
+
const conflict2 = claimConflict(
|
|
8190
|
+
exactRecord.workspace,
|
|
8191
|
+
exactRecord.devpodId,
|
|
8192
|
+
worktreePath,
|
|
8193
|
+
records,
|
|
8194
|
+
input2.providerWorkspaces,
|
|
8195
|
+
persistedOwners,
|
|
8196
|
+
exactRecord
|
|
8197
|
+
);
|
|
8198
|
+
if (conflict2) {
|
|
8199
|
+
throw new Error(
|
|
8200
|
+
`Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
|
|
8201
|
+
);
|
|
8202
|
+
}
|
|
8203
|
+
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
8204
|
+
return transaction.write({
|
|
8205
|
+
workspace: exactRecord.workspace,
|
|
8206
|
+
worktreePath,
|
|
8207
|
+
branch: input2.branch ?? null,
|
|
8208
|
+
devpodId: exactRecord.devpodId
|
|
8209
|
+
});
|
|
7743
8210
|
}
|
|
7744
|
-
if (
|
|
7745
|
-
throw new Error(
|
|
8211
|
+
if (persisted && exactProvider && persisted !== exactProvider.id) {
|
|
8212
|
+
throw new Error(
|
|
8213
|
+
`Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
|
|
8214
|
+
);
|
|
7746
8215
|
}
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
if (
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
8216
|
+
let workspace = persisted ?? exactProvider?.id;
|
|
8217
|
+
let devpodId = exactProvider?.id ?? persisted;
|
|
8218
|
+
if (!workspace || !devpodId) {
|
|
8219
|
+
if (input2.unavailableRuntimes.length > 0) {
|
|
8220
|
+
throw new Error(
|
|
8221
|
+
`Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
const candidate = workspaceIdentityCandidates(input2.source).find(
|
|
8225
|
+
(next) => !claimConflict(
|
|
8226
|
+
next,
|
|
8227
|
+
next,
|
|
8228
|
+
worktreePath,
|
|
8229
|
+
records,
|
|
8230
|
+
input2.providerWorkspaces,
|
|
8231
|
+
persistedOwners
|
|
8232
|
+
)
|
|
7757
8233
|
);
|
|
8234
|
+
if (!candidate) {
|
|
8235
|
+
throw new Error(
|
|
8236
|
+
`Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
|
|
8237
|
+
);
|
|
8238
|
+
}
|
|
8239
|
+
workspace = candidate;
|
|
8240
|
+
devpodId = candidate;
|
|
7758
8241
|
}
|
|
7759
|
-
|
|
7760
|
-
|
|
8242
|
+
const conflict = claimConflict(
|
|
8243
|
+
workspace,
|
|
8244
|
+
devpodId,
|
|
8245
|
+
worktreePath,
|
|
8246
|
+
records,
|
|
8247
|
+
input2.providerWorkspaces,
|
|
8248
|
+
persistedOwners
|
|
8249
|
+
);
|
|
8250
|
+
if (conflict) {
|
|
8251
|
+
throw new Error(
|
|
8252
|
+
`Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
|
|
8253
|
+
);
|
|
7761
8254
|
}
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
}
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
8255
|
+
const written = transaction.write({
|
|
8256
|
+
workspace,
|
|
8257
|
+
worktreePath,
|
|
8258
|
+
branch: input2.branch ?? null,
|
|
8259
|
+
devpodId
|
|
8260
|
+
});
|
|
8261
|
+
try {
|
|
8262
|
+
persistWorkspace(worktreePath, workspace);
|
|
8263
|
+
} catch (error) {
|
|
8264
|
+
const cleanup = transaction.removeIfMatches(written);
|
|
8265
|
+
if (cleanup !== "removed") {
|
|
8266
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8267
|
+
throw new Error(
|
|
8268
|
+
`Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
|
|
8269
|
+
);
|
|
8270
|
+
}
|
|
8271
|
+
throw error;
|
|
7775
8272
|
}
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8273
|
+
return written;
|
|
8274
|
+
});
|
|
8275
|
+
}
|
|
8276
|
+
function removeWorkspaceOwnership(repoPath, workspace) {
|
|
8277
|
+
return withWorkspaceOwnershipTransaction(
|
|
8278
|
+
repoPath,
|
|
8279
|
+
(transaction) => transaction.remove(workspace)
|
|
8280
|
+
);
|
|
8281
|
+
}
|
|
8282
|
+
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
8283
|
+
const worktree = worktrees.find(
|
|
8284
|
+
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
8285
|
+
);
|
|
8286
|
+
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
8287
|
+
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
8288
|
+
if (devpodStatus === "conflict") {
|
|
8289
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8290
|
+
}
|
|
8291
|
+
if (worktree?.locked) {
|
|
8292
|
+
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
8293
|
+
}
|
|
8294
|
+
if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
|
|
8295
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8296
|
+
}
|
|
8297
|
+
if (!worktree || worktree.prunable) {
|
|
8298
|
+
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
8299
|
+
}
|
|
8300
|
+
let persisted;
|
|
8301
|
+
try {
|
|
8302
|
+
persisted = readPersistedWorkspace(worktree.path);
|
|
8303
|
+
} catch {
|
|
8304
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
8305
|
+
}
|
|
8306
|
+
return {
|
|
8307
|
+
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
8308
|
+
devpodStatus,
|
|
8309
|
+
worktree
|
|
8310
|
+
};
|
|
8311
|
+
}
|
|
8312
|
+
function listMissingWorkspaceOwnership(repoPath) {
|
|
8313
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
8314
|
+
return listWorkspaceOwnership(repoPath).filter(
|
|
8315
|
+
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
8316
|
+
);
|
|
8317
|
+
}
|
|
8318
|
+
var import_node_child_process15, import_node_fs20, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
8319
|
+
var init_workspace_ownership = __esm({
|
|
8320
|
+
"src/core/workspace-ownership.ts"() {
|
|
8321
|
+
"use strict";
|
|
8322
|
+
import_node_child_process15 = require("child_process");
|
|
8323
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8324
|
+
import_node_path20 = __toESM(require("path"));
|
|
8325
|
+
init_atomic_file();
|
|
8326
|
+
init_devpod_workspaces();
|
|
8327
|
+
init_file_lock();
|
|
8328
|
+
init_workspace();
|
|
8329
|
+
READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
8330
|
+
OWNERSHIP_VERSION = 1;
|
|
8331
|
+
OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
|
|
8332
|
+
}
|
|
8333
|
+
});
|
|
8334
|
+
|
|
8335
|
+
// src/core/managed-devsy-stop.ts
|
|
8336
|
+
function sameSet(left, right) {
|
|
8337
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
8338
|
+
}
|
|
8339
|
+
function containerIdentity(container) {
|
|
8340
|
+
return JSON.stringify({ id: container.id, labels: container.labels, mounts: container.mounts });
|
|
8341
|
+
}
|
|
8342
|
+
function stopRetainedManagedDevsyWorkspace(options) {
|
|
8343
|
+
const { repoPath, devsyId } = options;
|
|
8344
|
+
const linked = isLinkedWorktree(repoPath);
|
|
8345
|
+
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
8346
|
+
const retainedState = readManagedRuntimeState(repoPath, workspace);
|
|
8347
|
+
if (!retainedState) return false;
|
|
8348
|
+
const state = retainedState;
|
|
8349
|
+
if (state.devpodId !== devsyId || linked && !workspace) {
|
|
8350
|
+
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
8351
|
+
}
|
|
8352
|
+
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
8353
|
+
function registration() {
|
|
8354
|
+
resetWorkspaceRuntimeCaches();
|
|
8355
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
8356
|
+
throw new Error("Managed stop provider selection changed.");
|
|
8357
|
+
}
|
|
8358
|
+
const owner = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
8359
|
+
if (owner.status !== "owned") {
|
|
8360
|
+
throw new Error("Managed stop requires one exact retained Devsy registration.");
|
|
8361
|
+
}
|
|
8362
|
+
if (workspace) {
|
|
8363
|
+
const record = readWorkspaceOwnership(repoPath, workspace);
|
|
8364
|
+
if (!record || record.devpodId !== devsyId || !sameWorkspacePath(record.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir) {
|
|
8365
|
+
throw new Error("Managed stop workspace ownership changed.");
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
return owner.workspace;
|
|
8369
|
+
}
|
|
8370
|
+
const context = registration().context;
|
|
8371
|
+
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
8372
|
+
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
8373
|
+
}
|
|
8374
|
+
const devsyRoot = import_node_path21.default.resolve(process.env.DEVSY_HOME || import_node_path21.default.join(import_node_os2.default.homedir(), ".devsy"));
|
|
8375
|
+
const featureDirectory = import_node_path21.default.join(
|
|
8376
|
+
devsyRoot,
|
|
8377
|
+
"contexts",
|
|
8378
|
+
context,
|
|
8379
|
+
"workspaces",
|
|
8380
|
+
devsyId,
|
|
8381
|
+
"agent",
|
|
8382
|
+
".docker-compose"
|
|
8383
|
+
);
|
|
8384
|
+
function prove(previous) {
|
|
8385
|
+
if (registration().context !== context || JSON.stringify(readManagedRuntimeState(repoPath, workspace)) !== JSON.stringify(state)) {
|
|
8386
|
+
throw new Error("Managed stop retained context or runtime record changed.");
|
|
8387
|
+
}
|
|
8388
|
+
const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
|
|
8389
|
+
const managed = runtime.config.managedRuntime;
|
|
8390
|
+
if (!managed || runtime.profile !== state.profile || runtime.workspace !== workspace) {
|
|
8391
|
+
throw new Error("Managed stop requires the recorded managed profile.");
|
|
8392
|
+
}
|
|
8393
|
+
const plan = inspectManagedDevcontainerConfig({
|
|
8394
|
+
repoPath,
|
|
8395
|
+
config: runtime.config,
|
|
8396
|
+
profile: runtime.resolvedProfile,
|
|
8397
|
+
linked
|
|
8398
|
+
});
|
|
8399
|
+
const selectedProcesses = runtime.resolvedProfile?.processes;
|
|
8400
|
+
const processes = !runtime.resolvedProfile || selectedProcesses?.length === 1 && selectedProcesses[0] === "*" ? managed.processes : selectedProcesses ?? [];
|
|
8401
|
+
if (state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || !sameSet(
|
|
8402
|
+
state.desired.apps,
|
|
8403
|
+
proxyAppsFromConfig(runtime.config).map((app) => app.name)
|
|
8404
|
+
) || !sameSet(state.desired.services, plan.desiredProfileServices) || !sameSet(state.desired.processes, processes) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
|
|
8405
|
+
throw new Error("Managed stop requires unchanged recorded resources and configuration.");
|
|
8406
|
+
}
|
|
8407
|
+
const containers = inspectManagedStopContainers(state.composeProject);
|
|
8408
|
+
const services = /* @__PURE__ */ new Set();
|
|
8409
|
+
for (const container of containers) {
|
|
8410
|
+
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
8411
|
+
if (!plan.nativeRunServices.includes(service) || services.has(service) || container.labels["com.docker.compose.project"] !== state.composeProject || !sameWorkspacePath(
|
|
8412
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
8413
|
+
plan.composeDirectory
|
|
8414
|
+
)) {
|
|
8415
|
+
throw new Error("Managed stop found unexpected or duplicate project membership.");
|
|
7782
8416
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
8417
|
+
services.add(service);
|
|
8418
|
+
const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
|
|
8419
|
+
if (files.some((file) => !file || !import_node_path21.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path21.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
|
|
8420
|
+
throw new Error("Managed stop Compose file identity changed.");
|
|
7785
8421
|
}
|
|
7786
|
-
|
|
8422
|
+
for (const file of files.slice(plan.composeFiles.length)) {
|
|
8423
|
+
if (import_node_path21.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
8424
|
+
import_node_path21.default.basename(file)
|
|
8425
|
+
) || import_node_fs21.default.realpathSync(file) !== import_node_path21.default.join(import_node_fs21.default.realpathSync(devsyRoot), import_node_path21.default.relative(devsyRoot, file))) {
|
|
8426
|
+
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
if (previous) {
|
|
8430
|
+
const retained = previous.find((entry) => entry.id === container.id);
|
|
8431
|
+
if (!retained || containerIdentity(retained) !== containerIdentity(container) || !retained.state.Running && container.state.Running) {
|
|
8432
|
+
throw new Error("Managed stop container identity or quiescent state changed.");
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
if (plan.desiredServices.some((service) => !services.has(service)) || previous && !sameSet(
|
|
8437
|
+
previous.map((c) => c.id),
|
|
8438
|
+
containers.map((c) => c.id)
|
|
8439
|
+
)) {
|
|
8440
|
+
throw new Error("Managed stop cannot prove the complete retained service population.");
|
|
8441
|
+
}
|
|
8442
|
+
const primary = containers.find(
|
|
8443
|
+
(c) => c.labels["com.docker.compose.service"] === plan.primaryService
|
|
8444
|
+
);
|
|
8445
|
+
if (primary?.mounts.filter(
|
|
8446
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
8447
|
+
).length !== 1) {
|
|
8448
|
+
throw new Error("Managed stop cannot prove the exact primary workspace mount.");
|
|
8449
|
+
}
|
|
8450
|
+
assertManagedContainerConfigUnchanged({ plan, containers, workspace: workspaceEnv });
|
|
8451
|
+
if (registration().context !== context) {
|
|
8452
|
+
throw new Error("Managed stop provider context changed during inspection.");
|
|
7787
8453
|
}
|
|
8454
|
+
const status = inspectDevsyRuntimeStatus(devsyId);
|
|
8455
|
+
if (status !== "running" && status !== "stopped" || status === "running" !== primary.state.Running) {
|
|
8456
|
+
throw new Error("Managed stop requires consistent provider and primary container state.");
|
|
8457
|
+
}
|
|
8458
|
+
return { containers, primary, status };
|
|
8459
|
+
}
|
|
8460
|
+
const initial = prove();
|
|
8461
|
+
let providerError;
|
|
8462
|
+
let providerFailed = false;
|
|
8463
|
+
if (initial.status === "running") {
|
|
7788
8464
|
try {
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
8465
|
+
options.stopProvider();
|
|
8466
|
+
} catch (error) {
|
|
8467
|
+
providerFailed = true;
|
|
8468
|
+
providerError = error;
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
try {
|
|
8472
|
+
const stopped = () => {
|
|
8473
|
+
const current = prove(initial.containers);
|
|
8474
|
+
if (current.status !== "stopped" || current.primary.state.Running) {
|
|
8475
|
+
throw new Error("Managed stop has not stopped the exact primary container.");
|
|
7793
8476
|
}
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
8477
|
+
return current.containers;
|
|
8478
|
+
};
|
|
8479
|
+
stopped();
|
|
8480
|
+
for (const retained of initial.containers) {
|
|
8481
|
+
if (!retained.state.Running || retained.id === initial.primary.id) continue;
|
|
8482
|
+
const current = stopped().find((entry) => entry.id === retained.id);
|
|
8483
|
+
if (current?.state.Running) {
|
|
8484
|
+
stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
|
|
8485
|
+
timeoutMs: 3e4
|
|
8486
|
+
});
|
|
7800
8487
|
}
|
|
7801
|
-
return devsyId;
|
|
7802
|
-
} catch (error) {
|
|
7803
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7804
|
-
throw new DevsyStartPostconditionError(message);
|
|
7805
8488
|
}
|
|
7806
|
-
|
|
8489
|
+
if (stopped().some((container) => container.state.Running)) {
|
|
8490
|
+
throw new Error("Managed stop left a retained service running.");
|
|
8491
|
+
}
|
|
8492
|
+
} catch (error) {
|
|
8493
|
+
if (providerFailed) {
|
|
8494
|
+
throw new AggregateError(
|
|
8495
|
+
[providerError, error],
|
|
8496
|
+
"Devsy provider stop failed; complete retained shutdown could not be verified.",
|
|
8497
|
+
{ cause: providerError }
|
|
8498
|
+
);
|
|
8499
|
+
}
|
|
8500
|
+
throw error;
|
|
8501
|
+
}
|
|
8502
|
+
if (providerFailed) throw providerError;
|
|
8503
|
+
return true;
|
|
7807
8504
|
}
|
|
7808
|
-
var
|
|
7809
|
-
var
|
|
7810
|
-
"src/core/devsy-
|
|
8505
|
+
var import_node_fs21, import_node_os2, import_node_path21;
|
|
8506
|
+
var init_managed_devsy_stop = __esm({
|
|
8507
|
+
"src/core/managed-devsy-stop.ts"() {
|
|
7811
8508
|
"use strict";
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
8509
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8510
|
+
import_node_os2 = __toESM(require("os"));
|
|
8511
|
+
import_node_path21 = __toESM(require("path"));
|
|
8512
|
+
init_devcontainer_profile();
|
|
8513
|
+
init_devpod_environment();
|
|
7816
8514
|
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;
|
|
8515
|
+
init_managed_runtime_state();
|
|
8516
|
+
init_repo_config();
|
|
8517
|
+
init_route_publication();
|
|
8518
|
+
init_workspace();
|
|
8519
|
+
init_workspace_ownership();
|
|
8520
|
+
init_workspace_runtime();
|
|
7825
8521
|
}
|
|
7826
8522
|
});
|
|
7827
8523
|
|
|
7828
|
-
// src/core/
|
|
7829
|
-
function
|
|
7830
|
-
|
|
8524
|
+
// src/core/devsy-mutation.ts
|
|
8525
|
+
function failedStartMayHaveAttached(devsyId, repoPath) {
|
|
8526
|
+
try {
|
|
8527
|
+
const attached = listDevsyWorkspaces();
|
|
8528
|
+
const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
|
|
8529
|
+
if (!attachedId) return false;
|
|
8530
|
+
return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
|
|
8531
|
+
} catch {
|
|
8532
|
+
return true;
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
function withMutationLock(activity, target, operation) {
|
|
8536
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7831
8537
|
return withFileLockSync(
|
|
7832
|
-
|
|
8538
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
7833
8539
|
{
|
|
7834
8540
|
activity,
|
|
7835
8541
|
target: `'${target}'`,
|
|
7836
|
-
waitMs:
|
|
8542
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
7837
8543
|
fair: true,
|
|
7838
8544
|
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
7839
8545
|
},
|
|
7840
8546
|
operation
|
|
7841
8547
|
);
|
|
7842
8548
|
}
|
|
7843
|
-
function
|
|
8549
|
+
function withMutationLockAsync(activity, target, operation) {
|
|
8550
|
+
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8551
|
+
return withFileLock(
|
|
8552
|
+
DEVSY_MUTATION_LOCK_FILE,
|
|
8553
|
+
{
|
|
8554
|
+
activity,
|
|
8555
|
+
target: `'${target}'`,
|
|
8556
|
+
waitMs: DEVSY_MUTATION_WAIT_MS,
|
|
8557
|
+
fair: true,
|
|
8558
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8559
|
+
},
|
|
8560
|
+
operation
|
|
8561
|
+
);
|
|
8562
|
+
}
|
|
8563
|
+
function commandFailure2(result) {
|
|
7844
8564
|
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7845
8565
|
}
|
|
7846
|
-
function
|
|
7847
|
-
|
|
7848
|
-
|
|
8566
|
+
function runDevsyUp(args, env, quiet) {
|
|
8567
|
+
return new Promise((resolve) => {
|
|
8568
|
+
const child = (0, import_node_child_process16.spawn)("devsy", args, {
|
|
8569
|
+
stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
|
|
8570
|
+
env
|
|
8571
|
+
});
|
|
8572
|
+
const stderr = child.stderr;
|
|
8573
|
+
if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
|
|
8574
|
+
let stderrTail = Buffer.alloc(0);
|
|
8575
|
+
let spawnError;
|
|
8576
|
+
stderr.on("data", (chunk) => {
|
|
8577
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8578
|
+
const writable = process.stderr.write(value);
|
|
8579
|
+
if (!writable) {
|
|
8580
|
+
stderr.pause();
|
|
8581
|
+
process.stderr.once("drain", () => stderr.resume());
|
|
8582
|
+
}
|
|
8583
|
+
if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
|
|
8584
|
+
stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
|
|
8585
|
+
} else {
|
|
8586
|
+
const combined = Buffer.concat([stderrTail, value]);
|
|
8587
|
+
stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
|
|
8588
|
+
}
|
|
8589
|
+
});
|
|
8590
|
+
child.once("error", (error) => {
|
|
8591
|
+
spawnError = error;
|
|
8592
|
+
});
|
|
8593
|
+
child.once("close", (status) => {
|
|
8594
|
+
resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
|
|
8595
|
+
});
|
|
8596
|
+
});
|
|
8597
|
+
}
|
|
8598
|
+
function runDevsyAction(action2, devsyId, force = false) {
|
|
8599
|
+
const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
|
|
8600
|
+
const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
|
|
7849
8601
|
if (result.status !== 0) {
|
|
7850
8602
|
throw new Error(
|
|
7851
|
-
`
|
|
8603
|
+
`devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
|
|
7852
8604
|
);
|
|
7853
8605
|
}
|
|
7854
8606
|
}
|
|
7855
|
-
function
|
|
7856
|
-
const ownership =
|
|
7857
|
-
listDevpodWorkspaces(worktreePath),
|
|
7858
|
-
devpodId,
|
|
7859
|
-
worktreePath
|
|
7860
|
-
);
|
|
8607
|
+
function inspectExactOwnership(devsyId, worktreePath) {
|
|
8608
|
+
const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
|
|
7861
8609
|
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
7862
8610
|
return ownership;
|
|
7863
8611
|
}
|
|
7864
|
-
function
|
|
7865
|
-
return
|
|
7866
|
-
|
|
8612
|
+
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
8613
|
+
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
8614
|
+
if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
|
|
8615
|
+
repoPath: worktreePath,
|
|
8616
|
+
devsyId,
|
|
8617
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
8618
|
+
}))
|
|
8619
|
+
return { status: "changed" };
|
|
8620
|
+
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
7867
8621
|
if (before.status === "absent") return { status: "absent" };
|
|
7868
|
-
|
|
7869
|
-
let after =
|
|
8622
|
+
runDevsyAction(action2, devsyId);
|
|
8623
|
+
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
7870
8624
|
if (action2 === "stop" && after.status !== "owned") {
|
|
7871
|
-
throw new Error(
|
|
8625
|
+
throw new Error(
|
|
8626
|
+
`Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
|
|
8627
|
+
);
|
|
7872
8628
|
}
|
|
7873
8629
|
if (action2 === "delete" && after.status === "owned") {
|
|
7874
|
-
const runtime =
|
|
8630
|
+
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
7875
8631
|
if (runtime !== "not-found") {
|
|
7876
8632
|
throw new Error(
|
|
7877
|
-
`
|
|
8633
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
7878
8634
|
);
|
|
7879
8635
|
}
|
|
7880
|
-
after =
|
|
8636
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7881
8637
|
if (after.status === "owned") {
|
|
7882
|
-
|
|
7883
|
-
after =
|
|
8638
|
+
runDevsyAction("delete", devsyId, true);
|
|
8639
|
+
after = inspectExactOwnership(devsyId, worktreePath);
|
|
7884
8640
|
}
|
|
7885
8641
|
if (after.status !== "absent") {
|
|
7886
8642
|
throw new Error(
|
|
7887
|
-
`
|
|
8643
|
+
`Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
|
|
7888
8644
|
);
|
|
7889
8645
|
}
|
|
7890
8646
|
}
|
|
7891
8647
|
return { status: "changed" };
|
|
7892
8648
|
});
|
|
7893
8649
|
}
|
|
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;
|
|
8650
|
+
function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8651
|
+
return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
|
|
7903
8652
|
}
|
|
7904
|
-
function
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
8653
|
+
function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
8654
|
+
return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
|
|
8655
|
+
}
|
|
8656
|
+
function assertDevsyTarget(devsyId, repoPath) {
|
|
8657
|
+
const workspaces = listDevsyWorkspaces();
|
|
8658
|
+
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
8659
|
+
const id = devsyId ?? existing?.id;
|
|
8660
|
+
if (id) {
|
|
8661
|
+
const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
|
|
8662
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
7909
8663
|
}
|
|
7910
|
-
|
|
7911
|
-
resetWorkspaceRuntimeCaches();
|
|
7912
|
-
return result;
|
|
8664
|
+
return id;
|
|
7913
8665
|
}
|
|
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
|
-
}
|
|
8666
|
+
async function startDevsyWorkspace(options) {
|
|
8667
|
+
const activity = options.recreate ? "Devsy recreate" : "Devsy start";
|
|
8668
|
+
let agent;
|
|
8669
|
+
try {
|
|
8670
|
+
agent = requireReadyDevsyAgent();
|
|
8671
|
+
} catch (error) {
|
|
8672
|
+
if (!(error instanceof DevsyAgentReadinessError)) throw error;
|
|
8673
|
+
const repair = devsyAgentRepairSuggestion(error.inspection);
|
|
8674
|
+
throw new Error(`${error.message}. ${repair}`);
|
|
7935
8675
|
}
|
|
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.`);
|
|
8676
|
+
return withMutationLockAsync(activity, options.repoPath, async () => {
|
|
8677
|
+
let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
|
|
8678
|
+
if (devsyId && options.recreate) {
|
|
8679
|
+
const attached = listDevsyWorkspaces();
|
|
8680
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8681
|
+
if (ownership.status !== "owned") {
|
|
8682
|
+
throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
|
|
7945
8683
|
}
|
|
7946
|
-
} else if (options.recreate) {
|
|
7947
|
-
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
7948
8684
|
}
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
if (options.devcontainerPath) {
|
|
7952
|
-
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8685
|
+
if (!devsyId && options.recreate) {
|
|
8686
|
+
throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
|
|
7953
8687
|
}
|
|
7954
|
-
args
|
|
8688
|
+
const args = ["workspace", "up", options.repoPath];
|
|
8689
|
+
if (devsyId) args.push("--id", devsyId);
|
|
8690
|
+
if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
|
|
8691
|
+
args.push("--ide-launch", "skip");
|
|
7955
8692
|
if (options.workspace) {
|
|
7956
8693
|
args.push(
|
|
7957
8694
|
"--workspace-env",
|
|
@@ -7960,8 +8697,12 @@ async function startDevpodWorkspace(options) {
|
|
|
7960
8697
|
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
7961
8698
|
);
|
|
7962
8699
|
}
|
|
8700
|
+
if (options.inactivityTimeout) {
|
|
8701
|
+
args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
|
|
8702
|
+
}
|
|
7963
8703
|
if (options.recreate) args.push("--recreate");
|
|
7964
8704
|
const env = { ...process.env };
|
|
8705
|
+
env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
7965
8706
|
if (options.workspace) {
|
|
7966
8707
|
env.WORKSPACE = options.workspace.token;
|
|
7967
8708
|
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
@@ -7973,515 +8714,252 @@ async function startDevpodWorkspace(options) {
|
|
|
7973
8714
|
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
7974
8715
|
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
7975
8716
|
}
|
|
7976
|
-
const result =
|
|
7977
|
-
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
7978
|
-
env
|
|
7979
|
-
});
|
|
8717
|
+
const result = await runDevsyUp(args, env, options.quiet ?? false);
|
|
7980
8718
|
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.`);
|
|
8719
|
+
let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
|
|
8720
|
+
if (result.error?.message) message += ` ${result.error.message}`;
|
|
8721
|
+
if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
|
|
8722
|
+
message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
7988
8723
|
}
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
if (ownership.status !== "owned") {
|
|
7992
|
-
throw new Error(
|
|
7993
|
-
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
7994
|
-
);
|
|
8724
|
+
if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
|
|
8725
|
+
throw new DevsyStartPostconditionError(message);
|
|
7995
8726
|
}
|
|
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}'`);
|
|
8727
|
+
throw new Error(message);
|
|
8147
8728
|
}
|
|
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;
|
|
8729
|
+
try {
|
|
8730
|
+
const attached = listDevsyWorkspaces();
|
|
8731
|
+
devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
|
|
8732
|
+
if (!devsyId) {
|
|
8733
|
+
throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
|
|
8734
|
+
}
|
|
8735
|
+
const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
|
|
8736
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8737
|
+
if (ownership.status !== "owned") {
|
|
8738
|
+
throw new Error(
|
|
8739
|
+
`Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
|
|
8740
|
+
);
|
|
8741
|
+
}
|
|
8742
|
+
return devsyId;
|
|
8743
|
+
} catch (error) {
|
|
8744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8745
|
+
throw new DevsyStartPostconditionError(message);
|
|
8746
|
+
}
|
|
8747
|
+
});
|
|
8230
8748
|
}
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8749
|
+
var import_node_child_process16, import_node_fs22, import_node_path22, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
|
|
8750
|
+
var init_devsy_mutation = __esm({
|
|
8751
|
+
"src/core/devsy-mutation.ts"() {
|
|
8752
|
+
"use strict";
|
|
8753
|
+
import_node_child_process16 = require("child_process");
|
|
8754
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
8755
|
+
import_node_path22 = __toESM(require("path"));
|
|
8756
|
+
init_devsy_agent();
|
|
8757
|
+
init_devsy_workspaces();
|
|
8758
|
+
init_file_lock();
|
|
8759
|
+
init_managed_devsy_stop();
|
|
8760
|
+
init_router();
|
|
8761
|
+
DEVSY_MUTATION_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
|
|
8762
|
+
DEVSY_MUTATION_WAIT_MS = 18e5;
|
|
8763
|
+
DevsyStartPostconditionError = class extends Error {
|
|
8764
|
+
};
|
|
8765
|
+
AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
|
|
8766
|
+
DEVSY_STDERR_TAIL_BYTES = 8192;
|
|
8242
8767
|
}
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
const directory = ownershipDirectory(repoPath);
|
|
8249
|
-
import_node_fs22.default.mkdirSync(directory, { recursive: true });
|
|
8768
|
+
});
|
|
8769
|
+
|
|
8770
|
+
// src/core/devpod-mutation.ts
|
|
8771
|
+
function withMutationLock2(activity, target, operation) {
|
|
8772
|
+
import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8250
8773
|
return withFileLockSync(
|
|
8251
|
-
|
|
8774
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
8252
8775
|
{
|
|
8253
|
-
activity
|
|
8254
|
-
target: `'${
|
|
8255
|
-
waitMs:
|
|
8776
|
+
activity,
|
|
8777
|
+
target: `'${target}'`,
|
|
8778
|
+
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
8779
|
+
fair: true,
|
|
8780
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
8256
8781
|
},
|
|
8257
|
-
|
|
8258
|
-
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
8259
|
-
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
8260
|
-
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
8261
|
-
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
8262
|
-
})
|
|
8782
|
+
operation
|
|
8263
8783
|
);
|
|
8264
8784
|
}
|
|
8265
|
-
function
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8785
|
+
function commandFailure3(result) {
|
|
8786
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
8787
|
+
}
|
|
8788
|
+
function runDevpodAction(action2, devpodId, force = false) {
|
|
8789
|
+
const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
|
|
8790
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
8791
|
+
if (result.status !== 0) {
|
|
8270
8792
|
throw new Error(
|
|
8271
|
-
`
|
|
8793
|
+
`devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
|
|
8272
8794
|
);
|
|
8273
8795
|
}
|
|
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
8796
|
}
|
|
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)
|
|
8797
|
+
function inspectExactOwnership2(devpodId, worktreePath) {
|
|
8798
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
8799
|
+
listDevpodWorkspaces(worktreePath),
|
|
8800
|
+
devpodId,
|
|
8801
|
+
worktreePath
|
|
8305
8802
|
);
|
|
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;
|
|
8803
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8804
|
+
return ownership;
|
|
8314
8805
|
}
|
|
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
|
-
);
|
|
8806
|
+
function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
|
|
8807
|
+
return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
|
|
8808
|
+
const before = inspectExactOwnership2(devpodId, worktreePath);
|
|
8809
|
+
if (before.status === "absent") return { status: "absent" };
|
|
8810
|
+
runDevpodAction(action2, devpodId);
|
|
8811
|
+
let after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8812
|
+
if (action2 === "stop" && after.status !== "owned") {
|
|
8813
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
8327
8814
|
}
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
if (exactRecord) {
|
|
8332
|
-
if (persisted && persisted !== exactRecord.workspace) {
|
|
8815
|
+
if (action2 === "delete" && after.status === "owned") {
|
|
8816
|
+
const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
|
|
8817
|
+
if (runtime !== "not-found") {
|
|
8333
8818
|
throw new Error(
|
|
8334
|
-
`
|
|
8819
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
8335
8820
|
);
|
|
8336
8821
|
}
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
);
|
|
8822
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8823
|
+
if (after.status === "owned") {
|
|
8824
|
+
runDevpodAction("delete", devpodId, true);
|
|
8825
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
8341
8826
|
}
|
|
8342
|
-
|
|
8343
|
-
exactRecord.workspace,
|
|
8344
|
-
exactRecord.devpodId,
|
|
8345
|
-
worktreePath,
|
|
8346
|
-
records,
|
|
8347
|
-
input2.providerWorkspaces,
|
|
8348
|
-
persistedOwners,
|
|
8349
|
-
exactRecord
|
|
8350
|
-
);
|
|
8351
|
-
if (conflict2) {
|
|
8827
|
+
if (after.status !== "absent") {
|
|
8352
8828
|
throw new Error(
|
|
8353
|
-
`
|
|
8829
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
8354
8830
|
);
|
|
8355
8831
|
}
|
|
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
8832
|
}
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8833
|
+
return { status: "changed" };
|
|
8834
|
+
});
|
|
8835
|
+
}
|
|
8836
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8837
|
+
try {
|
|
8838
|
+
resetWorkspaceRuntimeCaches();
|
|
8839
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8840
|
+
return stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8368
8841
|
}
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8842
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
8843
|
+
} finally {
|
|
8844
|
+
resetWorkspaceRuntimeCaches();
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
8848
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
8849
|
+
const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
8850
|
+
resetWorkspaceRuntimeCaches();
|
|
8851
|
+
return result2;
|
|
8852
|
+
}
|
|
8853
|
+
const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
8854
|
+
resetWorkspaceRuntimeCaches();
|
|
8855
|
+
return result;
|
|
8856
|
+
}
|
|
8857
|
+
async function startDevpodWorkspace(options) {
|
|
8858
|
+
if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
|
|
8859
|
+
try {
|
|
8860
|
+
const result = await startDevsyWorkspace({
|
|
8861
|
+
repoPath: options.repoPath,
|
|
8862
|
+
devsyId: options.devpodId,
|
|
8863
|
+
devcontainerPath: options.devcontainerPath,
|
|
8864
|
+
recreate: options.recreate,
|
|
8865
|
+
quiet: options.quiet,
|
|
8866
|
+
workspace: options.workspace,
|
|
8867
|
+
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
8868
|
+
});
|
|
8869
|
+
resetWorkspaceRuntimeCaches();
|
|
8870
|
+
return result;
|
|
8871
|
+
} catch (error) {
|
|
8872
|
+
if (error instanceof DevsyStartPostconditionError) {
|
|
8873
|
+
resetWorkspaceRuntimeCaches();
|
|
8874
|
+
throw new DevpodStartPostconditionError(error.message);
|
|
8376
8875
|
}
|
|
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
|
-
);
|
|
8876
|
+
throw error;
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8879
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
8880
|
+
return withMutationLock2(activity, options.repoPath, () => {
|
|
8881
|
+
const workspaces = listDevpodWorkspaces(options.repoPath);
|
|
8882
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
8883
|
+
if (devpodId) {
|
|
8884
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
8885
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
8886
|
+
if (options.recreate && before.status !== "owned") {
|
|
8887
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
8391
8888
|
}
|
|
8392
|
-
|
|
8393
|
-
|
|
8889
|
+
} else if (options.recreate) {
|
|
8890
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
8394
8891
|
}
|
|
8395
|
-
const
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8892
|
+
const args = ["up", options.repoPath];
|
|
8893
|
+
if (devpodId) args.push("--id", devpodId);
|
|
8894
|
+
if (options.devcontainerPath) {
|
|
8895
|
+
args.push("--devcontainer-path", options.devcontainerPath);
|
|
8896
|
+
}
|
|
8897
|
+
args.push("--open-ide=false");
|
|
8898
|
+
if (options.workspace) {
|
|
8899
|
+
args.push(
|
|
8900
|
+
"--workspace-env",
|
|
8901
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
8902
|
+
"--workspace-env",
|
|
8903
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
8406
8904
|
);
|
|
8407
8905
|
}
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8906
|
+
if (options.recreate) args.push("--recreate");
|
|
8907
|
+
const env = { ...process.env };
|
|
8908
|
+
if (options.workspace) {
|
|
8909
|
+
env.WORKSPACE = options.workspace.token;
|
|
8910
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
8911
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
8912
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
8913
|
+
} else {
|
|
8914
|
+
delete env.WORKSPACE;
|
|
8915
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
8916
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
8917
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
8918
|
+
}
|
|
8919
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
|
|
8920
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
8921
|
+
env
|
|
8413
8922
|
});
|
|
8923
|
+
if (result.status !== 0) {
|
|
8924
|
+
throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
|
|
8925
|
+
}
|
|
8414
8926
|
try {
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8927
|
+
const attached = listDevpodWorkspaces(options.repoPath);
|
|
8928
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
8929
|
+
if (!devpodId) {
|
|
8930
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
8931
|
+
}
|
|
8932
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
8933
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
8934
|
+
if (ownership.status !== "owned") {
|
|
8420
8935
|
throw new Error(
|
|
8421
|
-
`
|
|
8936
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
8422
8937
|
);
|
|
8423
8938
|
}
|
|
8424
|
-
|
|
8939
|
+
resetWorkspaceRuntimeCaches();
|
|
8940
|
+
return devpodId;
|
|
8941
|
+
} catch (error) {
|
|
8942
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8943
|
+
throw new DevpodStartPostconditionError(message);
|
|
8425
8944
|
}
|
|
8426
|
-
return written;
|
|
8427
8945
|
});
|
|
8428
8946
|
}
|
|
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"() {
|
|
8947
|
+
var import_node_child_process17, import_node_fs23, import_node_path23, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
8948
|
+
var init_devpod_mutation = __esm({
|
|
8949
|
+
"src/core/devpod-mutation.ts"() {
|
|
8474
8950
|
"use strict";
|
|
8475
8951
|
import_node_child_process17 = require("child_process");
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
init_atomic_file();
|
|
8952
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
8953
|
+
import_node_path23 = __toESM(require("path"));
|
|
8479
8954
|
init_devpod_workspaces();
|
|
8955
|
+
init_devsy_mutation();
|
|
8480
8956
|
init_file_lock();
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8957
|
+
init_router();
|
|
8958
|
+
init_workspace_runtime();
|
|
8959
|
+
DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8960
|
+
DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8961
|
+
DevpodStartPostconditionError = class extends Error {
|
|
8962
|
+
};
|
|
8485
8963
|
}
|
|
8486
8964
|
});
|
|
8487
8965
|
|
|
@@ -8495,10 +8973,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
8495
8973
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
8496
8974
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
8497
8975
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
8498
|
-
const localRoot =
|
|
8976
|
+
const localRoot = import_node_path24.default.join(comparableRepo, "trees") + import_node_path24.default.sep;
|
|
8499
8977
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
8500
|
-
const legacyPrefix = `${
|
|
8501
|
-
return
|
|
8978
|
+
const legacyPrefix = `${import_node_path24.default.basename(comparableRepo)}-`;
|
|
8979
|
+
return import_node_path24.default.dirname(comparableWorktree) === import_node_path24.default.dirname(comparableRepo) && import_node_path24.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
8502
8980
|
}
|
|
8503
8981
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
8504
8982
|
const actions = [
|
|
@@ -8750,11 +9228,11 @@ function applyWorkspaceGc(plan) {
|
|
|
8750
9228
|
candidates
|
|
8751
9229
|
};
|
|
8752
9230
|
}
|
|
8753
|
-
var
|
|
9231
|
+
var import_node_path24;
|
|
8754
9232
|
var init_workspace_gc = __esm({
|
|
8755
9233
|
"src/core/workspace-gc.ts"() {
|
|
8756
9234
|
"use strict";
|
|
8757
|
-
|
|
9235
|
+
import_node_path24 = __toESM(require("path"));
|
|
8758
9236
|
init_devpod_mutation();
|
|
8759
9237
|
init_devpod_workspaces();
|
|
8760
9238
|
init_host_routes();
|
|
@@ -8824,11 +9302,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
8824
9302
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
8825
9303
|
continue;
|
|
8826
9304
|
}
|
|
8827
|
-
if (!
|
|
9305
|
+
if (!import_node_fs24.default.existsSync(absolutePath)) {
|
|
8828
9306
|
continue;
|
|
8829
9307
|
}
|
|
8830
9308
|
try {
|
|
8831
|
-
const raw =
|
|
9309
|
+
const raw = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
|
|
8832
9310
|
const parsed = import_yaml5.default.parse(raw);
|
|
8833
9311
|
const root = asRecord2(parsed);
|
|
8834
9312
|
const services = asRecord2(root?.services);
|
|
@@ -9107,7 +9585,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
9107
9585
|
const config = runtimeConfig.config;
|
|
9108
9586
|
loadedConfig = config;
|
|
9109
9587
|
loadedWorkspace = runtimeConfig.workspace;
|
|
9110
|
-
const cliVersion = true ? "0.0.
|
|
9588
|
+
const cliVersion = true ? "0.0.53" : "0.0.0-dev";
|
|
9111
9589
|
const configVersion = config.devrouter?.version;
|
|
9112
9590
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
9113
9591
|
addCheck(checks, {
|
|
@@ -9137,9 +9615,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
9137
9615
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
9138
9616
|
app: app.name,
|
|
9139
9617
|
filePath,
|
|
9140
|
-
absolutePath:
|
|
9618
|
+
absolutePath: import_node_path25.default.resolve(repo.path, filePath)
|
|
9141
9619
|
}))
|
|
9142
|
-
).filter((entry) => !
|
|
9620
|
+
).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9143
9621
|
addCheck(checks, {
|
|
9144
9622
|
id: "repo.compose-files",
|
|
9145
9623
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -9182,8 +9660,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
9182
9660
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
9183
9661
|
app: app.name,
|
|
9184
9662
|
cwd: app.hostRun.cwd,
|
|
9185
|
-
absolutePath:
|
|
9186
|
-
})).filter((entry) => !
|
|
9663
|
+
absolutePath: import_node_path25.default.resolve(repo.path, app.hostRun.cwd)
|
|
9664
|
+
})).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
|
|
9187
9665
|
addCheck(checks, {
|
|
9188
9666
|
id: "repo.host-cwd",
|
|
9189
9667
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -9327,12 +9805,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
9327
9805
|
nextSteps
|
|
9328
9806
|
};
|
|
9329
9807
|
}
|
|
9330
|
-
var
|
|
9808
|
+
var import_node_fs24, import_node_path25, import_yaml5, POSTGRES_DEFAULTS;
|
|
9331
9809
|
var init_doctor = __esm({
|
|
9332
9810
|
"src/core/doctor.ts"() {
|
|
9333
9811
|
"use strict";
|
|
9334
|
-
|
|
9335
|
-
|
|
9812
|
+
import_node_fs24 = __toESM(require("fs"));
|
|
9813
|
+
import_node_path25 = __toESM(require("path"));
|
|
9336
9814
|
import_yaml5 = __toESM(require("yaml"));
|
|
9337
9815
|
init_docker();
|
|
9338
9816
|
init_host_routes();
|
|
@@ -9779,76 +10257,6 @@ var init_http_route_probe = __esm({
|
|
|
9779
10257
|
}
|
|
9780
10258
|
});
|
|
9781
10259
|
|
|
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
10260
|
// src/core/traefik-route-health.ts
|
|
9853
10261
|
function sleep(ms) {
|
|
9854
10262
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -9893,7 +10301,7 @@ function inspectRouterApi(protocol) {
|
|
|
9893
10301
|
)
|
|
9894
10302
|
};
|
|
9895
10303
|
}
|
|
9896
|
-
function
|
|
10304
|
+
function isRecord3(value) {
|
|
9897
10305
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9898
10306
|
}
|
|
9899
10307
|
function inspectNamedRouterApi(protocol, resource, name) {
|
|
@@ -9924,7 +10332,7 @@ function inspectNamedRouterApi(protocol, resource, name) {
|
|
|
9924
10332
|
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
9925
10333
|
};
|
|
9926
10334
|
}
|
|
9927
|
-
if (!
|
|
10335
|
+
if (!isRecord3(value)) {
|
|
9928
10336
|
return {
|
|
9929
10337
|
ok: false,
|
|
9930
10338
|
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
@@ -9953,19 +10361,19 @@ function renderExpectedRoute(route) {
|
|
|
9953
10361
|
};
|
|
9954
10362
|
const document = buildHostRoutesDocument([routeState], false);
|
|
9955
10363
|
const protocolDocument = document[protocol];
|
|
9956
|
-
if (!
|
|
10364
|
+
if (!isRecord3(protocolDocument)) return void 0;
|
|
9957
10365
|
const routers = protocolDocument.routers;
|
|
9958
10366
|
const services = protocolDocument.services;
|
|
9959
|
-
if (!
|
|
10367
|
+
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
9960
10368
|
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
9961
10369
|
const router = routers[routerKey];
|
|
9962
10370
|
const service = services[routerKey];
|
|
9963
|
-
if (!
|
|
10371
|
+
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
9964
10372
|
const rule = router.rule;
|
|
9965
10373
|
const serviceReference = router.service;
|
|
9966
10374
|
const loadBalancer = service.loadBalancer;
|
|
9967
|
-
const servers =
|
|
9968
|
-
if (typeof rule !== "string" || serviceReference !== routerKey || !
|
|
10375
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10376
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
9969
10377
|
return void 0;
|
|
9970
10378
|
}
|
|
9971
10379
|
const serverField = protocol === "http" ? "url" : "address";
|
|
@@ -10009,9 +10417,9 @@ function inspectExpectedRouteMatch(route) {
|
|
|
10009
10417
|
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
10010
10418
|
}
|
|
10011
10419
|
const loadBalancer = serviceResult.value.loadBalancer;
|
|
10012
|
-
const servers =
|
|
10420
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
10013
10421
|
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
10014
|
-
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !
|
|
10422
|
+
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
|
|
10015
10423
|
return {
|
|
10016
10424
|
ok: false,
|
|
10017
10425
|
routes: [routeName],
|
|
@@ -10108,7 +10516,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
10108
10516
|
throw new Error(
|
|
10109
10517
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
10110
10518
|
);
|
|
10111
|
-
|
|
10519
|
+
import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
10112
10520
|
return withFileLock(
|
|
10113
10521
|
ROUTER_RELOAD_LOCK_FILE,
|
|
10114
10522
|
{
|
|
@@ -10152,18 +10560,18 @@ async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
|
10152
10560
|
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
10153
10561
|
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
10154
10562
|
}
|
|
10155
|
-
var import_node_child_process20,
|
|
10563
|
+
var import_node_child_process20, import_node_fs25, import_node_path26, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
|
|
10156
10564
|
var init_traefik_route_health = __esm({
|
|
10157
10565
|
"src/core/traefik-route-health.ts"() {
|
|
10158
10566
|
"use strict";
|
|
10159
10567
|
import_node_child_process20 = require("child_process");
|
|
10160
|
-
|
|
10161
|
-
|
|
10568
|
+
import_node_fs25 = __toESM(require("fs"));
|
|
10569
|
+
import_node_path26 = __toESM(require("path"));
|
|
10162
10570
|
init_file_lock();
|
|
10163
10571
|
init_host_routes();
|
|
10164
10572
|
init_router();
|
|
10165
10573
|
ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
10166
|
-
ROUTER_RELOAD_LOCK_FILE =
|
|
10574
|
+
ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
10167
10575
|
ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
10168
10576
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
10169
10577
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
@@ -10175,11 +10583,11 @@ var init_traefik_route_health = __esm({
|
|
|
10175
10583
|
// src/core/workspace-ensure.ts
|
|
10176
10584
|
function assertOverlay(container, repoPath) {
|
|
10177
10585
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
10178
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
10586
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
|
|
10179
10587
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
10180
10588
|
}
|
|
10181
10589
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
10182
|
-
const expectedOverlay =
|
|
10590
|
+
const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
10183
10591
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
10184
10592
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
10185
10593
|
}
|
|
@@ -10369,7 +10777,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
10369
10777
|
}).some((container) => {
|
|
10370
10778
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
10371
10779
|
return Boolean(
|
|
10372
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
10780
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
|
|
10373
10781
|
);
|
|
10374
10782
|
});
|
|
10375
10783
|
} catch {
|
|
@@ -10726,7 +11134,7 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
10726
11134
|
}
|
|
10727
11135
|
function isPrimaryCheckout(repoPath) {
|
|
10728
11136
|
try {
|
|
10729
|
-
return
|
|
11137
|
+
return import_node_fs26.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
|
|
10730
11138
|
} catch {
|
|
10731
11139
|
return false;
|
|
10732
11140
|
}
|
|
@@ -10784,8 +11192,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10784
11192
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
10785
11193
|
let devpodId = target.devpodId;
|
|
10786
11194
|
if (target.kind === "linked") {
|
|
10787
|
-
const overlayPath =
|
|
10788
|
-
if (!
|
|
11195
|
+
const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
11196
|
+
if (!import_node_fs26.default.existsSync(overlayPath)) {
|
|
10789
11197
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
10790
11198
|
}
|
|
10791
11199
|
}
|
|
@@ -10859,7 +11267,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10859
11267
|
}
|
|
10860
11268
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
10861
11269
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
10862
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
11270
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
|
|
10863
11271
|
for (const [index, app] of apps.entries()) {
|
|
10864
11272
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
10865
11273
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -11427,9 +11835,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11427
11835
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11428
11836
|
if (!options.repair) return ensureLocked();
|
|
11429
11837
|
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
11430
|
-
|
|
11838
|
+
import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11431
11839
|
return withFileLock(
|
|
11432
|
-
|
|
11840
|
+
import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
11433
11841
|
{
|
|
11434
11842
|
activity: "Managed runtime repair",
|
|
11435
11843
|
target: repoPath,
|
|
@@ -11441,13 +11849,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11441
11849
|
);
|
|
11442
11850
|
});
|
|
11443
11851
|
}
|
|
11444
|
-
var import_node_child_process21,
|
|
11852
|
+
var import_node_child_process21, import_node_fs26, import_node_path27, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
11445
11853
|
var init_workspace_ensure = __esm({
|
|
11446
11854
|
"src/core/workspace-ensure.ts"() {
|
|
11447
11855
|
"use strict";
|
|
11448
11856
|
import_node_child_process21 = require("child_process");
|
|
11449
|
-
|
|
11450
|
-
|
|
11857
|
+
import_node_fs26 = __toESM(require("fs"));
|
|
11858
|
+
import_node_path27 = __toESM(require("path"));
|
|
11451
11859
|
init_devcontainer_profile();
|
|
11452
11860
|
init_devpod_environment();
|
|
11453
11861
|
init_devpod_mutation();
|
|
@@ -11536,7 +11944,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
11536
11944
|
);
|
|
11537
11945
|
}
|
|
11538
11946
|
function defaultWorktreePath(mainRepo, ws) {
|
|
11539
|
-
return
|
|
11947
|
+
return import_node_path28.default.join(mainRepo, "trees", ws);
|
|
11540
11948
|
}
|
|
11541
11949
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
11542
11950
|
const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
@@ -11544,12 +11952,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
|
11544
11952
|
});
|
|
11545
11953
|
if (ignored.status !== 0) {
|
|
11546
11954
|
throw new Error(
|
|
11547
|
-
`Default worktree root '${
|
|
11955
|
+
`Default worktree root '${import_node_path28.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path28.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
11548
11956
|
);
|
|
11549
11957
|
}
|
|
11550
11958
|
}
|
|
11551
11959
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
11552
|
-
return
|
|
11960
|
+
return import_node_path28.default.join(import_node_path28.default.dirname(mainRepo), `${import_node_path28.default.basename(mainRepo)}-${ws}`);
|
|
11553
11961
|
}
|
|
11554
11962
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
11555
11963
|
const candidates = [
|
|
@@ -11661,7 +12069,7 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
11661
12069
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
11662
12070
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
11663
12071
|
}
|
|
11664
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
12072
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs27.default.existsSync(target.worktreePath)) return;
|
|
11665
12073
|
if (target.worktree.locked) {
|
|
11666
12074
|
throw new Error(
|
|
11667
12075
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
@@ -11701,9 +12109,9 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11701
12109
|
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
11702
12110
|
);
|
|
11703
12111
|
const generatedPath = candidatePaths.find(
|
|
11704
|
-
(candidate) => !
|
|
12112
|
+
(candidate) => !import_node_fs27.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
11705
12113
|
);
|
|
11706
|
-
const selectedPath = opts.path ?
|
|
12114
|
+
const selectedPath = opts.path ? import_node_path28.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
11707
12115
|
if (!selectedPath) {
|
|
11708
12116
|
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
11709
12117
|
}
|
|
@@ -11712,7 +12120,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
11712
12120
|
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
11713
12121
|
);
|
|
11714
12122
|
}
|
|
11715
|
-
if (
|
|
12123
|
+
if (import_node_fs27.default.existsSync(selectedPath)) {
|
|
11716
12124
|
const registered = worktrees.find(
|
|
11717
12125
|
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
11718
12126
|
);
|
|
@@ -11806,6 +12214,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
11806
12214
|
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
11807
12215
|
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
11808
12216
|
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
12217
|
+
if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
|
|
12218
|
+
throw new Error(
|
|
12219
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12220
|
+
);
|
|
12221
|
+
}
|
|
11809
12222
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
11810
12223
|
await ensureTraefikRoutesRemoved(routes);
|
|
11811
12224
|
if (!quiet) {
|
|
@@ -11838,7 +12251,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11838
12251
|
opts.quiet
|
|
11839
12252
|
);
|
|
11840
12253
|
if (removeWorktree) {
|
|
11841
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
12254
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath)) {
|
|
11842
12255
|
const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
11843
12256
|
encoding: "utf-8"
|
|
11844
12257
|
});
|
|
@@ -11857,7 +12270,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11857
12270
|
}
|
|
11858
12271
|
return result;
|
|
11859
12272
|
};
|
|
11860
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
12273
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
11861
12274
|
}
|
|
11862
12275
|
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
11863
12276
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
@@ -11892,15 +12305,16 @@ async function workspaceStop(target, opts = {}) {
|
|
|
11892
12305
|
async function workspaceDown(target, opts = {}) {
|
|
11893
12306
|
return runWorkspaceLifecycle("down", target, opts);
|
|
11894
12307
|
}
|
|
11895
|
-
var import_node_child_process22,
|
|
12308
|
+
var import_node_child_process22, import_node_fs27, import_node_path28, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
11896
12309
|
var init_workspace_lifecycle = __esm({
|
|
11897
12310
|
"src/core/workspace-lifecycle.ts"() {
|
|
11898
12311
|
"use strict";
|
|
11899
12312
|
import_node_child_process22 = require("child_process");
|
|
11900
|
-
|
|
11901
|
-
|
|
12313
|
+
import_node_fs27 = __toESM(require("fs"));
|
|
12314
|
+
import_node_path28 = __toESM(require("path"));
|
|
11902
12315
|
init_devpod_mutation();
|
|
11903
12316
|
init_devpod_workspaces();
|
|
12317
|
+
init_managed_runtime_state();
|
|
11904
12318
|
init_repo_config();
|
|
11905
12319
|
init_route_state();
|
|
11906
12320
|
init_traefik_route_health();
|
|
@@ -11952,6 +12366,11 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
11952
12366
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
11953
12367
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
|
|
11954
12368
|
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
12369
|
+
if (!options.delete && mutation.status === "absent" && readManagedRuntimeState(repoPath, workspace)) {
|
|
12370
|
+
throw new Error(
|
|
12371
|
+
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
12372
|
+
);
|
|
12373
|
+
}
|
|
11955
12374
|
const removedRoutes = removeHostRoutesWhere(
|
|
11956
12375
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
11957
12376
|
);
|
|
@@ -11973,6 +12392,7 @@ var init_environment_stop = __esm({
|
|
|
11973
12392
|
init_devpod_mutation();
|
|
11974
12393
|
init_devpod_workspaces();
|
|
11975
12394
|
init_host_routes();
|
|
12395
|
+
init_managed_runtime_state();
|
|
11976
12396
|
init_traefik_route_health();
|
|
11977
12397
|
init_workspace();
|
|
11978
12398
|
init_workspace_lifecycle();
|
|
@@ -12470,17 +12890,17 @@ function asRecord3(value) {
|
|
|
12470
12890
|
return value;
|
|
12471
12891
|
}
|
|
12472
12892
|
function readJson(filePath) {
|
|
12473
|
-
if (!
|
|
12893
|
+
if (!import_node_fs28.default.existsSync(filePath)) {
|
|
12474
12894
|
return void 0;
|
|
12475
12895
|
}
|
|
12476
12896
|
try {
|
|
12477
|
-
return JSON.parse(
|
|
12897
|
+
return JSON.parse(import_node_fs28.default.readFileSync(filePath, "utf-8"));
|
|
12478
12898
|
} catch {
|
|
12479
12899
|
return void 0;
|
|
12480
12900
|
}
|
|
12481
12901
|
}
|
|
12482
12902
|
function relative(repoPath, filePath) {
|
|
12483
|
-
return
|
|
12903
|
+
return import_node_path29.default.relative(repoPath, filePath) || ".";
|
|
12484
12904
|
}
|
|
12485
12905
|
function redactEnvAssignments(value) {
|
|
12486
12906
|
return value.replace(
|
|
@@ -12521,7 +12941,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
12521
12941
|
["bun.lock", "bun"]
|
|
12522
12942
|
];
|
|
12523
12943
|
for (const [fileName, name] of lockfiles) {
|
|
12524
|
-
if (
|
|
12944
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
12525
12945
|
return { name, source: fileName };
|
|
12526
12946
|
}
|
|
12527
12947
|
}
|
|
@@ -12536,9 +12956,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
12536
12956
|
if (typeof engines?.node === "string") {
|
|
12537
12957
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
12538
12958
|
}
|
|
12539
|
-
const nvmrc =
|
|
12540
|
-
if (
|
|
12541
|
-
const version =
|
|
12959
|
+
const nvmrc = import_node_path29.default.join(repoPath, ".nvmrc");
|
|
12960
|
+
if (import_node_fs28.default.existsSync(nvmrc)) {
|
|
12961
|
+
const version = import_node_fs28.default.readFileSync(nvmrc, "utf-8").trim();
|
|
12542
12962
|
return { version, source: ".nvmrc" };
|
|
12543
12963
|
}
|
|
12544
12964
|
return void 0;
|
|
@@ -12599,7 +13019,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
12599
13019
|
const files = config.apps.filter(
|
|
12600
13020
|
(app) => app.runtime === "docker"
|
|
12601
13021
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
12602
|
-
(fileName) => !
|
|
13022
|
+
(fileName) => !import_node_path29.default.isAbsolute(fileName) && !import_node_path29.default.normalize(fileName).startsWith("..")
|
|
12603
13023
|
);
|
|
12604
13024
|
return Array.from(new Set(files));
|
|
12605
13025
|
} catch {
|
|
@@ -12617,7 +13037,7 @@ function composeFiles(repoPath) {
|
|
|
12617
13037
|
...configuredComposeFiles(repoPath)
|
|
12618
13038
|
];
|
|
12619
13039
|
return Array.from(new Set(candidates)).filter(
|
|
12620
|
-
(fileName) =>
|
|
13040
|
+
(fileName) => import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))
|
|
12621
13041
|
);
|
|
12622
13042
|
}
|
|
12623
13043
|
function stringArray2(value) {
|
|
@@ -12655,7 +13075,7 @@ function inspectServices(repoPath) {
|
|
|
12655
13075
|
const services = [];
|
|
12656
13076
|
for (const fileName of composeFiles(repoPath)) {
|
|
12657
13077
|
try {
|
|
12658
|
-
const parsed = import_yaml6.default.parse(
|
|
13078
|
+
const parsed = import_yaml6.default.parse(import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8"));
|
|
12659
13079
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
12660
13080
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
12661
13081
|
const service = asRecord3(value);
|
|
@@ -12690,8 +13110,8 @@ function inspectServices(repoPath) {
|
|
|
12690
13110
|
return services;
|
|
12691
13111
|
}
|
|
12692
13112
|
function inspectEnvFiles(repoPath) {
|
|
12693
|
-
const files =
|
|
12694
|
-
const content =
|
|
13113
|
+
const files = import_node_fs28.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
13114
|
+
const content = import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8");
|
|
12695
13115
|
const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
|
|
12696
13116
|
return { path: fileName, names };
|
|
12697
13117
|
});
|
|
@@ -12705,18 +13125,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
12705
13125
|
};
|
|
12706
13126
|
}
|
|
12707
13127
|
function inspectDevcontainer(repoPath) {
|
|
12708
|
-
const dir =
|
|
12709
|
-
if (!
|
|
13128
|
+
const dir = import_node_path29.default.join(repoPath, ".devcontainer");
|
|
13129
|
+
if (!import_node_fs28.default.existsSync(dir)) {
|
|
12710
13130
|
return { exists: false, files: [] };
|
|
12711
13131
|
}
|
|
12712
13132
|
return {
|
|
12713
13133
|
exists: true,
|
|
12714
|
-
files:
|
|
13134
|
+
files: import_node_fs28.default.readdirSync(dir).filter((fileName) => import_node_fs28.default.statSync(import_node_path29.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
12715
13135
|
};
|
|
12716
13136
|
}
|
|
12717
13137
|
function inspectDevrouter(repoPath) {
|
|
12718
13138
|
const configPath = getRepoConfigPath(repoPath);
|
|
12719
|
-
if (!
|
|
13139
|
+
if (!import_node_fs28.default.existsSync(configPath)) {
|
|
12720
13140
|
return {
|
|
12721
13141
|
exists: false,
|
|
12722
13142
|
configPath,
|
|
@@ -12760,15 +13180,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
12760
13180
|
["AGENTS.md", "agents"],
|
|
12761
13181
|
["CLAUDE.md", "claude"]
|
|
12762
13182
|
]) {
|
|
12763
|
-
if (
|
|
13183
|
+
if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
|
|
12764
13184
|
results.push({ path: fileName, kind });
|
|
12765
13185
|
}
|
|
12766
13186
|
}
|
|
12767
|
-
const skillsDir =
|
|
12768
|
-
if (
|
|
12769
|
-
for (const name of
|
|
12770
|
-
const skillPath =
|
|
12771
|
-
if (
|
|
13187
|
+
const skillsDir = import_node_path29.default.join(repoPath, ".agents", "skills");
|
|
13188
|
+
if (import_node_fs28.default.existsSync(skillsDir)) {
|
|
13189
|
+
for (const name of import_node_fs28.default.readdirSync(skillsDir).sort()) {
|
|
13190
|
+
const skillPath = import_node_path29.default.join(skillsDir, name, "SKILL.md");
|
|
13191
|
+
if (import_node_fs28.default.existsSync(skillPath)) {
|
|
12772
13192
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
12773
13193
|
}
|
|
12774
13194
|
}
|
|
@@ -12811,7 +13231,7 @@ function buildIssues(report) {
|
|
|
12811
13231
|
}
|
|
12812
13232
|
function inspectRepo(options = {}) {
|
|
12813
13233
|
const repoPath = resolveRepoPath(options.repo);
|
|
12814
|
-
const pkg = readJson(
|
|
13234
|
+
const pkg = readJson(import_node_path29.default.join(repoPath, "package.json"));
|
|
12815
13235
|
const scripts = inspectScripts(pkg);
|
|
12816
13236
|
const reportWithoutIssues = {
|
|
12817
13237
|
repoPath,
|
|
@@ -12830,12 +13250,12 @@ function inspectRepo(options = {}) {
|
|
|
12830
13250
|
issues: buildIssues(reportWithoutIssues)
|
|
12831
13251
|
};
|
|
12832
13252
|
}
|
|
12833
|
-
var
|
|
13253
|
+
var import_node_fs28, import_node_path29, import_yaml6;
|
|
12834
13254
|
var init_repo_inspect = __esm({
|
|
12835
13255
|
"src/core/repo-inspect.ts"() {
|
|
12836
13256
|
"use strict";
|
|
12837
|
-
|
|
12838
|
-
|
|
13257
|
+
import_node_fs28 = __toESM(require("fs"));
|
|
13258
|
+
import_node_path29 = __toESM(require("path"));
|
|
12839
13259
|
import_yaml6 = __toESM(require("yaml"));
|
|
12840
13260
|
init_repo_config();
|
|
12841
13261
|
}
|
|
@@ -12940,7 +13360,7 @@ function requiredFileChecks(repoPath) {
|
|
|
12940
13360
|
".devcontainer/docker-compose.yml",
|
|
12941
13361
|
".devrouter.yml"
|
|
12942
13362
|
];
|
|
12943
|
-
const missing = required.filter((fileName) => !
|
|
13363
|
+
const missing = required.filter((fileName) => !import_node_fs29.default.existsSync(import_node_path30.default.join(repoPath, fileName)));
|
|
12944
13364
|
return {
|
|
12945
13365
|
id: "repo.devcontainer.verify-files",
|
|
12946
13366
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -13159,12 +13579,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
13159
13579
|
nextSteps: collectNextSteps3(checks)
|
|
13160
13580
|
};
|
|
13161
13581
|
}
|
|
13162
|
-
var
|
|
13582
|
+
var import_node_fs29, import_node_path30;
|
|
13163
13583
|
var init_devcontainer_verify = __esm({
|
|
13164
13584
|
"src/core/devcontainer-verify.ts"() {
|
|
13165
13585
|
"use strict";
|
|
13166
|
-
|
|
13167
|
-
|
|
13586
|
+
import_node_fs29 = __toESM(require("fs"));
|
|
13587
|
+
import_node_path30 = __toESM(require("path"));
|
|
13168
13588
|
init_capabilities();
|
|
13169
13589
|
init_doctor();
|
|
13170
13590
|
init_host_routes();
|
|
@@ -13461,7 +13881,7 @@ function packageManagerIssues(repo) {
|
|
|
13461
13881
|
}
|
|
13462
13882
|
function plannedFiles(repoPath, version) {
|
|
13463
13883
|
const repo = inspectRepo({ repo: repoPath });
|
|
13464
|
-
const projectName = sanitizeProjectName(
|
|
13884
|
+
const projectName = sanitizeProjectName(import_node_path31.default.basename(repo.repoPath));
|
|
13465
13885
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
13466
13886
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
13467
13887
|
const port = inferPort2(repo);
|
|
@@ -13506,8 +13926,8 @@ function plannedFiles(repoPath, version) {
|
|
|
13506
13926
|
};
|
|
13507
13927
|
}
|
|
13508
13928
|
function classifyFile(repoPath, file) {
|
|
13509
|
-
const absolutePath =
|
|
13510
|
-
if (!
|
|
13929
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
13930
|
+
if (!import_node_fs30.default.existsSync(absolutePath)) {
|
|
13511
13931
|
return {
|
|
13512
13932
|
path: file.relativePath,
|
|
13513
13933
|
action: "create",
|
|
@@ -13515,7 +13935,7 @@ function classifyFile(repoPath, file) {
|
|
|
13515
13935
|
bytes: Buffer.byteLength(file.content)
|
|
13516
13936
|
};
|
|
13517
13937
|
}
|
|
13518
|
-
const current =
|
|
13938
|
+
const current = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
|
|
13519
13939
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
13520
13940
|
return {
|
|
13521
13941
|
path: file.relativePath,
|
|
@@ -13572,11 +13992,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
13572
13992
|
};
|
|
13573
13993
|
}
|
|
13574
13994
|
function writeFile(repoPath, file) {
|
|
13575
|
-
const absolutePath =
|
|
13576
|
-
|
|
13577
|
-
|
|
13995
|
+
const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
|
|
13996
|
+
import_node_fs30.default.mkdirSync(import_node_path31.default.dirname(absolutePath), { recursive: true });
|
|
13997
|
+
import_node_fs30.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
13578
13998
|
if (file.executable) {
|
|
13579
|
-
|
|
13999
|
+
import_node_fs30.default.chmodSync(absolutePath, 493);
|
|
13580
14000
|
}
|
|
13581
14001
|
}
|
|
13582
14002
|
function writeDevcontainer(options = {}) {
|
|
@@ -13614,12 +14034,12 @@ function writeDevcontainer(options = {}) {
|
|
|
13614
14034
|
nextSteps: postWriteNextSteps(repoPath)
|
|
13615
14035
|
};
|
|
13616
14036
|
}
|
|
13617
|
-
var
|
|
14037
|
+
var import_node_fs30, import_node_path31, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
13618
14038
|
var init_devcontainer_write = __esm({
|
|
13619
14039
|
"src/core/devcontainer-write.ts"() {
|
|
13620
14040
|
"use strict";
|
|
13621
|
-
|
|
13622
|
-
|
|
14041
|
+
import_node_fs30 = __toESM(require("fs"));
|
|
14042
|
+
import_node_path31 = __toESM(require("path"));
|
|
13623
14043
|
init_repo_config();
|
|
13624
14044
|
init_repo_inspect();
|
|
13625
14045
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -13805,11 +14225,11 @@ var init_profile_resolution = __esm({
|
|
|
13805
14225
|
function fail(message) {
|
|
13806
14226
|
throw new Error(message);
|
|
13807
14227
|
}
|
|
13808
|
-
function
|
|
14228
|
+
function isRecord4(value) {
|
|
13809
14229
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13810
14230
|
}
|
|
13811
14231
|
function requireRecord(value, label) {
|
|
13812
|
-
if (!
|
|
14232
|
+
if (!isRecord4(value)) fail(`${label} must be a mapping.`);
|
|
13813
14233
|
return value;
|
|
13814
14234
|
}
|
|
13815
14235
|
function requireExactKeys(value, allowed, label) {
|
|
@@ -14026,13 +14446,13 @@ function buildProfilePlanReport(options) {
|
|
|
14026
14446
|
return { ...report, contractPath, bindings };
|
|
14027
14447
|
}
|
|
14028
14448
|
function loadContract(repoPath, contractPath) {
|
|
14029
|
-
if (
|
|
14449
|
+
if (import_node_path32.default.isAbsolute(contractPath)) {
|
|
14030
14450
|
fail("profile plan contract path must be relative to the repository.");
|
|
14031
14451
|
}
|
|
14032
14452
|
const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
|
|
14033
14453
|
let metadata;
|
|
14034
14454
|
try {
|
|
14035
|
-
metadata =
|
|
14455
|
+
metadata = import_node_fs31.default.lstatSync(absolutePath);
|
|
14036
14456
|
} catch (error) {
|
|
14037
14457
|
fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
|
|
14038
14458
|
}
|
|
@@ -14042,16 +14462,16 @@ function loadContract(repoPath, contractPath) {
|
|
|
14042
14462
|
if (metadata.size > MAX_CONTRACT_BYTES) {
|
|
14043
14463
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14044
14464
|
}
|
|
14045
|
-
const realRepo =
|
|
14046
|
-
const realContract =
|
|
14465
|
+
const realRepo = import_node_fs31.default.realpathSync(repoPath);
|
|
14466
|
+
const realContract = import_node_fs31.default.realpathSync(absolutePath);
|
|
14047
14467
|
assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
|
|
14048
|
-
const source =
|
|
14468
|
+
const source = import_node_fs31.default.readFileSync(absolutePath, "utf-8");
|
|
14049
14469
|
if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
|
|
14050
14470
|
fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
14051
14471
|
}
|
|
14052
14472
|
return {
|
|
14053
14473
|
contract: parseProfilePlanContract(source),
|
|
14054
|
-
contractPath:
|
|
14474
|
+
contractPath: import_node_path32.default.relative(repoPath, absolutePath).split(import_node_path32.default.sep).join("/")
|
|
14055
14475
|
};
|
|
14056
14476
|
}
|
|
14057
14477
|
function resolveProfilePlan(options) {
|
|
@@ -14065,12 +14485,12 @@ function resolveProfilePlan(options) {
|
|
|
14065
14485
|
contractPath: loaded.contractPath
|
|
14066
14486
|
});
|
|
14067
14487
|
}
|
|
14068
|
-
var
|
|
14488
|
+
var import_node_fs31, import_node_path32, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
|
|
14069
14489
|
var init_profile_plan = __esm({
|
|
14070
14490
|
"src/core/profile-plan.ts"() {
|
|
14071
14491
|
"use strict";
|
|
14072
|
-
|
|
14073
|
-
|
|
14492
|
+
import_node_fs31 = __toESM(require("fs"));
|
|
14493
|
+
import_node_path32 = __toESM(require("path"));
|
|
14074
14494
|
import_yaml7 = require("yaml");
|
|
14075
14495
|
init_paths();
|
|
14076
14496
|
init_profile_resolution();
|
|
@@ -14102,7 +14522,7 @@ async function runProfileResolveCommand(options) {
|
|
|
14102
14522
|
async function runProfilePlanCommand(options) {
|
|
14103
14523
|
const report = resolveProfilePlan(options);
|
|
14104
14524
|
if (options.output) {
|
|
14105
|
-
writeFileAtomically(
|
|
14525
|
+
writeFileAtomically(import_node_path33.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
|
|
14106
14526
|
`);
|
|
14107
14527
|
}
|
|
14108
14528
|
if (options.json) {
|
|
@@ -14138,14 +14558,14 @@ function printProfilePlanSummary(report, output2) {
|
|
|
14138
14558
|
process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
|
|
14139
14559
|
`);
|
|
14140
14560
|
}
|
|
14141
|
-
if (output2) process.stdout.write(`Output: ${
|
|
14561
|
+
if (output2) process.stdout.write(`Output: ${import_node_path33.default.resolve(output2)}
|
|
14142
14562
|
`);
|
|
14143
14563
|
}
|
|
14144
|
-
var
|
|
14564
|
+
var import_node_path33;
|
|
14145
14565
|
var init_profile = __esm({
|
|
14146
14566
|
"src/commands/profile.ts"() {
|
|
14147
14567
|
"use strict";
|
|
14148
|
-
|
|
14568
|
+
import_node_path33 = __toESM(require("path"));
|
|
14149
14569
|
init_atomic_file();
|
|
14150
14570
|
init_output();
|
|
14151
14571
|
init_profile_plan();
|
|
@@ -14435,7 +14855,7 @@ function sanitizeRouterId(value) {
|
|
|
14435
14855
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
14436
14856
|
}
|
|
14437
14857
|
function repoHash(repoPath) {
|
|
14438
|
-
return (0, import_node_crypto10.createHash)("sha1").update(
|
|
14858
|
+
return (0, import_node_crypto10.createHash)("sha1").update(import_node_path34.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
14439
14859
|
}
|
|
14440
14860
|
function asDockerApp(app) {
|
|
14441
14861
|
return app.runtime === "docker";
|
|
@@ -14514,11 +14934,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
14514
14934
|
if (dockerApps.length === 0) {
|
|
14515
14935
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
14516
14936
|
}
|
|
14517
|
-
const cachePath =
|
|
14518
|
-
|
|
14519
|
-
const overlayPath =
|
|
14937
|
+
const cachePath = import_node_path34.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
14938
|
+
import_node_fs32.default.mkdirSync(cachePath, { recursive: true });
|
|
14939
|
+
const overlayPath = import_node_path34.default.join(cachePath, "compose.devrouter.yml");
|
|
14520
14940
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
14521
|
-
|
|
14941
|
+
import_node_fs32.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
14522
14942
|
return {
|
|
14523
14943
|
overlayPath,
|
|
14524
14944
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -14638,14 +15058,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
14638
15058
|
const port = Number(match[1]);
|
|
14639
15059
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
14640
15060
|
}
|
|
14641
|
-
var import_node_child_process27, import_node_crypto10,
|
|
15061
|
+
var import_node_child_process27, import_node_crypto10, import_node_fs32, import_node_path34, import_yaml8;
|
|
14642
15062
|
var init_docker_run = __esm({
|
|
14643
15063
|
"src/core/docker-run.ts"() {
|
|
14644
15064
|
"use strict";
|
|
14645
15065
|
import_node_child_process27 = require("child_process");
|
|
14646
15066
|
import_node_crypto10 = require("crypto");
|
|
14647
|
-
|
|
14648
|
-
|
|
15067
|
+
import_node_fs32 = __toESM(require("fs"));
|
|
15068
|
+
import_node_path34 = __toESM(require("path"));
|
|
14649
15069
|
import_yaml8 = __toESM(require("yaml"));
|
|
14650
15070
|
init_docker_error_guidance();
|
|
14651
15071
|
init_paths();
|
|
@@ -15467,13 +15887,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15467
15887
|
const startedAt = Date.now();
|
|
15468
15888
|
let rootStat;
|
|
15469
15889
|
try {
|
|
15470
|
-
rootStat =
|
|
15890
|
+
rootStat = import_node_fs33.default.lstatSync(worktreePath);
|
|
15471
15891
|
} catch (error) {
|
|
15472
15892
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
15473
15893
|
}
|
|
15474
15894
|
let rootEntries;
|
|
15475
15895
|
try {
|
|
15476
|
-
rootEntries =
|
|
15896
|
+
rootEntries = import_node_fs33.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
15477
15897
|
} catch (error) {
|
|
15478
15898
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
15479
15899
|
}
|
|
@@ -15499,10 +15919,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15499
15919
|
timedOut = true;
|
|
15500
15920
|
break;
|
|
15501
15921
|
}
|
|
15502
|
-
const entryPath =
|
|
15922
|
+
const entryPath = import_node_path35.default.join(dirPath, entry.name);
|
|
15503
15923
|
let entryStat;
|
|
15504
15924
|
try {
|
|
15505
|
-
entryStat =
|
|
15925
|
+
entryStat = import_node_fs33.default.lstatSync(entryPath);
|
|
15506
15926
|
} catch (error) {
|
|
15507
15927
|
if (error?.code === "ENOENT") continue;
|
|
15508
15928
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -15512,7 +15932,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
15512
15932
|
if (!entryStat.isDirectory()) continue;
|
|
15513
15933
|
let childEntries;
|
|
15514
15934
|
try {
|
|
15515
|
-
childEntries =
|
|
15935
|
+
childEntries = import_node_fs33.default.readdirSync(entryPath, { withFileTypes: true });
|
|
15516
15936
|
} catch (error) {
|
|
15517
15937
|
unreadableReason = describeIncompleteWalk(error);
|
|
15518
15938
|
break;
|
|
@@ -15576,12 +15996,12 @@ function describeError(error, worktreePath) {
|
|
|
15576
15996
|
}
|
|
15577
15997
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
15578
15998
|
}
|
|
15579
|
-
var
|
|
15999
|
+
var import_node_fs33, import_node_path35, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
15580
16000
|
var init_workspace_consumption = __esm({
|
|
15581
16001
|
"src/core/workspace-consumption.ts"() {
|
|
15582
16002
|
"use strict";
|
|
15583
|
-
|
|
15584
|
-
|
|
16003
|
+
import_node_fs33 = __toESM(require("fs"));
|
|
16004
|
+
import_node_path35 = __toESM(require("path"));
|
|
15585
16005
|
init_devpod_environment();
|
|
15586
16006
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
15587
16007
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -15589,7 +16009,7 @@ var init_workspace_consumption = __esm({
|
|
|
15589
16009
|
});
|
|
15590
16010
|
|
|
15591
16011
|
// src/core/workspace-cleanup.ts
|
|
15592
|
-
function
|
|
16012
|
+
function isRecord5(value) {
|
|
15593
16013
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15594
16014
|
}
|
|
15595
16015
|
function isSha(value) {
|
|
@@ -15682,7 +16102,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
15682
16102
|
}
|
|
15683
16103
|
function readGitSnapshot(worktree, commandRunner) {
|
|
15684
16104
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
15685
|
-
if (worktree.prunable || !
|
|
16105
|
+
if (worktree.prunable || !import_node_fs34.default.existsSync(comparablePath)) {
|
|
15686
16106
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
15687
16107
|
}
|
|
15688
16108
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -15733,14 +16153,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
15733
16153
|
if (!Array.isArray(value)) return void 0;
|
|
15734
16154
|
const changes = [];
|
|
15735
16155
|
for (const item of value) {
|
|
15736
|
-
if (!
|
|
15737
|
-
const repository =
|
|
15738
|
-
const headRepository =
|
|
16156
|
+
if (!isRecord5(item)) return void 0;
|
|
16157
|
+
const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
16158
|
+
const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
15739
16159
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
15740
16160
|
continue;
|
|
15741
16161
|
}
|
|
15742
16162
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
15743
|
-
const mergeCommit =
|
|
16163
|
+
const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
15744
16164
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
15745
16165
|
changes.push({
|
|
15746
16166
|
sourceBranch: branch,
|
|
@@ -15757,14 +16177,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
15757
16177
|
if (!Array.isArray(value)) return void 0;
|
|
15758
16178
|
const changes = [];
|
|
15759
16179
|
for (const item of value) {
|
|
15760
|
-
if (!
|
|
16180
|
+
if (!isRecord5(item)) return void 0;
|
|
15761
16181
|
const sourceProjectId = item.source_project_id;
|
|
15762
16182
|
const targetProjectId = item.target_project_id;
|
|
15763
16183
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
15764
16184
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
15765
16185
|
continue;
|
|
15766
16186
|
}
|
|
15767
|
-
const diffRefs =
|
|
16187
|
+
const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
|
|
15768
16188
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
15769
16189
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
15770
16190
|
changes.push({
|
|
@@ -16207,7 +16627,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
16207
16627
|
containers = measureContainersFn(worktreePaths);
|
|
16208
16628
|
} catch (error) {
|
|
16209
16629
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
16210
|
-
containers = new Map(worktreePaths.map((
|
|
16630
|
+
containers = new Map(worktreePaths.map((path35) => [path35, unknownContainers(reason)]));
|
|
16211
16631
|
}
|
|
16212
16632
|
}
|
|
16213
16633
|
const rows = records.map((record) => {
|
|
@@ -16256,12 +16676,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
16256
16676
|
workspaces: rows
|
|
16257
16677
|
};
|
|
16258
16678
|
}
|
|
16259
|
-
var import_node_child_process29,
|
|
16679
|
+
var import_node_child_process29, import_node_fs34, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
16260
16680
|
var init_workspace_cleanup = __esm({
|
|
16261
16681
|
"src/core/workspace-cleanup.ts"() {
|
|
16262
16682
|
"use strict";
|
|
16263
16683
|
import_node_child_process29 = require("child_process");
|
|
16264
|
-
|
|
16684
|
+
import_node_fs34 = __toESM(require("fs"));
|
|
16265
16685
|
init_devpod_workspaces();
|
|
16266
16686
|
init_host_routes();
|
|
16267
16687
|
init_repo_config();
|
|
@@ -16438,7 +16858,7 @@ var init_version = __esm({
|
|
|
16438
16858
|
|
|
16439
16859
|
// src/cli.ts
|
|
16440
16860
|
var import_commander = require("commander");
|
|
16441
|
-
var CLI_VERSION = true ? "0.0.
|
|
16861
|
+
var CLI_VERSION = true ? "0.0.53" : "0.0.0-dev";
|
|
16442
16862
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
16443
16863
|
function withErrorHandling(action2) {
|
|
16444
16864
|
return async (...args) => {
|