@devrouter/cli 0.2.0 → 0.3.1
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/README.md +7 -0
- package/dist/devrouter-lifecycle-worker.js +28 -3
- package/dist/devrouter.js +1173 -420
- package/package.json +1 -1
- package/upgrade-prompts/0.3.0.md +59 -0
- package/upgrade-prompts/0.3.1.md +27 -0
package/dist/devrouter.js
CHANGED
|
@@ -610,11 +610,11 @@ var init_file_lock = __esm({
|
|
|
610
610
|
|
|
611
611
|
// src/core/workspace.ts
|
|
612
612
|
function comparableWorkspacePath(filePath) {
|
|
613
|
-
const
|
|
613
|
+
const resolved2 = import_node_path3.default.resolve(filePath);
|
|
614
614
|
try {
|
|
615
|
-
return import_node_fs3.default.realpathSync.native(
|
|
615
|
+
return import_node_fs3.default.realpathSync.native(resolved2);
|
|
616
616
|
} catch {
|
|
617
|
-
return
|
|
617
|
+
return resolved2;
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
620
|
function sameWorkspacePath(left, right) {
|
|
@@ -1991,7 +1991,7 @@ function parseEnrollment(value, index, domains) {
|
|
|
1991
1991
|
label2
|
|
1992
1992
|
);
|
|
1993
1993
|
const repoPath = parseCanonicalAbsolutePath(enrollment.repoPath, `${label2}.repoPath`);
|
|
1994
|
-
const
|
|
1994
|
+
const gitCommonDir2 = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label2}.gitCommonDir`);
|
|
1995
1995
|
const workspace = parseBoundedString(enrollment.workspace, `${label2}.workspace`, 32);
|
|
1996
1996
|
if (!WORKSPACE_TOKEN_RE.test(workspace)) {
|
|
1997
1997
|
throw new Error(`${label2}.workspace must be an existing workspace token.`);
|
|
@@ -2032,7 +2032,7 @@ function parseEnrollment(value, index, domains) {
|
|
|
2032
2032
|
);
|
|
2033
2033
|
return {
|
|
2034
2034
|
repoPath,
|
|
2035
|
-
gitCommonDir,
|
|
2035
|
+
gitCommonDir: gitCommonDir2,
|
|
2036
2036
|
workspace,
|
|
2037
2037
|
provider,
|
|
2038
2038
|
providerId,
|
|
@@ -3163,10 +3163,10 @@ function normalizeOverridePattern(line) {
|
|
|
3163
3163
|
const converted = `**/${rest}`;
|
|
3164
3164
|
return isValidWorktreePattern(converted) ? converted : void 0;
|
|
3165
3165
|
}
|
|
3166
|
-
function readDisposableOverride(
|
|
3166
|
+
function readDisposableOverride(gitCommonDir2) {
|
|
3167
3167
|
let content;
|
|
3168
3168
|
try {
|
|
3169
|
-
content = import_node_fs7.default.readFileSync(import_node_path9.default.join(
|
|
3169
|
+
content = import_node_fs7.default.readFileSync(import_node_path9.default.join(gitCommonDir2, DISPOSABLE_OVERRIDE_FILE), "utf-8");
|
|
3170
3170
|
} catch {
|
|
3171
3171
|
return [];
|
|
3172
3172
|
}
|
|
@@ -3202,8 +3202,8 @@ function inspectLocalWorktreeSafety(worktreePath, options = {}) {
|
|
|
3202
3202
|
"--git-dir",
|
|
3203
3203
|
"--git-common-dir"
|
|
3204
3204
|
]);
|
|
3205
|
-
const [gitDir,
|
|
3206
|
-
if (!dirs.ok || !gitDir || !
|
|
3205
|
+
const [gitDir, gitCommonDir2] = dirs.stdout.split("\n").map((line) => line.trim());
|
|
3206
|
+
if (!dirs.ok || !gitDir || !gitCommonDir2) {
|
|
3207
3207
|
return { codes: ["git-error"], examples: {} };
|
|
3208
3208
|
}
|
|
3209
3209
|
if (!git(worktreePath, ["symbolic-ref", "-q", "HEAD"]).ok) safety.codes.push("detached");
|
|
@@ -3224,7 +3224,7 @@ function inspectLocalWorktreeSafety(worktreePath, options = {}) {
|
|
|
3224
3224
|
}
|
|
3225
3225
|
const disposable = [
|
|
3226
3226
|
...BUILTIN_DISPOSABLE_PATTERNS,
|
|
3227
|
-
...readDisposableOverride(
|
|
3227
|
+
...readDisposableOverride(gitCommonDir2),
|
|
3228
3228
|
...options.extraDisposablePatterns ?? []
|
|
3229
3229
|
];
|
|
3230
3230
|
const entries = status.stdout.split("\0");
|
|
@@ -3315,8 +3315,8 @@ function classifyWorktreeSafety(input2) {
|
|
|
3315
3315
|
}
|
|
3316
3316
|
return result(change);
|
|
3317
3317
|
}
|
|
3318
|
-
function readLedgerWorktreePaths(
|
|
3319
|
-
const directory = import_node_path9.default.join(
|
|
3318
|
+
function readLedgerWorktreePaths(gitCommonDir2) {
|
|
3319
|
+
const directory = import_node_path9.default.join(gitCommonDir2, "devrouter", "workspaces");
|
|
3320
3320
|
let names;
|
|
3321
3321
|
try {
|
|
3322
3322
|
names = import_node_fs7.default.readdirSync(directory).filter((name) => name.endsWith(".json"));
|
|
@@ -4764,7 +4764,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs8.default.read
|
|
|
4764
4764
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4765
4765
|
const requiredVersion = config.devrouter?.version;
|
|
4766
4766
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4767
|
-
const cliVersion = true ? "0.
|
|
4767
|
+
const cliVersion = true ? "0.3.1" : "0.0.0-dev";
|
|
4768
4768
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4769
4769
|
hasWarnedVersionMismatch = true;
|
|
4770
4770
|
process.stderr.write(
|
|
@@ -5041,13 +5041,13 @@ function applyWorkspace(config, workspace, repoPath) {
|
|
|
5041
5041
|
app.host = namespaced;
|
|
5042
5042
|
}
|
|
5043
5043
|
if ("upstream" in app && app.upstream.includes(WORKSPACE_PLACEHOLDER)) {
|
|
5044
|
-
const
|
|
5044
|
+
const resolved2 = app.upstream.replaceAll(WORKSPACE_PLACEHOLDER, substitutionToken);
|
|
5045
5045
|
try {
|
|
5046
|
-
parseUpstream(
|
|
5046
|
+
parseUpstream(resolved2);
|
|
5047
5047
|
} catch (err) {
|
|
5048
|
-
throw new Error(`Resolved upstream '${
|
|
5048
|
+
throw new Error(`Resolved upstream '${resolved2}' is invalid: ${err.message}`);
|
|
5049
5049
|
}
|
|
5050
|
-
app.upstream =
|
|
5050
|
+
app.upstream = resolved2;
|
|
5051
5051
|
}
|
|
5052
5052
|
if (workspace && app.runtime === "docker") {
|
|
5053
5053
|
app.docker.router = `${app.docker.router ?? app.name}-${workspace}`;
|
|
@@ -5056,11 +5056,11 @@ function applyWorkspace(config, workspace, repoPath) {
|
|
|
5056
5056
|
return next;
|
|
5057
5057
|
}
|
|
5058
5058
|
function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
|
|
5059
|
-
const
|
|
5060
|
-
const raw = loadRepoConfig(
|
|
5059
|
+
const resolved2 = resolveRepoPath(repoPath);
|
|
5060
|
+
const raw = loadRepoConfig(resolved2);
|
|
5061
5061
|
const resolvedProfile = resolveProfile(raw, profileOverride);
|
|
5062
|
-
const workspace = resolveWorkspace(
|
|
5063
|
-
const config = applyProfile(applyWorkspace(raw, workspace,
|
|
5062
|
+
const workspace = resolveWorkspace(resolved2, workspaceOverride);
|
|
5063
|
+
const config = applyProfile(applyWorkspace(raw, workspace, resolved2), resolvedProfile.profile);
|
|
5064
5064
|
return {
|
|
5065
5065
|
config,
|
|
5066
5066
|
workspace,
|
|
@@ -5213,28 +5213,28 @@ function selectedReadiness(config, profileName, profile, routedApps) {
|
|
|
5213
5213
|
return sortedUnique(readiness);
|
|
5214
5214
|
}
|
|
5215
5215
|
function buildProfileResolutionReport(config, repoPath, profileOverride) {
|
|
5216
|
-
const
|
|
5217
|
-
const filtered = applyProfile(config,
|
|
5216
|
+
const resolved2 = resolveProfile(config, profileOverride);
|
|
5217
|
+
const filtered = applyProfile(config, resolved2.profile);
|
|
5218
5218
|
const routedApps = filtered.apps.filter((app) => app.kind !== "dependency");
|
|
5219
5219
|
const dependencies = filtered.apps.filter((app) => app.kind === "dependency");
|
|
5220
|
-
const readiness = selectedReadiness(config,
|
|
5220
|
+
const readiness = selectedReadiness(config, resolved2.name, resolved2.profile, routedApps);
|
|
5221
5221
|
const managedRuntime = config.managedRuntime;
|
|
5222
5222
|
const baseServices = sortedUnique(managedRuntime?.devcontainer.baseServices ?? []);
|
|
5223
5223
|
const profileServices = managedRuntime ? selectedManagedResources(
|
|
5224
|
-
|
|
5225
|
-
|
|
5224
|
+
resolved2.profile,
|
|
5225
|
+
resolved2.profile?.devcontainerServices,
|
|
5226
5226
|
managedRuntime.devcontainer.profileServices
|
|
5227
5227
|
) : [];
|
|
5228
5228
|
const processes = managedRuntime ? selectedManagedResources(
|
|
5229
|
-
|
|
5230
|
-
|
|
5229
|
+
resolved2.profile,
|
|
5230
|
+
resolved2.profile?.processes,
|
|
5231
5231
|
managedRuntime.processes
|
|
5232
5232
|
) : [];
|
|
5233
|
-
const notices = managedFullExpansionNotices(config,
|
|
5233
|
+
const notices = managedFullExpansionNotices(config, resolved2.name);
|
|
5234
5234
|
return {
|
|
5235
5235
|
schemaVersion: 1,
|
|
5236
5236
|
repoPath,
|
|
5237
|
-
profile:
|
|
5237
|
+
profile: resolved2.name,
|
|
5238
5238
|
apps: sortedUnique(routedApps.map((app) => app.name)),
|
|
5239
5239
|
dependencies: sortedUnique(dependencies.map((app) => app.name)),
|
|
5240
5240
|
readiness: sortedUnique(readiness),
|
|
@@ -6763,7 +6763,7 @@ var init_reliability_contract = __esm({
|
|
|
6763
6763
|
|
|
6764
6764
|
// src/core/reliability-operation-store.ts
|
|
6765
6765
|
function installedCliVersion() {
|
|
6766
|
-
return true ? "0.
|
|
6766
|
+
return true ? "0.3.1" : "0.0.0-dev";
|
|
6767
6767
|
}
|
|
6768
6768
|
function compareCliVersions(left, right) {
|
|
6769
6769
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -7529,9 +7529,9 @@ async function resolveCapacityEnrollment(policy, request2, signal, bindingResolv
|
|
|
7529
7529
|
["-C", environment.repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
7530
7530
|
signal
|
|
7531
7531
|
)).trim();
|
|
7532
|
-
const
|
|
7532
|
+
const gitCommonDir2 = import_node_fs13.default.realpathSync(common);
|
|
7533
7533
|
const enrollment = policy.enrollments.find(
|
|
7534
|
-
(entry) => entry.repoPath === environment.repoPath && entry.gitCommonDir ===
|
|
7534
|
+
(entry) => entry.repoPath === environment.repoPath && entry.gitCommonDir === gitCommonDir2 && entry.workspace === environment.workspace && entry.provider === environment.provider && entry.providerId === environment.providerId
|
|
7535
7535
|
);
|
|
7536
7536
|
if (!enrollment?.profiles.includes(environment.profile))
|
|
7537
7537
|
throw new Error("Canonical runtime profile is not enrolled for capacity admission.");
|
|
@@ -7550,8 +7550,8 @@ async function enrollCapacityLifecycle(policy, request2, signal, directory = imp
|
|
|
7550
7550
|
const expectedPolicy = JSON.stringify(policy);
|
|
7551
7551
|
if (policy.admissions !== "enabled" || JSON.stringify(readCapacityPolicy(directory)) !== expectedPolicy)
|
|
7552
7552
|
throw new Error("Capacity enrollment policy is not current and enabled.");
|
|
7553
|
-
const
|
|
7554
|
-
const { environment, enrollment } =
|
|
7553
|
+
const resolved2 = await resolveCapacityEnrollment(policy, request2, signal, bindingResolver);
|
|
7554
|
+
const { environment, enrollment } = resolved2;
|
|
7555
7555
|
const runtime = policy.domains[enrollment.runtimeDomain];
|
|
7556
7556
|
if (policy.admissions !== "enabled" || runtime?.kind !== "runtime")
|
|
7557
7557
|
throw new Error("Capacity enrollment policy is not enabled.");
|
|
@@ -7575,7 +7575,7 @@ async function enrollCapacityLifecycle(policy, request2, signal, directory = imp
|
|
|
7575
7575
|
daemonId: runtime.daemonId,
|
|
7576
7576
|
estimatesDigest: enrollment.estimatesDigest
|
|
7577
7577
|
});
|
|
7578
|
-
return
|
|
7578
|
+
return resolved2;
|
|
7579
7579
|
}
|
|
7580
7580
|
var import_node_fs13, import_node_path15;
|
|
7581
7581
|
var init_capacity_enrollment = __esm({
|
|
@@ -10000,9 +10000,9 @@ function writeManifestRecord(cacheRoot, manifest) {
|
|
|
10000
10000
|
function pinnedManifest(version) {
|
|
10001
10001
|
return version === PINNED_DEVSY_AGENT_VERSION ? { version, origin: "pinned", assets: DEVSY_AGENT_ASSETS } : void 0;
|
|
10002
10002
|
}
|
|
10003
|
-
function manifestForVersion(
|
|
10004
|
-
if (
|
|
10005
|
-
return pinnedManifest(version) ?? readManifestRecord(
|
|
10003
|
+
function manifestForVersion(resolved2, version) {
|
|
10004
|
+
if (resolved2.assets) return { version, origin: "pinned", assets: resolved2.assets };
|
|
10005
|
+
return pinnedManifest(version) ?? readManifestRecord(resolved2.cacheRoot, version);
|
|
10006
10006
|
}
|
|
10007
10007
|
async function resolveReleaseDevsyAgentManifest(version, fetcher) {
|
|
10008
10008
|
const url = `${DEVSY_RELEASE_METADATA_BASE_URL}/${DEVSY_RELEASE_TAG_PREFIX}${version}`;
|
|
@@ -10124,9 +10124,9 @@ function resolvedOptions(options) {
|
|
|
10124
10124
|
};
|
|
10125
10125
|
}
|
|
10126
10126
|
function inspectDevsyAgent(options = {}) {
|
|
10127
|
-
const
|
|
10128
|
-
const installedVersion = parseVersion2(
|
|
10129
|
-
const explicitPath =
|
|
10127
|
+
const resolved2 = resolvedOptions(options);
|
|
10128
|
+
const installedVersion = parseVersion2(resolved2.versionOutput);
|
|
10129
|
+
const explicitPath = resolved2.env.DEVSY_AGENT_BINARY?.trim();
|
|
10130
10130
|
const source = explicitPath ? "explicit" : "managed";
|
|
10131
10131
|
if (!installedVersion) {
|
|
10132
10132
|
return {
|
|
@@ -10143,11 +10143,11 @@ function inspectDevsyAgent(options = {}) {
|
|
|
10143
10143
|
installedVersion
|
|
10144
10144
|
};
|
|
10145
10145
|
}
|
|
10146
|
-
const manifest = manifestForVersion(
|
|
10146
|
+
const manifest = manifestForVersion(resolved2, installedVersion);
|
|
10147
10147
|
if (!manifest) {
|
|
10148
10148
|
if (explicitPath) {
|
|
10149
10149
|
return {
|
|
10150
|
-
...inspectBinary(explicitPath,
|
|
10150
|
+
...inspectBinary(explicitPath, resolved2.assets ?? DEVSY_AGENT_ASSETS, "invalid"),
|
|
10151
10151
|
source,
|
|
10152
10152
|
binaryPath: explicitPath,
|
|
10153
10153
|
installedVersion
|
|
@@ -10161,13 +10161,13 @@ function inspectDevsyAgent(options = {}) {
|
|
|
10161
10161
|
drift: { installed: installedVersion, supported: PINNED_DEVSY_AGENT_VERSION }
|
|
10162
10162
|
};
|
|
10163
10163
|
}
|
|
10164
|
-
const expectedName =
|
|
10164
|
+
const expectedName = resolved2.nativeAssetName ?? nativeAssetName(resolved2.platform, resolved2.arch);
|
|
10165
10165
|
const asset = manifest.assets.find((candidate) => candidate.name === expectedName);
|
|
10166
10166
|
if (!asset) {
|
|
10167
10167
|
return {
|
|
10168
10168
|
state: "stale",
|
|
10169
10169
|
source,
|
|
10170
|
-
reason: `Devsy ${installedVersion} has no official agent for ${
|
|
10170
|
+
reason: `Devsy ${installedVersion} has no official agent for ${resolved2.platform}/${resolved2.arch}`,
|
|
10171
10171
|
installedVersion,
|
|
10172
10172
|
manifestOrigin: manifest.origin
|
|
10173
10173
|
};
|
|
@@ -10181,7 +10181,7 @@ function inspectDevsyAgent(options = {}) {
|
|
|
10181
10181
|
manifestOrigin: manifest.origin
|
|
10182
10182
|
};
|
|
10183
10183
|
}
|
|
10184
|
-
const binaryPath = managedBinaryPath(
|
|
10184
|
+
const binaryPath = managedBinaryPath(resolved2.cacheRoot, installedVersion, asset);
|
|
10185
10185
|
return {
|
|
10186
10186
|
...inspectBinary(binaryPath, [asset], "missing"),
|
|
10187
10187
|
source,
|
|
@@ -10354,28 +10354,28 @@ function preparedFromInspection(inspection) {
|
|
|
10354
10354
|
transport: "existing"
|
|
10355
10355
|
};
|
|
10356
10356
|
}
|
|
10357
|
-
function expectedAgentName(
|
|
10358
|
-
return
|
|
10357
|
+
function expectedAgentName(resolved2) {
|
|
10358
|
+
return resolved2.nativeAssetName ?? nativeAssetName(resolved2.platform, resolved2.arch);
|
|
10359
10359
|
}
|
|
10360
|
-
async function loadOrResolveManifest(
|
|
10361
|
-
const manifest = manifestForVersion(
|
|
10360
|
+
async function loadOrResolveManifest(resolved2, version, options) {
|
|
10361
|
+
const manifest = manifestForVersion(resolved2, version);
|
|
10362
10362
|
if (manifest) return manifest;
|
|
10363
10363
|
const release = await resolveReleaseDevsyAgentManifest(version, options.fetcher ?? fetch);
|
|
10364
|
-
writeManifestRecord(
|
|
10364
|
+
writeManifestRecord(resolved2.cacheRoot, release);
|
|
10365
10365
|
return release;
|
|
10366
10366
|
}
|
|
10367
10367
|
async function prepareDevsyAgent(options = {}) {
|
|
10368
|
-
const
|
|
10369
|
-
const inspectOptions =
|
|
10368
|
+
const resolved2 = resolvedOptions(options);
|
|
10369
|
+
const inspectOptions = resolved2;
|
|
10370
10370
|
const before = inspectDevsyAgent(inspectOptions);
|
|
10371
10371
|
if (before.state === "stale") throw new DevsyAgentReadinessError(before);
|
|
10372
10372
|
if (isReadyInspection(before) && before.source !== "host") {
|
|
10373
10373
|
return preparedFromInspection(before);
|
|
10374
10374
|
}
|
|
10375
10375
|
const lock = options.withLock ?? ((operation) => {
|
|
10376
|
-
import_node_fs17.default.mkdirSync(import_node_path23.default.dirname(
|
|
10376
|
+
import_node_fs17.default.mkdirSync(import_node_path23.default.dirname(resolved2.lockPath), { recursive: true });
|
|
10377
10377
|
return withFileLock(
|
|
10378
|
-
|
|
10378
|
+
resolved2.lockPath,
|
|
10379
10379
|
{
|
|
10380
10380
|
activity: "Devsy agent acquisition",
|
|
10381
10381
|
waitMs: DEVSY_AGENT_LOCK_WAIT_MS,
|
|
@@ -10393,27 +10393,27 @@ async function prepareDevsyAgent(options = {}) {
|
|
|
10393
10393
|
}
|
|
10394
10394
|
const version = current.installedVersion;
|
|
10395
10395
|
if (!version) throw new DevsyAgentReadinessError(current);
|
|
10396
|
-
const manifest = await loadOrResolveManifest(
|
|
10396
|
+
const manifest = await loadOrResolveManifest(resolved2, version, options);
|
|
10397
10397
|
if (current.source === "explicit") {
|
|
10398
10398
|
const verified = inspectDevsyAgent(inspectOptions);
|
|
10399
10399
|
if (isReadyInspection(verified)) return preparedFromInspection(verified);
|
|
10400
10400
|
throw new DevsyAgentReadinessError(verified);
|
|
10401
10401
|
}
|
|
10402
10402
|
const asset = manifest.assets.find(
|
|
10403
|
-
(candidate) => candidate.name === expectedAgentName(
|
|
10403
|
+
(candidate) => candidate.name === expectedAgentName(resolved2)
|
|
10404
10404
|
);
|
|
10405
10405
|
if (!asset) {
|
|
10406
10406
|
throw new DevsyAgentReadinessError({
|
|
10407
10407
|
state: "stale",
|
|
10408
10408
|
source: current.source,
|
|
10409
|
-
reason: `Devsy ${version} has no official agent for ${
|
|
10409
|
+
reason: `Devsy ${version} has no official agent for ${resolved2.platform}/${resolved2.arch}`,
|
|
10410
10410
|
installedVersion: version,
|
|
10411
10411
|
manifestOrigin: manifest.origin
|
|
10412
10412
|
});
|
|
10413
10413
|
}
|
|
10414
10414
|
const transport = await downloadAndPublish(
|
|
10415
10415
|
asset,
|
|
10416
|
-
managedBinaryPath(
|
|
10416
|
+
managedBinaryPath(resolved2.cacheRoot, version, asset),
|
|
10417
10417
|
options.fetcher ?? fetch,
|
|
10418
10418
|
options.githubCliDownloader ?? downloadWithGitHubCli
|
|
10419
10419
|
);
|
|
@@ -10525,11 +10525,11 @@ var init_devcontainer_config = __esm({
|
|
|
10525
10525
|
// src/core/paths.ts
|
|
10526
10526
|
function assertPathWithinRepo(filePath, repoRoot, label2) {
|
|
10527
10527
|
const resolvedRoot = import_node_path25.default.resolve(repoRoot);
|
|
10528
|
-
const
|
|
10529
|
-
if (
|
|
10528
|
+
const resolved2 = import_node_path25.default.resolve(repoRoot, filePath);
|
|
10529
|
+
if (resolved2 !== resolvedRoot && !resolved2.startsWith(resolvedRoot + import_node_path25.default.sep)) {
|
|
10530
10530
|
throw new Error(`${label2} path '${filePath}' escapes the repository root.`);
|
|
10531
10531
|
}
|
|
10532
|
-
return
|
|
10532
|
+
return resolved2;
|
|
10533
10533
|
}
|
|
10534
10534
|
var import_node_path25;
|
|
10535
10535
|
var init_paths = __esm({
|
|
@@ -10586,15 +10586,15 @@ function resolveComposeFiles(source, repoPath, linked) {
|
|
|
10586
10586
|
}
|
|
10587
10587
|
const directory = import_node_path26.default.join(repoPath, ".devcontainer");
|
|
10588
10588
|
const files = references.map((reference) => {
|
|
10589
|
-
const
|
|
10589
|
+
const resolved3 = assertPathWithinRepo(
|
|
10590
10590
|
resolveComposeReference(reference, linked),
|
|
10591
10591
|
directory,
|
|
10592
10592
|
"dockerComposeFile"
|
|
10593
10593
|
);
|
|
10594
|
-
if (!import_node_fs19.default.existsSync(
|
|
10595
|
-
throw new Error(`Managed Dev Container compose file does not exist: ${
|
|
10594
|
+
if (!import_node_fs19.default.existsSync(resolved3) || !import_node_fs19.default.lstatSync(resolved3).isFile()) {
|
|
10595
|
+
throw new Error(`Managed Dev Container compose file does not exist: ${resolved3}`);
|
|
10596
10596
|
}
|
|
10597
|
-
return
|
|
10597
|
+
return resolved3;
|
|
10598
10598
|
});
|
|
10599
10599
|
for (const file of files) {
|
|
10600
10600
|
let parsed;
|
|
@@ -10610,7 +10610,7 @@ function resolveComposeFiles(source, repoPath, linked) {
|
|
|
10610
10610
|
}
|
|
10611
10611
|
}
|
|
10612
10612
|
const fileArgs = files.flatMap((file) => ["-f", file]);
|
|
10613
|
-
const
|
|
10613
|
+
const resolved2 = (0, import_node_child_process10.spawnSync)(
|
|
10614
10614
|
"docker",
|
|
10615
10615
|
[
|
|
10616
10616
|
"compose",
|
|
@@ -10626,10 +10626,10 @@ function resolveComposeFiles(source, repoPath, linked) {
|
|
|
10626
10626
|
],
|
|
10627
10627
|
{ cwd: directory, encoding: "utf-8" }
|
|
10628
10628
|
);
|
|
10629
|
-
if (
|
|
10629
|
+
if (resolved2.status !== 0) {
|
|
10630
10630
|
throw new Error("Could not resolve the effective managed Dev Container Compose model.");
|
|
10631
10631
|
}
|
|
10632
|
-
const services = String(
|
|
10632
|
+
const services = String(resolved2.stdout ?? "").split(/\r?\n/).map((service) => service.trim()).filter(Boolean);
|
|
10633
10633
|
if (services.length === 0 || new Set(services).size !== services.length) {
|
|
10634
10634
|
throw new Error("The effective managed Dev Container Compose model has no unique services.");
|
|
10635
10635
|
}
|
|
@@ -12882,7 +12882,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
|
|
|
12882
12882
|
const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
12883
12883
|
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
12884
12884
|
if (ownership.status !== "absent") return void 0;
|
|
12885
|
-
const
|
|
12885
|
+
const gitCommonDir2 = resolveGitCommonDir(repoPath);
|
|
12886
12886
|
const endpoint = resolveManagedStopEndpoint();
|
|
12887
12887
|
if (!supportsManagedStopBaseline(endpoint))
|
|
12888
12888
|
throw new Error("Initial managed stop requires a local Docker endpoint.");
|
|
@@ -12894,7 +12894,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
|
|
|
12894
12894
|
const legacyWorkspaces = readLegacy();
|
|
12895
12895
|
const observe = () => {
|
|
12896
12896
|
resetWorkspaceRuntimeCaches();
|
|
12897
|
-
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy" || !isLinkedWorktree(repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !==
|
|
12897
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy" || !isLinkedWorktree(repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir2 || !(0, import_node_util10.isDeepStrictEqual)(readWorkspaceOwnership(repoPath, workspace), record5) || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present" || readManagedRuntimeState(repoPath, workspace))
|
|
12898
12898
|
throw new Error("Initial managed stop ownership or retained state changed.");
|
|
12899
12899
|
if (devpodRegistryRoot() !== legacyHome || !(0, import_node_util10.isDeepStrictEqual)(readLegacy(), legacyWorkspaces))
|
|
12900
12900
|
throw new Error("Initial managed stop legacy registry evidence changed.");
|
|
@@ -12920,7 +12920,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
|
|
|
12920
12920
|
return {
|
|
12921
12921
|
workspace,
|
|
12922
12922
|
record: record5,
|
|
12923
|
-
gitCommonDir,
|
|
12923
|
+
gitCommonDir: gitCommonDir2,
|
|
12924
12924
|
endpoint,
|
|
12925
12925
|
daemon,
|
|
12926
12926
|
routes,
|
|
@@ -12949,8 +12949,8 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
|
|
|
12949
12949
|
};
|
|
12950
12950
|
};
|
|
12951
12951
|
const authority = readAuthority();
|
|
12952
|
-
const
|
|
12953
|
-
const workspaceEnv = workspace &&
|
|
12952
|
+
const gitCommonDir2 = linked ? resolveGitCommonDir(repoPath) : void 0;
|
|
12953
|
+
const workspaceEnv = workspace && gitCommonDir2 ? { token: workspace, gitCommonDir: gitCommonDir2 } : void 0;
|
|
12954
12954
|
function canonicalProfile(recorded) {
|
|
12955
12955
|
return resolveProfile(loadRepoConfig(repoPath), recorded).name;
|
|
12956
12956
|
}
|
|
@@ -12963,7 +12963,7 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
|
|
|
12963
12963
|
throw new Error("Initial managed stop requires one exact Devsy registration.");
|
|
12964
12964
|
if (linked) {
|
|
12965
12965
|
const record5 = workspace ? readWorkspaceOwnership(repoPath, workspace) : void 0;
|
|
12966
|
-
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !==
|
|
12966
|
+
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir2 || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present")
|
|
12967
12967
|
throw new Error("Initial managed stop workspace ownership changed.");
|
|
12968
12968
|
}
|
|
12969
12969
|
return owner2.workspace;
|
|
@@ -14185,9 +14185,9 @@ function validateRoot(root) {
|
|
|
14185
14185
|
if (typeof root !== "string" || !import_node_path36.default.isAbsolute(root) || root.includes("\0")) {
|
|
14186
14186
|
throw claimError("invalid-input", "network claim root is invalid");
|
|
14187
14187
|
}
|
|
14188
|
-
const
|
|
14189
|
-
if (
|
|
14190
|
-
return
|
|
14188
|
+
const resolved2 = import_node_path36.default.resolve(root);
|
|
14189
|
+
if (resolved2 !== root) throw claimError("invalid-input", "network claim root is invalid");
|
|
14190
|
+
return resolved2;
|
|
14191
14191
|
}
|
|
14192
14192
|
function hashKey(value) {
|
|
14193
14193
|
return (0, import_node_crypto15.createHash)("sha256").update(value).digest("hex");
|
|
@@ -14526,6 +14526,25 @@ function inspectExactOwnership(devsyId, worktreePath) {
|
|
|
14526
14526
|
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
14527
14527
|
return ownership;
|
|
14528
14528
|
}
|
|
14529
|
+
function checkoutVanished(worktreePath) {
|
|
14530
|
+
try {
|
|
14531
|
+
import_node_fs30.default.lstatSync(worktreePath);
|
|
14532
|
+
return false;
|
|
14533
|
+
} catch (error) {
|
|
14534
|
+
return error.code === "ENOENT";
|
|
14535
|
+
}
|
|
14536
|
+
}
|
|
14537
|
+
function deleteVanishedCheckoutRegistration(devsyId, worktreePath, originalError) {
|
|
14538
|
+
if (!checkoutVanished(worktreePath)) throw originalError;
|
|
14539
|
+
const runtime = inspectDevsyRuntimeStatus(devsyId);
|
|
14540
|
+
if (runtime !== "stopped" && runtime !== "not-found") throw originalError;
|
|
14541
|
+
runDevsyAction("delete", devsyId, true, worktreePath);
|
|
14542
|
+
if (inspectExactOwnership(devsyId, worktreePath).status !== "absent") {
|
|
14543
|
+
throw new Error(
|
|
14544
|
+
`Devsy workspace '${devsyId}' still owns vanished checkout '${worktreePath}' after forced provider delete.`
|
|
14545
|
+
);
|
|
14546
|
+
}
|
|
14547
|
+
}
|
|
14529
14548
|
function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
14530
14549
|
return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
|
|
14531
14550
|
restoreRecordedManagedDevcontainerConfig(worktreePath);
|
|
@@ -14539,7 +14558,13 @@ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
|
|
|
14539
14558
|
}
|
|
14540
14559
|
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
14541
14560
|
if (before.status === "absent") return { status: "absent" };
|
|
14542
|
-
|
|
14561
|
+
try {
|
|
14562
|
+
runDevsyAction(action2, devsyId, false, worktreePath);
|
|
14563
|
+
} catch (error) {
|
|
14564
|
+
if (action2 !== "delete") throw error;
|
|
14565
|
+
deleteVanishedCheckoutRegistration(devsyId, worktreePath, error);
|
|
14566
|
+
return { status: "changed" };
|
|
14567
|
+
}
|
|
14543
14568
|
let after = inspectExactOwnership(devsyId, worktreePath);
|
|
14544
14569
|
if (action2 === "stop" && after.status !== "owned") {
|
|
14545
14570
|
throw new Error(
|
|
@@ -19554,9 +19579,9 @@ function createCapacityController(options) {
|
|
|
19554
19579
|
const resolveCapacityTarget = async (environment, signal) => {
|
|
19555
19580
|
const current = readCapacityPolicy(options.directory);
|
|
19556
19581
|
if (current?.admissions !== "enabled" || !(0, import_node_util13.isDeepStrictEqual)(current, policy)) return void 0;
|
|
19557
|
-
let
|
|
19582
|
+
let resolved2;
|
|
19558
19583
|
try {
|
|
19559
|
-
|
|
19584
|
+
resolved2 = await resolveCapacityEnrollment(
|
|
19560
19585
|
current,
|
|
19561
19586
|
{ path: environment.repoPath, profile: environment.profile, require: [] },
|
|
19562
19587
|
signal,
|
|
@@ -19565,12 +19590,12 @@ function createCapacityController(options) {
|
|
|
19565
19590
|
} catch {
|
|
19566
19591
|
return void 0;
|
|
19567
19592
|
}
|
|
19568
|
-
if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(
|
|
19593
|
+
if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment)) return void 0;
|
|
19569
19594
|
if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return void 0;
|
|
19570
|
-
const runtime = current.domains[
|
|
19571
|
-
if (runtime?.kind !== "runtime" || runtime.hostDomain !==
|
|
19595
|
+
const runtime = current.domains[resolved2.enrollment.runtimeDomain];
|
|
19596
|
+
if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
|
|
19572
19597
|
return void 0;
|
|
19573
|
-
return { current, resolved, runtime };
|
|
19598
|
+
return { current, resolved: resolved2, runtime };
|
|
19574
19599
|
};
|
|
19575
19600
|
const capacityPool = (runtime, runtimeDomain) => ({
|
|
19576
19601
|
daemonId: runtime.daemonId,
|
|
@@ -19692,7 +19717,7 @@ function createCapacityController(options) {
|
|
|
19692
19717
|
if (current?.admissions !== "enabled" || !(0, import_node_util13.isDeepStrictEqual)(current, policy))
|
|
19693
19718
|
throw new Error("Capacity controller policy changed.");
|
|
19694
19719
|
validate3();
|
|
19695
|
-
const
|
|
19720
|
+
const resolved2 = await enrollCapacityLifecycle(
|
|
19696
19721
|
current,
|
|
19697
19722
|
{
|
|
19698
19723
|
path: environment.repoPath,
|
|
@@ -19703,16 +19728,16 @@ function createCapacityController(options) {
|
|
|
19703
19728
|
options.directory,
|
|
19704
19729
|
bindingResolver
|
|
19705
19730
|
);
|
|
19706
|
-
if (!(0, import_node_util13.isDeepStrictEqual)(
|
|
19731
|
+
if (!(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment) || signal.aborted)
|
|
19707
19732
|
throw new Error("Capacity submission binding changed.");
|
|
19708
19733
|
if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current))
|
|
19709
19734
|
throw new Error("Capacity submission policy changed during resolution.");
|
|
19710
|
-
const runtime = current.domains[
|
|
19711
|
-
if (runtime?.kind !== "runtime" || runtime.hostDomain !==
|
|
19735
|
+
const runtime = current.domains[resolved2.enrollment.runtimeDomain];
|
|
19736
|
+
if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
|
|
19712
19737
|
throw new Error("Capacity pool does not match policy enrollment.");
|
|
19713
19738
|
const pool = {
|
|
19714
19739
|
daemonId: runtime.daemonId,
|
|
19715
|
-
runtimeDomain:
|
|
19740
|
+
runtimeDomain: resolved2.enrollment.runtimeDomain,
|
|
19716
19741
|
hostDomain: runtime.hostDomain,
|
|
19717
19742
|
hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
|
|
19718
19743
|
};
|
|
@@ -19723,11 +19748,11 @@ function createCapacityController(options) {
|
|
|
19723
19748
|
settlePreparedLifecycleCapacity({
|
|
19724
19749
|
identity: identity2,
|
|
19725
19750
|
controller,
|
|
19726
|
-
estimates:
|
|
19727
|
-
enrollment:
|
|
19751
|
+
estimates: resolved2.estimates,
|
|
19752
|
+
enrollment: resolved2.enrollment,
|
|
19728
19753
|
directory: options.directory
|
|
19729
19754
|
});
|
|
19730
|
-
const charge = capacityRequest(
|
|
19755
|
+
const charge = capacityRequest(resolved2.estimates, resolved2.enrollment, {
|
|
19731
19756
|
environmentId: record5.state.environmentId,
|
|
19732
19757
|
profile: environment.profile,
|
|
19733
19758
|
...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
|
|
@@ -19751,8 +19776,8 @@ function createCapacityController(options) {
|
|
|
19751
19776
|
try {
|
|
19752
19777
|
await publishQueuedStartupWitness({
|
|
19753
19778
|
identity: identity2,
|
|
19754
|
-
provider:
|
|
19755
|
-
providerId:
|
|
19779
|
+
provider: resolved2.enrollment.provider,
|
|
19780
|
+
providerId: resolved2.enrollment.providerId,
|
|
19756
19781
|
operationId: prepared.operationId,
|
|
19757
19782
|
fence: prepared.request.fence,
|
|
19758
19783
|
profile: environment.profile,
|
|
@@ -19786,7 +19811,7 @@ function createCapacityController(options) {
|
|
|
19786
19811
|
reservationId: (0, import_node_crypto24.randomUUID)(),
|
|
19787
19812
|
policyRevision: current.revision
|
|
19788
19813
|
},
|
|
19789
|
-
{ estimates:
|
|
19814
|
+
{ estimates: resolved2.estimates, enrollment: resolved2.enrollment, pool }
|
|
19790
19815
|
);
|
|
19791
19816
|
acceptedPayloads.set(prepared.operationId, signature);
|
|
19792
19817
|
for (const id5 of acceptedPayloads.keys()) {
|
|
@@ -19851,9 +19876,9 @@ function createCapacityController(options) {
|
|
|
19851
19876
|
}
|
|
19852
19877
|
if (!failedCapabilities.some((capability) => observed.failedCapabilities.includes(capability)))
|
|
19853
19878
|
return;
|
|
19854
|
-
let
|
|
19879
|
+
let resolved2;
|
|
19855
19880
|
try {
|
|
19856
|
-
|
|
19881
|
+
resolved2 = await resolveCapacityEnrollment(
|
|
19857
19882
|
current,
|
|
19858
19883
|
{ path: environment.repoPath, profile: environment.profile, require: [] },
|
|
19859
19884
|
signal,
|
|
@@ -19862,15 +19887,15 @@ function createCapacityController(options) {
|
|
|
19862
19887
|
} catch {
|
|
19863
19888
|
return;
|
|
19864
19889
|
}
|
|
19865
|
-
if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(
|
|
19890
|
+
if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment)) return;
|
|
19866
19891
|
if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return;
|
|
19867
|
-
const runtime = current.domains[
|
|
19868
|
-
if (runtime?.kind !== "runtime" || runtime.hostDomain !==
|
|
19892
|
+
const runtime = current.domains[resolved2.enrollment.runtimeDomain];
|
|
19893
|
+
if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
|
|
19869
19894
|
return;
|
|
19870
19895
|
const identity2 = environmentIdentity(environment);
|
|
19871
19896
|
const record5 = readReliabilityOperation(identity2);
|
|
19872
19897
|
const durable = record5?.enrollment;
|
|
19873
|
-
if (!record5 || !durable || durable.policyRevision !== current.revision || durable.gitCommonDir !==
|
|
19898
|
+
if (!record5 || !durable || durable.policyRevision !== current.revision || durable.gitCommonDir !== resolved2.enrollment.gitCommonDir || durable.providerId !== resolved2.enrollment.providerId || durable.hostDomain !== resolved2.enrollment.hostDomain || durable.runtimeDomain !== resolved2.enrollment.runtimeDomain || durable.endpoint !== runtime.endpoint || durable.daemonId !== runtime.daemonId || durable.estimatesDigest !== resolved2.enrollment.estimatesDigest)
|
|
19874
19899
|
return;
|
|
19875
19900
|
if (record5.state.operation && queue.hasOperation(record5.state.operation.id)) return;
|
|
19876
19901
|
let proof;
|
|
@@ -19885,7 +19910,7 @@ function createCapacityController(options) {
|
|
|
19885
19910
|
if (required.length === 0) return;
|
|
19886
19911
|
const pool = {
|
|
19887
19912
|
daemonId: runtime.daemonId,
|
|
19888
|
-
runtimeDomain:
|
|
19913
|
+
runtimeDomain: resolved2.enrollment.runtimeDomain,
|
|
19889
19914
|
hostDomain: runtime.hostDomain,
|
|
19890
19915
|
hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
|
|
19891
19916
|
};
|
|
@@ -19924,7 +19949,7 @@ function createCapacityController(options) {
|
|
|
19924
19949
|
return;
|
|
19925
19950
|
}
|
|
19926
19951
|
if (!prepared) return;
|
|
19927
|
-
const charge = capacityRequest(
|
|
19952
|
+
const charge = capacityRequest(resolved2.estimates, resolved2.enrollment, {
|
|
19928
19953
|
environmentId: record5.state.environmentId,
|
|
19929
19954
|
profile: environment.profile,
|
|
19930
19955
|
...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
|
|
@@ -19939,7 +19964,7 @@ function createCapacityController(options) {
|
|
|
19939
19964
|
reservationId: (0, import_node_crypto24.randomUUID)(),
|
|
19940
19965
|
policyRevision: current.revision
|
|
19941
19966
|
},
|
|
19942
|
-
{ estimates:
|
|
19967
|
+
{ estimates: resolved2.estimates, enrollment: resolved2.enrollment, pool }
|
|
19943
19968
|
);
|
|
19944
19969
|
} catch {
|
|
19945
19970
|
try {
|
|
@@ -20968,7 +20993,7 @@ async function runController(options) {
|
|
|
20968
20993
|
);
|
|
20969
20994
|
void initial.then(async ({ environment, identity: identity2, revision, requirements: requirements2 }) => {
|
|
20970
20995
|
let persisted;
|
|
20971
|
-
const
|
|
20996
|
+
const resolved2 = await resolve(
|
|
20972
20997
|
{
|
|
20973
20998
|
path: environment.repoPath,
|
|
20974
20999
|
profile: environment.profile,
|
|
@@ -20980,7 +21005,7 @@ async function runController(options) {
|
|
|
20980
21005
|
}
|
|
20981
21006
|
);
|
|
20982
21007
|
assertActive();
|
|
20983
|
-
if (!persisted || JSON.stringify(
|
|
21008
|
+
if (!persisted || JSON.stringify(resolved2) !== JSON.stringify(environment))
|
|
20984
21009
|
throw new Error("Protection ownership evidence unavailable or changed.");
|
|
20985
21010
|
const proof = persisted;
|
|
20986
21011
|
const finish2 = serial.then(() => {
|
|
@@ -21979,12 +22004,13 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
21979
22004
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
21980
22005
|
- \`devrouter workspace ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
21981
22006
|
- \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
|
|
21982
|
-
- \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim] [--output <file>]] [--json]\`: report-only cleanup evidence and exact guarded suggestions; \`--manifest\` emits an approval manifest instead of the report
|
|
22007
|
+
- \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim|leftover] [--output <file>]] [--json]\`: report-only cleanup evidence and exact guarded suggestions; \`--all-worktrees\` also lists Docker leftovers of deleted checkouts; \`--manifest\` emits an approval manifest instead of the report
|
|
21983
22008
|
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
21984
22009
|
- \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
|
|
21985
22010
|
- \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
|
|
21986
22011
|
- \`devrouter workspace manifest hash|select <file>\`: print a manifest's SHA-256, or a canonical subset of exact candidate IDs (\`--expected-sha256\`, \`--id\`)
|
|
21987
22012
|
- \`devrouter workspace reclaim|trim --manifest <file> --sha256 <hash> [--veto-command <abs>] [--yes] [--json]\`: apply an approved manifest; dry run without \`--yes\`
|
|
22013
|
+
- \`devrouter workspace leftover --manifest <file> --sha256 <hash> [--yes] [--json]\`: remove the Docker containers, volumes and networks of an approved leftover manifest; dry run without \`--yes\`
|
|
21988
22014
|
|
|
21989
22015
|
## Agent harness gating
|
|
21990
22016
|
|
|
@@ -22461,8 +22487,8 @@ var init_ai_prompt = __esm({
|
|
|
22461
22487
|
purpose: "List owner, Git, workspace runtime, route, path, and branch evidence for managed workspaces."
|
|
22462
22488
|
},
|
|
22463
22489
|
{
|
|
22464
|
-
command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim] [--output <file>]] [--json]",
|
|
22465
|
-
purpose: "Report-only evidence for managed linked workspaces; activity is advisory, --check-merged alone enables read-only origin/forge checks, --all-worktrees adds RECLAIM/KEEP/PRUNE verdicts for every linked worktree, and --manifest emits an approval manifest instead of the report."
|
|
22490
|
+
command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim|leftover] [--output <file>]] [--json]",
|
|
22491
|
+
purpose: "Report-only evidence for managed linked workspaces; activity is advisory, --check-merged alone enables read-only origin/forge checks, --all-worktrees adds RECLAIM/KEEP/PRUNE verdicts for every linked worktree and lists Docker leftovers of deleted checkouts, and --manifest emits an approval manifest instead of the report."
|
|
22466
22492
|
},
|
|
22467
22493
|
{
|
|
22468
22494
|
command: "devrouter workspace stop <workspace|branch>",
|
|
@@ -22480,6 +22506,10 @@ var init_ai_prompt = __esm({
|
|
|
22480
22506
|
command: "devrouter workspace reclaim|trim --manifest <file> --sha256 <hash> [--veto-command <abs>] [--yes] [--json]",
|
|
22481
22507
|
purpose: "Apply a person-approved manifest by its SHA-256; dry run without --yes, never adds targets, and re-checks each target inside its lifecycle lock."
|
|
22482
22508
|
},
|
|
22509
|
+
{
|
|
22510
|
+
command: "devrouter workspace leftover --manifest <file> --sha256 <hash> [--yes] [--json]",
|
|
22511
|
+
purpose: "Remove the Docker containers, volumes and networks of a person-approved leftover manifest; dry run without --yes, and each stack is re-derived inside the provider locks and skipped if it changed."
|
|
22512
|
+
},
|
|
22483
22513
|
{
|
|
22484
22514
|
command: "devrouter workspace journal settle [path] [--json]",
|
|
22485
22515
|
purpose: "Settle a lifecycle operation whose worker is provably gone as unobservable so ensure/stop can proceed; never hand-edit the devrouter state directory."
|
|
@@ -22661,6 +22691,48 @@ ${renderTable(
|
|
|
22661
22691
|
if (!report.checkMerged) {
|
|
22662
22692
|
process.stdout.write("Forge not checked: rerun with --check-merged for RECLAIM verdicts.\n");
|
|
22663
22693
|
}
|
|
22694
|
+
printDockerLeftovers(report);
|
|
22695
|
+
}
|
|
22696
|
+
function printDockerLeftovers(report) {
|
|
22697
|
+
const leftovers = report.dockerLeftovers;
|
|
22698
|
+
if (!leftovers) return;
|
|
22699
|
+
if (leftovers.status === "unavailable") {
|
|
22700
|
+
process.stdout.write(`
|
|
22701
|
+
Docker leftovers: not checked (${leftovers.reason}).
|
|
22702
|
+
`);
|
|
22703
|
+
return;
|
|
22704
|
+
}
|
|
22705
|
+
const rows2 = [
|
|
22706
|
+
...leftovers.leftovers.map((entry) => [
|
|
22707
|
+
entry.project,
|
|
22708
|
+
entry.checkout,
|
|
22709
|
+
`removable: ${entry.containers.length} container(s), ${entry.volumes.length} volume(s), ${entry.networks.length} network(s)`
|
|
22710
|
+
]),
|
|
22711
|
+
...leftovers.blocked.map((entry) => [
|
|
22712
|
+
entry.project,
|
|
22713
|
+
entry.checkout,
|
|
22714
|
+
`blocked: ${entry.reason}; next: ${entry.next}`
|
|
22715
|
+
])
|
|
22716
|
+
];
|
|
22717
|
+
if (rows2.length) {
|
|
22718
|
+
process.stdout.write(
|
|
22719
|
+
`
|
|
22720
|
+
Docker leftovers of deleted checkouts:
|
|
22721
|
+
${renderTable(["PROJECT", "CHECKOUT", "STATUS"], rows2)}
|
|
22722
|
+
`
|
|
22723
|
+
);
|
|
22724
|
+
}
|
|
22725
|
+
if (leftovers.leftovers.length) {
|
|
22726
|
+
process.stdout.write(
|
|
22727
|
+
"Approve removal with `workspace cleanup --all-worktrees --manifest --action leftover --output <file>`, then `workspace leftover`.\n"
|
|
22728
|
+
);
|
|
22729
|
+
}
|
|
22730
|
+
if (leftovers.unattributedVolumes.length) {
|
|
22731
|
+
process.stdout.write(
|
|
22732
|
+
`${leftovers.unattributedVolumes.length} unused Compose volume(s) cannot be traced to a checkout and are never removed by devrouter; list them with --json.
|
|
22733
|
+
`
|
|
22734
|
+
);
|
|
22735
|
+
}
|
|
22664
22736
|
}
|
|
22665
22737
|
function printReclaimReport(report, verb = "reclaim") {
|
|
22666
22738
|
process.stdout.write(
|
|
@@ -23357,8 +23429,8 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
|
|
|
23357
23429
|
profile: runtime.resolvedProfile,
|
|
23358
23430
|
linked
|
|
23359
23431
|
});
|
|
23360
|
-
const
|
|
23361
|
-
const workspaceEnv = linked && workspace &&
|
|
23432
|
+
const gitCommonDir2 = linked ? resolveGitCommonDir(repoPath) : void 0;
|
|
23433
|
+
const workspaceEnv = linked && workspace && gitCommonDir2 ? { token: workspace, gitCommonDir: gitCommonDir2 } : void 0;
|
|
23362
23434
|
const conflicts = detectHostPortClaimConflicts({ repoPath, plan, workspace: workspaceEnv });
|
|
23363
23435
|
if (conflicts.length === 0) {
|
|
23364
23436
|
return {
|
|
@@ -24134,7 +24206,7 @@ function probeInstalledCliVersion(executable) {
|
|
|
24134
24206
|
}
|
|
24135
24207
|
function buildCliPathCheck(options = {}) {
|
|
24136
24208
|
const id5 = "global.cli-path";
|
|
24137
|
-
const runningVersion = options.runningVersion ?? (true ? "0.
|
|
24209
|
+
const runningVersion = options.runningVersion ?? (true ? "0.3.1" : "0.0.0-dev");
|
|
24138
24210
|
if (runningVersion === "0.0.0-dev") {
|
|
24139
24211
|
return {
|
|
24140
24212
|
id: id5,
|
|
@@ -24155,12 +24227,12 @@ function buildCliPathCheck(options = {}) {
|
|
|
24155
24227
|
if (!isExecutableFile(executable)) {
|
|
24156
24228
|
continue;
|
|
24157
24229
|
}
|
|
24158
|
-
const
|
|
24159
|
-
if (seen.has(
|
|
24230
|
+
const resolved2 = realpathOrUndefined(executable) ?? executable;
|
|
24231
|
+
if (seen.has(resolved2)) {
|
|
24160
24232
|
continue;
|
|
24161
24233
|
}
|
|
24162
|
-
seen.add(
|
|
24163
|
-
const running = runningEntry !== void 0 &&
|
|
24234
|
+
seen.add(resolved2);
|
|
24235
|
+
const running = runningEntry !== void 0 && resolved2 === runningEntry;
|
|
24164
24236
|
entries.push({
|
|
24165
24237
|
executable,
|
|
24166
24238
|
running,
|
|
@@ -25030,7 +25102,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
25030
25102
|
suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
|
|
25031
25103
|
});
|
|
25032
25104
|
}
|
|
25033
|
-
const cliVersion = true ? "0.
|
|
25105
|
+
const cliVersion = true ? "0.3.1" : "0.0.0-dev";
|
|
25034
25106
|
const configVersion = config.devrouter?.version;
|
|
25035
25107
|
const installedKnown = cliVersion !== "0.0.0-dev";
|
|
25036
25108
|
const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
|
|
@@ -25689,11 +25761,11 @@ var init_up = __esm({
|
|
|
25689
25761
|
|
|
25690
25762
|
// src/commands/environment-path.ts
|
|
25691
25763
|
function resolveGitCheckoutPath(repoPath) {
|
|
25692
|
-
const
|
|
25764
|
+
const resolved2 = resolveRepoPath(repoPath);
|
|
25693
25765
|
try {
|
|
25694
|
-
return resolveGitTopLevel(
|
|
25766
|
+
return resolveGitTopLevel(resolved2);
|
|
25695
25767
|
} catch (error) {
|
|
25696
|
-
throw new Error(`Environment commands require a Git repository: '${
|
|
25768
|
+
throw new Error(`Environment commands require a Git repository: '${resolved2}'.`, {
|
|
25697
25769
|
cause: error
|
|
25698
25770
|
});
|
|
25699
25771
|
}
|
|
@@ -28714,8 +28786,8 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
28714
28786
|
function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
28715
28787
|
const fileArgs = [];
|
|
28716
28788
|
for (const composeFile of composeFiles2) {
|
|
28717
|
-
const
|
|
28718
|
-
fileArgs.push("-f",
|
|
28789
|
+
const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
28790
|
+
fileArgs.push("-f", resolved2);
|
|
28719
28791
|
}
|
|
28720
28792
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
28721
28793
|
const result = (0, import_node_child_process36.spawnSync)("docker", args, {
|
|
@@ -28732,8 +28804,8 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
28732
28804
|
function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, services) {
|
|
28733
28805
|
const fileArgs = [];
|
|
28734
28806
|
for (const composeFile of composeFiles2) {
|
|
28735
|
-
const
|
|
28736
|
-
fileArgs.push("-f",
|
|
28807
|
+
const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
28808
|
+
fileArgs.push("-f", resolved2);
|
|
28737
28809
|
}
|
|
28738
28810
|
const args = [
|
|
28739
28811
|
"compose",
|
|
@@ -28768,8 +28840,8 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
28768
28840
|
function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
28769
28841
|
const fileArgs = [];
|
|
28770
28842
|
for (const composeFile of composeFiles2) {
|
|
28771
|
-
const
|
|
28772
|
-
fileArgs.push("-f",
|
|
28843
|
+
const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
28844
|
+
fileArgs.push("-f", resolved2);
|
|
28773
28845
|
}
|
|
28774
28846
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
28775
28847
|
const result = (0, import_node_child_process36.spawnSync)("docker", args, {
|
|
@@ -28785,8 +28857,8 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
28785
28857
|
function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, tail = 20) {
|
|
28786
28858
|
const fileArgs = [];
|
|
28787
28859
|
for (const composeFile of composeFiles2) {
|
|
28788
|
-
const
|
|
28789
|
-
fileArgs.push("-f",
|
|
28860
|
+
const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
28861
|
+
fileArgs.push("-f", resolved2);
|
|
28790
28862
|
}
|
|
28791
28863
|
const args = [
|
|
28792
28864
|
"compose",
|
|
@@ -28806,8 +28878,8 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
28806
28878
|
function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internalPort) {
|
|
28807
28879
|
const fileArgs = [];
|
|
28808
28880
|
for (const composeFile of composeFiles2) {
|
|
28809
|
-
const
|
|
28810
|
-
fileArgs.push("-f",
|
|
28881
|
+
const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
28882
|
+
fileArgs.push("-f", resolved2);
|
|
28811
28883
|
}
|
|
28812
28884
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
28813
28885
|
const result = (0, import_node_child_process36.spawnSync)("docker", args, {
|
|
@@ -29647,133 +29719,6 @@ var init_tls2 = __esm({
|
|
|
29647
29719
|
}
|
|
29648
29720
|
});
|
|
29649
29721
|
|
|
29650
|
-
// src/core/workspace-consumption.ts
|
|
29651
|
-
function measureWorktreeConsumption(worktreePath, options) {
|
|
29652
|
-
const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
29653
|
-
const startedAt = Date.now();
|
|
29654
|
-
let rootStat;
|
|
29655
|
-
try {
|
|
29656
|
-
rootStat = import_node_fs53.default.lstatSync(worktreePath);
|
|
29657
|
-
} catch (error) {
|
|
29658
|
-
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
29659
|
-
}
|
|
29660
|
-
let rootEntries;
|
|
29661
|
-
try {
|
|
29662
|
-
rootEntries = import_node_fs53.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
29663
|
-
} catch (error) {
|
|
29664
|
-
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
29665
|
-
}
|
|
29666
|
-
const seenInodes = /* @__PURE__ */ new Set();
|
|
29667
|
-
let bytes2 = 0;
|
|
29668
|
-
let timedOut = false;
|
|
29669
|
-
let unreadableReason = null;
|
|
29670
|
-
const accumulate = (stat) => {
|
|
29671
|
-
if (stat.nlink > 1) {
|
|
29672
|
-
const key = `${stat.dev}:${stat.ino}`;
|
|
29673
|
-
if (seenInodes.has(key)) return;
|
|
29674
|
-
seenInodes.add(key);
|
|
29675
|
-
}
|
|
29676
|
-
bytes2 += stat.blocks * BLOCK_SIZE_BYTES;
|
|
29677
|
-
};
|
|
29678
|
-
const deadlineExceeded = () => Date.now() - startedAt >= deadlineMs;
|
|
29679
|
-
accumulate(rootStat);
|
|
29680
|
-
const stack = [{ dirPath: worktreePath, entries: rootEntries }];
|
|
29681
|
-
while (stack.length > 0 && !timedOut && !unreadableReason) {
|
|
29682
|
-
const { dirPath, entries } = stack.pop();
|
|
29683
|
-
for (const entry of entries) {
|
|
29684
|
-
if (deadlineExceeded()) {
|
|
29685
|
-
timedOut = true;
|
|
29686
|
-
break;
|
|
29687
|
-
}
|
|
29688
|
-
const entryPath = import_node_path66.default.join(dirPath, entry.name);
|
|
29689
|
-
let entryStat;
|
|
29690
|
-
try {
|
|
29691
|
-
entryStat = import_node_fs53.default.lstatSync(entryPath);
|
|
29692
|
-
} catch (error) {
|
|
29693
|
-
if (error?.code === "ENOENT") continue;
|
|
29694
|
-
unreadableReason = describeIncompleteWalk(error);
|
|
29695
|
-
break;
|
|
29696
|
-
}
|
|
29697
|
-
accumulate(entryStat);
|
|
29698
|
-
if (!entryStat.isDirectory()) continue;
|
|
29699
|
-
let childEntries;
|
|
29700
|
-
try {
|
|
29701
|
-
childEntries = import_node_fs53.default.readdirSync(entryPath, { withFileTypes: true });
|
|
29702
|
-
} catch (error) {
|
|
29703
|
-
unreadableReason = describeIncompleteWalk(error);
|
|
29704
|
-
break;
|
|
29705
|
-
}
|
|
29706
|
-
stack.push({ dirPath: entryPath, entries: childEntries });
|
|
29707
|
-
}
|
|
29708
|
-
}
|
|
29709
|
-
if (timedOut) {
|
|
29710
|
-
return { status: "unknown", reason: `exceeded deadline of ${deadlineMs}ms` };
|
|
29711
|
-
}
|
|
29712
|
-
if (unreadableReason) {
|
|
29713
|
-
return { status: "unknown", reason: unreadableReason };
|
|
29714
|
-
}
|
|
29715
|
-
return { status: "measured", bytes: bytes2 };
|
|
29716
|
-
}
|
|
29717
|
-
function describeIncompleteWalk(error) {
|
|
29718
|
-
return `could not read every path inside the worktree: ${error?.message ?? String(error)}`;
|
|
29719
|
-
}
|
|
29720
|
-
function measureContainerConsumption(worktreePaths, dependencies) {
|
|
29721
|
-
const byWorktree = /* @__PURE__ */ new Map();
|
|
29722
|
-
if (worktreePaths.length === 0) return byWorktree;
|
|
29723
|
-
const inspect = dependencies?.inspect ?? inspectWorkspaceContainers;
|
|
29724
|
-
const containers2 = inspect();
|
|
29725
|
-
const attributedIds = /* @__PURE__ */ new Set();
|
|
29726
|
-
for (const worktreePath of worktreePaths) {
|
|
29727
|
-
for (const container of workspaceAppContainers(containers2, worktreePath)) {
|
|
29728
|
-
attributedIds.add(container.id);
|
|
29729
|
-
}
|
|
29730
|
-
}
|
|
29731
|
-
const sized = attributedIds.size === 0 ? [] : inspect({ withSize: true, ids: Array.from(attributedIds) });
|
|
29732
|
-
for (const worktreePath of worktreePaths) {
|
|
29733
|
-
byWorktree.set(worktreePath, summarizeContainers(workspaceAppContainers(sized, worktreePath)));
|
|
29734
|
-
}
|
|
29735
|
-
return byWorktree;
|
|
29736
|
-
}
|
|
29737
|
-
function summarizeContainers(containers2) {
|
|
29738
|
-
let writable = 0;
|
|
29739
|
-
let shared = 0;
|
|
29740
|
-
for (const container of containers2) {
|
|
29741
|
-
const { sizeRw, sizeRootFs } = container;
|
|
29742
|
-
if (typeof sizeRw !== "number" || typeof sizeRootFs !== "number" || !Number.isFinite(sizeRw) || !Number.isFinite(sizeRootFs) || sizeRootFs < sizeRw) {
|
|
29743
|
-
const unknown = {
|
|
29744
|
-
status: "unknown",
|
|
29745
|
-
reason: `container ${container.id.slice(0, 12)} reported no usable size`
|
|
29746
|
-
};
|
|
29747
|
-
return { containerWritable: unknown, imageShared: unknown };
|
|
29748
|
-
}
|
|
29749
|
-
writable += sizeRw;
|
|
29750
|
-
shared += sizeRootFs - sizeRw;
|
|
29751
|
-
}
|
|
29752
|
-
return {
|
|
29753
|
-
containerWritable: { status: "measured", bytes: writable },
|
|
29754
|
-
imageShared: { status: "measured", bytes: shared }
|
|
29755
|
-
};
|
|
29756
|
-
}
|
|
29757
|
-
function describeError(error, worktreePath) {
|
|
29758
|
-
const code = error?.code;
|
|
29759
|
-
if (code === "ENOENT") return `worktree path '${worktreePath}' does not exist`;
|
|
29760
|
-
if (code === "EACCES" || code === "EPERM") {
|
|
29761
|
-
return `permission denied reading worktree path '${worktreePath}'`;
|
|
29762
|
-
}
|
|
29763
|
-
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
29764
|
-
}
|
|
29765
|
-
var import_node_fs53, import_node_path66, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
29766
|
-
var init_workspace_consumption = __esm({
|
|
29767
|
-
"src/core/workspace-consumption.ts"() {
|
|
29768
|
-
"use strict";
|
|
29769
|
-
import_node_fs53 = __toESM(require("fs"));
|
|
29770
|
-
import_node_path66 = __toESM(require("path"));
|
|
29771
|
-
init_devpod_environment();
|
|
29772
|
-
DEFAULT_DEADLINE_MS = 1e4;
|
|
29773
|
-
BLOCK_SIZE_BYTES = 512;
|
|
29774
|
-
}
|
|
29775
|
-
});
|
|
29776
|
-
|
|
29777
29722
|
// src/core/worktree-reclaim.ts
|
|
29778
29723
|
function sortKeys(value) {
|
|
29779
29724
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -29854,7 +29799,7 @@ function validateCandidate(value, index, action2, legacy) {
|
|
|
29854
29799
|
}
|
|
29855
29800
|
}
|
|
29856
29801
|
for (const key of ["repo", "path", "commonDir", "gitDir"]) {
|
|
29857
|
-
if (!
|
|
29802
|
+
if (!import_node_path66.default.isAbsolute(candidate[key])) {
|
|
29858
29803
|
throw new Error(`${label2}.${key} must be an absolute path`);
|
|
29859
29804
|
}
|
|
29860
29805
|
}
|
|
@@ -29960,7 +29905,7 @@ function validateManifest(value) {
|
|
|
29960
29905
|
return root;
|
|
29961
29906
|
}
|
|
29962
29907
|
function readManifest(file) {
|
|
29963
|
-
const raw =
|
|
29908
|
+
const raw = import_node_fs53.default.readFileSync(file);
|
|
29964
29909
|
let parsed;
|
|
29965
29910
|
try {
|
|
29966
29911
|
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
|
|
@@ -30007,7 +29952,7 @@ function gitLine(cwd, args) {
|
|
|
30007
29952
|
return result.status === 0 && !result.error && output2 ? output2 : void 0;
|
|
30008
29953
|
}
|
|
30009
29954
|
function readWorktreeIdentity(worktreePath) {
|
|
30010
|
-
if (!
|
|
29955
|
+
if (!import_node_fs53.default.existsSync(worktreePath)) return void 0;
|
|
30011
29956
|
const real = comparableWorkspacePath(worktreePath);
|
|
30012
29957
|
const commonDir = gitLine(real, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
30013
29958
|
const gitDir = gitLine(real, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
@@ -30027,13 +29972,13 @@ function identityMismatch(expected, actual) {
|
|
|
30027
29972
|
function unmanagedActivityEpoch(worktreePath, gitDir) {
|
|
30028
29973
|
let latest;
|
|
30029
29974
|
for (const file of [
|
|
30030
|
-
|
|
30031
|
-
|
|
30032
|
-
|
|
29975
|
+
import_node_path66.default.join(gitDir, "index"),
|
|
29976
|
+
import_node_path66.default.join(gitDir, "HEAD"),
|
|
29977
|
+
import_node_path66.default.join(gitDir, "logs", "HEAD"),
|
|
30033
29978
|
worktreePath
|
|
30034
29979
|
]) {
|
|
30035
29980
|
try {
|
|
30036
|
-
const seconds = Math.floor(
|
|
29981
|
+
const seconds = Math.floor(import_node_fs53.default.statSync(file).mtimeMs / 1e3);
|
|
30037
29982
|
latest = latest === void 0 ? seconds : Math.max(latest, seconds);
|
|
30038
29983
|
} catch {
|
|
30039
29984
|
}
|
|
@@ -30087,12 +30032,12 @@ function buildReclaimManifest(report, createdAtEpoch) {
|
|
|
30087
30032
|
return canonicalJson(manifest);
|
|
30088
30033
|
}
|
|
30089
30034
|
function receiptPath(commonDir, manifestSha256, id5) {
|
|
30090
|
-
return
|
|
30035
|
+
return import_node_path66.default.join(commonDir, "worktree-reclaim", "receipts", manifestSha256, `${id5}.json`);
|
|
30091
30036
|
}
|
|
30092
30037
|
function matchingReceipt(candidate, manifestSha256) {
|
|
30093
30038
|
try {
|
|
30094
30039
|
const receipt = JSON.parse(
|
|
30095
|
-
|
|
30040
|
+
import_node_fs53.default.readFileSync(receiptPath(candidate.commonDir, manifestSha256, candidate.id), "utf-8")
|
|
30096
30041
|
);
|
|
30097
30042
|
return receipt.kind === RECEIPT_KIND && receipt.schemaVersion === SCHEMA_VERSION && receipt.manifestSha256 === manifestSha256 && receipt.candidateId === candidate.id && receipt.path === candidate.path && receipt.head === candidate.head;
|
|
30098
30043
|
} catch {
|
|
@@ -30101,7 +30046,7 @@ function matchingReceipt(candidate, manifestSha256) {
|
|
|
30101
30046
|
}
|
|
30102
30047
|
function writeReceipt(candidate, manifestSha256, steps, nowEpoch) {
|
|
30103
30048
|
const file = receiptPath(candidate.commonDir, manifestSha256, candidate.id);
|
|
30104
|
-
|
|
30049
|
+
import_node_fs53.default.mkdirSync(import_node_path66.default.dirname(file), { recursive: true });
|
|
30105
30050
|
writeFileAtomically(
|
|
30106
30051
|
file,
|
|
30107
30052
|
canonicalJson({
|
|
@@ -30132,9 +30077,9 @@ function runVetoCommand(command, worktreePath) {
|
|
|
30132
30077
|
return result.status === 0 && !result.error && result.signal === null;
|
|
30133
30078
|
}
|
|
30134
30079
|
function assertVetoCommand(command) {
|
|
30135
|
-
if (!
|
|
30080
|
+
if (!import_node_path66.default.isAbsolute(command)) throw new Error("--veto-command must be an absolute path");
|
|
30136
30081
|
try {
|
|
30137
|
-
|
|
30082
|
+
import_node_fs53.default.accessSync(command, import_node_fs53.default.constants.X_OK);
|
|
30138
30083
|
} catch {
|
|
30139
30084
|
throw new Error(`--veto-command '${command}' is missing or not executable`);
|
|
30140
30085
|
}
|
|
@@ -30216,7 +30161,7 @@ async function applyReclaimManifest(options, dependencies) {
|
|
|
30216
30161
|
outcome.status = "not-attempted";
|
|
30217
30162
|
continue;
|
|
30218
30163
|
}
|
|
30219
|
-
if (!
|
|
30164
|
+
if (!import_node_fs53.default.existsSync(candidate.path) && matchingReceipt(candidate, manifestSha256)) {
|
|
30220
30165
|
outcome.status = "already-done";
|
|
30221
30166
|
continue;
|
|
30222
30167
|
}
|
|
@@ -30285,14 +30230,14 @@ async function applyReclaimManifest(options, dependencies) {
|
|
|
30285
30230
|
}
|
|
30286
30231
|
return { manifestSha256, dryRun: !options.yes, targets, stoppedEarly };
|
|
30287
30232
|
}
|
|
30288
|
-
var import_node_child_process38, import_node_crypto30,
|
|
30233
|
+
var import_node_child_process38, import_node_crypto30, import_node_fs53, import_node_path66, MANIFEST_KIND, LEGACY_MANIFEST_KIND, RECEIPT_KIND, SCHEMA_VERSION, HEX_SHA, HEX_ID, VETO_TIMEOUT_MS, FUTURE_TOLERANCE_SECONDS, ROOT_KEYS, SESSION_ROOT_KEYS, LEGACY_ROOT_KEYS, CANDIDATE_KEYS, SESSION_CANDIDATE_KEYS, OPTIONAL_CANDIDATE_KEYS, ReclaimSkip;
|
|
30289
30234
|
var init_worktree_reclaim = __esm({
|
|
30290
30235
|
"src/core/worktree-reclaim.ts"() {
|
|
30291
30236
|
"use strict";
|
|
30292
30237
|
import_node_child_process38 = require("child_process");
|
|
30293
30238
|
import_node_crypto30 = require("crypto");
|
|
30294
|
-
|
|
30295
|
-
|
|
30239
|
+
import_node_fs53 = __toESM(require("fs"));
|
|
30240
|
+
import_node_path66 = __toESM(require("path"));
|
|
30296
30241
|
init_atomic_file();
|
|
30297
30242
|
init_workspace();
|
|
30298
30243
|
MANIFEST_KIND = "worktree-reclaim-manifest";
|
|
@@ -30339,72 +30284,801 @@ var init_worktree_reclaim = __esm({
|
|
|
30339
30284
|
}
|
|
30340
30285
|
});
|
|
30341
30286
|
|
|
30342
|
-
// src/core/
|
|
30343
|
-
function
|
|
30344
|
-
|
|
30287
|
+
// src/core/docker-leftovers.ts
|
|
30288
|
+
function dockerOutput(run, args) {
|
|
30289
|
+
const result = run(args);
|
|
30290
|
+
if (result.status !== 0) {
|
|
30291
|
+
throw new Error(`docker ${args[0]} failed: ${result.stderr.trim() || "unknown error"}`);
|
|
30292
|
+
}
|
|
30293
|
+
return result.stdout;
|
|
30345
30294
|
}
|
|
30346
|
-
function
|
|
30347
|
-
|
|
30295
|
+
function listIds(run, args, kind) {
|
|
30296
|
+
const ids2 = dockerOutput(run, args).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
30297
|
+
if (ids2.length > MAX_DOCKER_OBJECTS) {
|
|
30298
|
+
throw new DockerListingBoundError(
|
|
30299
|
+
`docker lists ${ids2.length} ${kind}; the leftover report reads at most ${MAX_DOCKER_OBJECTS}`
|
|
30300
|
+
);
|
|
30301
|
+
}
|
|
30302
|
+
return ids2;
|
|
30348
30303
|
}
|
|
30349
|
-
function
|
|
30350
|
-
|
|
30351
|
-
|
|
30352
|
-
|
|
30304
|
+
function inspectAll(run, command, ids2) {
|
|
30305
|
+
const objects = [];
|
|
30306
|
+
for (let start = 0; start < ids2.length; start += 200) {
|
|
30307
|
+
const parsed = JSON.parse(dockerOutput(run, [...command, ...ids2.slice(start, start + 200)]));
|
|
30308
|
+
if (!Array.isArray(parsed)) throw new Error(`docker ${command.join(" ")} returned no array`);
|
|
30309
|
+
objects.push(...parsed);
|
|
30310
|
+
}
|
|
30311
|
+
return objects;
|
|
30353
30312
|
}
|
|
30354
|
-
function
|
|
30355
|
-
|
|
30313
|
+
function labelsOf(value) {
|
|
30314
|
+
if (!value || typeof value !== "object") return {};
|
|
30315
|
+
return Object.fromEntries(
|
|
30316
|
+
Object.entries(value).filter(
|
|
30317
|
+
(entry) => typeof entry[1] === "string"
|
|
30318
|
+
)
|
|
30319
|
+
);
|
|
30356
30320
|
}
|
|
30357
|
-
function
|
|
30358
|
-
|
|
30359
|
-
|
|
30360
|
-
|
|
30321
|
+
function readDockerInventory(run = defaultDockerRunner) {
|
|
30322
|
+
const daemonId = dockerOutput(run, ["info", "--format", "{{.ID}}"]).trim();
|
|
30323
|
+
if (!daemonId) throw new Error("docker info returned no daemon ID");
|
|
30324
|
+
const containerIds = listIds(run, ["ps", "-a", "-q", "--no-trunc"], "containers");
|
|
30325
|
+
const containers2 = inspectAll(run, ["container", "inspect"], containerIds).map((raw) => {
|
|
30326
|
+
const entry = raw;
|
|
30327
|
+
return {
|
|
30328
|
+
id: entry.Id,
|
|
30329
|
+
name: (entry.Name ?? "").replace(/^\//, ""),
|
|
30330
|
+
state: entry.State?.Status ?? "unknown",
|
|
30331
|
+
labels: labelsOf(entry.Config?.Labels),
|
|
30332
|
+
volumes: (entry.Mounts ?? []).filter((mount) => mount.Type === "volume" && mount.Name).map((mount) => mount.Name)
|
|
30333
|
+
};
|
|
30334
|
+
});
|
|
30335
|
+
const volumeNames = listIds(run, ["volume", "ls", "-q"], "volumes");
|
|
30336
|
+
const volumes = inspectAll(run, ["volume", "inspect"], volumeNames).map((raw) => {
|
|
30337
|
+
const entry = raw;
|
|
30338
|
+
return { name: entry.Name, labels: labelsOf(entry.Labels), createdAt: entry.CreatedAt ?? "" };
|
|
30339
|
+
});
|
|
30340
|
+
const networkIds = listIds(
|
|
30341
|
+
run,
|
|
30342
|
+
["network", "ls", "-q", "--no-trunc", "--filter", `label=${PROJECT_LABEL}`],
|
|
30343
|
+
"networks"
|
|
30344
|
+
);
|
|
30345
|
+
const networks = inspectAll(run, ["network", "inspect"], networkIds).map((raw) => {
|
|
30346
|
+
const entry = raw;
|
|
30347
|
+
return {
|
|
30348
|
+
id: entry.Id,
|
|
30349
|
+
name: entry.Name,
|
|
30350
|
+
labels: labelsOf(entry.Labels),
|
|
30351
|
+
attached: Object.keys(entry.Containers ?? {}).length
|
|
30352
|
+
};
|
|
30353
|
+
});
|
|
30354
|
+
return { daemonId, containers: containers2, volumes, networks };
|
|
30361
30355
|
}
|
|
30362
|
-
function
|
|
30363
|
-
return
|
|
30356
|
+
function checkoutOf(workingDir) {
|
|
30357
|
+
return workingDir.replace(/\/+$/, "").replace(/\/\.devcontainer$/, "");
|
|
30364
30358
|
}
|
|
30365
|
-
function
|
|
30366
|
-
|
|
30359
|
+
function isInsideOrEqual(parent, child) {
|
|
30360
|
+
const relative2 = import_node_path67.default.relative(parent, child);
|
|
30361
|
+
return relative2 === "" || !relative2.startsWith("..") && !import_node_path67.default.isAbsolute(relative2);
|
|
30367
30362
|
}
|
|
30368
|
-
function
|
|
30369
|
-
return [
|
|
30370
|
-
"devrouter",
|
|
30371
|
-
"workspace",
|
|
30372
|
-
...args,
|
|
30373
|
-
...beforeRepo,
|
|
30374
|
-
"--repo",
|
|
30375
|
-
quoteCommandArg(repoPath),
|
|
30376
|
-
...afterRepo
|
|
30377
|
-
].join(" ");
|
|
30363
|
+
function isAnonymousVolume(volume) {
|
|
30364
|
+
return /^[0-9a-f]{64}$/.test(volume.name) && !volume.labels[PROJECT_LABEL];
|
|
30378
30365
|
}
|
|
30379
|
-
function
|
|
30380
|
-
const
|
|
30381
|
-
if (!
|
|
30382
|
-
|
|
30383
|
-
|
|
30384
|
-
|
|
30385
|
-
|
|
30386
|
-
|
|
30387
|
-
|
|
30388
|
-
|
|
30389
|
-
|
|
30390
|
-
|
|
30366
|
+
function vanishedInsideTrees(checkout, context) {
|
|
30367
|
+
const treesReal = context.realpath(context.treesPath);
|
|
30368
|
+
if (!treesReal) return "out-of-scope";
|
|
30369
|
+
const lexical = [context.treesPath, treesReal].some(
|
|
30370
|
+
(prefix) => checkout !== prefix && isInsideOrEqual(prefix, checkout)
|
|
30371
|
+
);
|
|
30372
|
+
if (!lexical || context.exists(checkout)) return "out-of-scope";
|
|
30373
|
+
let ancestor = import_node_path67.default.dirname(checkout);
|
|
30374
|
+
while (!context.exists(ancestor)) {
|
|
30375
|
+
const parent = import_node_path67.default.dirname(ancestor);
|
|
30376
|
+
if (parent === ancestor) return "out-of-scope";
|
|
30377
|
+
ancestor = parent;
|
|
30378
|
+
}
|
|
30379
|
+
const ancestorReal = context.realpath(ancestor);
|
|
30380
|
+
if (ancestorReal === void 0 || !isInsideOrEqual(treesReal, ancestorReal)) {
|
|
30381
|
+
return "out-of-scope";
|
|
30382
|
+
}
|
|
30383
|
+
for (let current = ancestor; ; current = import_node_path67.default.dirname(current)) {
|
|
30384
|
+
const real = context.realpath(current);
|
|
30385
|
+
if (real === void 0 || real === treesReal || !isInsideOrEqual(treesReal, real)) break;
|
|
30386
|
+
if (context.exists(import_node_path67.default.join(current, ".git"))) return "inside-worktree";
|
|
30387
|
+
}
|
|
30388
|
+
return "vanished";
|
|
30389
|
+
}
|
|
30390
|
+
function resolved(file, context) {
|
|
30391
|
+
const real = context.realpath(file);
|
|
30392
|
+
return real && real !== import_node_path67.default.resolve(file) ? [import_node_path67.default.resolve(file), real] : [import_node_path67.default.resolve(file)];
|
|
30393
|
+
}
|
|
30394
|
+
function relation(entry, checkout, context) {
|
|
30395
|
+
const entries = resolved(entry, context);
|
|
30396
|
+
const checkouts = resolved(checkout, context);
|
|
30397
|
+
if (entries.some((left) => checkouts.includes(left))) return "same";
|
|
30398
|
+
const trees = resolved(context.treesPath, context);
|
|
30399
|
+
const belowTrees = (left) => trees.some((root) => left !== root && isInsideOrEqual(root, left));
|
|
30400
|
+
if (entries.some(
|
|
30401
|
+
(left) => belowTrees(left) && checkouts.some((right) => isInsideOrEqual(left, right))
|
|
30402
|
+
)) {
|
|
30403
|
+
return "contains";
|
|
30391
30404
|
}
|
|
30392
|
-
return
|
|
30405
|
+
return void 0;
|
|
30393
30406
|
}
|
|
30394
|
-
function
|
|
30395
|
-
const
|
|
30396
|
-
|
|
30397
|
-
|
|
30407
|
+
function findDockerLeftovers(inventory, context) {
|
|
30408
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
30409
|
+
for (const container of inventory.containers) {
|
|
30410
|
+
const project = container.labels[PROJECT_LABEL];
|
|
30411
|
+
if (!project) continue;
|
|
30412
|
+
byProject.set(project, [...byProject.get(project) ?? [], container]);
|
|
30413
|
+
}
|
|
30414
|
+
const leftovers = [];
|
|
30415
|
+
const blocked3 = [];
|
|
30416
|
+
for (const [project, containers2] of [...byProject].sort(([a], [b]) => byteOrder(a, b))) {
|
|
30417
|
+
const checkouts = new Set(
|
|
30418
|
+
containers2.filter((container) => container.labels[WORKING_DIR_LABEL]).map((container) => checkoutOf(container.labels[WORKING_DIR_LABEL]))
|
|
30419
|
+
);
|
|
30420
|
+
if (checkouts.size !== 1) continue;
|
|
30421
|
+
if (!containers2.some((container) => container.labels[PROVIDER_LABEL])) continue;
|
|
30422
|
+
const [checkout] = checkouts;
|
|
30423
|
+
const scope = vanishedInsideTrees(checkout, context);
|
|
30424
|
+
if (scope === "out-of-scope") continue;
|
|
30425
|
+
const block = (reason, next) => blocked3.push({ project, checkout, reason, next });
|
|
30426
|
+
if (scope === "inside-worktree") {
|
|
30427
|
+
block("the missing folder lies inside an existing checkout", "investigate");
|
|
30428
|
+
continue;
|
|
30429
|
+
}
|
|
30430
|
+
if (!context.registriesReadable) {
|
|
30431
|
+
block("a provider registry could not be read", "investigate");
|
|
30432
|
+
continue;
|
|
30433
|
+
}
|
|
30434
|
+
const registered = (paths) => paths.map((entry) => relation(entry, checkout, context));
|
|
30435
|
+
const all = [
|
|
30436
|
+
...registered(context.worktreePaths),
|
|
30437
|
+
...registered([...context.ownershipPaths, ...context.providerPaths])
|
|
30438
|
+
];
|
|
30439
|
+
if (all.includes("contains")) {
|
|
30440
|
+
block("a registered checkout contains the missing folder", "investigate");
|
|
30441
|
+
continue;
|
|
30442
|
+
}
|
|
30443
|
+
if (registered(context.worktreePaths).includes("same")) {
|
|
30444
|
+
block("Git still registers the vanished worktree", "git worktree prune");
|
|
30445
|
+
continue;
|
|
30446
|
+
}
|
|
30447
|
+
if (all.includes("same")) {
|
|
30448
|
+
block("a devrouter record or provider registration still names the checkout", "workspace gc");
|
|
30449
|
+
continue;
|
|
30450
|
+
}
|
|
30451
|
+
const running = containers2.filter((container) => !STOPPED_STATES.has(container.state));
|
|
30452
|
+
if (running.length) {
|
|
30453
|
+
block(`${running.length} container(s) are not stopped`, "stop the stack");
|
|
30454
|
+
continue;
|
|
30455
|
+
}
|
|
30456
|
+
leftovers.push(removableSet(project, checkout, containers2, inventory));
|
|
30398
30457
|
}
|
|
30399
|
-
|
|
30400
|
-
|
|
30401
|
-
|
|
30402
|
-
|
|
30403
|
-
(
|
|
30404
|
-
|
|
30405
|
-
|
|
30406
|
-
|
|
30407
|
-
|
|
30458
|
+
return {
|
|
30459
|
+
daemonId: inventory.daemonId,
|
|
30460
|
+
leftovers,
|
|
30461
|
+
blocked: blocked3,
|
|
30462
|
+
unattributedVolumes: unattributedVolumes(inventory, byProject)
|
|
30463
|
+
};
|
|
30464
|
+
}
|
|
30465
|
+
function removableSet(project, checkout, containers2, inventory) {
|
|
30466
|
+
const own = new Set(containers2.map((container) => container.id));
|
|
30467
|
+
const mountedHere = new Set(containers2.flatMap((container) => container.volumes));
|
|
30468
|
+
const mountedElsewhere = new Set(
|
|
30469
|
+
inventory.containers.filter((container) => !own.has(container.id)).flatMap((container) => container.volumes)
|
|
30470
|
+
);
|
|
30471
|
+
return {
|
|
30472
|
+
project,
|
|
30473
|
+
checkout,
|
|
30474
|
+
containers: containers2.map(({ id: id5, name, state }) => ({ id: id5, name, state })).sort((a, b) => byteOrder(a.id, b.id)),
|
|
30475
|
+
volumes: inventory.volumes.filter(
|
|
30476
|
+
(volume) => !mountedElsewhere.has(volume.name) && (volume.labels[PROJECT_LABEL] === project && volume.name.startsWith(`${project}_`) || isAnonymousVolume(volume) && mountedHere.has(volume.name))
|
|
30477
|
+
).map(({ name, createdAt }) => ({ name, createdAt })).sort((a, b) => byteOrder(a.name, b.name)),
|
|
30478
|
+
networks: inventory.networks.filter(
|
|
30479
|
+
(network) => network.labels[PROJECT_LABEL] === project && network.name.startsWith(`${project}_`) && network.attached === 0
|
|
30480
|
+
).map(({ id: id5, name }) => ({ id: id5, name })).sort((a, b) => byteOrder(a.id, b.id))
|
|
30481
|
+
};
|
|
30482
|
+
}
|
|
30483
|
+
function unattributedVolumes(inventory, byProject) {
|
|
30484
|
+
const mounted = new Set(inventory.containers.flatMap((container) => container.volumes));
|
|
30485
|
+
return inventory.volumes.filter(
|
|
30486
|
+
(volume) => volume.labels[PROJECT_LABEL] !== void 0 && !byProject.has(volume.labels[PROJECT_LABEL]) && !mounted.has(volume.name)
|
|
30487
|
+
).map((volume) => ({
|
|
30488
|
+
name: volume.name,
|
|
30489
|
+
project: volume.labels[PROJECT_LABEL],
|
|
30490
|
+
reason: "no container of its Compose project remains, so no checkout can be proven"
|
|
30491
|
+
})).sort((a, b) => byteOrder(a.name, b.name));
|
|
30492
|
+
}
|
|
30493
|
+
function readLeftoverContext(repoPath) {
|
|
30494
|
+
resetWorkspaceRuntimeCaches();
|
|
30495
|
+
const snapshots = getWorkspaceRegistrySnapshots();
|
|
30496
|
+
return {
|
|
30497
|
+
treesPath: import_node_path67.default.join(repoPath, "trees"),
|
|
30498
|
+
worktreePaths: listGitWorktrees(repoPath).map((worktree) => worktree.path),
|
|
30499
|
+
ownershipPaths: listWorkspaceOwnership(repoPath).map((record5) => record5.worktreePath),
|
|
30500
|
+
providerPaths: listMergedRuntimeWorkspaces().map((workspace) => workspace.source.localFolder),
|
|
30501
|
+
registriesReadable: snapshots.unavailable.length === 0,
|
|
30502
|
+
realpath: (file) => {
|
|
30503
|
+
try {
|
|
30504
|
+
return import_node_fs54.default.realpathSync.native(file);
|
|
30505
|
+
} catch {
|
|
30506
|
+
return void 0;
|
|
30507
|
+
}
|
|
30508
|
+
},
|
|
30509
|
+
exists: (file) => {
|
|
30510
|
+
try {
|
|
30511
|
+
import_node_fs54.default.lstatSync(file);
|
|
30512
|
+
return true;
|
|
30513
|
+
} catch (error) {
|
|
30514
|
+
return error.code !== "ENOENT";
|
|
30515
|
+
}
|
|
30516
|
+
}
|
|
30517
|
+
};
|
|
30518
|
+
}
|
|
30519
|
+
function leftoverCandidateId(daemonId, leftover) {
|
|
30520
|
+
return sha256Hex(canonicalJson({ daemonId, ...leftover }));
|
|
30521
|
+
}
|
|
30522
|
+
function buildLeftoverManifest(repo, report, createdAtEpoch) {
|
|
30523
|
+
const manifest = {
|
|
30524
|
+
kind: LEFTOVER_MANIFEST_KIND,
|
|
30525
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
30526
|
+
repo,
|
|
30527
|
+
daemonId: report.daemonId,
|
|
30528
|
+
createdAtEpoch,
|
|
30529
|
+
candidateCount: report.leftovers.length,
|
|
30530
|
+
candidates: report.leftovers.map((leftover) => ({
|
|
30531
|
+
...leftover,
|
|
30532
|
+
id: leftoverCandidateId(report.daemonId, leftover)
|
|
30533
|
+
}))
|
|
30534
|
+
};
|
|
30535
|
+
validateLeftoverManifest(manifest);
|
|
30536
|
+
return canonicalJson(manifest);
|
|
30537
|
+
}
|
|
30538
|
+
function requireExactKeys2(value, keys2, label2) {
|
|
30539
|
+
const actual = Object.keys(value).sort();
|
|
30540
|
+
if (actual.join("\0") !== [...keys2].sort().join("\0")) {
|
|
30541
|
+
throw new Error(`${label2} fields must be exactly: ${[...keys2].sort().join(", ")}`);
|
|
30542
|
+
}
|
|
30543
|
+
}
|
|
30544
|
+
function requireString(value, label2) {
|
|
30545
|
+
if (typeof value !== "string" || !value || value.includes("\0")) {
|
|
30546
|
+
throw new Error(`${label2} must be a non-empty string`);
|
|
30547
|
+
}
|
|
30548
|
+
}
|
|
30549
|
+
function requireSortedUnique(values, label2) {
|
|
30550
|
+
const sorted = [...new Set(values)].sort(byteOrder);
|
|
30551
|
+
if (sorted.length !== values.length || sorted.some((entry, index) => entry !== values[index])) {
|
|
30552
|
+
throw new Error(`${label2} must be sorted and unique`);
|
|
30553
|
+
}
|
|
30554
|
+
}
|
|
30555
|
+
function isLeftoverManifest(value) {
|
|
30556
|
+
return Boolean(value) && typeof value === "object" && value.kind === LEFTOVER_MANIFEST_KIND;
|
|
30557
|
+
}
|
|
30558
|
+
function validateLeftoverManifest(value) {
|
|
30559
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
30560
|
+
throw new Error("manifest root must be an object");
|
|
30561
|
+
}
|
|
30562
|
+
const root = value;
|
|
30563
|
+
if (root.kind !== LEFTOVER_MANIFEST_KIND) {
|
|
30564
|
+
throw new Error(`manifest kind must be ${LEFTOVER_MANIFEST_KIND}`);
|
|
30565
|
+
}
|
|
30566
|
+
requireExactKeys2(
|
|
30567
|
+
root,
|
|
30568
|
+
["candidateCount", "candidates", "createdAtEpoch", "daemonId", "kind", "repo", "schemaVersion"],
|
|
30569
|
+
"manifest"
|
|
30570
|
+
);
|
|
30571
|
+
if (root.schemaVersion !== SCHEMA_VERSION2) {
|
|
30572
|
+
throw new Error(`manifest schemaVersion must be ${SCHEMA_VERSION2}`);
|
|
30573
|
+
}
|
|
30574
|
+
requireString(root.repo, "manifest repo");
|
|
30575
|
+
if (!import_node_path67.default.isAbsolute(root.repo)) throw new Error("manifest repo must be an absolute path");
|
|
30576
|
+
requireString(root.daemonId, "manifest daemonId");
|
|
30577
|
+
if (!Number.isInteger(root.createdAtEpoch) || root.createdAtEpoch < 0) {
|
|
30578
|
+
throw new Error("manifest createdAtEpoch must be a non-negative integer");
|
|
30579
|
+
}
|
|
30580
|
+
if (!Array.isArray(root.candidates) || root.candidateCount !== root.candidates.length) {
|
|
30581
|
+
throw new Error("manifest candidateCount does not match candidates");
|
|
30582
|
+
}
|
|
30583
|
+
const daemonId = root.daemonId;
|
|
30584
|
+
root.candidates.forEach((raw, index) => {
|
|
30585
|
+
const label2 = `candidate[${index}]`;
|
|
30586
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
30587
|
+
throw new Error(`${label2} must be an object`);
|
|
30588
|
+
}
|
|
30589
|
+
const candidate = raw;
|
|
30590
|
+
requireExactKeys2(
|
|
30591
|
+
candidate,
|
|
30592
|
+
["checkout", "containers", "id", "networks", "project", "volumes"],
|
|
30593
|
+
label2
|
|
30594
|
+
);
|
|
30595
|
+
requireString(candidate.project, `${label2}.project`);
|
|
30596
|
+
requireString(candidate.checkout, `${label2}.checkout`);
|
|
30597
|
+
if (!import_node_path67.default.isAbsolute(candidate.checkout)) {
|
|
30598
|
+
throw new Error(`${label2}.checkout must be an absolute path`);
|
|
30599
|
+
}
|
|
30600
|
+
const members = [
|
|
30601
|
+
["containers", ["id", "name", "state"], "id"],
|
|
30602
|
+
["volumes", ["createdAt", "name"], "name"],
|
|
30603
|
+
["networks", ["id", "name"], "id"]
|
|
30604
|
+
];
|
|
30605
|
+
for (const [key, fields2, sortKey] of members) {
|
|
30606
|
+
const list2 = candidate[key];
|
|
30607
|
+
if (!Array.isArray(list2)) throw new Error(`${label2}.${key} must be an array`);
|
|
30608
|
+
for (const member of list2) {
|
|
30609
|
+
requireExactKeys2(member, [...fields2], `${label2}.${key}[]`);
|
|
30610
|
+
for (const field of fields2) {
|
|
30611
|
+
if (field !== "createdAt") requireString(member[field], `${label2}.${key}[].${field}`);
|
|
30612
|
+
}
|
|
30613
|
+
}
|
|
30614
|
+
requireSortedUnique(
|
|
30615
|
+
list2.map((member) => member[sortKey]),
|
|
30616
|
+
`${label2}.${key}`
|
|
30617
|
+
);
|
|
30618
|
+
}
|
|
30619
|
+
if (!candidate.containers.length) {
|
|
30620
|
+
throw new Error(`${label2}.containers must prove the checkout with at least one container`);
|
|
30621
|
+
}
|
|
30622
|
+
if (!HEX_ID2.test(String(candidate.id))) throw new Error(`${label2}.id is not a SHA-256`);
|
|
30623
|
+
const { id: id5, ...leftover } = candidate;
|
|
30624
|
+
if (id5 !== leftoverCandidateId(daemonId, leftover)) {
|
|
30625
|
+
throw new Error(`${label2}.id does not match its contents`);
|
|
30626
|
+
}
|
|
30627
|
+
});
|
|
30628
|
+
requireSortedUnique(
|
|
30629
|
+
root.candidates.map((candidate) => candidate.project),
|
|
30630
|
+
"manifest candidate projects"
|
|
30631
|
+
);
|
|
30632
|
+
return root;
|
|
30633
|
+
}
|
|
30634
|
+
function readLeftoverManifest(file) {
|
|
30635
|
+
const raw = import_node_fs54.default.readFileSync(file);
|
|
30636
|
+
let parsed;
|
|
30637
|
+
try {
|
|
30638
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
|
|
30639
|
+
} catch (error) {
|
|
30640
|
+
throw new Error(`manifest is not valid UTF-8 JSON: ${error.message}`);
|
|
30641
|
+
}
|
|
30642
|
+
const manifest = validateLeftoverManifest(parsed);
|
|
30643
|
+
if (canonicalJson(parsed) !== raw.toString("utf-8")) {
|
|
30644
|
+
throw new Error("manifest is not in canonical JSON form");
|
|
30645
|
+
}
|
|
30646
|
+
return { raw, manifest };
|
|
30647
|
+
}
|
|
30648
|
+
function gitCommonDir(repo) {
|
|
30649
|
+
const result = (0, import_node_child_process39.spawnSync)(
|
|
30650
|
+
"git",
|
|
30651
|
+
["-C", repo, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
30652
|
+
{ encoding: "utf-8" }
|
|
30653
|
+
);
|
|
30654
|
+
const commonDir = result.stdout?.trim();
|
|
30655
|
+
if (result.status !== 0 || !commonDir)
|
|
30656
|
+
throw new Error(`cannot read the Git directory of ${repo}`);
|
|
30657
|
+
return commonDir;
|
|
30658
|
+
}
|
|
30659
|
+
function currentRemovals(candidate, inventory, context) {
|
|
30660
|
+
const report = findDockerLeftovers(inventory, context);
|
|
30661
|
+
const projectContainers = inventory.containers.filter(
|
|
30662
|
+
(container) => container.labels[PROJECT_LABEL] === candidate.project
|
|
30663
|
+
);
|
|
30664
|
+
let current;
|
|
30665
|
+
if (projectContainers.length) {
|
|
30666
|
+
const blocked3 = report.blocked.find((entry) => entry.project === candidate.project);
|
|
30667
|
+
if (blocked3) throw new LeftoverSkip("blocked", `${blocked3.reason}; next: ${blocked3.next}`);
|
|
30668
|
+
const leftover = report.leftovers.find((entry) => entry.project === candidate.project);
|
|
30669
|
+
if (!leftover || leftover.checkout !== candidate.checkout) {
|
|
30670
|
+
throw new LeftoverSkip("changed", "the project is no longer a leftover of this checkout");
|
|
30671
|
+
}
|
|
30672
|
+
current = leftover;
|
|
30673
|
+
} else {
|
|
30674
|
+
if (context.exists(candidate.checkout)) {
|
|
30675
|
+
throw new LeftoverSkip("changed", "the checkout folder exists again");
|
|
30676
|
+
}
|
|
30677
|
+
const mounted = new Set(inventory.containers.flatMap((container) => container.volumes));
|
|
30678
|
+
const ours = (object4) => object4.labels[PROJECT_LABEL] === candidate.project && object4.name.startsWith(`${candidate.project}_`);
|
|
30679
|
+
current = {
|
|
30680
|
+
project: candidate.project,
|
|
30681
|
+
checkout: candidate.checkout,
|
|
30682
|
+
containers: [],
|
|
30683
|
+
volumes: inventory.volumes.filter(
|
|
30684
|
+
(volume) => !mounted.has(volume.name) && (ours(volume) || isAnonymousVolume(volume) && candidate.volumes.some((approved) => approved.name === volume.name))
|
|
30685
|
+
).map(({ name, createdAt }) => ({ name, createdAt })),
|
|
30686
|
+
networks: inventory.networks.filter((network) => ours(network) && network.attached === 0).map(({ id: id5, name }) => ({ id: id5, name }))
|
|
30687
|
+
};
|
|
30688
|
+
}
|
|
30689
|
+
const approve = (present, approved, key, kind) => {
|
|
30690
|
+
const allowed = new Set(approved.map(key));
|
|
30691
|
+
const extra = present.filter((entry) => !allowed.has(key(entry)));
|
|
30692
|
+
if (extra.length) {
|
|
30693
|
+
throw new LeftoverSkip("changed", `${extra.length} unapproved ${kind} now belong to it`);
|
|
30694
|
+
}
|
|
30695
|
+
};
|
|
30696
|
+
approve(current.containers, candidate.containers, (entry) => entry.id, "container(s)");
|
|
30697
|
+
approve(
|
|
30698
|
+
current.volumes,
|
|
30699
|
+
candidate.volumes,
|
|
30700
|
+
(entry) => `${entry.name}\0${entry.createdAt}`,
|
|
30701
|
+
"volume(s)"
|
|
30702
|
+
);
|
|
30703
|
+
approve(current.networks, candidate.networks, (entry) => entry.id, "network(s)");
|
|
30704
|
+
const stillPresent = (name) => inventory.volumes.some((volume) => volume.name === name);
|
|
30705
|
+
const notRemovable = candidate.volumes.filter(
|
|
30706
|
+
(volume) => stillPresent(volume.name) && !current.volumes.some((entry) => entry.name === volume.name)
|
|
30707
|
+
);
|
|
30708
|
+
if (notRemovable.length) {
|
|
30709
|
+
throw new LeftoverSkip(
|
|
30710
|
+
"changed",
|
|
30711
|
+
`${notRemovable.length} approved volume(s) are now in use or re-created`
|
|
30712
|
+
);
|
|
30713
|
+
}
|
|
30714
|
+
return current;
|
|
30715
|
+
}
|
|
30716
|
+
function removeMember(run, args, step, steps) {
|
|
30717
|
+
const result = run(args);
|
|
30718
|
+
if (result.status === 0) {
|
|
30719
|
+
steps.push(`removed-${step}`);
|
|
30720
|
+
return;
|
|
30721
|
+
}
|
|
30722
|
+
if (/no such (container|volume|network)/i.test(result.stderr)) {
|
|
30723
|
+
steps.push(`gone-${step}`);
|
|
30724
|
+
return;
|
|
30725
|
+
}
|
|
30726
|
+
throw new Error(`docker ${args.slice(0, 2).join(" ")} failed: ${result.stderr.trim()}`);
|
|
30727
|
+
}
|
|
30728
|
+
function applyLeftoverManifest(options, dependencies = {}) {
|
|
30729
|
+
const { raw, manifest } = readLeftoverManifest(options.manifestFile);
|
|
30730
|
+
const manifestSha256 = requireManifestHash(raw, options.sha256);
|
|
30731
|
+
const now = dependencies.nowEpoch?.() ?? Math.floor(Date.now() / 1e3);
|
|
30732
|
+
if (manifest.createdAtEpoch > now + FUTURE_TOLERANCE_SECONDS2) {
|
|
30733
|
+
throw new Error("manifest creation time is in the future");
|
|
30734
|
+
}
|
|
30735
|
+
const age = Math.max(0, now - manifest.createdAtEpoch);
|
|
30736
|
+
if (age > options.maxAgeSeconds) {
|
|
30737
|
+
throw new Error(`manifest is ${age}s old; maximum allowed age is ${options.maxAgeSeconds}s`);
|
|
30738
|
+
}
|
|
30739
|
+
if (!manifest.candidates.length) throw new Error("approved manifest is empty");
|
|
30740
|
+
const run = dependencies.run ?? defaultDockerRunner;
|
|
30741
|
+
const readInventory = dependencies.readInventory ?? (() => readDockerInventory(run));
|
|
30742
|
+
const readContext = dependencies.readContext ?? readLeftoverContext;
|
|
30743
|
+
const withLocks = dependencies.withLocks ?? defaultWithLocks;
|
|
30744
|
+
const targets = [];
|
|
30745
|
+
for (const candidate of manifest.candidates) {
|
|
30746
|
+
const outcome = {
|
|
30747
|
+
id: candidate.id,
|
|
30748
|
+
project: candidate.project,
|
|
30749
|
+
status: "skipped",
|
|
30750
|
+
steps: []
|
|
30751
|
+
};
|
|
30752
|
+
targets.push(outcome);
|
|
30753
|
+
try {
|
|
30754
|
+
withLocks(candidate.project, () => {
|
|
30755
|
+
const inventory = readInventory();
|
|
30756
|
+
if (inventory.daemonId !== manifest.daemonId) {
|
|
30757
|
+
throw new LeftoverSkip(
|
|
30758
|
+
"daemon-mismatch",
|
|
30759
|
+
"the manifest was made for another Docker engine"
|
|
30760
|
+
);
|
|
30761
|
+
}
|
|
30762
|
+
const current = currentRemovals(candidate, inventory, readContext(manifest.repo));
|
|
30763
|
+
const count = current.containers.length + current.volumes.length + current.networks.length;
|
|
30764
|
+
if (count === 0) {
|
|
30765
|
+
outcome.status = "already-done";
|
|
30766
|
+
return;
|
|
30767
|
+
}
|
|
30768
|
+
if (!options.yes) {
|
|
30769
|
+
outcome.status = "would-remove";
|
|
30770
|
+
outcome.reason = `${current.containers.length} container(s), ${current.volumes.length} volume(s), ${current.networks.length} network(s)`;
|
|
30771
|
+
return;
|
|
30772
|
+
}
|
|
30773
|
+
const proves = (id5) => Boolean(
|
|
30774
|
+
inventory.containers.find((container) => container.id === id5)?.labels[PROVIDER_LABEL]
|
|
30775
|
+
);
|
|
30776
|
+
const ordered = [
|
|
30777
|
+
...current.containers.filter((container) => !proves(container.id)),
|
|
30778
|
+
...current.containers.filter((container) => proves(container.id))
|
|
30779
|
+
];
|
|
30780
|
+
for (const container of ordered) {
|
|
30781
|
+
removeMember(run, ["rm", container.id], `container:${container.id}`, outcome.steps);
|
|
30782
|
+
}
|
|
30783
|
+
for (const volume of current.volumes) {
|
|
30784
|
+
removeMember(run, ["volume", "rm", volume.name], `volume:${volume.name}`, outcome.steps);
|
|
30785
|
+
}
|
|
30786
|
+
for (const network of current.networks) {
|
|
30787
|
+
removeMember(
|
|
30788
|
+
run,
|
|
30789
|
+
["network", "rm", network.id],
|
|
30790
|
+
`network:${network.name}`,
|
|
30791
|
+
outcome.steps
|
|
30792
|
+
);
|
|
30793
|
+
}
|
|
30794
|
+
writeLeftoverReceipt(
|
|
30795
|
+
(dependencies.commonDir ?? gitCommonDir)(manifest.repo),
|
|
30796
|
+
manifestSha256,
|
|
30797
|
+
candidate,
|
|
30798
|
+
outcome.steps,
|
|
30799
|
+
now
|
|
30800
|
+
);
|
|
30801
|
+
outcome.status = "removed";
|
|
30802
|
+
});
|
|
30803
|
+
} catch (error) {
|
|
30804
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30805
|
+
outcome.reason = message;
|
|
30806
|
+
if (error instanceof LeftoverSkip) {
|
|
30807
|
+
outcome.code = error.code;
|
|
30808
|
+
continue;
|
|
30809
|
+
}
|
|
30810
|
+
outcome.status = "failed";
|
|
30811
|
+
outcome.code = outcome.steps.length ? "teardown-failed" : "environment";
|
|
30812
|
+
}
|
|
30813
|
+
}
|
|
30814
|
+
return { manifestSha256, dryRun: !options.yes, targets };
|
|
30815
|
+
}
|
|
30816
|
+
function leftoverReceiptPath(commonDir, manifestSha256, id5) {
|
|
30817
|
+
return import_node_path67.default.join(commonDir, "devrouter-leftovers", "receipts", manifestSha256, `${id5}.json`);
|
|
30818
|
+
}
|
|
30819
|
+
function writeLeftoverReceipt(commonDir, manifestSha256, candidate, steps, nowEpoch) {
|
|
30820
|
+
const file = leftoverReceiptPath(commonDir, manifestSha256, candidate.id);
|
|
30821
|
+
import_node_fs54.default.mkdirSync(import_node_path67.default.dirname(file), { recursive: true });
|
|
30822
|
+
writeFileAtomically(
|
|
30823
|
+
file,
|
|
30824
|
+
canonicalJson({
|
|
30825
|
+
candidateId: candidate.id,
|
|
30826
|
+
checkout: candidate.checkout,
|
|
30827
|
+
completedAtEpoch: nowEpoch,
|
|
30828
|
+
kind: LEFTOVER_RECEIPT_KIND,
|
|
30829
|
+
manifestSha256,
|
|
30830
|
+
project: candidate.project,
|
|
30831
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
30832
|
+
steps,
|
|
30833
|
+
tool: "devrouter"
|
|
30834
|
+
})
|
|
30835
|
+
);
|
|
30836
|
+
}
|
|
30837
|
+
var import_node_child_process39, import_node_fs54, import_node_path67, LEFTOVER_MANIFEST_KIND, LEFTOVER_RECEIPT_KIND, SCHEMA_VERSION2, MAX_DOCKER_OBJECTS, FUTURE_TOLERANCE_SECONDS2, PROJECT_LABEL, WORKING_DIR_LABEL, PROVIDER_LABEL, STOPPED_STATES, HEX_ID2, DockerListingBoundError, defaultDockerRunner, LeftoverSkip, defaultWithLocks;
|
|
30838
|
+
var init_docker_leftovers = __esm({
|
|
30839
|
+
"src/core/docker-leftovers.ts"() {
|
|
30840
|
+
"use strict";
|
|
30841
|
+
import_node_child_process39 = require("child_process");
|
|
30842
|
+
import_node_fs54 = __toESM(require("fs"));
|
|
30843
|
+
import_node_path67 = __toESM(require("path"));
|
|
30844
|
+
init_atomic_file();
|
|
30845
|
+
init_devpod_mutation();
|
|
30846
|
+
init_devpod_workspaces();
|
|
30847
|
+
init_devsy_mutation();
|
|
30848
|
+
init_workspace_ownership();
|
|
30849
|
+
init_workspace_runtime();
|
|
30850
|
+
init_worktree_reclaim();
|
|
30851
|
+
LEFTOVER_MANIFEST_KIND = "docker-leftover-manifest";
|
|
30852
|
+
LEFTOVER_RECEIPT_KIND = "docker-leftover-receipt";
|
|
30853
|
+
SCHEMA_VERSION2 = 1;
|
|
30854
|
+
MAX_DOCKER_OBJECTS = 5e3;
|
|
30855
|
+
FUTURE_TOLERANCE_SECONDS2 = 300;
|
|
30856
|
+
PROJECT_LABEL = "com.docker.compose.project";
|
|
30857
|
+
WORKING_DIR_LABEL = "com.docker.compose.project.working_dir";
|
|
30858
|
+
PROVIDER_LABEL = "dev.containers.id";
|
|
30859
|
+
STOPPED_STATES = /* @__PURE__ */ new Set(["exited", "dead", "created"]);
|
|
30860
|
+
HEX_ID2 = /^[0-9a-f]{64}$/;
|
|
30861
|
+
DockerListingBoundError = class extends Error {
|
|
30862
|
+
};
|
|
30863
|
+
defaultDockerRunner = (args) => {
|
|
30864
|
+
const result = (0, import_node_child_process39.spawnSync)("docker", args, {
|
|
30865
|
+
encoding: "utf-8",
|
|
30866
|
+
timeout: 6e4,
|
|
30867
|
+
maxBuffer: 256 * 1024 * 1024
|
|
30868
|
+
});
|
|
30869
|
+
return {
|
|
30870
|
+
status: result.error ? null : result.status,
|
|
30871
|
+
stdout: result.stdout ?? "",
|
|
30872
|
+
stderr: result.error?.message ?? result.stderr ?? ""
|
|
30873
|
+
};
|
|
30874
|
+
};
|
|
30875
|
+
LeftoverSkip = class extends Error {
|
|
30876
|
+
constructor(code, message) {
|
|
30877
|
+
super(message);
|
|
30878
|
+
this.code = code;
|
|
30879
|
+
}
|
|
30880
|
+
};
|
|
30881
|
+
defaultWithLocks = (target, operation) => withMutationLock(
|
|
30882
|
+
"Docker leftover removal",
|
|
30883
|
+
target,
|
|
30884
|
+
() => withMutationLock2("Docker leftover removal", target, operation)
|
|
30885
|
+
);
|
|
30886
|
+
}
|
|
30887
|
+
});
|
|
30888
|
+
|
|
30889
|
+
// src/core/workspace-consumption.ts
|
|
30890
|
+
function measureWorktreeConsumption(worktreePath, options) {
|
|
30891
|
+
const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
30892
|
+
const startedAt = Date.now();
|
|
30893
|
+
let rootStat;
|
|
30894
|
+
try {
|
|
30895
|
+
rootStat = import_node_fs55.default.lstatSync(worktreePath);
|
|
30896
|
+
} catch (error) {
|
|
30897
|
+
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
30898
|
+
}
|
|
30899
|
+
let rootEntries;
|
|
30900
|
+
try {
|
|
30901
|
+
rootEntries = import_node_fs55.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
30902
|
+
} catch (error) {
|
|
30903
|
+
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
30904
|
+
}
|
|
30905
|
+
const seenInodes = /* @__PURE__ */ new Set();
|
|
30906
|
+
let bytes2 = 0;
|
|
30907
|
+
let timedOut = false;
|
|
30908
|
+
let unreadableReason = null;
|
|
30909
|
+
const accumulate = (stat) => {
|
|
30910
|
+
if (stat.nlink > 1) {
|
|
30911
|
+
const key = `${stat.dev}:${stat.ino}`;
|
|
30912
|
+
if (seenInodes.has(key)) return;
|
|
30913
|
+
seenInodes.add(key);
|
|
30914
|
+
}
|
|
30915
|
+
bytes2 += stat.blocks * BLOCK_SIZE_BYTES;
|
|
30916
|
+
};
|
|
30917
|
+
const deadlineExceeded = () => Date.now() - startedAt >= deadlineMs;
|
|
30918
|
+
accumulate(rootStat);
|
|
30919
|
+
const stack = [{ dirPath: worktreePath, entries: rootEntries }];
|
|
30920
|
+
while (stack.length > 0 && !timedOut && !unreadableReason) {
|
|
30921
|
+
const { dirPath, entries } = stack.pop();
|
|
30922
|
+
for (const entry of entries) {
|
|
30923
|
+
if (deadlineExceeded()) {
|
|
30924
|
+
timedOut = true;
|
|
30925
|
+
break;
|
|
30926
|
+
}
|
|
30927
|
+
const entryPath = import_node_path68.default.join(dirPath, entry.name);
|
|
30928
|
+
let entryStat;
|
|
30929
|
+
try {
|
|
30930
|
+
entryStat = import_node_fs55.default.lstatSync(entryPath);
|
|
30931
|
+
} catch (error) {
|
|
30932
|
+
if (error?.code === "ENOENT") continue;
|
|
30933
|
+
unreadableReason = describeIncompleteWalk(error);
|
|
30934
|
+
break;
|
|
30935
|
+
}
|
|
30936
|
+
accumulate(entryStat);
|
|
30937
|
+
if (!entryStat.isDirectory()) continue;
|
|
30938
|
+
let childEntries;
|
|
30939
|
+
try {
|
|
30940
|
+
childEntries = import_node_fs55.default.readdirSync(entryPath, { withFileTypes: true });
|
|
30941
|
+
} catch (error) {
|
|
30942
|
+
unreadableReason = describeIncompleteWalk(error);
|
|
30943
|
+
break;
|
|
30944
|
+
}
|
|
30945
|
+
stack.push({ dirPath: entryPath, entries: childEntries });
|
|
30946
|
+
}
|
|
30947
|
+
}
|
|
30948
|
+
if (timedOut) {
|
|
30949
|
+
return { status: "unknown", reason: `exceeded deadline of ${deadlineMs}ms` };
|
|
30950
|
+
}
|
|
30951
|
+
if (unreadableReason) {
|
|
30952
|
+
return { status: "unknown", reason: unreadableReason };
|
|
30953
|
+
}
|
|
30954
|
+
return { status: "measured", bytes: bytes2 };
|
|
30955
|
+
}
|
|
30956
|
+
function describeIncompleteWalk(error) {
|
|
30957
|
+
return `could not read every path inside the worktree: ${error?.message ?? String(error)}`;
|
|
30958
|
+
}
|
|
30959
|
+
function measureContainerConsumption(worktreePaths, dependencies) {
|
|
30960
|
+
const byWorktree = /* @__PURE__ */ new Map();
|
|
30961
|
+
if (worktreePaths.length === 0) return byWorktree;
|
|
30962
|
+
const inspect = dependencies?.inspect ?? inspectWorkspaceContainers;
|
|
30963
|
+
const containers2 = inspect();
|
|
30964
|
+
const attributedIds = /* @__PURE__ */ new Set();
|
|
30965
|
+
for (const worktreePath of worktreePaths) {
|
|
30966
|
+
for (const container of workspaceAppContainers(containers2, worktreePath)) {
|
|
30967
|
+
attributedIds.add(container.id);
|
|
30968
|
+
}
|
|
30969
|
+
}
|
|
30970
|
+
const sized = attributedIds.size === 0 ? [] : inspect({ withSize: true, ids: Array.from(attributedIds) });
|
|
30971
|
+
for (const worktreePath of worktreePaths) {
|
|
30972
|
+
byWorktree.set(worktreePath, summarizeContainers(workspaceAppContainers(sized, worktreePath)));
|
|
30973
|
+
}
|
|
30974
|
+
return byWorktree;
|
|
30975
|
+
}
|
|
30976
|
+
function summarizeContainers(containers2) {
|
|
30977
|
+
let writable = 0;
|
|
30978
|
+
let shared = 0;
|
|
30979
|
+
for (const container of containers2) {
|
|
30980
|
+
const { sizeRw, sizeRootFs } = container;
|
|
30981
|
+
if (typeof sizeRw !== "number" || typeof sizeRootFs !== "number" || !Number.isFinite(sizeRw) || !Number.isFinite(sizeRootFs) || sizeRootFs < sizeRw) {
|
|
30982
|
+
const unknown = {
|
|
30983
|
+
status: "unknown",
|
|
30984
|
+
reason: `container ${container.id.slice(0, 12)} reported no usable size`
|
|
30985
|
+
};
|
|
30986
|
+
return { containerWritable: unknown, imageShared: unknown };
|
|
30987
|
+
}
|
|
30988
|
+
writable += sizeRw;
|
|
30989
|
+
shared += sizeRootFs - sizeRw;
|
|
30990
|
+
}
|
|
30991
|
+
return {
|
|
30992
|
+
containerWritable: { status: "measured", bytes: writable },
|
|
30993
|
+
imageShared: { status: "measured", bytes: shared }
|
|
30994
|
+
};
|
|
30995
|
+
}
|
|
30996
|
+
function describeError(error, worktreePath) {
|
|
30997
|
+
const code = error?.code;
|
|
30998
|
+
if (code === "ENOENT") return `worktree path '${worktreePath}' does not exist`;
|
|
30999
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
31000
|
+
return `permission denied reading worktree path '${worktreePath}'`;
|
|
31001
|
+
}
|
|
31002
|
+
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
31003
|
+
}
|
|
31004
|
+
var import_node_fs55, import_node_path68, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
31005
|
+
var init_workspace_consumption = __esm({
|
|
31006
|
+
"src/core/workspace-consumption.ts"() {
|
|
31007
|
+
"use strict";
|
|
31008
|
+
import_node_fs55 = __toESM(require("fs"));
|
|
31009
|
+
import_node_path68 = __toESM(require("path"));
|
|
31010
|
+
init_devpod_environment();
|
|
31011
|
+
DEFAULT_DEADLINE_MS = 1e4;
|
|
31012
|
+
BLOCK_SIZE_BYTES = 512;
|
|
31013
|
+
}
|
|
31014
|
+
});
|
|
31015
|
+
|
|
31016
|
+
// src/core/workspace-cleanup.ts
|
|
31017
|
+
function isRecord9(value) {
|
|
31018
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31019
|
+
}
|
|
31020
|
+
function isSha2(value) {
|
|
31021
|
+
return typeof value === "string" && /^[0-9a-f]{40,64}$/i.test(value);
|
|
31022
|
+
}
|
|
31023
|
+
function parseTimestamp(value) {
|
|
31024
|
+
if (typeof value !== "string") return void 0;
|
|
31025
|
+
const timestamp = Date.parse(value);
|
|
31026
|
+
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
31027
|
+
}
|
|
31028
|
+
function validTimestamp(value) {
|
|
31029
|
+
return parseTimestamp(value) !== void 0;
|
|
31030
|
+
}
|
|
31031
|
+
function successfulOutput(result) {
|
|
31032
|
+
if (result.status !== 0 || result.error) return void 0;
|
|
31033
|
+
const output2 = result.stdout.trim();
|
|
31034
|
+
return output2.length > 0 ? output2 : void 0;
|
|
31035
|
+
}
|
|
31036
|
+
function gitOutput(repoPath, args, commandRunner) {
|
|
31037
|
+
return successfulOutput(commandRunner("git", ["-C", repoPath, ...args]));
|
|
31038
|
+
}
|
|
31039
|
+
function quoteCommandArg(value) {
|
|
31040
|
+
return /^[a-zA-Z0-9_./:@+-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
31041
|
+
}
|
|
31042
|
+
function workspaceCommand(repoPath, args, beforeRepo = [], afterRepo = []) {
|
|
31043
|
+
return [
|
|
31044
|
+
"devrouter",
|
|
31045
|
+
"workspace",
|
|
31046
|
+
...args,
|
|
31047
|
+
...beforeRepo,
|
|
31048
|
+
"--repo",
|
|
31049
|
+
quoteCommandArg(repoPath),
|
|
31050
|
+
...afterRepo
|
|
31051
|
+
].join(" ");
|
|
31052
|
+
}
|
|
31053
|
+
function parseInactiveFor(value = DEFAULT_INACTIVE_FOR) {
|
|
31054
|
+
const match = /^(?<amount>[1-9]\d*)(?<unit>[smhdw])$/.exec(value);
|
|
31055
|
+
if (!match?.groups) {
|
|
31056
|
+
throw new Error(
|
|
31057
|
+
"--inactive-for must be a positive integer followed by s, m, h, d, or w (for example 30d)."
|
|
31058
|
+
);
|
|
31059
|
+
}
|
|
31060
|
+
const amount = Number(match.groups.amount);
|
|
31061
|
+
const multiplier = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[match.groups.unit];
|
|
31062
|
+
const seconds = amount * multiplier;
|
|
31063
|
+
if (!Number.isSafeInteger(seconds)) {
|
|
31064
|
+
throw new Error("--inactive-for is too large.");
|
|
31065
|
+
}
|
|
31066
|
+
return { input: value, seconds };
|
|
31067
|
+
}
|
|
31068
|
+
function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
31069
|
+
const cutoffTime = parseTimestamp(cutoff);
|
|
31070
|
+
if (cutoffTime === void 0) {
|
|
31071
|
+
throw new Error(`Invalid activity cutoff '${cutoff}'.`);
|
|
31072
|
+
}
|
|
31073
|
+
const validEvidence = evidence.filter(
|
|
31074
|
+
(entry) => entry.status === "valid" && validTimestamp(entry.timestamp)
|
|
31075
|
+
);
|
|
31076
|
+
const latestTime = validEvidence.reduce(
|
|
31077
|
+
(latest, entry) => Math.max(
|
|
31078
|
+
latest ?? Number.NEGATIVE_INFINITY,
|
|
31079
|
+
parseTimestamp(entry.timestamp) ?? Number.NEGATIVE_INFINITY
|
|
31080
|
+
),
|
|
31081
|
+
void 0
|
|
30408
31082
|
);
|
|
30409
31083
|
const latestTimestamp = latestTime === void 0 ? null : validEvidence.find((entry) => parseTimestamp(entry.timestamp) === latestTime)?.timestamp ?? null;
|
|
30410
31084
|
const contributingEvidence = ACTIVITY_SOURCES.filter(
|
|
@@ -30433,7 +31107,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
30433
31107
|
}
|
|
30434
31108
|
function readGitSnapshot(worktree, commandRunner) {
|
|
30435
31109
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
30436
|
-
if (worktree.prunable || !
|
|
31110
|
+
if (worktree.prunable || !import_node_fs56.default.existsSync(comparablePath)) {
|
|
30437
31111
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
30438
31112
|
}
|
|
30439
31113
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -31006,7 +31680,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
31006
31680
|
containers2 = measureContainersFn(worktreePaths);
|
|
31007
31681
|
} catch (error) {
|
|
31008
31682
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
31009
|
-
containers2 = new Map(worktreePaths.map((
|
|
31683
|
+
containers2 = new Map(worktreePaths.map((path72) => [path72, unknownContainers(reason)]));
|
|
31010
31684
|
}
|
|
31011
31685
|
}
|
|
31012
31686
|
const rows2 = records.map((record5) => {
|
|
@@ -31065,7 +31739,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
31065
31739
|
dependencies.listForge ?? ((identity2) => listForgeChanges(identity2))
|
|
31066
31740
|
);
|
|
31067
31741
|
const sources = (dependencies.readRuntimeSources ?? readRuntimeSources)(repoPath);
|
|
31068
|
-
const disposable =
|
|
31742
|
+
const disposable = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
|
|
31069
31743
|
const [primary, ...linked] = worktrees;
|
|
31070
31744
|
const safetyFor = (worktree, worktreePath, evidence) => toSafety(
|
|
31071
31745
|
classify({
|
|
@@ -31073,7 +31747,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
31073
31747
|
branch: worktree?.branch,
|
|
31074
31748
|
primary: primary !== void 0 && sameWorkspacePath(primary.path, worktreePath),
|
|
31075
31749
|
locked: worktree?.locked ?? false,
|
|
31076
|
-
missing: !worktree || worktree.prunable || !
|
|
31750
|
+
missing: !worktree || worktree.prunable || !import_node_fs56.default.existsSync(worktreePath),
|
|
31077
31751
|
runtimeEvidence: evidence,
|
|
31078
31752
|
forge,
|
|
31079
31753
|
extraDisposablePatterns: disposable
|
|
@@ -31101,9 +31775,18 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
31101
31775
|
...base,
|
|
31102
31776
|
workspaces: managedRows,
|
|
31103
31777
|
worktrees: unmanaged,
|
|
31778
|
+
dockerLeftovers: readDockerLeftoverEvidence(repoPath, dependencies),
|
|
31104
31779
|
networkCapacity
|
|
31105
31780
|
};
|
|
31106
31781
|
}
|
|
31782
|
+
function readDockerLeftoverEvidence(repoPath, dependencies) {
|
|
31783
|
+
try {
|
|
31784
|
+
const read2 = dependencies.readDockerLeftovers ?? ((repo) => findDockerLeftovers(readDockerInventory(), readLeftoverContext(repo)));
|
|
31785
|
+
return { status: "listed", ...read2(repoPath) };
|
|
31786
|
+
} catch (error) {
|
|
31787
|
+
return { status: "unavailable", reason: describeCause(error) };
|
|
31788
|
+
}
|
|
31789
|
+
}
|
|
31107
31790
|
function readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies) {
|
|
31108
31791
|
const records = (dependencies.listOwnership ?? listWorkspaceOwnership)(repoPath).filter(
|
|
31109
31792
|
(record5) => sameWorkspacePath(record5.worktreePath, worktreePath)
|
|
@@ -31125,7 +31808,7 @@ function readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies) {
|
|
|
31125
31808
|
}
|
|
31126
31809
|
const sources = (dependencies.readRuntimeSources ?? readRuntimeSources)(repoPath);
|
|
31127
31810
|
const cutoffEpoch = Math.floor(Date.now() / 1e3) - parseInactiveFor(inactiveFor).seconds;
|
|
31128
|
-
const gitDir = (0,
|
|
31811
|
+
const gitDir = (0, import_node_child_process40.spawnSync)(
|
|
31129
31812
|
"git",
|
|
31130
31813
|
["-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-dir"],
|
|
31131
31814
|
{ encoding: "utf-8", env: READ_ONLY_GIT_ENV2 }
|
|
@@ -31145,7 +31828,7 @@ function classifyExactPath(repoPath, worktreePath, evidence, forge, extraDisposa
|
|
|
31145
31828
|
branch: worktree?.branch,
|
|
31146
31829
|
primary: worktrees[0] !== void 0 && sameWorkspacePath(worktrees[0].path, worktreePath),
|
|
31147
31830
|
locked: worktree?.locked ?? false,
|
|
31148
|
-
missing: !worktree || worktree.prunable || !
|
|
31831
|
+
missing: !worktree || worktree.prunable || !import_node_fs56.default.existsSync(worktreePath),
|
|
31149
31832
|
runtimeEvidence: evidence.runtimeEvidence,
|
|
31150
31833
|
forge,
|
|
31151
31834
|
extraDisposablePatterns
|
|
@@ -31171,7 +31854,7 @@ function prepareReclaimVerdicts(repo, inactiveFor, dependencies = {}) {
|
|
|
31171
31854
|
dependencies.listForge ?? ((identity2) => listForgeChanges(identity2))
|
|
31172
31855
|
);
|
|
31173
31856
|
const forge = evidence.status === "not-checked" ? { status: "unavailable", reason: "forge not checked" } : evidence;
|
|
31174
|
-
const disposable =
|
|
31857
|
+
const disposable = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
|
|
31175
31858
|
const paths = evidenceCache(
|
|
31176
31859
|
(worktreePath) => readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies)
|
|
31177
31860
|
);
|
|
@@ -31190,7 +31873,7 @@ function prepareReclaimVerdicts(repo, inactiveFor, dependencies = {}) {
|
|
|
31190
31873
|
}
|
|
31191
31874
|
function prepareTrimChecks(repo, inactiveFor, dependencies = {}) {
|
|
31192
31875
|
const repoPath = resolveRepoPath(repo);
|
|
31193
|
-
const configured =
|
|
31876
|
+
const configured = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
|
|
31194
31877
|
const patterns = configured?.trim ?? [];
|
|
31195
31878
|
const paths = evidenceCache(
|
|
31196
31879
|
(worktreePath) => readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies)
|
|
@@ -31215,14 +31898,15 @@ function prepareTrimChecks(repo, inactiveFor, dependencies = {}) {
|
|
|
31215
31898
|
isActive: (worktreePath) => paths.last(worktreePath).active
|
|
31216
31899
|
};
|
|
31217
31900
|
}
|
|
31218
|
-
var
|
|
31901
|
+
var import_node_child_process40, import_node_fs56, import_node_path69, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
31219
31902
|
var init_workspace_cleanup = __esm({
|
|
31220
31903
|
"src/core/workspace-cleanup.ts"() {
|
|
31221
31904
|
"use strict";
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31905
|
+
import_node_child_process40 = require("child_process");
|
|
31906
|
+
import_node_fs56 = __toESM(require("fs"));
|
|
31907
|
+
import_node_path69 = __toESM(require("path"));
|
|
31225
31908
|
init_devpod_workspaces();
|
|
31909
|
+
init_docker_leftovers();
|
|
31226
31910
|
init_forge_changes();
|
|
31227
31911
|
init_host_routes();
|
|
31228
31912
|
init_network_diagnostics();
|
|
@@ -31243,7 +31927,7 @@ var init_workspace_cleanup = __esm({
|
|
|
31243
31927
|
];
|
|
31244
31928
|
defaultCommandRunner = (command, args, input2) => {
|
|
31245
31929
|
const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
|
|
31246
|
-
const result = (0,
|
|
31930
|
+
const result = (0, import_node_child_process40.spawnSync)(command, args, {
|
|
31247
31931
|
encoding: "utf-8",
|
|
31248
31932
|
env: environment,
|
|
31249
31933
|
...input2 === void 0 ? {} : { input: input2 }
|
|
@@ -31337,20 +32021,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
31337
32021
|
);
|
|
31338
32022
|
}
|
|
31339
32023
|
function defaultWorktreePath(mainRepo, ws) {
|
|
31340
|
-
return
|
|
32024
|
+
return import_node_path70.default.join(mainRepo, "trees", ws);
|
|
31341
32025
|
}
|
|
31342
32026
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
31343
|
-
const ignored = (0,
|
|
32027
|
+
const ignored = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
31344
32028
|
encoding: "utf-8"
|
|
31345
32029
|
});
|
|
31346
32030
|
if (ignored.status !== 0) {
|
|
31347
32031
|
throw new Error(
|
|
31348
|
-
`Default worktree root '${
|
|
32032
|
+
`Default worktree root '${import_node_path70.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path70.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
31349
32033
|
);
|
|
31350
32034
|
}
|
|
31351
32035
|
}
|
|
31352
32036
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
31353
|
-
return
|
|
32037
|
+
return import_node_path70.default.join(import_node_path70.default.dirname(mainRepo), `${import_node_path70.default.basename(mainRepo)}-${ws}`);
|
|
31354
32038
|
}
|
|
31355
32039
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
31356
32040
|
const candidates = [
|
|
@@ -31462,7 +32146,7 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
31462
32146
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
31463
32147
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
31464
32148
|
}
|
|
31465
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
32149
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs57.default.existsSync(target.worktreePath)) return;
|
|
31466
32150
|
if (target.worktree.locked) {
|
|
31467
32151
|
throw new Error(
|
|
31468
32152
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
@@ -31494,9 +32178,9 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
31494
32178
|
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
31495
32179
|
);
|
|
31496
32180
|
const generatedPath = candidatePaths.find(
|
|
31497
|
-
(candidate) => !
|
|
32181
|
+
(candidate) => !import_node_fs57.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
31498
32182
|
);
|
|
31499
|
-
const selectedPath = opts.path ?
|
|
32183
|
+
const selectedPath = opts.path ? import_node_path70.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
31500
32184
|
if (!selectedPath) {
|
|
31501
32185
|
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
31502
32186
|
}
|
|
@@ -31505,7 +32189,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
31505
32189
|
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
31506
32190
|
);
|
|
31507
32191
|
}
|
|
31508
|
-
if (
|
|
32192
|
+
if (import_node_fs57.default.existsSync(selectedPath)) {
|
|
31509
32193
|
const registered = worktrees.find(
|
|
31510
32194
|
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
31511
32195
|
);
|
|
@@ -31524,11 +32208,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
31524
32208
|
return selectedPath;
|
|
31525
32209
|
}
|
|
31526
32210
|
if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
|
|
31527
|
-
const add = (0,
|
|
32211
|
+
const add = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
|
|
31528
32212
|
encoding: "utf-8"
|
|
31529
32213
|
});
|
|
31530
32214
|
if (add.status !== 0) {
|
|
31531
|
-
const addNew = (0,
|
|
32215
|
+
const addNew = (0, import_node_child_process41.spawnSync)(
|
|
31532
32216
|
"git",
|
|
31533
32217
|
["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
|
|
31534
32218
|
{ encoding: "utf-8" }
|
|
@@ -31596,43 +32280,43 @@ function workspaceLs(repoPath) {
|
|
|
31596
32280
|
};
|
|
31597
32281
|
});
|
|
31598
32282
|
}
|
|
31599
|
-
async function mutateWorkspaceRuntime(action2,
|
|
31600
|
-
const devpods = listDevpodWorkspaces(
|
|
31601
|
-
assertDevpodTargetSafe(
|
|
31602
|
-
const devpodId =
|
|
32283
|
+
async function mutateWorkspaceRuntime(action2, resolved2, worktrees, quiet = false) {
|
|
32284
|
+
const devpods = listDevpodWorkspaces(resolved2.worktreePath);
|
|
32285
|
+
assertDevpodTargetSafe(resolved2, worktrees, devpods);
|
|
32286
|
+
const devpodId = resolved2.record?.devpodId ?? resolved2.workspace;
|
|
31603
32287
|
claimLifecycleEffect();
|
|
31604
|
-
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId,
|
|
31605
|
-
if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(
|
|
32288
|
+
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved2.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved2.worktreePath);
|
|
32289
|
+
if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved2.worktreePath, resolved2.workspace)) {
|
|
31606
32290
|
throw new Error(
|
|
31607
32291
|
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
31608
32292
|
);
|
|
31609
32293
|
}
|
|
31610
|
-
const retained = action2 === "stop" ? readManagedRuntimeState(
|
|
32294
|
+
const retained = action2 === "stop" ? readManagedRuntimeState(resolved2.worktreePath, resolved2.workspace) : void 0;
|
|
31611
32295
|
let absent = mutation.status === "proven-absent";
|
|
31612
32296
|
const remove = () => {
|
|
31613
32297
|
if (retained?.stopBaseline) {
|
|
31614
32298
|
absent = proveManagedStop(retained).status === "proven-absent";
|
|
31615
|
-
if (absent && listRoutesForWorktreePath(
|
|
31616
|
-
(route) => route.workspace !==
|
|
32299
|
+
if (absent && listRoutesForWorktreePath(resolved2.worktreePath).some(
|
|
32300
|
+
(route) => route.workspace !== resolved2.workspace || !retained.desired.apps.includes(route.name)
|
|
31617
32301
|
))
|
|
31618
32302
|
throw new Error("Absent stop contains routes outside the retained desired set.");
|
|
31619
32303
|
}
|
|
31620
32304
|
if (absent && !retained) {
|
|
31621
|
-
if (listRoutesForWorktreePath(
|
|
32305
|
+
if (listRoutesForWorktreePath(resolved2.worktreePath).length)
|
|
31622
32306
|
throw new Error("Initial managed stop observed remaining workspace routes.");
|
|
31623
32307
|
return [];
|
|
31624
32308
|
}
|
|
31625
32309
|
claimLifecycleEffect();
|
|
31626
|
-
return removeWorkspaceRoutesForWorktree(
|
|
32310
|
+
return removeWorkspaceRoutesForWorktree(resolved2.workspace, resolved2.worktreePath);
|
|
31627
32311
|
};
|
|
31628
|
-
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes",
|
|
32312
|
+
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved2.worktreePath, remove) : remove();
|
|
31629
32313
|
await ensureTraefikRoutesRemoved([
|
|
31630
32314
|
...routes,
|
|
31631
32315
|
...absent && retained ? managedStopRouteReferences(retained) : []
|
|
31632
32316
|
]);
|
|
31633
32317
|
if (!quiet) {
|
|
31634
32318
|
process.stdout.write(
|
|
31635
|
-
`Freed ${routes.length} route(s) for workspace '${
|
|
32319
|
+
`Freed ${routes.length} route(s) for workspace '${resolved2.workspace}'.
|
|
31636
32320
|
`
|
|
31637
32321
|
);
|
|
31638
32322
|
}
|
|
@@ -31641,20 +32325,20 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
|
|
|
31641
32325
|
freedRoutes: routes.length,
|
|
31642
32326
|
providerChanged: mutation.status === "changed",
|
|
31643
32327
|
...absent ? { runtimeAbsent: true } : {},
|
|
31644
|
-
workspace:
|
|
32328
|
+
workspace: resolved2.workspace
|
|
31645
32329
|
};
|
|
31646
32330
|
}
|
|
31647
32331
|
async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
31648
32332
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
31649
32333
|
const worktrees = identifyGitWorktrees(mainRepo);
|
|
31650
32334
|
const records = listWorkspaceOwnership(mainRepo);
|
|
31651
|
-
const
|
|
32335
|
+
const resolved2 = resolveWorkspaceTarget(mainRepo, target, worktrees, records);
|
|
31652
32336
|
if (action2 === "stop") {
|
|
31653
|
-
const stopped = await superviseLifecycle("stop",
|
|
32337
|
+
const stopped = await superviseLifecycle("stop", resolved2.worktreePath, {
|
|
31654
32338
|
quiet: opts.quiet
|
|
31655
32339
|
});
|
|
31656
32340
|
return {
|
|
31657
|
-
workspace:
|
|
32341
|
+
workspace: resolved2.workspace,
|
|
31658
32342
|
devpodId: stopped.devpodId,
|
|
31659
32343
|
freedRoutes: stopped.freedRoutes,
|
|
31660
32344
|
providerChanged: stopped.stopped && !stopped.runtimeAbsent,
|
|
@@ -31664,30 +32348,30 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
31664
32348
|
const operation = async () => {
|
|
31665
32349
|
const removeWorktree = action2 === "down" && !opts.keepWorktree;
|
|
31666
32350
|
if (removeWorktree) {
|
|
31667
|
-
assertFullDownPreflight(mainRepo,
|
|
32351
|
+
assertFullDownPreflight(mainRepo, resolved2);
|
|
31668
32352
|
}
|
|
31669
|
-
const result = await mutateWorkspaceRuntime("delete",
|
|
32353
|
+
const result = await mutateWorkspaceRuntime("delete", resolved2, worktrees, opts.quiet);
|
|
31670
32354
|
if (removeWorktree) {
|
|
31671
|
-
if (
|
|
31672
|
-
const rm = (0,
|
|
32355
|
+
if (resolved2.worktree && !resolved2.worktree.prunable && import_node_fs57.default.existsSync(resolved2.worktreePath)) {
|
|
32356
|
+
const rm = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved2.worktreePath], {
|
|
31673
32357
|
encoding: "utf-8"
|
|
31674
32358
|
});
|
|
31675
32359
|
if (rm.status !== 0) {
|
|
31676
32360
|
const detail = [rm.error?.message, rm.stderr].filter(Boolean).join("\n").trim();
|
|
31677
32361
|
throw new Error(
|
|
31678
|
-
`git worktree remove failed for '${
|
|
32362
|
+
`git worktree remove failed for '${resolved2.worktreePath}': ${detail || "unknown error"}`
|
|
31679
32363
|
);
|
|
31680
32364
|
}
|
|
31681
|
-
process.stdout.write(`Removed worktree ${
|
|
32365
|
+
process.stdout.write(`Removed worktree ${resolved2.worktreePath}.
|
|
31682
32366
|
`);
|
|
31683
32367
|
}
|
|
31684
|
-
if (
|
|
31685
|
-
removeWorkspaceOwnership(mainRepo,
|
|
32368
|
+
if (resolved2.record) {
|
|
32369
|
+
removeWorkspaceOwnership(mainRepo, resolved2.workspace);
|
|
31686
32370
|
}
|
|
31687
32371
|
}
|
|
31688
32372
|
return result;
|
|
31689
32373
|
};
|
|
31690
|
-
return
|
|
32374
|
+
return resolved2.worktree && !resolved2.worktree.prunable && import_node_fs57.default.existsSync(resolved2.worktreePath) ? withLifecycleOperationLock(resolved2.worktreePath, operation) : operation();
|
|
31691
32375
|
}
|
|
31692
32376
|
async function reclaimWorktreeExactPath(mainRepo, worktreePath, verify, afterRemove, steps) {
|
|
31693
32377
|
await withLifecycleOperationLock(worktreePath, async () => {
|
|
@@ -31713,7 +32397,7 @@ async function reclaimWorktreeExactPath(mainRepo, worktreePath, verify, afterRem
|
|
|
31713
32397
|
);
|
|
31714
32398
|
steps.push("runtime-deleted");
|
|
31715
32399
|
}
|
|
31716
|
-
const rm = (0,
|
|
32400
|
+
const rm = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
|
|
31717
32401
|
encoding: "utf-8"
|
|
31718
32402
|
});
|
|
31719
32403
|
if (rm.status !== 0) {
|
|
@@ -31734,13 +32418,13 @@ async function workspaceStop(target, opts = {}) {
|
|
|
31734
32418
|
async function workspaceDown(target, opts = {}) {
|
|
31735
32419
|
return runWorkspaceLifecycle("down", target, opts);
|
|
31736
32420
|
}
|
|
31737
|
-
var
|
|
32421
|
+
var import_node_child_process41, import_node_fs57, import_node_path70, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
31738
32422
|
var init_workspace_lifecycle = __esm({
|
|
31739
32423
|
"src/core/workspace-lifecycle.ts"() {
|
|
31740
32424
|
"use strict";
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
32425
|
+
import_node_child_process41 = require("child_process");
|
|
32426
|
+
import_node_fs57 = __toESM(require("fs"));
|
|
32427
|
+
import_node_path70 = __toESM(require("path"));
|
|
31744
32428
|
init_devpod_mutation();
|
|
31745
32429
|
init_devpod_workspaces();
|
|
31746
32430
|
init_devsy_mutation();
|
|
@@ -31759,7 +32443,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
31759
32443
|
|
|
31760
32444
|
// src/core/worktree-trim.ts
|
|
31761
32445
|
function listIgnoredPaths(worktreePath) {
|
|
31762
|
-
const result = (0,
|
|
32446
|
+
const result = (0, import_node_child_process42.spawnSync)(
|
|
31763
32447
|
"git",
|
|
31764
32448
|
[
|
|
31765
32449
|
"-C",
|
|
@@ -31843,19 +32527,19 @@ function buildTrimManifest(report, patterns, createdAtEpoch) {
|
|
|
31843
32527
|
return canonicalJson(manifest);
|
|
31844
32528
|
}
|
|
31845
32529
|
function isInside(parent, child) {
|
|
31846
|
-
const relative2 =
|
|
31847
|
-
return relative2 !== "" && !relative2.startsWith("..") && !
|
|
32530
|
+
const relative2 = import_node_path71.default.relative(parent, child);
|
|
32531
|
+
return relative2 !== "" && !relative2.startsWith("..") && !import_node_path71.default.isAbsolute(relative2);
|
|
31848
32532
|
}
|
|
31849
32533
|
function exists(file) {
|
|
31850
32534
|
try {
|
|
31851
|
-
|
|
32535
|
+
import_node_fs58.default.lstatSync(file);
|
|
31852
32536
|
return true;
|
|
31853
32537
|
} catch {
|
|
31854
32538
|
return false;
|
|
31855
32539
|
}
|
|
31856
32540
|
}
|
|
31857
32541
|
function hasTrackedFiles(worktreePath, relative2) {
|
|
31858
|
-
const result = (0,
|
|
32542
|
+
const result = (0, import_node_child_process42.spawnSync)(
|
|
31859
32543
|
"git",
|
|
31860
32544
|
["-C", worktreePath, "--literal-pathspecs", "ls-files", "-z", "--", relative2],
|
|
31861
32545
|
{ encoding: "utf-8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } }
|
|
@@ -31863,10 +32547,10 @@ function hasTrackedFiles(worktreePath, relative2) {
|
|
|
31863
32547
|
return result.status !== 0 || Boolean(result.error) || result.stdout.length > 0;
|
|
31864
32548
|
}
|
|
31865
32549
|
function trimOnePath(candidate, relative2, patterns, steps) {
|
|
31866
|
-
const target =
|
|
32550
|
+
const target = import_node_path71.default.join(candidate.path, relative2);
|
|
31867
32551
|
let stat;
|
|
31868
32552
|
try {
|
|
31869
|
-
stat =
|
|
32553
|
+
stat = import_node_fs58.default.lstatSync(target);
|
|
31870
32554
|
} catch (error) {
|
|
31871
32555
|
if (error.code === "ENOENT") {
|
|
31872
32556
|
steps.push(`gone:${relative2}`);
|
|
@@ -31878,7 +32562,7 @@ function trimOnePath(candidate, relative2, patterns, steps) {
|
|
|
31878
32562
|
steps.push(`skipped-symlink:${relative2}`);
|
|
31879
32563
|
return;
|
|
31880
32564
|
}
|
|
31881
|
-
if (!isInside(
|
|
32565
|
+
if (!isInside(import_node_fs58.default.realpathSync.native(candidate.path), import_node_fs58.default.realpathSync.native(target))) {
|
|
31882
32566
|
steps.push(`skipped-outside:${relative2}`);
|
|
31883
32567
|
return;
|
|
31884
32568
|
}
|
|
@@ -31891,7 +32575,7 @@ function trimOnePath(candidate, relative2, patterns, steps) {
|
|
|
31891
32575
|
return;
|
|
31892
32576
|
}
|
|
31893
32577
|
try {
|
|
31894
|
-
|
|
32578
|
+
import_node_fs58.default.rmSync(target, { recursive: true, force: true });
|
|
31895
32579
|
} catch (error) {
|
|
31896
32580
|
steps.push(`partial:${relative2}`);
|
|
31897
32581
|
throw error;
|
|
@@ -31931,7 +32615,7 @@ async function applyTrimManifest(options, dependencies) {
|
|
|
31931
32615
|
continue;
|
|
31932
32616
|
}
|
|
31933
32617
|
const listed = candidate.paths ?? [];
|
|
31934
|
-
if (listed.every((relative2) => !exists(
|
|
32618
|
+
if (listed.every((relative2) => !exists(import_node_path71.default.join(candidate.path, relative2))) && matchingReceipt(candidate, manifestSha256)) {
|
|
31935
32619
|
outcome.status = "already-done";
|
|
31936
32620
|
continue;
|
|
31937
32621
|
}
|
|
@@ -31955,7 +32639,7 @@ async function applyTrimManifest(options, dependencies) {
|
|
|
31955
32639
|
if (!options.yes) {
|
|
31956
32640
|
verify();
|
|
31957
32641
|
outcome.status = "would-trim";
|
|
31958
|
-
const present = listed.filter((relative2) => exists(
|
|
32642
|
+
const present = listed.filter((relative2) => exists(import_node_path71.default.join(candidate.path, relative2)));
|
|
31959
32643
|
outcome.reason = present.length ? `would remove: ${present.join(", ")}` : "nothing present";
|
|
31960
32644
|
continue;
|
|
31961
32645
|
}
|
|
@@ -31992,13 +32676,13 @@ async function applyTrimManifest(options, dependencies) {
|
|
|
31992
32676
|
}
|
|
31993
32677
|
return { manifestSha256, dryRun: !options.yes, targets, stoppedEarly };
|
|
31994
32678
|
}
|
|
31995
|
-
var
|
|
32679
|
+
var import_node_child_process42, import_node_fs58, import_node_path71;
|
|
31996
32680
|
var init_worktree_trim = __esm({
|
|
31997
32681
|
"src/core/worktree-trim.ts"() {
|
|
31998
32682
|
"use strict";
|
|
31999
|
-
|
|
32000
|
-
|
|
32001
|
-
|
|
32683
|
+
import_node_child_process42 = require("child_process");
|
|
32684
|
+
import_node_fs58 = __toESM(require("fs"));
|
|
32685
|
+
import_node_path71 = __toESM(require("path"));
|
|
32002
32686
|
init_reliability_lifecycle();
|
|
32003
32687
|
init_worktree_reclaim();
|
|
32004
32688
|
init_worktree_safety();
|
|
@@ -32012,6 +32696,7 @@ __export(workspace_exports, {
|
|
|
32012
32696
|
runWorkspaceDownCommand: () => runWorkspaceDownCommand,
|
|
32013
32697
|
runWorkspaceGcCommand: () => runWorkspaceGcCommand,
|
|
32014
32698
|
runWorkspaceJournalSettleCommand: () => runWorkspaceJournalSettleCommand,
|
|
32699
|
+
runWorkspaceLeftoverCommand: () => runWorkspaceLeftoverCommand,
|
|
32015
32700
|
runWorkspaceLsCommand: () => runWorkspaceLsCommand,
|
|
32016
32701
|
runWorkspaceManifestHashCommand: () => runWorkspaceManifestHashCommand,
|
|
32017
32702
|
runWorkspaceManifestSelectCommand: () => runWorkspaceManifestSelectCommand,
|
|
@@ -32021,15 +32706,15 @@ __export(workspace_exports, {
|
|
|
32021
32706
|
runWorkspaceUpCommand: () => runWorkspaceUpCommand
|
|
32022
32707
|
});
|
|
32023
32708
|
function resolveGitWorkspaceRepo(repoPath) {
|
|
32024
|
-
const
|
|
32709
|
+
const resolved2 = resolveRepoPath(repoPath);
|
|
32025
32710
|
try {
|
|
32026
|
-
resolveGitCommonDir(
|
|
32711
|
+
resolveGitCommonDir(resolved2);
|
|
32027
32712
|
} catch (error) {
|
|
32028
|
-
throw new Error(`Workspace commands require a Git repository: '${
|
|
32713
|
+
throw new Error(`Workspace commands require a Git repository: '${resolved2}'.`, {
|
|
32029
32714
|
cause: error
|
|
32030
32715
|
});
|
|
32031
32716
|
}
|
|
32032
|
-
return
|
|
32717
|
+
return resolved2;
|
|
32033
32718
|
}
|
|
32034
32719
|
async function runWorkspaceUpCommand(branch, options) {
|
|
32035
32720
|
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
@@ -32061,13 +32746,30 @@ function runWorkspaceLsCommand(options) {
|
|
|
32061
32746
|
);
|
|
32062
32747
|
}
|
|
32063
32748
|
}
|
|
32749
|
+
function writeManifestOutput(manifest, output2) {
|
|
32750
|
+
if (output2) {
|
|
32751
|
+
import_node_fs59.default.writeFileSync(output2, manifest, { flag: "wx" });
|
|
32752
|
+
process.stdout.write(`${sha256Hex(manifest)} ${output2}
|
|
32753
|
+
`);
|
|
32754
|
+
} else {
|
|
32755
|
+
process.stdout.write(manifest);
|
|
32756
|
+
}
|
|
32757
|
+
}
|
|
32064
32758
|
function runWorkspaceCleanupCommand(options) {
|
|
32065
32759
|
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
32760
|
+
if (options.manifest && options.action === "leftover") {
|
|
32761
|
+
const leftovers = findDockerLeftovers(readDockerInventory(), readLeftoverContext(repoPath));
|
|
32762
|
+
writeManifestOutput(
|
|
32763
|
+
buildLeftoverManifest(repoPath, leftovers, Math.floor(Date.now() / 1e3)),
|
|
32764
|
+
options.output
|
|
32765
|
+
);
|
|
32766
|
+
return;
|
|
32767
|
+
}
|
|
32066
32768
|
const report = buildWorkspaceCleanupReport({ ...options, repo: repoPath });
|
|
32067
32769
|
if (options.manifest) {
|
|
32068
32770
|
const action2 = options.action ?? "reclaim";
|
|
32069
32771
|
if (action2 !== "reclaim" && action2 !== "trim") {
|
|
32070
|
-
throw new Error("--action must be reclaim or
|
|
32772
|
+
throw new Error("--action must be reclaim, trim or leftover.");
|
|
32071
32773
|
}
|
|
32072
32774
|
if (action2 === "trim") {
|
|
32073
32775
|
if (!options.allWorktrees)
|
|
@@ -32076,19 +32778,13 @@ function runWorkspaceCleanupCommand(options) {
|
|
|
32076
32778
|
throw new Error("--manifest requires --all-worktrees and --check-merged.");
|
|
32077
32779
|
}
|
|
32078
32780
|
const createdAtEpoch = Math.floor(Date.now() / 1e3);
|
|
32079
|
-
const worktrees =
|
|
32781
|
+
const worktrees = import_node_fs59.default.existsSync(import_node_path72.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
|
|
32080
32782
|
const manifest = action2 === "trim" ? buildTrimManifest(
|
|
32081
32783
|
report,
|
|
32082
32784
|
{ trim: worktrees?.trim ?? [], disposable: worktrees?.disposable },
|
|
32083
32785
|
createdAtEpoch
|
|
32084
32786
|
) : buildReclaimManifest(report, createdAtEpoch);
|
|
32085
|
-
|
|
32086
|
-
import_node_fs58.default.writeFileSync(options.output, manifest, { flag: "wx" });
|
|
32087
|
-
process.stdout.write(`${sha256Hex(manifest)} ${options.output}
|
|
32088
|
-
`);
|
|
32089
|
-
} else {
|
|
32090
|
-
process.stdout.write(manifest);
|
|
32091
|
-
}
|
|
32787
|
+
writeManifestOutput(manifest, options.output);
|
|
32092
32788
|
return;
|
|
32093
32789
|
}
|
|
32094
32790
|
if (options.json) {
|
|
@@ -32153,10 +32849,57 @@ async function runWorkspaceJournalSettleCommand(options) {
|
|
|
32153
32849
|
);
|
|
32154
32850
|
}
|
|
32155
32851
|
function runWorkspaceManifestHashCommand(file) {
|
|
32156
|
-
|
|
32852
|
+
let leftover = false;
|
|
32853
|
+
try {
|
|
32854
|
+
leftover = isLeftoverManifest(JSON.parse(import_node_fs59.default.readFileSync(file, "utf-8")));
|
|
32855
|
+
} catch {
|
|
32856
|
+
}
|
|
32857
|
+
const { raw } = leftover ? readLeftoverManifest(file) : readManifest(file);
|
|
32157
32858
|
process.stdout.write(`${sha256Hex(raw)}
|
|
32158
32859
|
`);
|
|
32159
32860
|
}
|
|
32861
|
+
function runWorkspaceLeftoverCommand(options) {
|
|
32862
|
+
const report = applyLeftoverManifest({
|
|
32863
|
+
manifestFile: options.manifest,
|
|
32864
|
+
sha256: options.sha256,
|
|
32865
|
+
maxAgeSeconds: parseInactiveFor(options.maxAge ?? "24h").seconds,
|
|
32866
|
+
yes: Boolean(options.yes)
|
|
32867
|
+
});
|
|
32868
|
+
if (options.json) {
|
|
32869
|
+
printJSON(report);
|
|
32870
|
+
} else {
|
|
32871
|
+
printReclaimReport(
|
|
32872
|
+
{
|
|
32873
|
+
dryRun: report.dryRun,
|
|
32874
|
+
stoppedEarly: false,
|
|
32875
|
+
targets: report.targets.map((target) => ({
|
|
32876
|
+
...target,
|
|
32877
|
+
path: target.project,
|
|
32878
|
+
steps: summarizeLeftoverSteps(target.steps)
|
|
32879
|
+
}))
|
|
32880
|
+
},
|
|
32881
|
+
"remove the leftovers"
|
|
32882
|
+
);
|
|
32883
|
+
if (!report.dryRun && report.targets.some((target) => target.steps.length)) {
|
|
32884
|
+
process.stdout.write(
|
|
32885
|
+
"Each removed object is listed in the stack's receipt; use --json for the full list.\n"
|
|
32886
|
+
);
|
|
32887
|
+
}
|
|
32888
|
+
}
|
|
32889
|
+
if (report.targets.some((target) => target.status === "failed")) process.exitCode = 1;
|
|
32890
|
+
}
|
|
32891
|
+
function summarizeLeftoverSteps(steps) {
|
|
32892
|
+
const counts = /* @__PURE__ */ new Map();
|
|
32893
|
+
for (const step of steps) {
|
|
32894
|
+
const [action2, kind] = step.slice(0, step.indexOf(":")).split("-");
|
|
32895
|
+
const byKind = counts.get(action2) ?? /* @__PURE__ */ new Map();
|
|
32896
|
+
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
|
|
32897
|
+
counts.set(action2, byKind);
|
|
32898
|
+
}
|
|
32899
|
+
return [...counts].map(
|
|
32900
|
+
([action2, byKind]) => `${action2 === "gone" ? "already gone" : action2} ${[...byKind].map(([kind, count]) => `${count} ${kind}(s)`).join(", ")}`
|
|
32901
|
+
);
|
|
32902
|
+
}
|
|
32160
32903
|
function runWorkspaceManifestSelectCommand(file, options) {
|
|
32161
32904
|
process.stdout.write(selectManifest(file, options.expectedSha256, options.id));
|
|
32162
32905
|
}
|
|
@@ -32205,12 +32948,13 @@ async function runWorkspaceTrimCommand(options) {
|
|
|
32205
32948
|
process.exitCode = 1;
|
|
32206
32949
|
}
|
|
32207
32950
|
}
|
|
32208
|
-
var
|
|
32951
|
+
var import_node_fs59, import_node_path72;
|
|
32209
32952
|
var init_workspace2 = __esm({
|
|
32210
32953
|
"src/commands/workspace.ts"() {
|
|
32211
32954
|
"use strict";
|
|
32212
|
-
|
|
32213
|
-
|
|
32955
|
+
import_node_fs59 = __toESM(require("fs"));
|
|
32956
|
+
import_node_path72 = __toESM(require("path"));
|
|
32957
|
+
init_docker_leftovers();
|
|
32214
32958
|
init_output();
|
|
32215
32959
|
init_repo_config();
|
|
32216
32960
|
init_workspace_cleanup();
|
|
@@ -32262,7 +33006,7 @@ var init_version = __esm({
|
|
|
32262
33006
|
|
|
32263
33007
|
// src/cli.ts
|
|
32264
33008
|
var import_commander = require("commander");
|
|
32265
|
-
var CLI_VERSION = true ? "0.
|
|
33009
|
+
var CLI_VERSION = true ? "0.3.1" : "0.0.0-dev";
|
|
32266
33010
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
32267
33011
|
function withErrorHandling(action2) {
|
|
32268
33012
|
return async (...args) => {
|
|
@@ -32622,7 +33366,7 @@ workspaceCommand2.command("cleanup").description("Report managed workspace activ
|
|
|
32622
33366
|
"Emit a canonical reclaim manifest of quiet RECLAIM trees (needs --all-worktrees --check-merged)"
|
|
32623
33367
|
).option("--output <file>", "Write the manifest to a new file and print its SHA-256").option(
|
|
32624
33368
|
"--action <action>",
|
|
32625
|
-
"Manifest action: reclaim (needs --check-merged)
|
|
33369
|
+
"Manifest action: reclaim (needs --check-merged), trim (declared worktrees.trim paths) or leftover (Docker stacks of deleted checkouts)",
|
|
32626
33370
|
"reclaim"
|
|
32627
33371
|
).action(
|
|
32628
33372
|
withErrorHandling(async (_options, command) => {
|
|
@@ -32691,6 +33435,15 @@ workspaceCommand2.command("trim").description(
|
|
|
32691
33435
|
await runWorkspaceTrimCommand2(options);
|
|
32692
33436
|
})
|
|
32693
33437
|
);
|
|
33438
|
+
workspaceCommand2.command("leftover").description(
|
|
33439
|
+
"Remove the Docker containers, volumes and networks of an approved leftover manifest, re-verifying each stack inside the provider locks"
|
|
33440
|
+
).requiredOption("--manifest <file>", "Approved leftover manifest").requiredOption("--sha256 <hash>", "Approved SHA-256 of the manifest").option("--max-age <duration>", "Refuse a manifest older than this (Ns, Nm, Nh, Nd, Nw)", "24h").option("--yes", "Remove; without it the command is a dry run").option("--json", "Output JSON").action(
|
|
33441
|
+
withErrorHandling(async (_options, command) => {
|
|
33442
|
+
const options = command.opts();
|
|
33443
|
+
const { runWorkspaceLeftoverCommand: runWorkspaceLeftoverCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
33444
|
+
runWorkspaceLeftoverCommand2(options);
|
|
33445
|
+
})
|
|
33446
|
+
);
|
|
32694
33447
|
workspaceCommand2.command("gc").description("Report or clean missing ledger-owned workspace resources").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").option("--yes", "Delete eligible workspace runtime, route, and ownership resources").action(
|
|
32695
33448
|
withErrorHandling(async (_options, command) => {
|
|
32696
33449
|
const options = command.opts();
|
|
@@ -32715,10 +33468,10 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
|
|
|
32715
33468
|
workspaceCommand2.command("journal").description("Inspect and recover workspace lifecycle journals").command("settle").description(
|
|
32716
33469
|
"Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
|
|
32717
33470
|
).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
|
|
32718
|
-
withErrorHandling(async (
|
|
33471
|
+
withErrorHandling(async (path72, _options, command) => {
|
|
32719
33472
|
const options = command.opts();
|
|
32720
33473
|
const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
32721
|
-
await runWorkspaceJournalSettleCommand2({ ...
|
|
33474
|
+
await runWorkspaceJournalSettleCommand2({ ...path72 ? { path: path72 } : {}, ...options });
|
|
32722
33475
|
})
|
|
32723
33476
|
);
|
|
32724
33477
|
function parseVersionRequest(argv) {
|