@devrouter/cli 0.0.29 → 0.0.31
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 +48 -11
- package/bin/devrouter-process +274 -0
- package/dist/devrouter.js +1107 -288
- package/package.json +7 -3
- package/upgrade-prompts/0.0.30.md +13 -0
- package/upgrade-prompts/0.0.31.md +23 -0
package/dist/devrouter.js
CHANGED
|
@@ -235,13 +235,15 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
|
|
|
235
235
|
|
|
236
236
|
## Workspace isolation (parallel git worktrees / agents)
|
|
237
237
|
|
|
238
|
-
Run several worktrees of one repo in parallel without host/route collisions. A **workspace token**
|
|
238
|
+
Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the DevPod id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
|
|
239
239
|
|
|
240
|
-
- **Identity**: each linked worktree stores
|
|
240
|
+
- **Identity**: each managed linked worktree stores a local token in Git metadata plus a durable owner record in the repository's Git common directory. The record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path slug. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Ambiguous identities fail closed. The primary checkout remains non-namespaced.
|
|
241
241
|
- **When active**: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`\${WORKSPACE}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
|
|
242
242
|
- **TLS**: namespaced hosts (\`web.<ws>.localhost\`) are not covered by the \`*.localhost\` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.
|
|
243
243
|
- **devcontainer integration**: managed scaffolds list the base compose file, then \`\${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}\`; custom repositories may keep another default overlay. Selecting \`.devcontainer/docker-compose.devrouter.yml\` for linked worktrees must pass \`WORKSPACE\` and \`DEVROUTER_WORKSPACE\` across the combined base/overlay config and bind-mount \`\${DEVROUTER_GIT_COMMON_DIR}\` to the same absolute app-container path. The app exposes \`\${WORKSPACE}-app\`; the proxy uses \`upstream: \${WORKSPACE}-app:<port>\`.
|
|
244
|
-
- **Lifecycle**: \`
|
|
244
|
+
- **Lifecycle**: \`workspace up\` creates and starts a worktree; \`workspace ensure .\` starts/reconciles an existing linked worktree; \`workspace ls\` reports owner/Git/DevPod/route state; \`workspace stop\` stops DevPod/routes but keeps checkout, record, and data; full \`workspace down\` deletes runtime/routes and removes only a clean, unlocked worktree, then its record; \`down --keep-worktree\` keeps checkout and record. Dirty or locked full down fails before side effects.
|
|
245
|
+
- **Cleanup**: owner status is \`present\`, \`missing\`, \`locked\`, or \`conflict\`. \`workspace gc\` is a dry-run report; \`--yes\` deletes only exact ledger-owned missing/prunable resources and their records. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
|
|
246
|
+
- **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
|
|
245
247
|
|
|
246
248
|
## Secret manager interop (Infisical/Doppler)
|
|
247
249
|
|
|
@@ -312,8 +314,10 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
312
314
|
- \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
|
|
313
315
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
314
316
|
- \`devrouter workspace ensure [path] [--open]\`: start/reconcile and prove an existing linked worktree environment
|
|
315
|
-
- \`devrouter workspace ls [--json]\`: list
|
|
316
|
-
- \`devrouter workspace
|
|
317
|
+
- \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
|
|
318
|
+
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
319
|
+
- \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
|
|
320
|
+
- \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
|
|
317
321
|
|
|
318
322
|
## Validation workflow
|
|
319
323
|
|
|
@@ -1787,7 +1791,7 @@ function loadRepoConfig(repoPath) {
|
|
|
1787
1791
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
1788
1792
|
const requiredVersion = config.devrouter?.version;
|
|
1789
1793
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
1790
|
-
const cliVersion = true ? "0.0.
|
|
1794
|
+
const cliVersion = true ? "0.0.31" : "0.0.0-dev";
|
|
1791
1795
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
1792
1796
|
hasWarnedVersionMismatch = true;
|
|
1793
1797
|
process.stderr.write(
|
|
@@ -2287,11 +2291,13 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2287
2291
|
"- `envMap` on dependency references (config-level) aliases per-dep vars after dependency env resolution. `envMap` fails fast when source var is missing.",
|
|
2288
2292
|
"",
|
|
2289
2293
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
2290
|
-
`- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each linked worktree
|
|
2291
|
-
"- First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename
|
|
2294
|
+
`- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each managed linked worktree has a local Git token plus a durable owner record in the repository's Git common directory spanning the DevPod id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
|
|
2295
|
+
"- The owner record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Ambiguous identities fail closed. The primary checkout stays non-namespaced.",
|
|
2292
2296
|
`- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
|
|
2293
2297
|
"- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
|
|
2294
|
-
"- Lifecycle: `
|
|
2298
|
+
"- Lifecycle: `workspace up` creates and starts a worktree; `workspace ensure .` starts/reconciles an existing linked worktree; `workspace ls` reports owner/Git/DevPod/route state; `workspace stop` preserves checkout, record, and data; full `workspace down` deletes runtime/routes and removes only a clean, unlocked worktree; `down --keep-worktree` retains checkout and record.",
|
|
2299
|
+
"- Owner status is `present`, `missing`, `locked`, or `conflict`. Dirty or locked full down fails before side effects. `workspace gc` is a dry run; only `--yes` deletes exact eligible missing resources and records, never Git worktrees, branches, or prune state.",
|
|
2300
|
+
"- Workspace commands require Git. Normal config, app, status, and doctor flows work from a `.devrouter.yml` folder without `.git`. Git has no worktree-removal hook; use `workspace ls`, doctor, or dry-run GC after out-of-band removal.",
|
|
2295
2301
|
"- devcontainer integration: `devcontainer.json` lists the base compose file then `${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}`. The default overlay contains `services: {}`; `.devcontainer/docker-compose.devrouter.yml` passes `WORKSPACE` and `DEVROUTER_WORKSPACE` into the app and bind-mounts `${DEVROUTER_GIT_COMMON_DIR}` to the same absolute app-container path. Ensure proves exact DevPod ownership, overlay/Git mounts, env, aliases, health, Git, HTTP route reachability, and unique running TCP upstream ownership before success.",
|
|
2296
2302
|
"",
|
|
2297
2303
|
"Secret Manager Integration (config-based):",
|
|
@@ -2458,11 +2464,19 @@ var init_ai_prompt = __esm({
|
|
|
2458
2464
|
},
|
|
2459
2465
|
{
|
|
2460
2466
|
command: "devrouter workspace ls [--json]",
|
|
2461
|
-
purpose: "List
|
|
2467
|
+
purpose: "List owner, Git, DevPod, route, path, and branch evidence for managed workspaces."
|
|
2462
2468
|
},
|
|
2463
2469
|
{
|
|
2464
|
-
command: "devrouter workspace
|
|
2465
|
-
purpose: "
|
|
2470
|
+
command: "devrouter workspace stop <workspace|branch>",
|
|
2471
|
+
purpose: "Stop the exact DevPod and remove exact routes while preserving the worktree, owner record, and data."
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
command: "devrouter workspace down <workspace|branch> [--keep-worktree]",
|
|
2475
|
+
purpose: "Delete exact runtime/routes and remove only a clean, unlocked worktree and its owner record; --keep-worktree preserves checkout and record."
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
command: "devrouter workspace gc [--json] [--yes]",
|
|
2479
|
+
purpose: "Report missing owners without mutation by default; --yes deletes only exact eligible ledger-owned resources and records, never Git worktrees."
|
|
2466
2480
|
}
|
|
2467
2481
|
];
|
|
2468
2482
|
}
|
|
@@ -3396,12 +3410,6 @@ function reconcileRouteRunConflict(repoPath, app) {
|
|
|
3396
3410
|
}
|
|
3397
3411
|
}
|
|
3398
3412
|
}
|
|
3399
|
-
function isOrphanedWorkspaceProxyRoute(route) {
|
|
3400
|
-
return route.mode === "proxy" && route.workspace !== void 0 && !import_node_fs9.default.existsSync(route.repoPath);
|
|
3401
|
-
}
|
|
3402
|
-
function findOrphanedWorkspaceProxyRoutes(routes = listHostRouteState()) {
|
|
3403
|
-
return routes.filter((route) => isOrphanedWorkspaceProxyRoute(route));
|
|
3404
|
-
}
|
|
3405
3413
|
var import_node_fs9;
|
|
3406
3414
|
var init_route_state = __esm({
|
|
3407
3415
|
"src/core/route-state.ts"() {
|
|
@@ -4064,6 +4072,634 @@ var init_tool_diagnostics = __esm({
|
|
|
4064
4072
|
}
|
|
4065
4073
|
});
|
|
4066
4074
|
|
|
4075
|
+
// src/core/devpod-workspaces.ts
|
|
4076
|
+
function listDevpodWorkspaces() {
|
|
4077
|
+
const result = (0, import_node_child_process6.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
|
|
4078
|
+
if (result.status !== 0) {
|
|
4079
|
+
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4080
|
+
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
4081
|
+
}
|
|
4082
|
+
let parsed;
|
|
4083
|
+
try {
|
|
4084
|
+
parsed = JSON.parse(result.stdout);
|
|
4085
|
+
} catch {
|
|
4086
|
+
throw new Error("devpod list returned invalid JSON.");
|
|
4087
|
+
}
|
|
4088
|
+
if (!Array.isArray(parsed)) {
|
|
4089
|
+
throw new Error("devpod list returned an unexpected response.");
|
|
4090
|
+
}
|
|
4091
|
+
return parsed.map((entry) => {
|
|
4092
|
+
const candidate = entry;
|
|
4093
|
+
if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
|
|
4094
|
+
throw new Error("devpod list returned a workspace without id/source.localFolder.");
|
|
4095
|
+
}
|
|
4096
|
+
return candidate;
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
function inspectDevpodWorkspaceOwnership(workspaces, devpodId, worktreePath) {
|
|
4100
|
+
const idOwners = workspaces.filter((workspace) => workspace.id === devpodId);
|
|
4101
|
+
const pathOwners = workspaces.filter(
|
|
4102
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
4103
|
+
);
|
|
4104
|
+
const exact = idOwners.filter(
|
|
4105
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
4106
|
+
);
|
|
4107
|
+
if (idOwners.length > 1 || pathOwners.length > 1 || idOwners[0] && exact.length === 0 || pathOwners[0] && pathOwners[0].id !== devpodId) {
|
|
4108
|
+
return {
|
|
4109
|
+
status: "conflict",
|
|
4110
|
+
reason: `DevPod '${devpodId}' and worktree '${worktreePath}' do not have one exact owner.`
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
return exact[0] ? { status: "owned", workspace: exact[0] } : { status: "absent" };
|
|
4114
|
+
}
|
|
4115
|
+
function selectDevpodWorkspace(workspaces, repoPath) {
|
|
4116
|
+
const matches = workspaces.filter(
|
|
4117
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
|
|
4118
|
+
);
|
|
4119
|
+
if (matches.length > 1) {
|
|
4120
|
+
throw new Error(
|
|
4121
|
+
`Multiple DevPod workspaces reference '${repoPath}': ${matches.map((match) => match.id).join(", ")}`
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
return matches[0];
|
|
4125
|
+
}
|
|
4126
|
+
function runDevpodWorkspaceAction(action2, devpodId) {
|
|
4127
|
+
const args = action2 === "delete" ? [action2, devpodId, "--ignore-not-found"] : [action2, devpodId];
|
|
4128
|
+
const result = (0, import_node_child_process6.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
4129
|
+
if (result.status !== 0) {
|
|
4130
|
+
const detail = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4131
|
+
throw new Error(`devpod ${action2} failed for '${devpodId}': ${detail || "unknown error"}`);
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
var import_node_child_process6;
|
|
4135
|
+
var init_devpod_workspaces = __esm({
|
|
4136
|
+
"src/core/devpod-workspaces.ts"() {
|
|
4137
|
+
"use strict";
|
|
4138
|
+
import_node_child_process6 = require("child_process");
|
|
4139
|
+
init_workspace();
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
|
|
4143
|
+
// src/core/workspace-ownership.ts
|
|
4144
|
+
function commandError(command, repoPath, stderr) {
|
|
4145
|
+
return new Error(
|
|
4146
|
+
`${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
|
|
4147
|
+
);
|
|
4148
|
+
}
|
|
4149
|
+
function resolveGitCommonDir(repoPath) {
|
|
4150
|
+
const result = (0, import_node_child_process7.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
4151
|
+
encoding: "utf-8"
|
|
4152
|
+
});
|
|
4153
|
+
const output2 = result.stdout.trim();
|
|
4154
|
+
if (result.status !== 0 || !output2) {
|
|
4155
|
+
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
4156
|
+
}
|
|
4157
|
+
return comparableWorkspacePath(import_node_path10.default.isAbsolute(output2) ? output2 : import_node_path10.default.resolve(repoPath, output2));
|
|
4158
|
+
}
|
|
4159
|
+
function listGitWorktrees(repoPath) {
|
|
4160
|
+
const result = (0, import_node_child_process7.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
4161
|
+
encoding: "utf-8"
|
|
4162
|
+
});
|
|
4163
|
+
if (result.status !== 0) {
|
|
4164
|
+
throw commandError("git worktree list", repoPath, result.stderr);
|
|
4165
|
+
}
|
|
4166
|
+
const worktrees = [];
|
|
4167
|
+
let current = {};
|
|
4168
|
+
const finish = () => {
|
|
4169
|
+
if (!current.path) return;
|
|
4170
|
+
worktrees.push({
|
|
4171
|
+
path: comparableWorkspacePath(current.path),
|
|
4172
|
+
branch: current.branch,
|
|
4173
|
+
locked: current.locked ?? false,
|
|
4174
|
+
prunable: current.prunable ?? false
|
|
4175
|
+
});
|
|
4176
|
+
current = {};
|
|
4177
|
+
};
|
|
4178
|
+
for (const line of `${result.stdout}
|
|
4179
|
+
`.split("\n")) {
|
|
4180
|
+
if (line.startsWith("worktree ")) {
|
|
4181
|
+
finish();
|
|
4182
|
+
current.path = line.slice("worktree ".length).trim();
|
|
4183
|
+
} else if (line.startsWith("branch ")) {
|
|
4184
|
+
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
4185
|
+
} else if (line === "locked" || line.startsWith("locked ")) {
|
|
4186
|
+
current.locked = true;
|
|
4187
|
+
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
4188
|
+
current.prunable = true;
|
|
4189
|
+
} else if (line === "") {
|
|
4190
|
+
finish();
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return worktrees;
|
|
4194
|
+
}
|
|
4195
|
+
function ownershipDirectory(repoPath) {
|
|
4196
|
+
return import_node_path10.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
4197
|
+
}
|
|
4198
|
+
function validateWorkspace(value, label) {
|
|
4199
|
+
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
4200
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
4201
|
+
}
|
|
4202
|
+
return value;
|
|
4203
|
+
}
|
|
4204
|
+
function validateTimestamp(value, label) {
|
|
4205
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
4206
|
+
throw new Error(`invalid workspace ownership ${label}`);
|
|
4207
|
+
}
|
|
4208
|
+
return value;
|
|
4209
|
+
}
|
|
4210
|
+
function validateRecord(value, expectedWorkspace) {
|
|
4211
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4212
|
+
throw new Error("invalid workspace ownership record");
|
|
4213
|
+
}
|
|
4214
|
+
const candidate = value;
|
|
4215
|
+
if (candidate.version !== OWNERSHIP_VERSION) {
|
|
4216
|
+
throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
|
|
4217
|
+
}
|
|
4218
|
+
const workspace = validateWorkspace(candidate.workspace, "workspace");
|
|
4219
|
+
if (expectedWorkspace && workspace !== expectedWorkspace) {
|
|
4220
|
+
throw new Error(
|
|
4221
|
+
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path10.default.isAbsolute(candidate.worktreePath)) {
|
|
4225
|
+
throw new Error("invalid workspace ownership worktreePath");
|
|
4226
|
+
}
|
|
4227
|
+
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
4228
|
+
throw new Error("invalid workspace ownership branch");
|
|
4229
|
+
}
|
|
4230
|
+
const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
|
|
4231
|
+
return {
|
|
4232
|
+
version: OWNERSHIP_VERSION,
|
|
4233
|
+
workspace,
|
|
4234
|
+
worktreePath: comparableWorkspacePath(candidate.worktreePath),
|
|
4235
|
+
branch: candidate.branch,
|
|
4236
|
+
devpodId,
|
|
4237
|
+
createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
|
|
4238
|
+
updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function readRecordFile(filePath, expectedWorkspace) {
|
|
4242
|
+
let parsed;
|
|
4243
|
+
try {
|
|
4244
|
+
parsed = JSON.parse(import_node_fs13.default.readFileSync(filePath, "utf-8"));
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
if (error instanceof SyntaxError) {
|
|
4247
|
+
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
4248
|
+
}
|
|
4249
|
+
throw error;
|
|
4250
|
+
}
|
|
4251
|
+
return validateRecord(parsed, expectedWorkspace);
|
|
4252
|
+
}
|
|
4253
|
+
function listWorkspaceOwnership(repoPath) {
|
|
4254
|
+
const directory = ownershipDirectory(repoPath);
|
|
4255
|
+
return listWorkspaceOwnershipInDirectory(directory);
|
|
4256
|
+
}
|
|
4257
|
+
function listWorkspaceOwnershipInDirectory(directory) {
|
|
4258
|
+
let entries;
|
|
4259
|
+
try {
|
|
4260
|
+
entries = import_node_fs13.default.readdirSync(directory, { withFileTypes: true });
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
if (error.code === "ENOENT") return [];
|
|
4263
|
+
throw error;
|
|
4264
|
+
}
|
|
4265
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
4266
|
+
const workspace = entry.name.slice(0, -".json".length);
|
|
4267
|
+
validateWorkspace(workspace, "filename");
|
|
4268
|
+
return readRecordFile(import_node_path10.default.join(directory, entry.name), workspace);
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
4272
|
+
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
4273
|
+
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
4274
|
+
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
4275
|
+
const filePath = import_node_path10.default.join(directory, `${workspace}.json`);
|
|
4276
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4277
|
+
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
4278
|
+
const existing = records.find((record2) => record2.workspace === workspace);
|
|
4279
|
+
if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
|
|
4280
|
+
throw new Error(
|
|
4281
|
+
`Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
4284
|
+
const pathOwner = records.find(
|
|
4285
|
+
(record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
|
|
4286
|
+
);
|
|
4287
|
+
if (pathOwner) {
|
|
4288
|
+
throw new Error(
|
|
4289
|
+
`Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
|
|
4290
|
+
);
|
|
4291
|
+
}
|
|
4292
|
+
if (existing && existing.devpodId !== devpodId) {
|
|
4293
|
+
throw new Error(
|
|
4294
|
+
`Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
const record = {
|
|
4298
|
+
version: OWNERSHIP_VERSION,
|
|
4299
|
+
workspace,
|
|
4300
|
+
worktreePath,
|
|
4301
|
+
branch: input2.branch ?? null,
|
|
4302
|
+
devpodId,
|
|
4303
|
+
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
4304
|
+
updatedAt: validateTimestamp(now, "updatedAt")
|
|
4305
|
+
};
|
|
4306
|
+
const tempPath = `${filePath}.${process.pid}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
|
|
4307
|
+
try {
|
|
4308
|
+
import_node_fs13.default.writeFileSync(tempPath, `${JSON.stringify(record, null, 2)}
|
|
4309
|
+
`, {
|
|
4310
|
+
encoding: "utf-8",
|
|
4311
|
+
flag: "wx"
|
|
4312
|
+
});
|
|
4313
|
+
import_node_fs13.default.renameSync(tempPath, filePath);
|
|
4314
|
+
} finally {
|
|
4315
|
+
import_node_fs13.default.rmSync(tempPath, { force: true });
|
|
4316
|
+
}
|
|
4317
|
+
return record;
|
|
4318
|
+
}
|
|
4319
|
+
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
4320
|
+
const filePath = import_node_path10.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
4321
|
+
try {
|
|
4322
|
+
import_node_fs13.default.rmSync(filePath);
|
|
4323
|
+
return true;
|
|
4324
|
+
} catch (error) {
|
|
4325
|
+
if (error.code === "ENOENT") return false;
|
|
4326
|
+
throw error;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
function sameOwnershipRecord(left, right) {
|
|
4330
|
+
return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
|
|
4331
|
+
}
|
|
4332
|
+
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
4333
|
+
const filePath = import_node_path10.default.join(
|
|
4334
|
+
directory,
|
|
4335
|
+
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
4336
|
+
);
|
|
4337
|
+
let current;
|
|
4338
|
+
try {
|
|
4339
|
+
current = readRecordFile(filePath, expected.workspace);
|
|
4340
|
+
} catch (error) {
|
|
4341
|
+
if (error.code === "ENOENT") return "absent";
|
|
4342
|
+
throw error;
|
|
4343
|
+
}
|
|
4344
|
+
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
4345
|
+
import_node_fs13.default.rmSync(filePath);
|
|
4346
|
+
return "removed";
|
|
4347
|
+
}
|
|
4348
|
+
function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
4349
|
+
const directory = ownershipDirectory(repoPath);
|
|
4350
|
+
import_node_fs13.default.mkdirSync(directory, { recursive: true });
|
|
4351
|
+
return withFileLockSync(
|
|
4352
|
+
import_node_path10.default.join(directory, ".lock"),
|
|
4353
|
+
{ activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
|
|
4354
|
+
() => operation({
|
|
4355
|
+
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
4356
|
+
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
4357
|
+
remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
|
|
4358
|
+
removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
|
|
4359
|
+
})
|
|
4360
|
+
);
|
|
4361
|
+
}
|
|
4362
|
+
function writeWorkspaceOwnership(repoPath, input2) {
|
|
4363
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => transaction.write(input2));
|
|
4364
|
+
}
|
|
4365
|
+
function removeWorkspaceOwnership(repoPath, workspace) {
|
|
4366
|
+
return withWorkspaceOwnershipTransaction(
|
|
4367
|
+
repoPath,
|
|
4368
|
+
(transaction) => transaction.remove(workspace)
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
4371
|
+
function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
4372
|
+
const worktree = worktrees.find(
|
|
4373
|
+
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
4374
|
+
);
|
|
4375
|
+
const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
|
|
4376
|
+
const devpodStatus = devpodOwnership?.status ?? "unknown";
|
|
4377
|
+
if (devpodStatus === "conflict") {
|
|
4378
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4379
|
+
}
|
|
4380
|
+
if (worktree?.locked) {
|
|
4381
|
+
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
4382
|
+
}
|
|
4383
|
+
if ((!worktree || worktree.prunable) && import_node_fs13.default.existsSync(record.worktreePath)) {
|
|
4384
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4385
|
+
}
|
|
4386
|
+
if (!worktree || worktree.prunable) {
|
|
4387
|
+
return { ownerStatus: "missing", devpodStatus, worktree };
|
|
4388
|
+
}
|
|
4389
|
+
let persisted;
|
|
4390
|
+
try {
|
|
4391
|
+
persisted = readPersistedWorkspace(worktree.path);
|
|
4392
|
+
} catch {
|
|
4393
|
+
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4394
|
+
}
|
|
4395
|
+
return {
|
|
4396
|
+
ownerStatus: persisted === record.workspace ? "present" : "conflict",
|
|
4397
|
+
devpodStatus,
|
|
4398
|
+
worktree
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
function listMissingWorkspaceOwnership(repoPath) {
|
|
4402
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
4403
|
+
return listWorkspaceOwnership(repoPath).filter(
|
|
4404
|
+
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
var import_node_child_process7, import_node_crypto3, import_node_fs13, import_node_path10, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
4408
|
+
var init_workspace_ownership = __esm({
|
|
4409
|
+
"src/core/workspace-ownership.ts"() {
|
|
4410
|
+
"use strict";
|
|
4411
|
+
import_node_child_process7 = require("child_process");
|
|
4412
|
+
import_node_crypto3 = require("crypto");
|
|
4413
|
+
import_node_fs13 = __toESM(require("fs"));
|
|
4414
|
+
import_node_path10 = __toESM(require("path"));
|
|
4415
|
+
init_devpod_workspaces();
|
|
4416
|
+
init_file_lock();
|
|
4417
|
+
init_workspace();
|
|
4418
|
+
OWNERSHIP_VERSION = 1;
|
|
4419
|
+
OWNERSHIP_DIR = import_node_path10.default.join("devrouter", "workspaces");
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
4422
|
+
|
|
4423
|
+
// src/core/workspace-gc.ts
|
|
4424
|
+
function routesForOwner(routes, workspace, worktreePath) {
|
|
4425
|
+
return routes.filter(
|
|
4426
|
+
(route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)
|
|
4427
|
+
);
|
|
4428
|
+
}
|
|
4429
|
+
function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
4430
|
+
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
4431
|
+
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
4432
|
+
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
4433
|
+
const localRoot = import_node_path11.default.join(comparableRepo, "trees") + import_node_path11.default.sep;
|
|
4434
|
+
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
4435
|
+
const legacyPrefix = `${import_node_path11.default.basename(comparableRepo)}-`;
|
|
4436
|
+
return import_node_path11.default.dirname(comparableWorktree) === import_node_path11.default.dirname(comparableRepo) && import_node_path11.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
4437
|
+
}
|
|
4438
|
+
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
4439
|
+
const actions = [
|
|
4440
|
+
{
|
|
4441
|
+
resource: "devpod",
|
|
4442
|
+
status: devpodStatus === "owned" ? "would-delete" : "already-absent"
|
|
4443
|
+
},
|
|
4444
|
+
{
|
|
4445
|
+
resource: "routes",
|
|
4446
|
+
status: routeCount > 0 ? "would-delete" : "already-absent",
|
|
4447
|
+
count: routeCount
|
|
4448
|
+
}
|
|
4449
|
+
];
|
|
4450
|
+
if (includeRecord) actions.push({ resource: "record", status: "would-delete" });
|
|
4451
|
+
return actions;
|
|
4452
|
+
}
|
|
4453
|
+
function ownedReason(ownerStatus) {
|
|
4454
|
+
switch (ownerStatus) {
|
|
4455
|
+
case "missing":
|
|
4456
|
+
return "Ownership record is missing from live Git registration or marked prunable.";
|
|
4457
|
+
case "present":
|
|
4458
|
+
return "Registered worktree is present.";
|
|
4459
|
+
case "locked":
|
|
4460
|
+
return "Git worktree is locked and protected from garbage collection.";
|
|
4461
|
+
case "conflict":
|
|
4462
|
+
return "Ownership conflicts with live Git, persisted token, or DevPod source evidence.";
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
function ownedCandidate(record, worktrees, devpods, routes) {
|
|
4466
|
+
const status = inspectWorkspaceOwnership(record, worktrees, devpods);
|
|
4467
|
+
const routeCount = routesForOwner(routes, record.workspace, record.worktreePath).length;
|
|
4468
|
+
const eligible = status.ownerStatus === "missing" && status.devpodStatus !== "conflict";
|
|
4469
|
+
return {
|
|
4470
|
+
kind: "owned",
|
|
4471
|
+
workspace: record.workspace,
|
|
4472
|
+
devpodId: record.devpodId,
|
|
4473
|
+
worktreePath: record.worktreePath,
|
|
4474
|
+
ownerStatus: status.ownerStatus,
|
|
4475
|
+
devpodStatus: status.devpodStatus,
|
|
4476
|
+
routeCount,
|
|
4477
|
+
eligible,
|
|
4478
|
+
reason: ownedReason(status.ownerStatus),
|
|
4479
|
+
actions: eligible ? previewActions(status.devpodStatus, routeCount, true) : [
|
|
4480
|
+
{ resource: "devpod", status: "skipped" },
|
|
4481
|
+
{ resource: "routes", status: "skipped", count: routeCount },
|
|
4482
|
+
{ resource: "record", status: "skipped" }
|
|
4483
|
+
]
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
function legacyCandidates(repoPath, records, worktrees, devpods, routes) {
|
|
4487
|
+
const livePaths = worktrees.map((worktree) => worktree.path);
|
|
4488
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
4489
|
+
const isRecordedRoute = (workspace, worktreePath) => records.some(
|
|
4490
|
+
(record) => record.workspace === workspace && sameWorkspacePath(record.worktreePath, worktreePath)
|
|
4491
|
+
);
|
|
4492
|
+
const isRecordedDevpod = (devpodId, worktreePath) => records.some(
|
|
4493
|
+
(record) => record.devpodId === devpodId && sameWorkspacePath(record.worktreePath, worktreePath)
|
|
4494
|
+
);
|
|
4495
|
+
const key = (workspace, worktreePath) => `${workspace}\0${comparableWorkspacePath(worktreePath)}`;
|
|
4496
|
+
for (const devpod of devpods) {
|
|
4497
|
+
if (isRecordedDevpod(devpod.id, devpod.source.localFolder) || !inRepositoryWorkspaceScope(repoPath, devpod.source.localFolder, livePaths)) {
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
evidence.set(key(devpod.id, devpod.source.localFolder), {
|
|
4501
|
+
workspace: devpod.id,
|
|
4502
|
+
worktreePath: comparableWorkspacePath(devpod.source.localFolder),
|
|
4503
|
+
devpodStatus: "owned",
|
|
4504
|
+
routeCount: 0
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
for (const route of routes) {
|
|
4508
|
+
if (!route.workspace || isRecordedRoute(route.workspace, route.repoPath) || !inRepositoryWorkspaceScope(repoPath, route.repoPath, livePaths)) {
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
const evidenceKey = key(route.workspace, route.repoPath);
|
|
4512
|
+
const existing = evidence.get(evidenceKey);
|
|
4513
|
+
evidence.set(evidenceKey, {
|
|
4514
|
+
workspace: route.workspace,
|
|
4515
|
+
worktreePath: comparableWorkspacePath(route.repoPath),
|
|
4516
|
+
devpodStatus: existing?.devpodStatus ?? "absent",
|
|
4517
|
+
routeCount: (existing?.routeCount ?? 0) + 1
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
return Array.from(evidence.values()).map((entry) => ({
|
|
4521
|
+
kind: "legacy",
|
|
4522
|
+
...entry,
|
|
4523
|
+
eligible: false,
|
|
4524
|
+
reason: "Legacy workspace has no ownership record; use explicit workspace down or manual cleanup.",
|
|
4525
|
+
actions: [
|
|
4526
|
+
{ resource: "devpod", status: "skipped" },
|
|
4527
|
+
{ resource: "routes", status: "skipped", count: entry.routeCount }
|
|
4528
|
+
]
|
|
4529
|
+
}));
|
|
4530
|
+
}
|
|
4531
|
+
function revalidateCandidate(candidate, repoPath, transaction) {
|
|
4532
|
+
const record = transaction.list().find((entry) => entry.workspace === candidate.workspace);
|
|
4533
|
+
if (!record || !sameWorkspacePath(record.worktreePath, candidate.worktreePath) || record.devpodId !== candidate.devpodId) {
|
|
4534
|
+
return {
|
|
4535
|
+
status: "blocked",
|
|
4536
|
+
candidate: {
|
|
4537
|
+
...candidate,
|
|
4538
|
+
eligible: false,
|
|
4539
|
+
reason: "Ownership record disappeared or changed before cleanup.",
|
|
4540
|
+
actions: [
|
|
4541
|
+
{ resource: "devpod", status: "skipped" },
|
|
4542
|
+
{ resource: "routes", status: "skipped", count: candidate.routeCount },
|
|
4543
|
+
{ resource: "record", status: "skipped" }
|
|
4544
|
+
]
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
const fresh = ownedCandidate(
|
|
4549
|
+
record,
|
|
4550
|
+
listGitWorktrees(repoPath),
|
|
4551
|
+
listDevpodWorkspaces(),
|
|
4552
|
+
listHostRouteState()
|
|
4553
|
+
);
|
|
4554
|
+
if (fresh.eligible) return { status: "ready", candidate: fresh, record };
|
|
4555
|
+
return {
|
|
4556
|
+
status: "blocked",
|
|
4557
|
+
candidate: {
|
|
4558
|
+
...fresh,
|
|
4559
|
+
reason: `Ownership changed before cleanup: ${fresh.reason}`
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
function recheckDevpodOwnership(record) {
|
|
4564
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
4565
|
+
listDevpodWorkspaces(),
|
|
4566
|
+
record.devpodId,
|
|
4567
|
+
record.worktreePath
|
|
4568
|
+
);
|
|
4569
|
+
if (ownership.status === "conflict") {
|
|
4570
|
+
throw new Error(ownership.reason);
|
|
4571
|
+
}
|
|
4572
|
+
return ownership.status;
|
|
4573
|
+
}
|
|
4574
|
+
function failedAction(resource, error) {
|
|
4575
|
+
return {
|
|
4576
|
+
resource,
|
|
4577
|
+
status: "failed",
|
|
4578
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
function applyCandidate(candidate, repoPath) {
|
|
4582
|
+
try {
|
|
4583
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
|
|
4584
|
+
let revalidated;
|
|
4585
|
+
try {
|
|
4586
|
+
revalidated = revalidateCandidate(candidate, repoPath, transaction);
|
|
4587
|
+
} catch (error) {
|
|
4588
|
+
return {
|
|
4589
|
+
...candidate,
|
|
4590
|
+
actions: [
|
|
4591
|
+
failedAction(
|
|
4592
|
+
"record",
|
|
4593
|
+
`Ownership revalidation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4594
|
+
)
|
|
4595
|
+
]
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
if (revalidated.status === "blocked") return revalidated.candidate;
|
|
4599
|
+
const { candidate: fresh, record } = revalidated;
|
|
4600
|
+
const actions = [];
|
|
4601
|
+
let devpodStatus;
|
|
4602
|
+
try {
|
|
4603
|
+
devpodStatus = recheckDevpodOwnership(record);
|
|
4604
|
+
if (devpodStatus === "owned") runDevpodWorkspaceAction("delete", record.devpodId);
|
|
4605
|
+
} catch (error) {
|
|
4606
|
+
return { ...fresh, actions: [failedAction("devpod", error)] };
|
|
4607
|
+
}
|
|
4608
|
+
actions.push({
|
|
4609
|
+
resource: "devpod",
|
|
4610
|
+
status: devpodStatus === "owned" ? "deleted" : "already-absent"
|
|
4611
|
+
});
|
|
4612
|
+
try {
|
|
4613
|
+
const routes = removeWorkspaceRoutesForWorktree(fresh.workspace, fresh.worktreePath);
|
|
4614
|
+
actions.push({
|
|
4615
|
+
resource: "routes",
|
|
4616
|
+
status: routes.length > 0 ? "deleted" : "already-absent",
|
|
4617
|
+
count: routes.length
|
|
4618
|
+
});
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
return { ...fresh, actions: [...actions, failedAction("routes", error)] };
|
|
4621
|
+
}
|
|
4622
|
+
try {
|
|
4623
|
+
const removal = transaction.removeIfMatches(record);
|
|
4624
|
+
if (removal === "changed") {
|
|
4625
|
+
throw new Error("Ownership record changed during cleanup; record was retained.");
|
|
4626
|
+
}
|
|
4627
|
+
actions.push({
|
|
4628
|
+
resource: "record",
|
|
4629
|
+
status: removal === "removed" ? "deleted" : "already-absent"
|
|
4630
|
+
});
|
|
4631
|
+
} catch (error) {
|
|
4632
|
+
return { ...fresh, actions: [...actions, failedAction("record", error)] };
|
|
4633
|
+
}
|
|
4634
|
+
return { ...fresh, actions };
|
|
4635
|
+
});
|
|
4636
|
+
} catch (error) {
|
|
4637
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4638
|
+
return {
|
|
4639
|
+
...candidate,
|
|
4640
|
+
actions: [failedAction("record", `Ownership transaction failed: ${detail}`)]
|
|
4641
|
+
};
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
function summarizeGc(mode, candidates) {
|
|
4645
|
+
return {
|
|
4646
|
+
total: candidates.length,
|
|
4647
|
+
eligible: candidates.filter((candidate) => candidate.eligible).length,
|
|
4648
|
+
cleaned: mode === "apply" ? candidates.filter(
|
|
4649
|
+
(candidate) => candidate.actions.some(
|
|
4650
|
+
(action2) => action2.resource === "record" && (action2.status === "deleted" || action2.status === "already-absent")
|
|
4651
|
+
)
|
|
4652
|
+
).length : 0,
|
|
4653
|
+
blocked: candidates.filter((candidate) => !candidate.eligible).length,
|
|
4654
|
+
errors: candidates.filter(
|
|
4655
|
+
(candidate) => candidate.actions.some((action2) => action2.status === "failed")
|
|
4656
|
+
).length
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
function inspectWorkspaceGc(repo) {
|
|
4660
|
+
const repoPath = comparableWorkspacePath(resolveRepoPath(repo));
|
|
4661
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
4662
|
+
const records = listWorkspaceOwnership(repoPath);
|
|
4663
|
+
const devpods = listDevpodWorkspaces();
|
|
4664
|
+
const routes = listHostRouteState();
|
|
4665
|
+
const candidates = [
|
|
4666
|
+
...records.map((record) => ownedCandidate(record, worktrees, devpods, routes)),
|
|
4667
|
+
...legacyCandidates(repoPath, records, worktrees, devpods, routes)
|
|
4668
|
+
];
|
|
4669
|
+
return {
|
|
4670
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4671
|
+
repoPath,
|
|
4672
|
+
mode: "dry-run",
|
|
4673
|
+
summary: summarizeGc("dry-run", candidates),
|
|
4674
|
+
candidates
|
|
4675
|
+
};
|
|
4676
|
+
}
|
|
4677
|
+
function applyWorkspaceGc(plan) {
|
|
4678
|
+
const candidates = plan.candidates.map(
|
|
4679
|
+
(candidate) => candidate.eligible ? applyCandidate(candidate, plan.repoPath) : candidate
|
|
4680
|
+
);
|
|
4681
|
+
return {
|
|
4682
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4683
|
+
repoPath: plan.repoPath,
|
|
4684
|
+
mode: "apply",
|
|
4685
|
+
summary: summarizeGc("apply", candidates),
|
|
4686
|
+
candidates
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
var import_node_path11;
|
|
4690
|
+
var init_workspace_gc = __esm({
|
|
4691
|
+
"src/core/workspace-gc.ts"() {
|
|
4692
|
+
"use strict";
|
|
4693
|
+
import_node_path11 = __toESM(require("path"));
|
|
4694
|
+
init_devpod_workspaces();
|
|
4695
|
+
init_host_routes();
|
|
4696
|
+
init_repo_config();
|
|
4697
|
+
init_route_state();
|
|
4698
|
+
init_workspace();
|
|
4699
|
+
init_workspace_ownership();
|
|
4700
|
+
}
|
|
4701
|
+
});
|
|
4702
|
+
|
|
4067
4703
|
// src/core/doctor.ts
|
|
4068
4704
|
function asRecord2(value) {
|
|
4069
4705
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4123,11 +4759,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
4123
4759
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
4124
4760
|
continue;
|
|
4125
4761
|
}
|
|
4126
|
-
if (!
|
|
4762
|
+
if (!import_node_fs14.default.existsSync(absolutePath)) {
|
|
4127
4763
|
continue;
|
|
4128
4764
|
}
|
|
4129
4765
|
try {
|
|
4130
|
-
const raw =
|
|
4766
|
+
const raw = import_node_fs14.default.readFileSync(absolutePath, "utf-8");
|
|
4131
4767
|
const parsed = import_yaml4.default.parse(raw);
|
|
4132
4768
|
const root = asRecord2(parsed);
|
|
4133
4769
|
const services = asRecord2(root?.services);
|
|
@@ -4390,7 +5026,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
4390
5026
|
const config = runtimeConfig.config;
|
|
4391
5027
|
loadedConfig = config;
|
|
4392
5028
|
loadedWorkspace = runtimeConfig.workspace;
|
|
4393
|
-
const cliVersion = true ? "0.0.
|
|
5029
|
+
const cliVersion = true ? "0.0.31" : "0.0.0-dev";
|
|
4394
5030
|
const configVersion = config.devrouter?.version;
|
|
4395
5031
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
4396
5032
|
addCheck(checks, {
|
|
@@ -4420,9 +5056,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
4420
5056
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
4421
5057
|
app: app.name,
|
|
4422
5058
|
filePath,
|
|
4423
|
-
absolutePath:
|
|
5059
|
+
absolutePath: import_node_path12.default.resolve(repo.path, filePath)
|
|
4424
5060
|
}))
|
|
4425
|
-
).filter((entry) => !
|
|
5061
|
+
).filter((entry) => !import_node_fs14.default.existsSync(entry.absolutePath));
|
|
4426
5062
|
addCheck(checks, {
|
|
4427
5063
|
id: "repo.compose-files",
|
|
4428
5064
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -4465,8 +5101,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
4465
5101
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
4466
5102
|
app: app.name,
|
|
4467
5103
|
cwd: app.hostRun.cwd,
|
|
4468
|
-
absolutePath:
|
|
4469
|
-
})).filter((entry) => !
|
|
5104
|
+
absolutePath: import_node_path12.default.resolve(repo.path, app.hostRun.cwd)
|
|
5105
|
+
})).filter((entry) => !import_node_fs14.default.existsSync(entry.absolutePath));
|
|
4470
5106
|
addCheck(checks, {
|
|
4471
5107
|
id: "repo.host-cwd",
|
|
4472
5108
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -4569,13 +5205,37 @@ async function buildDoctorReport(options = {}) {
|
|
|
4569
5205
|
summary: staleCount === 0 ? "No stale host route entries detected." : `${staleCount} stale host route entr${staleCount === 1 ? "y" : "ies"} detected (dead PID).`,
|
|
4570
5206
|
suggestion: staleCount === 0 ? void 0 : "Re-run the affected host app(s) or remove routes with: dev app rm <name> --repo <path> --keep-config"
|
|
4571
5207
|
});
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
5208
|
+
let gitWorkspaceRepo = false;
|
|
5209
|
+
try {
|
|
5210
|
+
resolveGitCommonDir(resolvedRepoPath);
|
|
5211
|
+
gitWorkspaceRepo = true;
|
|
5212
|
+
} catch {
|
|
5213
|
+
}
|
|
5214
|
+
if (gitWorkspaceRepo) {
|
|
5215
|
+
try {
|
|
5216
|
+
const gc = inspectWorkspaceGc(resolvedRepoPath);
|
|
5217
|
+
const needsAttention = gc.candidates.filter(
|
|
5218
|
+
(candidate) => candidate.eligible || candidate.kind === "legacy" || candidate.ownerStatus === "conflict"
|
|
5219
|
+
);
|
|
5220
|
+
addCheck(checks, {
|
|
5221
|
+
id: "workspace.ownership-cleanup",
|
|
5222
|
+
level: needsAttention.length === 0 ? "ok" : "warn",
|
|
5223
|
+
summary: needsAttention.length === 0 ? "No missing or legacy workspace ownership detected." : `${needsAttention.length} workspace ownership entr${needsAttention.length === 1 ? "y needs" : "ies need"} explicit cleanup review.`,
|
|
5224
|
+
details: needsAttention.length === 0 ? void 0 : needsAttention.map(
|
|
5225
|
+
(candidate) => `${candidate.workspace}: ${candidate.worktreePath} (${candidate.reason})`
|
|
5226
|
+
).join("\n"),
|
|
5227
|
+
suggestion: needsAttention.length === 0 ? void 0 : `Run: dev workspace gc --repo ${resolvedRepoPath}`
|
|
5228
|
+
});
|
|
5229
|
+
} catch (error) {
|
|
5230
|
+
addCheck(checks, {
|
|
5231
|
+
id: "workspace.ownership-cleanup",
|
|
5232
|
+
level: "warn",
|
|
5233
|
+
summary: "Could not inspect workspace ownership cleanup state.",
|
|
5234
|
+
details: error instanceof Error ? error.message : String(error),
|
|
5235
|
+
suggestion: `Run: dev workspace gc --repo ${resolvedRepoPath}`
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
4579
5239
|
const summary = collectSummary(checks);
|
|
4580
5240
|
const nextSteps = collectNextSteps(checks, statusNextSteps);
|
|
4581
5241
|
return {
|
|
@@ -4586,12 +5246,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
4586
5246
|
nextSteps
|
|
4587
5247
|
};
|
|
4588
5248
|
}
|
|
4589
|
-
var
|
|
5249
|
+
var import_node_fs14, import_node_path12, import_yaml4, POSTGRES_DEFAULTS;
|
|
4590
5250
|
var init_doctor = __esm({
|
|
4591
5251
|
"src/core/doctor.ts"() {
|
|
4592
5252
|
"use strict";
|
|
4593
|
-
|
|
4594
|
-
|
|
5253
|
+
import_node_fs14 = __toESM(require("fs"));
|
|
5254
|
+
import_node_path12 = __toESM(require("path"));
|
|
4595
5255
|
import_yaml4 = __toESM(require("yaml"));
|
|
4596
5256
|
init_docker();
|
|
4597
5257
|
init_host_routes();
|
|
@@ -4604,6 +5264,8 @@ var init_doctor = __esm({
|
|
|
4604
5264
|
init_status();
|
|
4605
5265
|
init_tls();
|
|
4606
5266
|
init_tool_diagnostics();
|
|
5267
|
+
init_workspace_gc();
|
|
5268
|
+
init_workspace_ownership();
|
|
4607
5269
|
POSTGRES_DEFAULTS = {
|
|
4608
5270
|
POSTGRES_USER: "prisma",
|
|
4609
5271
|
POSTGRES_PASSWORD: "prisma",
|
|
@@ -4840,12 +5502,12 @@ function parseSsPortListeners(stdout, port) {
|
|
|
4840
5502
|
return listeners;
|
|
4841
5503
|
}
|
|
4842
5504
|
function findPortListeners(port) {
|
|
4843
|
-
const result = (0,
|
|
5505
|
+
const result = (0, import_node_child_process8.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
4844
5506
|
encoding: "utf-8"
|
|
4845
5507
|
});
|
|
4846
5508
|
if (result.error && result.error.code === "ENOENT") {
|
|
4847
5509
|
if (process.platform === "linux") {
|
|
4848
|
-
const ssResult = (0,
|
|
5510
|
+
const ssResult = (0, import_node_child_process8.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
4849
5511
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
4850
5512
|
return parseSsPortListeners(ssResult.stdout, port);
|
|
4851
5513
|
}
|
|
@@ -4874,11 +5536,11 @@ function findPortListeners(port) {
|
|
|
4874
5536
|
};
|
|
4875
5537
|
});
|
|
4876
5538
|
}
|
|
4877
|
-
var
|
|
5539
|
+
var import_node_child_process8;
|
|
4878
5540
|
var init_ports = __esm({
|
|
4879
5541
|
"src/util/ports.ts"() {
|
|
4880
5542
|
"use strict";
|
|
4881
|
-
|
|
5543
|
+
import_node_child_process8 = require("child_process");
|
|
4882
5544
|
}
|
|
4883
5545
|
});
|
|
4884
5546
|
|
|
@@ -5074,7 +5736,7 @@ async function runOpenCommand(name) {
|
|
|
5074
5736
|
);
|
|
5075
5737
|
return;
|
|
5076
5738
|
}
|
|
5077
|
-
const result = (0,
|
|
5739
|
+
const result = (0, import_node_child_process9.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
5078
5740
|
if (result.status !== 0) {
|
|
5079
5741
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
5080
5742
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -5082,11 +5744,11 @@ async function runOpenCommand(name) {
|
|
|
5082
5744
|
process.stdout.write(`Opened ${url}
|
|
5083
5745
|
`);
|
|
5084
5746
|
}
|
|
5085
|
-
var
|
|
5747
|
+
var import_node_child_process9;
|
|
5086
5748
|
var init_open = __esm({
|
|
5087
5749
|
"src/commands/open.ts"() {
|
|
5088
5750
|
"use strict";
|
|
5089
|
-
|
|
5751
|
+
import_node_child_process9 = require("child_process");
|
|
5090
5752
|
init_docker();
|
|
5091
5753
|
init_host_routes();
|
|
5092
5754
|
init_repo_config();
|
|
@@ -5109,7 +5771,7 @@ async function runLogsCommand(options) {
|
|
|
5109
5771
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
5110
5772
|
if (options.follow) {
|
|
5111
5773
|
args.splice(1, 0, "-f");
|
|
5112
|
-
const child = (0,
|
|
5774
|
+
const child = (0, import_node_child_process10.spawn)("docker", args, { stdio: "inherit" });
|
|
5113
5775
|
const onSignal = () => {
|
|
5114
5776
|
child.kill("SIGTERM");
|
|
5115
5777
|
};
|
|
@@ -5123,17 +5785,17 @@ async function runLogsCommand(options) {
|
|
|
5123
5785
|
});
|
|
5124
5786
|
});
|
|
5125
5787
|
} else {
|
|
5126
|
-
const result = (0,
|
|
5788
|
+
const result = (0, import_node_child_process10.spawnSync)("docker", args, { stdio: "inherit" });
|
|
5127
5789
|
if (result.status !== 0) {
|
|
5128
5790
|
throw new Error("Failed to retrieve router logs.");
|
|
5129
5791
|
}
|
|
5130
5792
|
}
|
|
5131
5793
|
}
|
|
5132
|
-
var
|
|
5794
|
+
var import_node_child_process10;
|
|
5133
5795
|
var init_logs = __esm({
|
|
5134
5796
|
"src/commands/logs.ts"() {
|
|
5135
5797
|
"use strict";
|
|
5136
|
-
|
|
5798
|
+
import_node_child_process10 = require("child_process");
|
|
5137
5799
|
init_docker();
|
|
5138
5800
|
init_router();
|
|
5139
5801
|
}
|
|
@@ -5173,17 +5835,17 @@ function asRecord3(value) {
|
|
|
5173
5835
|
return value;
|
|
5174
5836
|
}
|
|
5175
5837
|
function readJson(filePath) {
|
|
5176
|
-
if (!
|
|
5838
|
+
if (!import_node_fs15.default.existsSync(filePath)) {
|
|
5177
5839
|
return void 0;
|
|
5178
5840
|
}
|
|
5179
5841
|
try {
|
|
5180
|
-
return JSON.parse(
|
|
5842
|
+
return JSON.parse(import_node_fs15.default.readFileSync(filePath, "utf-8"));
|
|
5181
5843
|
} catch {
|
|
5182
5844
|
return void 0;
|
|
5183
5845
|
}
|
|
5184
5846
|
}
|
|
5185
5847
|
function relative(repoPath, filePath) {
|
|
5186
|
-
return
|
|
5848
|
+
return import_node_path13.default.relative(repoPath, filePath) || ".";
|
|
5187
5849
|
}
|
|
5188
5850
|
function redactEnvAssignments(value) {
|
|
5189
5851
|
return value.replace(
|
|
@@ -5224,7 +5886,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
5224
5886
|
["bun.lock", "bun"]
|
|
5225
5887
|
];
|
|
5226
5888
|
for (const [fileName, name] of lockfiles) {
|
|
5227
|
-
if (
|
|
5889
|
+
if (import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))) {
|
|
5228
5890
|
return { name, source: fileName };
|
|
5229
5891
|
}
|
|
5230
5892
|
}
|
|
@@ -5239,9 +5901,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
5239
5901
|
if (typeof engines?.node === "string") {
|
|
5240
5902
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
5241
5903
|
}
|
|
5242
|
-
const nvmrc =
|
|
5243
|
-
if (
|
|
5244
|
-
const version =
|
|
5904
|
+
const nvmrc = import_node_path13.default.join(repoPath, ".nvmrc");
|
|
5905
|
+
if (import_node_fs15.default.existsSync(nvmrc)) {
|
|
5906
|
+
const version = import_node_fs15.default.readFileSync(nvmrc, "utf-8").trim();
|
|
5245
5907
|
return { version, source: ".nvmrc" };
|
|
5246
5908
|
}
|
|
5247
5909
|
return void 0;
|
|
@@ -5302,7 +5964,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
5302
5964
|
const files = config.apps.filter(
|
|
5303
5965
|
(app) => app.runtime === "docker"
|
|
5304
5966
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
5305
|
-
(fileName) => !
|
|
5967
|
+
(fileName) => !import_node_path13.default.isAbsolute(fileName) && !import_node_path13.default.normalize(fileName).startsWith("..")
|
|
5306
5968
|
);
|
|
5307
5969
|
return Array.from(new Set(files));
|
|
5308
5970
|
} catch {
|
|
@@ -5320,7 +5982,7 @@ function composeFiles(repoPath) {
|
|
|
5320
5982
|
...configuredComposeFiles(repoPath)
|
|
5321
5983
|
];
|
|
5322
5984
|
return Array.from(new Set(candidates)).filter(
|
|
5323
|
-
(fileName) =>
|
|
5985
|
+
(fileName) => import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))
|
|
5324
5986
|
);
|
|
5325
5987
|
}
|
|
5326
5988
|
function stringArray(value) {
|
|
@@ -5358,7 +6020,7 @@ function inspectServices(repoPath) {
|
|
|
5358
6020
|
const services = [];
|
|
5359
6021
|
for (const fileName of composeFiles(repoPath)) {
|
|
5360
6022
|
try {
|
|
5361
|
-
const parsed = import_yaml5.default.parse(
|
|
6023
|
+
const parsed = import_yaml5.default.parse(import_node_fs15.default.readFileSync(import_node_path13.default.join(repoPath, fileName), "utf-8"));
|
|
5362
6024
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
5363
6025
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
5364
6026
|
const service = asRecord3(value);
|
|
@@ -5393,8 +6055,8 @@ function inspectServices(repoPath) {
|
|
|
5393
6055
|
return services;
|
|
5394
6056
|
}
|
|
5395
6057
|
function inspectEnvFiles(repoPath) {
|
|
5396
|
-
const files =
|
|
5397
|
-
const content =
|
|
6058
|
+
const files = import_node_fs15.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
6059
|
+
const content = import_node_fs15.default.readFileSync(import_node_path13.default.join(repoPath, fileName), "utf-8");
|
|
5398
6060
|
const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
|
|
5399
6061
|
return { path: fileName, names };
|
|
5400
6062
|
});
|
|
@@ -5408,18 +6070,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
5408
6070
|
};
|
|
5409
6071
|
}
|
|
5410
6072
|
function inspectDevcontainer(repoPath) {
|
|
5411
|
-
const dir =
|
|
5412
|
-
if (!
|
|
6073
|
+
const dir = import_node_path13.default.join(repoPath, ".devcontainer");
|
|
6074
|
+
if (!import_node_fs15.default.existsSync(dir)) {
|
|
5413
6075
|
return { exists: false, files: [] };
|
|
5414
6076
|
}
|
|
5415
6077
|
return {
|
|
5416
6078
|
exists: true,
|
|
5417
|
-
files:
|
|
6079
|
+
files: import_node_fs15.default.readdirSync(dir).filter((fileName) => import_node_fs15.default.statSync(import_node_path13.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
5418
6080
|
};
|
|
5419
6081
|
}
|
|
5420
6082
|
function inspectDevrouter(repoPath) {
|
|
5421
6083
|
const configPath = getRepoConfigPath(repoPath);
|
|
5422
|
-
if (!
|
|
6084
|
+
if (!import_node_fs15.default.existsSync(configPath)) {
|
|
5423
6085
|
return {
|
|
5424
6086
|
exists: false,
|
|
5425
6087
|
configPath,
|
|
@@ -5463,15 +6125,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
5463
6125
|
["AGENTS.md", "agents"],
|
|
5464
6126
|
["CLAUDE.md", "claude"]
|
|
5465
6127
|
]) {
|
|
5466
|
-
if (
|
|
6128
|
+
if (import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))) {
|
|
5467
6129
|
results.push({ path: fileName, kind });
|
|
5468
6130
|
}
|
|
5469
6131
|
}
|
|
5470
|
-
const skillsDir =
|
|
5471
|
-
if (
|
|
5472
|
-
for (const name of
|
|
5473
|
-
const skillPath =
|
|
5474
|
-
if (
|
|
6132
|
+
const skillsDir = import_node_path13.default.join(repoPath, ".agents", "skills");
|
|
6133
|
+
if (import_node_fs15.default.existsSync(skillsDir)) {
|
|
6134
|
+
for (const name of import_node_fs15.default.readdirSync(skillsDir).sort()) {
|
|
6135
|
+
const skillPath = import_node_path13.default.join(skillsDir, name, "SKILL.md");
|
|
6136
|
+
if (import_node_fs15.default.existsSync(skillPath)) {
|
|
5475
6137
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
5476
6138
|
}
|
|
5477
6139
|
}
|
|
@@ -5514,7 +6176,7 @@ function buildIssues(report) {
|
|
|
5514
6176
|
}
|
|
5515
6177
|
function inspectRepo(options = {}) {
|
|
5516
6178
|
const repoPath = resolveRepoPath(options.repo);
|
|
5517
|
-
const pkg = readJson(
|
|
6179
|
+
const pkg = readJson(import_node_path13.default.join(repoPath, "package.json"));
|
|
5518
6180
|
const scripts = inspectScripts(pkg);
|
|
5519
6181
|
const reportWithoutIssues = {
|
|
5520
6182
|
repoPath,
|
|
@@ -5533,12 +6195,12 @@ function inspectRepo(options = {}) {
|
|
|
5533
6195
|
issues: buildIssues(reportWithoutIssues)
|
|
5534
6196
|
};
|
|
5535
6197
|
}
|
|
5536
|
-
var
|
|
6198
|
+
var import_node_fs15, import_node_path13, import_yaml5;
|
|
5537
6199
|
var init_repo_inspect = __esm({
|
|
5538
6200
|
"src/core/repo-inspect.ts"() {
|
|
5539
6201
|
"use strict";
|
|
5540
|
-
|
|
5541
|
-
|
|
6202
|
+
import_node_fs15 = __toESM(require("fs"));
|
|
6203
|
+
import_node_path13 = __toESM(require("path"));
|
|
5542
6204
|
import_yaml5 = __toESM(require("yaml"));
|
|
5543
6205
|
init_repo_config();
|
|
5544
6206
|
}
|
|
@@ -5716,7 +6378,7 @@ function requiredFileChecks(repoPath) {
|
|
|
5716
6378
|
".devcontainer/docker-compose.yml",
|
|
5717
6379
|
".devrouter.yml"
|
|
5718
6380
|
];
|
|
5719
|
-
const missing = required.filter((fileName) => !
|
|
6381
|
+
const missing = required.filter((fileName) => !import_node_fs16.default.existsSync(import_node_path14.default.join(repoPath, fileName)));
|
|
5720
6382
|
return {
|
|
5721
6383
|
id: "repo.devcontainer.verify-files",
|
|
5722
6384
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -5781,7 +6443,7 @@ function routeUrl2(host) {
|
|
|
5781
6443
|
return `${isTLSEnabled() ? "https" : "http"}://${host}`;
|
|
5782
6444
|
}
|
|
5783
6445
|
function curlRoute(host) {
|
|
5784
|
-
const result = (0,
|
|
6446
|
+
const result = (0, import_node_child_process11.spawnSync)("curl", ["-k", "-fsS", "--max-time", "5", routeUrl2(host)], {
|
|
5785
6447
|
encoding: "utf-8"
|
|
5786
6448
|
});
|
|
5787
6449
|
if (result.status === 0) {
|
|
@@ -5955,13 +6617,13 @@ async function verifyDevcontainer(options = {}) {
|
|
|
5955
6617
|
nextSteps: collectNextSteps3(checks)
|
|
5956
6618
|
};
|
|
5957
6619
|
}
|
|
5958
|
-
var
|
|
6620
|
+
var import_node_child_process11, import_node_fs16, import_node_path14;
|
|
5959
6621
|
var init_devcontainer_verify = __esm({
|
|
5960
6622
|
"src/core/devcontainer-verify.ts"() {
|
|
5961
6623
|
"use strict";
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
6624
|
+
import_node_child_process11 = require("child_process");
|
|
6625
|
+
import_node_fs16 = __toESM(require("fs"));
|
|
6626
|
+
import_node_path14 = __toESM(require("path"));
|
|
5965
6627
|
init_capabilities();
|
|
5966
6628
|
init_concurrency();
|
|
5967
6629
|
init_doctor();
|
|
@@ -5984,27 +6646,39 @@ function majorVersion(value, fallback) {
|
|
|
5984
6646
|
function devrouterVersion(value) {
|
|
5985
6647
|
return value && /^\d+\.\d+\.\d+$/.test(value) ? value : DEFAULT_DEVROUTER_VERSION;
|
|
5986
6648
|
}
|
|
5987
|
-
function
|
|
6649
|
+
function escapeExtendedRegex(value) {
|
|
6650
|
+
return value.replace(/[\\.^$|?*+()[\]{}]/g, "\\$&");
|
|
6651
|
+
}
|
|
6652
|
+
function inferDevProcess(repo) {
|
|
5988
6653
|
const script = repo.scripts.find((entry) => entry.name === "dev") ?? repo.scripts.find((entry) => entry.name.endsWith(":dev"));
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
6654
|
+
const scriptName = script?.name ?? "dev";
|
|
6655
|
+
return {
|
|
6656
|
+
command: scriptName === "dev" ? "pnpm dev" : `pnpm run -- ${shellSingleQuote(scriptName)}`,
|
|
6657
|
+
match: `pnpm(\\.cjs)? .*${escapeExtendedRegex(scriptName)}`
|
|
6658
|
+
};
|
|
5993
6659
|
}
|
|
5994
6660
|
function inferPort2(repo) {
|
|
5995
6661
|
return repo.apps.find((app) => app.port)?.port ?? 3e3;
|
|
5996
6662
|
}
|
|
5997
|
-
function renderDockerfile(nodeMajor, pnpmVersion) {
|
|
6663
|
+
function renderDockerfile(nodeMajor, pnpmVersion, version) {
|
|
5998
6664
|
const pnpmPackageSpec = `pnpm@${pnpmVersion}`;
|
|
6665
|
+
const devrouterPackageSpec = `@devrouter/cli@${version}`;
|
|
6666
|
+
const devrouterTarball = `devrouter-cli-${version}.tgz`;
|
|
5999
6667
|
return `# ${MANAGED_MARKER}
|
|
6000
6668
|
FROM node:${nodeMajor}-bookworm-slim
|
|
6001
6669
|
|
|
6002
6670
|
RUN apt-get update \\
|
|
6003
|
-
&& apt-get install -y --no-install-recommends git ca-certificates curl procps openssl \\
|
|
6671
|
+
&& apt-get install -y --no-install-recommends git ca-certificates curl procps openssl tar util-linux \\
|
|
6004
6672
|
&& rm -rf /var/lib/apt/lists/*
|
|
6005
6673
|
|
|
6006
6674
|
RUN npm install -g ${shellSingleQuote(pnpmPackageSpec)}
|
|
6007
6675
|
|
|
6676
|
+
RUN npm pack --silent ${shellSingleQuote(devrouterPackageSpec)} \\
|
|
6677
|
+
&& tar -xzf ${shellSingleQuote(devrouterTarball)} --strip-components=2 \\
|
|
6678
|
+
-C /usr/local/bin package/bin/devrouter-process \\
|
|
6679
|
+
&& chmod +x /usr/local/bin/devrouter-process \\
|
|
6680
|
+
&& rm ${shellSingleQuote(devrouterTarball)}
|
|
6681
|
+
|
|
6008
6682
|
WORKDIR /workspaces/app
|
|
6009
6683
|
`;
|
|
6010
6684
|
}
|
|
@@ -6140,7 +6814,7 @@ fi
|
|
|
6140
6814
|
function shellSingleQuote(value) {
|
|
6141
6815
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
6142
6816
|
}
|
|
6143
|
-
function renderPostStart(
|
|
6817
|
+
function renderPostStart(devProcess) {
|
|
6144
6818
|
return `#!/usr/bin/env bash
|
|
6145
6819
|
# ${MANAGED_MARKER}
|
|
6146
6820
|
set -euo pipefail
|
|
@@ -6151,11 +6825,11 @@ set -a
|
|
|
6151
6825
|
. .devcontainer/devcontainer.env
|
|
6152
6826
|
set +a
|
|
6153
6827
|
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6828
|
+
devrouter-process ensure \\
|
|
6829
|
+
--name app \\
|
|
6830
|
+
--match ${shellSingleQuote(devProcess.match)} \\
|
|
6831
|
+
--log /tmp/devrouter-app.log \\
|
|
6832
|
+
-- bash -lc ${shellSingleQuote(devProcess.command)}
|
|
6159
6833
|
`;
|
|
6160
6834
|
}
|
|
6161
6835
|
function renderDevrouter(projectName, port, version) {
|
|
@@ -6268,11 +6942,11 @@ function packageManagerIssues(repo) {
|
|
|
6268
6942
|
}
|
|
6269
6943
|
function plannedFiles(repoPath, version) {
|
|
6270
6944
|
const repo = inspectRepo({ repo: repoPath });
|
|
6271
|
-
const projectName = sanitizeProjectName(
|
|
6945
|
+
const projectName = sanitizeProjectName(import_node_path15.default.basename(repo.repoPath));
|
|
6272
6946
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
6273
6947
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
6274
6948
|
const port = inferPort2(repo);
|
|
6275
|
-
const
|
|
6949
|
+
const devProcess = inferDevProcess(repo);
|
|
6276
6950
|
const issues = packageManagerIssues(repo);
|
|
6277
6951
|
return {
|
|
6278
6952
|
projectName,
|
|
@@ -6280,7 +6954,7 @@ function plannedFiles(repoPath, version) {
|
|
|
6280
6954
|
files: [
|
|
6281
6955
|
{
|
|
6282
6956
|
relativePath: ".devcontainer/Dockerfile",
|
|
6283
|
-
content: renderDockerfile(nodeMajor, pnpmVersion)
|
|
6957
|
+
content: renderDockerfile(nodeMajor, pnpmVersion, version)
|
|
6284
6958
|
},
|
|
6285
6959
|
{ relativePath: ".devcontainer/docker-compose.yml", content: renderCompose(projectName) },
|
|
6286
6960
|
{
|
|
@@ -6304,7 +6978,7 @@ function plannedFiles(repoPath, version) {
|
|
|
6304
6978
|
},
|
|
6305
6979
|
{
|
|
6306
6980
|
relativePath: ".devcontainer/post-start.sh",
|
|
6307
|
-
content: renderPostStart(
|
|
6981
|
+
content: renderPostStart(devProcess),
|
|
6308
6982
|
executable: true
|
|
6309
6983
|
},
|
|
6310
6984
|
{ relativePath: ".devcontainer/README.md", content: renderReadme(projectName) },
|
|
@@ -6313,8 +6987,8 @@ function plannedFiles(repoPath, version) {
|
|
|
6313
6987
|
};
|
|
6314
6988
|
}
|
|
6315
6989
|
function classifyFile(repoPath, file) {
|
|
6316
|
-
const absolutePath =
|
|
6317
|
-
if (!
|
|
6990
|
+
const absolutePath = import_node_path15.default.join(repoPath, file.relativePath);
|
|
6991
|
+
if (!import_node_fs17.default.existsSync(absolutePath)) {
|
|
6318
6992
|
return {
|
|
6319
6993
|
path: file.relativePath,
|
|
6320
6994
|
action: "create",
|
|
@@ -6322,7 +6996,7 @@ function classifyFile(repoPath, file) {
|
|
|
6322
6996
|
bytes: Buffer.byteLength(file.content)
|
|
6323
6997
|
};
|
|
6324
6998
|
}
|
|
6325
|
-
const current =
|
|
6999
|
+
const current = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
|
|
6326
7000
|
if (!current.includes(MANAGED_MARKER)) {
|
|
6327
7001
|
return {
|
|
6328
7002
|
path: file.relativePath,
|
|
@@ -6379,11 +7053,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
6379
7053
|
};
|
|
6380
7054
|
}
|
|
6381
7055
|
function writeFile(repoPath, file) {
|
|
6382
|
-
const absolutePath =
|
|
6383
|
-
|
|
6384
|
-
|
|
7056
|
+
const absolutePath = import_node_path15.default.join(repoPath, file.relativePath);
|
|
7057
|
+
import_node_fs17.default.mkdirSync(import_node_path15.default.dirname(absolutePath), { recursive: true });
|
|
7058
|
+
import_node_fs17.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
6385
7059
|
if (file.executable) {
|
|
6386
|
-
|
|
7060
|
+
import_node_fs17.default.chmodSync(absolutePath, 493);
|
|
6387
7061
|
}
|
|
6388
7062
|
}
|
|
6389
7063
|
function writeDevcontainer(options = {}) {
|
|
@@ -6421,12 +7095,12 @@ function writeDevcontainer(options = {}) {
|
|
|
6421
7095
|
nextSteps: postWriteNextSteps(repoPath)
|
|
6422
7096
|
};
|
|
6423
7097
|
}
|
|
6424
|
-
var
|
|
7098
|
+
var import_node_fs17, import_node_path15, MANAGED_MARKER, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
6425
7099
|
var init_devcontainer_write = __esm({
|
|
6426
7100
|
"src/core/devcontainer-write.ts"() {
|
|
6427
7101
|
"use strict";
|
|
6428
|
-
|
|
6429
|
-
|
|
7102
|
+
import_node_fs17 = __toESM(require("fs"));
|
|
7103
|
+
import_node_path15 = __toESM(require("path"));
|
|
6430
7104
|
init_repo_config();
|
|
6431
7105
|
init_repo_inspect();
|
|
6432
7106
|
init_workspace();
|
|
@@ -6753,7 +7427,7 @@ function sanitizeRouterId(value) {
|
|
|
6753
7427
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
6754
7428
|
}
|
|
6755
7429
|
function repoHash(repoPath) {
|
|
6756
|
-
return (0,
|
|
7430
|
+
return (0, import_node_crypto4.createHash)("sha1").update(import_node_path16.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
6757
7431
|
}
|
|
6758
7432
|
function asDockerApp(app) {
|
|
6759
7433
|
return app.runtime === "docker";
|
|
@@ -6832,11 +7506,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
6832
7506
|
if (dockerApps.length === 0) {
|
|
6833
7507
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
6834
7508
|
}
|
|
6835
|
-
const cachePath =
|
|
6836
|
-
|
|
6837
|
-
const overlayPath =
|
|
7509
|
+
const cachePath = import_node_path16.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
7510
|
+
import_node_fs18.default.mkdirSync(cachePath, { recursive: true });
|
|
7511
|
+
const overlayPath = import_node_path16.default.join(cachePath, "compose.devrouter.yml");
|
|
6838
7512
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
6839
|
-
|
|
7513
|
+
import_node_fs18.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
6840
7514
|
return {
|
|
6841
7515
|
overlayPath,
|
|
6842
7516
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -6850,7 +7524,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
6850
7524
|
fileArgs.push("-f", resolved);
|
|
6851
7525
|
}
|
|
6852
7526
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
6853
|
-
const result = (0,
|
|
7527
|
+
const result = (0, import_node_child_process12.spawnSync)("docker", args, {
|
|
6854
7528
|
encoding: "utf-8",
|
|
6855
7529
|
cwd: repoPath
|
|
6856
7530
|
});
|
|
@@ -6878,7 +7552,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
6878
7552
|
"--services",
|
|
6879
7553
|
...services
|
|
6880
7554
|
];
|
|
6881
|
-
const result = (0,
|
|
7555
|
+
const result = (0, import_node_child_process12.spawnSync)("docker", args, {
|
|
6882
7556
|
encoding: "utf-8",
|
|
6883
7557
|
cwd: repoPath
|
|
6884
7558
|
});
|
|
@@ -6904,7 +7578,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
6904
7578
|
fileArgs.push("-f", resolved);
|
|
6905
7579
|
}
|
|
6906
7580
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
6907
|
-
const result = (0,
|
|
7581
|
+
const result = (0, import_node_child_process12.spawnSync)("docker", args, {
|
|
6908
7582
|
encoding: "utf-8",
|
|
6909
7583
|
cwd: repoPath
|
|
6910
7584
|
});
|
|
@@ -6930,7 +7604,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
6930
7604
|
String(tail),
|
|
6931
7605
|
...services
|
|
6932
7606
|
];
|
|
6933
|
-
(0,
|
|
7607
|
+
(0, import_node_child_process12.spawnSync)("docker", args, {
|
|
6934
7608
|
stdio: "inherit",
|
|
6935
7609
|
cwd: repoPath
|
|
6936
7610
|
});
|
|
@@ -6942,7 +7616,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
6942
7616
|
fileArgs.push("-f", resolved);
|
|
6943
7617
|
}
|
|
6944
7618
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
6945
|
-
const result = (0,
|
|
7619
|
+
const result = (0, import_node_child_process12.spawnSync)("docker", args, {
|
|
6946
7620
|
encoding: "utf-8",
|
|
6947
7621
|
cwd: repoPath
|
|
6948
7622
|
});
|
|
@@ -6956,14 +7630,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
6956
7630
|
const port = Number(match[1]);
|
|
6957
7631
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
6958
7632
|
}
|
|
6959
|
-
var
|
|
7633
|
+
var import_node_child_process12, import_node_crypto4, import_node_fs18, import_node_path16, import_yaml6;
|
|
6960
7634
|
var init_docker_run = __esm({
|
|
6961
7635
|
"src/core/docker-run.ts"() {
|
|
6962
7636
|
"use strict";
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
7637
|
+
import_node_child_process12 = require("child_process");
|
|
7638
|
+
import_node_crypto4 = require("crypto");
|
|
7639
|
+
import_node_fs18 = __toESM(require("fs"));
|
|
7640
|
+
import_node_path16 = __toESM(require("path"));
|
|
6967
7641
|
import_yaml6 = __toESM(require("yaml"));
|
|
6968
7642
|
init_docker_error_guidance();
|
|
6969
7643
|
init_paths();
|
|
@@ -7053,7 +7727,7 @@ function isProcessRunning(pid) {
|
|
|
7053
7727
|
}
|
|
7054
7728
|
}
|
|
7055
7729
|
function readProcessTree(rootPid) {
|
|
7056
|
-
const result = (0,
|
|
7730
|
+
const result = (0, import_node_child_process13.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
7057
7731
|
if (result.status !== 0) {
|
|
7058
7732
|
return [rootPid];
|
|
7059
7733
|
}
|
|
@@ -7164,12 +7838,12 @@ function detectListeningPorts(pids) {
|
|
|
7164
7838
|
if (pids.length === 0) {
|
|
7165
7839
|
return [];
|
|
7166
7840
|
}
|
|
7167
|
-
const result = (0,
|
|
7841
|
+
const result = (0, import_node_child_process13.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
|
|
7168
7842
|
encoding: "utf-8"
|
|
7169
7843
|
});
|
|
7170
7844
|
if (result.error && result.error.code === "ENOENT") {
|
|
7171
7845
|
if (process.platform === "linux") {
|
|
7172
|
-
const ssResult = (0,
|
|
7846
|
+
const ssResult = (0, import_node_child_process13.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
7173
7847
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
7174
7848
|
return parseSsListeningPorts(ssResult.stdout, new Set(pids));
|
|
7175
7849
|
}
|
|
@@ -7228,7 +7902,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
7228
7902
|
app.hostRun.command,
|
|
7229
7903
|
true
|
|
7230
7904
|
) : app.hostRun.command;
|
|
7231
|
-
const child = (0,
|
|
7905
|
+
const child = (0, import_node_child_process13.spawn)(spawnCommand, {
|
|
7232
7906
|
cwd: commandCwd,
|
|
7233
7907
|
stdio: "inherit",
|
|
7234
7908
|
shell: true,
|
|
@@ -7611,7 +8285,7 @@ async function execWithAppEnv(options) {
|
|
|
7611
8285
|
options.command[0],
|
|
7612
8286
|
true
|
|
7613
8287
|
);
|
|
7614
|
-
child = (0,
|
|
8288
|
+
child = (0, import_node_child_process13.spawn)(wrapped, {
|
|
7615
8289
|
cwd: deps.repoPath,
|
|
7616
8290
|
stdio: "inherit",
|
|
7617
8291
|
shell: true,
|
|
@@ -7625,7 +8299,7 @@ async function execWithAppEnv(options) {
|
|
|
7625
8299
|
false
|
|
7626
8300
|
);
|
|
7627
8301
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
7628
|
-
child = (0,
|
|
8302
|
+
child = (0, import_node_child_process13.spawn)(cmd, wrappedArgs, {
|
|
7629
8303
|
cwd: deps.repoPath,
|
|
7630
8304
|
stdio: "inherit",
|
|
7631
8305
|
shell: false,
|
|
@@ -7633,7 +8307,7 @@ async function execWithAppEnv(options) {
|
|
|
7633
8307
|
});
|
|
7634
8308
|
}
|
|
7635
8309
|
} else if (options.shell) {
|
|
7636
|
-
child = (0,
|
|
8310
|
+
child = (0, import_node_child_process13.spawn)(options.command[0], {
|
|
7637
8311
|
cwd: deps.repoPath,
|
|
7638
8312
|
stdio: "inherit",
|
|
7639
8313
|
shell: true,
|
|
@@ -7641,7 +8315,7 @@ async function execWithAppEnv(options) {
|
|
|
7641
8315
|
});
|
|
7642
8316
|
} else {
|
|
7643
8317
|
const [command, ...args] = options.command;
|
|
7644
|
-
child = (0,
|
|
8318
|
+
child = (0, import_node_child_process13.spawn)(command, args, {
|
|
7645
8319
|
cwd: deps.repoPath,
|
|
7646
8320
|
stdio: "inherit",
|
|
7647
8321
|
shell: false,
|
|
@@ -7662,11 +8336,11 @@ async function execWithAppEnv(options) {
|
|
|
7662
8336
|
deps.stopDeps();
|
|
7663
8337
|
}
|
|
7664
8338
|
}
|
|
7665
|
-
var
|
|
8339
|
+
var import_node_child_process13, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
|
|
7666
8340
|
var init_app_run = __esm({
|
|
7667
8341
|
"src/core/app-run.ts"() {
|
|
7668
8342
|
"use strict";
|
|
7669
|
-
|
|
8343
|
+
import_node_child_process13 = require("child_process");
|
|
7670
8344
|
import_node_net = __toESM(require("net"));
|
|
7671
8345
|
import_node_process = require("process");
|
|
7672
8346
|
import_promises = require("readline/promises");
|
|
@@ -7796,47 +8470,13 @@ var init_tls2 = __esm({
|
|
|
7796
8470
|
});
|
|
7797
8471
|
|
|
7798
8472
|
// src/core/workspace-ensure.ts
|
|
7799
|
-
function selectDevpodWorkspace(workspaces, repoPath) {
|
|
7800
|
-
const matches = workspaces.filter(
|
|
7801
|
-
(workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
|
|
7802
|
-
);
|
|
7803
|
-
if (matches.length > 1) {
|
|
7804
|
-
throw new Error(
|
|
7805
|
-
`Multiple DevPod workspaces reference '${repoPath}': ${matches.map((match) => match.id).join(", ")}`
|
|
7806
|
-
);
|
|
7807
|
-
}
|
|
7808
|
-
return matches[0];
|
|
7809
|
-
}
|
|
7810
|
-
function listDevpodWorkspaces() {
|
|
7811
|
-
const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
|
|
7812
|
-
if (result.status !== 0) {
|
|
7813
|
-
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7814
|
-
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
7815
|
-
}
|
|
7816
|
-
let parsed;
|
|
7817
|
-
try {
|
|
7818
|
-
parsed = JSON.parse(result.stdout);
|
|
7819
|
-
} catch {
|
|
7820
|
-
throw new Error("devpod list returned invalid JSON.");
|
|
7821
|
-
}
|
|
7822
|
-
if (!Array.isArray(parsed)) {
|
|
7823
|
-
throw new Error("devpod list returned an unexpected response.");
|
|
7824
|
-
}
|
|
7825
|
-
return parsed.map((entry) => {
|
|
7826
|
-
const candidate = entry;
|
|
7827
|
-
if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
|
|
7828
|
-
throw new Error("devpod list returned a workspace without id/source.localFolder.");
|
|
7829
|
-
}
|
|
7830
|
-
return candidate;
|
|
7831
|
-
});
|
|
7832
|
-
}
|
|
7833
8473
|
function assertOverlay(container, repoPath) {
|
|
7834
8474
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
7835
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
8475
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) {
|
|
7836
8476
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
7837
8477
|
}
|
|
7838
8478
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
7839
|
-
const expectedOverlay =
|
|
8479
|
+
const expectedOverlay = import_node_path17.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
7840
8480
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
7841
8481
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
7842
8482
|
}
|
|
@@ -7854,7 +8494,7 @@ function validateWorkspaceContainers(containers, options) {
|
|
|
7854
8494
|
const owned = containers.filter((container) => {
|
|
7855
8495
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
7856
8496
|
return Boolean(
|
|
7857
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
8497
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(options.repoPath, ".devcontainer"))
|
|
7858
8498
|
);
|
|
7859
8499
|
});
|
|
7860
8500
|
const appContainers = owned.filter(
|
|
@@ -7902,7 +8542,7 @@ function validateWorkspaceContainers(containers, options) {
|
|
|
7902
8542
|
return { id: appContainer.id, workspacePath: repoMount.Destination };
|
|
7903
8543
|
}
|
|
7904
8544
|
function inspectWorkspaceContainers() {
|
|
7905
|
-
const listed = (0,
|
|
8545
|
+
const listed = (0, import_node_child_process14.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
|
|
7906
8546
|
encoding: "utf-8"
|
|
7907
8547
|
});
|
|
7908
8548
|
if (listed.status !== 0) {
|
|
@@ -7914,7 +8554,7 @@ function inspectWorkspaceContainers() {
|
|
|
7914
8554
|
if (ids.length === 0) {
|
|
7915
8555
|
return [];
|
|
7916
8556
|
}
|
|
7917
|
-
const inspected = (0,
|
|
8557
|
+
const inspected = (0, import_node_child_process14.spawnSync)("docker", ["inspect", "--format", SAFE_INSPECT_TEMPLATE, ...ids], {
|
|
7918
8558
|
encoding: "utf-8"
|
|
7919
8559
|
});
|
|
7920
8560
|
if (inspected.status !== 0) {
|
|
@@ -7927,18 +8567,18 @@ function inspectWorkspaceContainers() {
|
|
|
7927
8567
|
function sleep2(ms) {
|
|
7928
8568
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7929
8569
|
}
|
|
7930
|
-
async function waitForContainerPreflight(repoPath,
|
|
8570
|
+
async function waitForContainerPreflight(repoPath, gitCommonDir, workspace, upstreamHosts, timeoutMs) {
|
|
7931
8571
|
const deadline = Date.now() + timeoutMs;
|
|
7932
8572
|
let lastError;
|
|
7933
8573
|
do {
|
|
7934
8574
|
try {
|
|
7935
8575
|
const appContainer = validateWorkspaceContainers(inspectWorkspaceContainers(), {
|
|
7936
8576
|
repoPath,
|
|
7937
|
-
gitCommonDir
|
|
8577
|
+
gitCommonDir,
|
|
7938
8578
|
workspace,
|
|
7939
8579
|
upstreamHosts
|
|
7940
8580
|
});
|
|
7941
|
-
const workspaceEnv = (0,
|
|
8581
|
+
const workspaceEnv = (0, import_node_child_process14.spawnSync)("docker", ["exec", appContainer.id, "printenv", "WORKSPACE"], {
|
|
7942
8582
|
encoding: "utf-8"
|
|
7943
8583
|
});
|
|
7944
8584
|
if (workspaceEnv.status !== 0 || workspaceEnv.stdout.trim() !== workspace) {
|
|
@@ -7946,7 +8586,7 @@ async function waitForContainerPreflight(repoPath, gitCommonDir2, workspace, ups
|
|
|
7946
8586
|
`Workspace app container must expose WORKSPACE='${workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
7947
8587
|
);
|
|
7948
8588
|
}
|
|
7949
|
-
const devrouterWorkspaceEnv = (0,
|
|
8589
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
7950
8590
|
"docker",
|
|
7951
8591
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
7952
8592
|
{ encoding: "utf-8" }
|
|
@@ -7954,7 +8594,7 @@ async function waitForContainerPreflight(repoPath, gitCommonDir2, workspace, ups
|
|
|
7954
8594
|
if (devrouterWorkspaceEnv.status !== 0 || devrouterWorkspaceEnv.stdout.trim() !== workspace) {
|
|
7955
8595
|
throw new Error(`Workspace app container must expose DEVROUTER_WORKSPACE='${workspace}'.`);
|
|
7956
8596
|
}
|
|
7957
|
-
const gitCheck = (0,
|
|
8597
|
+
const gitCheck = (0, import_node_child_process14.spawnSync)(
|
|
7958
8598
|
"docker",
|
|
7959
8599
|
[
|
|
7960
8600
|
"exec",
|
|
@@ -8020,7 +8660,7 @@ async function waitForHttpRoutes(apps, timeoutMs) {
|
|
|
8020
8660
|
const deadline = Date.now() + timeoutMs;
|
|
8021
8661
|
do {
|
|
8022
8662
|
for (const [name, app] of pending) {
|
|
8023
|
-
const result = (0,
|
|
8663
|
+
const result = (0, import_node_child_process14.spawnSync)(
|
|
8024
8664
|
"curl",
|
|
8025
8665
|
[
|
|
8026
8666
|
"-k",
|
|
@@ -8079,17 +8719,6 @@ function resolveIdentity(repoPath) {
|
|
|
8079
8719
|
hadExactDevpod: Boolean(existingDevpod)
|
|
8080
8720
|
};
|
|
8081
8721
|
}
|
|
8082
|
-
function gitCommonDir(repoPath) {
|
|
8083
|
-
const result = (0, import_node_child_process12.spawnSync)(
|
|
8084
|
-
"git",
|
|
8085
|
-
["-C", repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
8086
|
-
{ encoding: "utf-8" }
|
|
8087
|
-
);
|
|
8088
|
-
if (result.status !== 0 || !result.stdout.trim()) {
|
|
8089
|
-
throw new Error(`Could not resolve the Git common directory for '${repoPath}'.`);
|
|
8090
|
-
}
|
|
8091
|
-
return comparableWorkspacePath(result.stdout.trim());
|
|
8092
|
-
}
|
|
8093
8722
|
function startDevpod(repoPath, workspace, commonDir, recreate = false) {
|
|
8094
8723
|
const args = [
|
|
8095
8724
|
"up",
|
|
@@ -8105,7 +8734,7 @@ function startDevpod(repoPath, workspace, commonDir, recreate = false) {
|
|
|
8105
8734
|
if (recreate) {
|
|
8106
8735
|
args.push("--recreate");
|
|
8107
8736
|
}
|
|
8108
|
-
const result = (0,
|
|
8737
|
+
const result = (0, import_node_child_process14.spawnSync)("devpod", args, {
|
|
8109
8738
|
stdio: "inherit",
|
|
8110
8739
|
env: {
|
|
8111
8740
|
...process.env,
|
|
@@ -8127,7 +8756,7 @@ function assertDevpodAttachment(repoPath, workspace) {
|
|
|
8127
8756
|
}
|
|
8128
8757
|
function openUrls(urls) {
|
|
8129
8758
|
for (const url of urls) {
|
|
8130
|
-
const opened = (0,
|
|
8759
|
+
const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
8131
8760
|
if (opened.status !== 0) {
|
|
8132
8761
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
8133
8762
|
`);
|
|
@@ -8139,12 +8768,19 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8139
8768
|
if (!isLinkedWorktree(repoPath)) {
|
|
8140
8769
|
throw new Error(`workspace ensure requires a linked Git worktree (got '${repoPath}').`);
|
|
8141
8770
|
}
|
|
8771
|
+
const missingOwners = listMissingWorkspaceOwnership(repoPath);
|
|
8772
|
+
if (missingOwners.length > 0) {
|
|
8773
|
+
process.stderr.write(
|
|
8774
|
+
`Warning: ${missingOwners.length} managed workspace owner${missingOwners.length === 1 ? " is" : "s are"} missing. Review: dev workspace gc --repo ${repoPath}
|
|
8775
|
+
`
|
|
8776
|
+
);
|
|
8777
|
+
}
|
|
8142
8778
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
8143
8779
|
let environmentStarted = false;
|
|
8144
8780
|
try {
|
|
8145
8781
|
const { workspace, hadExactDevpod } = resolveIdentity(repoPath);
|
|
8146
|
-
const overlayPath =
|
|
8147
|
-
if (!
|
|
8782
|
+
const overlayPath = import_node_path17.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
8783
|
+
if (!import_node_fs19.default.existsSync(overlayPath)) {
|
|
8148
8784
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
8149
8785
|
}
|
|
8150
8786
|
const runtime = loadRuntimeConfig(repoPath, workspace);
|
|
@@ -8157,12 +8793,20 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8157
8793
|
);
|
|
8158
8794
|
}
|
|
8159
8795
|
}
|
|
8160
|
-
const commonDir =
|
|
8796
|
+
const commonDir = resolveGitCommonDir(repoPath);
|
|
8161
8797
|
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
8798
|
+
const ownership = {
|
|
8799
|
+
workspace,
|
|
8800
|
+
worktreePath: repoPath,
|
|
8801
|
+
branch: currentBranch(repoPath),
|
|
8802
|
+
devpodId: workspace
|
|
8803
|
+
};
|
|
8804
|
+
writeWorkspaceOwnership(repoPath, ownership);
|
|
8162
8805
|
const startAndProveAttachment = (recreate = false) => {
|
|
8163
8806
|
startDevpod(repoPath, workspace, commonDir, recreate);
|
|
8164
8807
|
environmentStarted = true;
|
|
8165
8808
|
assertDevpodAttachment(repoPath, workspace);
|
|
8809
|
+
writeWorkspaceOwnership(repoPath, ownership);
|
|
8166
8810
|
};
|
|
8167
8811
|
const recreateAndWait = async () => {
|
|
8168
8812
|
startAndProveAttachment(true);
|
|
@@ -8240,19 +8884,21 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8240
8884
|
}
|
|
8241
8885
|
});
|
|
8242
8886
|
}
|
|
8243
|
-
var
|
|
8887
|
+
var import_node_child_process14, import_node_fs19, import_node_path17, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS2, SAFE_INSPECT_TEMPLATE;
|
|
8244
8888
|
var init_workspace_ensure = __esm({
|
|
8245
8889
|
"src/core/workspace-ensure.ts"() {
|
|
8246
8890
|
"use strict";
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8891
|
+
import_node_child_process14 = require("child_process");
|
|
8892
|
+
import_node_fs19 = __toESM(require("fs"));
|
|
8893
|
+
import_node_path17 = __toESM(require("path"));
|
|
8894
|
+
init_devpod_workspaces();
|
|
8250
8895
|
init_docker();
|
|
8251
8896
|
init_host_routes();
|
|
8252
8897
|
init_repo_config();
|
|
8253
8898
|
init_router();
|
|
8254
8899
|
init_tls();
|
|
8255
8900
|
init_workspace();
|
|
8901
|
+
init_workspace_ownership();
|
|
8256
8902
|
DEVCONTAINER_OVERLAY = "docker-compose.devrouter.yml";
|
|
8257
8903
|
DEFAULT_READINESS_TIMEOUT_MS = 12e4;
|
|
8258
8904
|
POLL_INTERVAL_MS2 = 1e3;
|
|
@@ -8261,49 +8907,29 @@ var init_workspace_ensure = __esm({
|
|
|
8261
8907
|
});
|
|
8262
8908
|
|
|
8263
8909
|
// src/core/workspace-lifecycle.ts
|
|
8264
|
-
function
|
|
8265
|
-
const
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
});
|
|
8272
|
-
if (result.status !== 0) {
|
|
8273
|
-
return [];
|
|
8274
|
-
}
|
|
8275
|
-
const worktrees = [];
|
|
8276
|
-
let current = {};
|
|
8277
|
-
for (const line of result.stdout.split("\n")) {
|
|
8278
|
-
if (line.startsWith("worktree ")) {
|
|
8279
|
-
current = { path: line.slice("worktree ".length).trim() };
|
|
8280
|
-
} else if (line.startsWith("branch ")) {
|
|
8281
|
-
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
8282
|
-
} else if (line.trim() === "" && current.path) {
|
|
8283
|
-
worktrees.push({ path: current.path, branch: current.branch });
|
|
8284
|
-
current = {};
|
|
8285
|
-
}
|
|
8286
|
-
}
|
|
8287
|
-
if (current.path) {
|
|
8288
|
-
worktrees.push({ path: current.path, branch: current.branch });
|
|
8289
|
-
}
|
|
8290
|
-
return worktrees;
|
|
8910
|
+
function warnMissingWorkspaceOwnership(repoPath) {
|
|
8911
|
+
const missing = listMissingWorkspaceOwnership(repoPath);
|
|
8912
|
+
if (missing.length === 0) return;
|
|
8913
|
+
process.stderr.write(
|
|
8914
|
+
`Warning: ${missing.length} managed workspace owner${missing.length === 1 ? " is" : "s are"} missing. Review: dev workspace gc --repo ${repoPath}
|
|
8915
|
+
`
|
|
8916
|
+
);
|
|
8291
8917
|
}
|
|
8292
8918
|
function defaultWorktreePath(mainRepo, ws) {
|
|
8293
|
-
return
|
|
8919
|
+
return import_node_path18.default.join(mainRepo, "trees", ws);
|
|
8294
8920
|
}
|
|
8295
8921
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
8296
|
-
const ignored = (0,
|
|
8922
|
+
const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
8297
8923
|
encoding: "utf-8"
|
|
8298
8924
|
});
|
|
8299
8925
|
if (ignored.status !== 0) {
|
|
8300
8926
|
throw new Error(
|
|
8301
|
-
`Default worktree root '${
|
|
8927
|
+
`Default worktree root '${import_node_path18.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path18.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
8302
8928
|
);
|
|
8303
8929
|
}
|
|
8304
8930
|
}
|
|
8305
8931
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
8306
|
-
return
|
|
8932
|
+
return import_node_path18.default.join(import_node_path18.default.dirname(mainRepo), `${import_node_path18.default.basename(mainRepo)}-${ws}`);
|
|
8307
8933
|
}
|
|
8308
8934
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
8309
8935
|
const candidates = [
|
|
@@ -8332,14 +8958,119 @@ function oneWorkspaceMatch(target, matches) {
|
|
|
8332
8958
|
}
|
|
8333
8959
|
return matches[0];
|
|
8334
8960
|
}
|
|
8961
|
+
function oneRecordMatch(target, matches) {
|
|
8962
|
+
if (matches.length > 1) {
|
|
8963
|
+
throw new Error(
|
|
8964
|
+
`Workspace target '${target}' is ambiguous: ${matches.map((match) => match.worktreePath).join(", ")}`
|
|
8965
|
+
);
|
|
8966
|
+
}
|
|
8967
|
+
return matches[0];
|
|
8968
|
+
}
|
|
8969
|
+
function resolveWorkspaceTarget(mainRepo, target, worktrees, records) {
|
|
8970
|
+
const requestedWorkspace = wsFromBranch(target);
|
|
8971
|
+
if (!requestedWorkspace) {
|
|
8972
|
+
throw new Error(`'${target}' does not yield a valid workspace token.`);
|
|
8973
|
+
}
|
|
8974
|
+
const liveBranchWorktree = oneWorkspaceMatch(
|
|
8975
|
+
target,
|
|
8976
|
+
worktrees.filter((worktree2) => worktree2.workspace !== void 0 && worktree2.branch === target)
|
|
8977
|
+
);
|
|
8978
|
+
if (liveBranchWorktree) {
|
|
8979
|
+
const liveWorkspace = liveBranchWorktree.workspace;
|
|
8980
|
+
if (!liveWorkspace) {
|
|
8981
|
+
throw new Error(`Live branch '${target}' is not an isolated workspace.`);
|
|
8982
|
+
}
|
|
8983
|
+
const liveRecord = oneRecordMatch(
|
|
8984
|
+
target,
|
|
8985
|
+
records.filter((record2) => sameWorkspacePath(record2.worktreePath, liveBranchWorktree.path))
|
|
8986
|
+
);
|
|
8987
|
+
return {
|
|
8988
|
+
workspace: liveRecord?.workspace ?? liveWorkspace,
|
|
8989
|
+
worktreePath: liveBranchWorktree.path,
|
|
8990
|
+
worktree: liveBranchWorktree,
|
|
8991
|
+
record: liveRecord
|
|
8992
|
+
};
|
|
8993
|
+
}
|
|
8994
|
+
const record = records.find((candidate) => candidate.workspace === requestedWorkspace);
|
|
8995
|
+
if (record) {
|
|
8996
|
+
return {
|
|
8997
|
+
workspace: record.workspace,
|
|
8998
|
+
worktreePath: record.worktreePath,
|
|
8999
|
+
worktree: worktreeForRecord(worktrees, record),
|
|
9000
|
+
record
|
|
9001
|
+
};
|
|
9002
|
+
}
|
|
9003
|
+
const linked = worktrees.filter((worktree2) => worktree2.workspace !== void 0);
|
|
9004
|
+
const worktree = oneWorkspaceMatch(
|
|
9005
|
+
target,
|
|
9006
|
+
linked.filter((candidate) => candidate.workspace === requestedWorkspace)
|
|
9007
|
+
) ?? oneWorkspaceMatch(
|
|
9008
|
+
target,
|
|
9009
|
+
linked.filter(
|
|
9010
|
+
(candidate) => candidate.branch !== void 0 && wsFromBranch(candidate.branch) === requestedWorkspace
|
|
9011
|
+
)
|
|
9012
|
+
);
|
|
9013
|
+
const workspace = worktree?.workspace ?? requestedWorkspace;
|
|
9014
|
+
return {
|
|
9015
|
+
workspace,
|
|
9016
|
+
worktreePath: worktree?.path ?? teardownFallbackPath(mainRepo, workspace),
|
|
9017
|
+
worktree,
|
|
9018
|
+
record: void 0
|
|
9019
|
+
};
|
|
9020
|
+
}
|
|
9021
|
+
function worktreeForRecord(worktrees, record) {
|
|
9022
|
+
return worktrees.find((candidate) => sameWorkspacePath(candidate.path, record.worktreePath));
|
|
9023
|
+
}
|
|
9024
|
+
function devpodForTarget(target, worktrees, devpods) {
|
|
9025
|
+
if (target.record) {
|
|
9026
|
+
const status = inspectWorkspaceOwnership(target.record, worktrees, devpods);
|
|
9027
|
+
if (status.ownerStatus === "conflict") {
|
|
9028
|
+
throw new Error(
|
|
9029
|
+
`Workspace '${target.workspace}' ownership conflicts with live Git or DevPod evidence; no resources were changed.`
|
|
9030
|
+
);
|
|
9031
|
+
}
|
|
9032
|
+
return status.devpodStatus === "owned" ? devpods.find((devpod) => devpod.id === target.record?.devpodId) : void 0;
|
|
9033
|
+
}
|
|
9034
|
+
const devpodId = target.workspace;
|
|
9035
|
+
const ownership = inspectDevpodWorkspaceOwnership(devpods, devpodId, target.worktreePath);
|
|
9036
|
+
if (ownership.status === "conflict") {
|
|
9037
|
+
throw new Error(ownership.reason);
|
|
9038
|
+
}
|
|
9039
|
+
return ownership.status === "owned" ? ownership.workspace : void 0;
|
|
9040
|
+
}
|
|
9041
|
+
function assertFullDownPreflight(mainRepo, target) {
|
|
9042
|
+
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
9043
|
+
throw new Error("Refusing to remove the primary Git checkout.");
|
|
9044
|
+
}
|
|
9045
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs20.default.existsSync(target.worktreePath)) return;
|
|
9046
|
+
if (target.worktree.locked) {
|
|
9047
|
+
throw new Error(
|
|
9048
|
+
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
9049
|
+
);
|
|
9050
|
+
}
|
|
9051
|
+
const status = (0, import_node_child_process15.spawnSync)(
|
|
9052
|
+
"git",
|
|
9053
|
+
["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
|
|
9054
|
+
{ encoding: "utf-8" }
|
|
9055
|
+
);
|
|
9056
|
+
if (status.status !== 0) {
|
|
9057
|
+
const detail = [status.error?.message, status.stderr].filter(Boolean).join("\n").trim();
|
|
9058
|
+
throw new Error(`git status failed for '${target.worktreePath}': ${detail || "unknown error"}`);
|
|
9059
|
+
}
|
|
9060
|
+
if (status.stdout.trim()) {
|
|
9061
|
+
throw new Error(
|
|
9062
|
+
`Worktree '${target.worktreePath}' has uncommitted changes; use --keep-worktree or clean it before workspace down.`
|
|
9063
|
+
);
|
|
9064
|
+
}
|
|
9065
|
+
}
|
|
8335
9066
|
async function workspaceUp(branch, opts = {}) {
|
|
8336
9067
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
8337
9068
|
const ws = wsFromBranch(branch);
|
|
8338
9069
|
if (!ws) {
|
|
8339
9070
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
8340
9071
|
}
|
|
8341
|
-
const worktreePath = opts.path ?
|
|
8342
|
-
if (
|
|
9072
|
+
const worktreePath = opts.path ? import_node_path18.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
9073
|
+
if (import_node_fs20.default.existsSync(worktreePath)) {
|
|
8343
9074
|
const registered = listGitWorktrees(mainRepo).find(
|
|
8344
9075
|
(worktree) => sameWorkspacePath(worktree.path, worktreePath)
|
|
8345
9076
|
);
|
|
@@ -8357,11 +9088,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
8357
9088
|
if (!opts.path) {
|
|
8358
9089
|
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
8359
9090
|
}
|
|
8360
|
-
const add = (0,
|
|
9091
|
+
const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
8361
9092
|
encoding: "utf-8"
|
|
8362
9093
|
});
|
|
8363
9094
|
if (add.status !== 0) {
|
|
8364
|
-
const addNew = (0,
|
|
9095
|
+
const addNew = (0, import_node_child_process15.spawnSync)(
|
|
8365
9096
|
"git",
|
|
8366
9097
|
["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
|
|
8367
9098
|
{
|
|
@@ -8377,6 +9108,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
8377
9108
|
`);
|
|
8378
9109
|
}
|
|
8379
9110
|
if (opts.noDevpod) {
|
|
9111
|
+
warnMissingWorkspaceOwnership(mainRepo);
|
|
8380
9112
|
process.stdout.write("Skipped environment startup; no routes were changed.\n");
|
|
8381
9113
|
return;
|
|
8382
9114
|
}
|
|
@@ -8393,79 +9125,104 @@ ${ensured.urls.map((url) => ` ${url}`).join("\n")}
|
|
|
8393
9125
|
function workspaceLs(repoPath) {
|
|
8394
9126
|
const mainRepo = resolveRepoPath(repoPath);
|
|
8395
9127
|
const worktrees = identifyGitWorktrees(mainRepo);
|
|
8396
|
-
const
|
|
8397
|
-
|
|
9128
|
+
const records = listWorkspaceOwnership(mainRepo);
|
|
9129
|
+
let devpods;
|
|
9130
|
+
try {
|
|
9131
|
+
devpods = listDevpodWorkspaces();
|
|
9132
|
+
} catch {
|
|
9133
|
+
devpods = void 0;
|
|
9134
|
+
}
|
|
9135
|
+
const paths = Array.from(
|
|
9136
|
+
/* @__PURE__ */ new Set([
|
|
9137
|
+
...worktrees.map((worktree) => worktree.path),
|
|
9138
|
+
...records.map((record) => record.worktreePath)
|
|
9139
|
+
])
|
|
8398
9140
|
);
|
|
8399
|
-
|
|
8400
|
-
|
|
9141
|
+
const routesByWorktreePath = listRoutesForWorktreePaths(paths);
|
|
9142
|
+
const worktreesByPath = new Map(worktrees.map((worktree) => [worktree.path, worktree]));
|
|
9143
|
+
const recordsByPath = new Map(records.map((record) => [record.worktreePath, record]));
|
|
9144
|
+
return paths.map((worktreePath) => {
|
|
9145
|
+
const worktree = worktreesByPath.get(worktreePath);
|
|
9146
|
+
const record = recordsByPath.get(worktreePath);
|
|
9147
|
+
const status = record ? inspectWorkspaceOwnership(record, worktrees, devpods) : void 0;
|
|
9148
|
+
const matchingDevpods = devpods?.filter(
|
|
9149
|
+
(devpod) => sameWorkspacePath(devpod.source.localFolder, worktreePath)
|
|
9150
|
+
);
|
|
9151
|
+
const wsRoutes = routesByWorktreePath.get(worktreePath) ?? [];
|
|
8401
9152
|
return {
|
|
8402
|
-
workspace:
|
|
8403
|
-
branch:
|
|
8404
|
-
worktreePath
|
|
9153
|
+
workspace: record?.workspace ?? worktree?.workspace,
|
|
9154
|
+
branch: worktree?.branch ?? record?.branch ?? void 0,
|
|
9155
|
+
worktreePath,
|
|
9156
|
+
legacy: !record && worktree?.workspace !== void 0,
|
|
9157
|
+
ownerStatus: status?.ownerStatus,
|
|
9158
|
+
devpodStatus: status?.devpodStatus ?? (devpods === void 0 ? "unknown" : matchingDevpods?.length === 0 ? "absent" : matchingDevpods?.length === 1 ? "owned" : "conflict"),
|
|
8405
9159
|
routeCount: wsRoutes.length,
|
|
8406
9160
|
hosts: wsRoutes.map((route) => route.host)
|
|
8407
9161
|
};
|
|
8408
9162
|
});
|
|
8409
9163
|
}
|
|
8410
|
-
async function
|
|
8411
|
-
const requestedWorkspace = wsFromBranch(target);
|
|
8412
|
-
if (!requestedWorkspace) {
|
|
8413
|
-
throw new Error(`'${target}' does not yield a valid workspace token.`);
|
|
8414
|
-
}
|
|
9164
|
+
async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
8415
9165
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
8416
|
-
const worktrees = identifyGitWorktrees(mainRepo)
|
|
8417
|
-
|
|
8418
|
-
);
|
|
8419
|
-
const
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
)
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
if (
|
|
8436
|
-
(
|
|
8437
|
-
|
|
8438
|
-
if (!opts.keepWorktree) {
|
|
8439
|
-
if (import_node_fs19.default.existsSync(worktreePath) && worktreePath !== mainRepo) {
|
|
8440
|
-
const rm = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
|
|
9166
|
+
const worktrees = identifyGitWorktrees(mainRepo);
|
|
9167
|
+
const records = listWorkspaceOwnership(mainRepo);
|
|
9168
|
+
const resolved = resolveWorkspaceTarget(mainRepo, target, worktrees, records);
|
|
9169
|
+
const operation = async () => {
|
|
9170
|
+
const removeWorktree = action2 === "down" && !opts.keepWorktree;
|
|
9171
|
+
const devpodAction = action2 === "stop" ? "stop" : "delete";
|
|
9172
|
+
if (removeWorktree) {
|
|
9173
|
+
assertFullDownPreflight(mainRepo, resolved);
|
|
9174
|
+
}
|
|
9175
|
+
const devpods = listDevpodWorkspaces();
|
|
9176
|
+
const devpod = devpodForTarget(resolved, worktrees, devpods);
|
|
9177
|
+
if (devpod) {
|
|
9178
|
+
runDevpodWorkspaceAction(devpodAction, devpod.id);
|
|
9179
|
+
}
|
|
9180
|
+
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
9181
|
+
process.stdout.write(
|
|
9182
|
+
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
9183
|
+
`
|
|
9184
|
+
);
|
|
9185
|
+
if (removeWorktree) {
|
|
9186
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs20.default.existsSync(resolved.worktreePath)) {
|
|
9187
|
+
const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
8441
9188
|
encoding: "utf-8"
|
|
8442
9189
|
});
|
|
8443
|
-
if (rm.status
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
process.stderr.write(
|
|
8448
|
-
`Warning: could not remove worktree ${worktreePath}: ${rm.stderr || "unknown error"}
|
|
8449
|
-
`
|
|
9190
|
+
if (rm.status !== 0) {
|
|
9191
|
+
const detail = [rm.error?.message, rm.stderr].filter(Boolean).join("\n").trim();
|
|
9192
|
+
throw new Error(
|
|
9193
|
+
`git worktree remove failed for '${resolved.worktreePath}': ${detail || "unknown error"}`
|
|
8450
9194
|
);
|
|
8451
9195
|
}
|
|
9196
|
+
process.stdout.write(`Removed worktree ${resolved.worktreePath}.
|
|
9197
|
+
`);
|
|
9198
|
+
}
|
|
9199
|
+
if (resolved.record) {
|
|
9200
|
+
removeWorkspaceOwnership(mainRepo, resolved.workspace);
|
|
8452
9201
|
}
|
|
8453
9202
|
}
|
|
8454
|
-
return { freedRoutes: routes.length, workspace };
|
|
9203
|
+
return { freedRoutes: routes.length, workspace: resolved.workspace };
|
|
8455
9204
|
};
|
|
8456
|
-
return
|
|
9205
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs20.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
9206
|
+
}
|
|
9207
|
+
async function workspaceStop(target, opts = {}) {
|
|
9208
|
+
return runWorkspaceLifecycle("stop", target, opts);
|
|
9209
|
+
}
|
|
9210
|
+
async function workspaceDown(target, opts = {}) {
|
|
9211
|
+
return runWorkspaceLifecycle("down", target, opts);
|
|
8457
9212
|
}
|
|
8458
|
-
var
|
|
9213
|
+
var import_node_child_process15, import_node_fs20, import_node_path18;
|
|
8459
9214
|
var init_workspace_lifecycle = __esm({
|
|
8460
9215
|
"src/core/workspace-lifecycle.ts"() {
|
|
8461
9216
|
"use strict";
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
9217
|
+
import_node_child_process15 = require("child_process");
|
|
9218
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
9219
|
+
import_node_path18 = __toESM(require("path"));
|
|
9220
|
+
init_devpod_workspaces();
|
|
8465
9221
|
init_repo_config();
|
|
8466
9222
|
init_route_state();
|
|
8467
9223
|
init_workspace();
|
|
8468
9224
|
init_workspace_ensure();
|
|
9225
|
+
init_workspace_ownership();
|
|
8469
9226
|
}
|
|
8470
9227
|
});
|
|
8471
9228
|
|
|
@@ -8474,19 +9231,34 @@ var workspace_exports = {};
|
|
|
8474
9231
|
__export(workspace_exports, {
|
|
8475
9232
|
runWorkspaceDownCommand: () => runWorkspaceDownCommand,
|
|
8476
9233
|
runWorkspaceEnsureCommand: () => runWorkspaceEnsureCommand,
|
|
9234
|
+
runWorkspaceGcCommand: () => runWorkspaceGcCommand,
|
|
8477
9235
|
runWorkspaceLsCommand: () => runWorkspaceLsCommand,
|
|
9236
|
+
runWorkspaceStopCommand: () => runWorkspaceStopCommand,
|
|
8478
9237
|
runWorkspaceUpCommand: () => runWorkspaceUpCommand
|
|
8479
9238
|
});
|
|
9239
|
+
function resolveGitWorkspaceRepo(repoPath) {
|
|
9240
|
+
const resolved = resolveRepoPath(repoPath);
|
|
9241
|
+
try {
|
|
9242
|
+
resolveGitCommonDir(resolved);
|
|
9243
|
+
} catch (error) {
|
|
9244
|
+
throw new Error(`Workspace commands require a Git repository: '${resolved}'.`, {
|
|
9245
|
+
cause: error
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
9248
|
+
return resolved;
|
|
9249
|
+
}
|
|
8480
9250
|
async function runWorkspaceUpCommand(branch, options) {
|
|
9251
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
8481
9252
|
await workspaceUp(branch, {
|
|
8482
9253
|
path: options.path,
|
|
8483
9254
|
noDevpod: options.noDevpod,
|
|
8484
9255
|
open: options.open,
|
|
8485
|
-
repoPath
|
|
9256
|
+
repoPath
|
|
8486
9257
|
});
|
|
8487
9258
|
}
|
|
8488
9259
|
async function runWorkspaceEnsureCommand(options) {
|
|
8489
|
-
const
|
|
9260
|
+
const repoPath = resolveGitWorkspaceRepo(options.path);
|
|
9261
|
+
const result = await workspaceEnsure(repoPath, { open: options.open });
|
|
8490
9262
|
process.stdout.write(
|
|
8491
9263
|
`Workspace '${result.workspace}' is ready (${result.devpodId}).
|
|
8492
9264
|
${result.urls.map((url) => ` ${url}`).join("\n")}
|
|
@@ -8494,7 +9266,8 @@ ${result.urls.map((url) => ` ${url}`).join("\n")}
|
|
|
8494
9266
|
);
|
|
8495
9267
|
}
|
|
8496
9268
|
function runWorkspaceLsCommand(options) {
|
|
8497
|
-
const
|
|
9269
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
9270
|
+
const rows = workspaceLs(repoPath);
|
|
8498
9271
|
if (options.json) {
|
|
8499
9272
|
process.stdout.write(`${JSON.stringify(rows, null, 2)}
|
|
8500
9273
|
`);
|
|
@@ -8506,24 +9279,56 @@ function runWorkspaceLsCommand(options) {
|
|
|
8506
9279
|
}
|
|
8507
9280
|
for (const row of rows) {
|
|
8508
9281
|
const label = row.workspace ?? "(primary)";
|
|
9282
|
+
const ownership = row.ownerStatus ?? (row.legacy ? "legacy" : "unmanaged");
|
|
8509
9283
|
process.stdout.write(
|
|
8510
|
-
`${label} ${row.branch ?? "-"} ${row.routeCount} route(s) ${row.worktreePath}
|
|
9284
|
+
`${label} ${row.branch ?? "-"} ${ownership} devpod:${row.devpodStatus} ${row.routeCount} route(s) ${row.worktreePath}
|
|
8511
9285
|
`
|
|
8512
9286
|
);
|
|
8513
9287
|
}
|
|
8514
9288
|
}
|
|
8515
9289
|
async function runWorkspaceDownCommand(target, options) {
|
|
9290
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
8516
9291
|
await workspaceDown(target, {
|
|
8517
9292
|
keepWorktree: options.keepWorktree,
|
|
8518
|
-
|
|
8519
|
-
repoPath: options.repo
|
|
9293
|
+
repoPath
|
|
8520
9294
|
});
|
|
8521
9295
|
}
|
|
9296
|
+
async function runWorkspaceStopCommand(target, options) {
|
|
9297
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
9298
|
+
await workspaceStop(target, { repoPath });
|
|
9299
|
+
}
|
|
9300
|
+
function runWorkspaceGcCommand(options) {
|
|
9301
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
9302
|
+
const plan = inspectWorkspaceGc(repoPath);
|
|
9303
|
+
const report = options.yes ? applyWorkspaceGc(plan) : plan;
|
|
9304
|
+
if (options.json) {
|
|
9305
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
9306
|
+
`);
|
|
9307
|
+
} else {
|
|
9308
|
+
process.stdout.write(
|
|
9309
|
+
`Workspace GC ${report.mode}: ${report.summary.eligible} eligible, ${report.summary.blocked} blocked, ${report.summary.cleaned} cleaned, ${report.summary.errors} error(s).
|
|
9310
|
+
`
|
|
9311
|
+
);
|
|
9312
|
+
for (const candidate of report.candidates) {
|
|
9313
|
+
process.stdout.write(
|
|
9314
|
+
`${candidate.workspace} ${candidate.kind} ${candidate.ownerStatus ?? "legacy"} ${candidate.eligible ? "eligible" : "blocked"} ${candidate.worktreePath}
|
|
9315
|
+
`
|
|
9316
|
+
);
|
|
9317
|
+
}
|
|
9318
|
+
if (!options.yes && report.summary.eligible > 0) {
|
|
9319
|
+
process.stdout.write("Dry run only. Re-run with --yes to apply eligible cleanup.\n");
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
if (report.summary.errors > 0) process.exitCode = 1;
|
|
9323
|
+
}
|
|
8522
9324
|
var init_workspace2 = __esm({
|
|
8523
9325
|
"src/commands/workspace.ts"() {
|
|
8524
9326
|
"use strict";
|
|
9327
|
+
init_repo_config();
|
|
8525
9328
|
init_workspace_ensure();
|
|
9329
|
+
init_workspace_gc();
|
|
8526
9330
|
init_workspace_lifecycle();
|
|
9331
|
+
init_workspace_ownership();
|
|
8527
9332
|
}
|
|
8528
9333
|
});
|
|
8529
9334
|
|
|
@@ -8565,7 +9370,7 @@ var init_version = __esm({
|
|
|
8565
9370
|
|
|
8566
9371
|
// src/cli.ts
|
|
8567
9372
|
var import_commander = require("commander");
|
|
8568
|
-
var CLI_VERSION = true ? "0.0.
|
|
9373
|
+
var CLI_VERSION = true ? "0.0.31" : "0.0.0-dev";
|
|
8569
9374
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
8570
9375
|
function withErrorHandling(action2) {
|
|
8571
9376
|
return async (...args) => {
|
|
@@ -8805,7 +9610,21 @@ workspaceCommand.command("ls").description("List git worktrees with their worksp
|
|
|
8805
9610
|
runWorkspaceLsCommand2(options);
|
|
8806
9611
|
})
|
|
8807
9612
|
);
|
|
8808
|
-
workspaceCommand.command("
|
|
9613
|
+
workspaceCommand.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 DevPod, route, and ownership resources").action(
|
|
9614
|
+
withErrorHandling(async (_options, command) => {
|
|
9615
|
+
const options = command.opts();
|
|
9616
|
+
const { runWorkspaceGcCommand: runWorkspaceGcCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
9617
|
+
runWorkspaceGcCommand2(options);
|
|
9618
|
+
})
|
|
9619
|
+
);
|
|
9620
|
+
workspaceCommand.command("stop").description("Stop a workspace's DevPod and free routes while preserving its worktree and data").argument("<workspace>", "Workspace token or live branch name").option("--repo <path>", "Main repository path (defaults to current directory)").action(
|
|
9621
|
+
withErrorHandling(async (target, _options, command) => {
|
|
9622
|
+
const options = command.opts();
|
|
9623
|
+
const { runWorkspaceStopCommand: runWorkspaceStopCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
9624
|
+
await runWorkspaceStopCommand2(target, options);
|
|
9625
|
+
})
|
|
9626
|
+
);
|
|
9627
|
+
workspaceCommand.command("down").description("Delete a workspace's DevPod and routes, then remove its clean Git worktree").argument("<workspace>", "Workspace token or live branch name").option("--keep-worktree", "Delete runtime resources but preserve the Git worktree and record").option("--repo <path>", "Main repository path (defaults to current directory)").action(
|
|
8809
9628
|
withErrorHandling(async (target, _options, command) => {
|
|
8810
9629
|
const options = command.opts();
|
|
8811
9630
|
const { runWorkspaceDownCommand: runWorkspaceDownCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|