@devrouter/cli 0.0.34 → 0.0.36
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 +63 -449
- package/bin/devrouter-process +52 -5
- package/dist/devrouter.js +2016 -567
- package/package.json +4 -1
- package/upgrade-prompts/0.0.35.md +29 -0
- package/upgrade-prompts/0.0.36.md +31 -0
package/dist/devrouter.js
CHANGED
|
@@ -156,6 +156,8 @@ apps:
|
|
|
156
156
|
# to target a per-workspace devcontainer alias \u2014 substituted with the resolved
|
|
157
157
|
# workspace token at runtime. See "Workspace isolation" below. Do NOT put
|
|
158
158
|
# \${WORKSPACE} in \`host\` (rejected); the host is auto-namespaced.
|
|
159
|
+
# Managed \`ensure\` requires every HTTP/TCP upstream to begin with the exact
|
|
160
|
+
# resolved workspace/project alias prefix before DevPod or route mutation.
|
|
159
161
|
#
|
|
160
162
|
# proxy + tcp (front a DB in an externally-managed container, e.g. a
|
|
161
163
|
# devcontainer's Postgres on devnet) \u2014 no per-DB host port:
|
|
@@ -239,11 +241,14 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
|
|
|
239
241
|
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.
|
|
240
242
|
|
|
241
243
|
- **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.
|
|
242
|
-
- **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.
|
|
244
|
+
- **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. Managed \`ensure\` rejects every HTTP/TCP proxy upstream outside that exact alias namespace before it mutates DevPod or routes. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
|
|
243
245
|
- **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.
|
|
244
246
|
- **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>\`.
|
|
245
|
-
- **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive and \`exec . -- <command...>\` runs one-shot commands only in the exact running DevPod. \`workspace up\` creates linked worktrees; destructive
|
|
246
|
-
- **
|
|
247
|
+
- **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running DevPod. Never substitute raw \`devpod up\`, \`stop\`, or \`delete\`: they bypass devrouter's machine-global ownership lock. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects.
|
|
248
|
+
- **Managed process identity**: \`ensure\` executes an exact captured adapter snapshot. Default reuse includes command argv, workspace, and adapter SHA-256. Set \`DEVROUTER_PROCESS_FINGERPRINT_ENV\` only to comma-separated non-secret environment names whose values affect runtime identity; secret-like names are rejected and raw values are never persisted.
|
|
249
|
+
- **Route state**: the versioned Traefik dynamic file is authoritative for both metadata and rendering. JSON is a compatibility mirror; valid headerless generations migrate automatically, while corrupt canonical metadata fails closed.
|
|
250
|
+
- **Cleanup**: \`workspace cleanup --repo . --inactive-for 30d --json\` is a report-only, no-\`--yes\` report for managed linked workspaces. It joins ownership (\`present|missing|locked|conflict\`), DevPod registration, runtime (\`running|stopped|busy|not-found|absent|unknown\`), checkout, route, advisory activity, and integration evidence without mutating DevPod, routes, ownership, Git, Docker, applications, worktrees, or branches. Local DevPod list/status checks always run; \`--check-merged\` alone enables read-only origin and matching GitHub/GitLab checks. Treat \`not-found\` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit \`gc\`/\`down\` can remove exact stale registration only after expected-ID \`NotFound\` proof and ownership revalidation. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
|
|
251
|
+
- **Sizing**: \`--measure-size\` adds per-workspace storage consumption to that report and stays read-only, but it walks each worktree and runs \`docker ps\` / \`docker inspect --size\`, so leave it off when you only need the evidence states. Each row reports reclaimable \`worktree\` and \`containerWritable\` bytes, non-reclaimable \`imageShared\` bytes, and a \`reclaimable\` total of the first two. \`imageShared\` covers image layers shared with other containers and overlaps across rows, so never sum it. Attribution is the workspace's own app container; sibling compose services such as a database are excluded. Any untrustworthy figure reports \`unknown\` with a reason rather than zero, and a workspace with no container reports a measured \`0\`.
|
|
247
252
|
- **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
|
|
248
253
|
|
|
249
254
|
## Secret manager interop (Infisical/Doppler)
|
|
@@ -295,7 +300,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
295
300
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
296
301
|
- \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
|
|
297
302
|
- \`devrouter ensure [path] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts
|
|
298
|
-
- \`devrouter stop [path] [--json]\`: stop the exact DevPod and remove exact routes without
|
|
303
|
+
- \`devrouter stop [path] [--delete] [--json]\`: stop the exact DevPod and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
|
|
299
304
|
- \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
|
|
300
305
|
- \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
|
|
301
306
|
- \`devrouter status\`: router/container/network/TLS health
|
|
@@ -319,6 +324,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
319
324
|
- \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
|
|
320
325
|
- \`devrouter workspace ensure [path] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
|
|
321
326
|
- \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
|
|
327
|
+
- \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]\`: report-only cleanup evidence and exact guarded suggestions; no \`--yes\` or apply mode
|
|
322
328
|
- \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
|
|
323
329
|
- \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
|
|
324
330
|
- \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
|
|
@@ -721,6 +727,51 @@ var init_capabilities = __esm({
|
|
|
721
727
|
}
|
|
722
728
|
});
|
|
723
729
|
|
|
730
|
+
// src/core/atomic-file.ts
|
|
731
|
+
function fsyncDirectory(directory) {
|
|
732
|
+
const handle = import_node_fs3.default.openSync(directory, "r");
|
|
733
|
+
try {
|
|
734
|
+
import_node_fs3.default.fsyncSync(handle);
|
|
735
|
+
} finally {
|
|
736
|
+
import_node_fs3.default.closeSync(handle);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function writeFileAtomically(filePath, contents) {
|
|
740
|
+
const directory = import_node_path3.default.dirname(filePath);
|
|
741
|
+
import_node_fs3.default.mkdirSync(directory, { recursive: true });
|
|
742
|
+
const temporaryPath = import_node_path3.default.join(
|
|
743
|
+
directory,
|
|
744
|
+
`.${import_node_path3.default.basename(filePath)}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`
|
|
745
|
+
);
|
|
746
|
+
let temporaryHandle;
|
|
747
|
+
try {
|
|
748
|
+
temporaryHandle = import_node_fs3.default.openSync(temporaryPath, "wx", 384);
|
|
749
|
+
import_node_fs3.default.writeFileSync(temporaryHandle, contents, "utf-8");
|
|
750
|
+
import_node_fs3.default.fsyncSync(temporaryHandle);
|
|
751
|
+
import_node_fs3.default.closeSync(temporaryHandle);
|
|
752
|
+
temporaryHandle = void 0;
|
|
753
|
+
import_node_fs3.default.renameSync(temporaryPath, filePath);
|
|
754
|
+
fsyncDirectory(directory);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (temporaryHandle !== void 0) import_node_fs3.default.closeSync(temporaryHandle);
|
|
757
|
+
try {
|
|
758
|
+
import_node_fs3.default.unlinkSync(temporaryPath);
|
|
759
|
+
} catch (cleanupError) {
|
|
760
|
+
if (cleanupError.code !== "ENOENT") throw cleanupError;
|
|
761
|
+
}
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
var import_node_crypto, import_node_fs3, import_node_path3;
|
|
766
|
+
var init_atomic_file = __esm({
|
|
767
|
+
"src/core/atomic-file.ts"() {
|
|
768
|
+
"use strict";
|
|
769
|
+
import_node_crypto = require("crypto");
|
|
770
|
+
import_node_fs3 = __toESM(require("fs"));
|
|
771
|
+
import_node_path3 = __toESM(require("path"));
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
724
775
|
// src/core/file-lock.ts
|
|
725
776
|
function sleepSync(ms) {
|
|
726
777
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -732,17 +783,56 @@ function isProcessAlive(pid) {
|
|
|
732
783
|
try {
|
|
733
784
|
process.kill(pid, 0);
|
|
734
785
|
return true;
|
|
786
|
+
} catch (error) {
|
|
787
|
+
return error.code === "EPERM";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function processBirthIdentity(pid) {
|
|
791
|
+
try {
|
|
792
|
+
const stat = import_node_fs4.default.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
793
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
794
|
+
if (commandEnd >= 0) {
|
|
795
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
796
|
+
const startTime = fields[19];
|
|
797
|
+
if (startTime) return `proc:${startTime}`;
|
|
798
|
+
}
|
|
735
799
|
} catch {
|
|
736
|
-
|
|
800
|
+
}
|
|
801
|
+
const result = (0, import_node_child_process2.spawnSync)("ps", ["-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
802
|
+
encoding: "utf-8",
|
|
803
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
804
|
+
});
|
|
805
|
+
const startedAt = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
806
|
+
return startedAt ? `ps:${(0, import_node_crypto2.createHash)("sha256").update(startedAt).digest("hex")}` : void 0;
|
|
807
|
+
}
|
|
808
|
+
function parseLockOwner(value) {
|
|
809
|
+
const fields = value.split(":");
|
|
810
|
+
const pid = Number(fields[0]);
|
|
811
|
+
if (!Number.isInteger(pid) || pid <= 0) return void 0;
|
|
812
|
+
if (fields.length !== 3 || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(fields[2])) {
|
|
813
|
+
return { pid };
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const processBirth = Buffer.from(fields[1], "base64url").toString("utf-8");
|
|
817
|
+
const canonical = Buffer.from(processBirth).toString("base64url");
|
|
818
|
+
return canonical === fields[1] && /^(proc|ps):/.test(processBirth) ? { pid, processBirth } : { pid };
|
|
819
|
+
} catch {
|
|
820
|
+
return { pid };
|
|
737
821
|
}
|
|
738
822
|
}
|
|
823
|
+
function isLockOwnerLive(owner) {
|
|
824
|
+
if (!isProcessAlive(owner.pid)) return false;
|
|
825
|
+
if (!owner.processBirth) return true;
|
|
826
|
+
const currentBirth = processBirthIdentity(owner.pid);
|
|
827
|
+
return currentBirth === void 0 || currentBirth === owner.processBirth;
|
|
828
|
+
}
|
|
739
829
|
function sameFile(left, right) {
|
|
740
830
|
return left.dev === right.dev && left.ino === right.ino;
|
|
741
831
|
}
|
|
742
832
|
function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
743
833
|
let fd;
|
|
744
834
|
try {
|
|
745
|
-
fd =
|
|
835
|
+
fd = import_node_fs4.default.openSync(lockPath, "r");
|
|
746
836
|
} catch (error) {
|
|
747
837
|
if (error.code === "ENOENT") {
|
|
748
838
|
return { kind: "retry" };
|
|
@@ -750,17 +840,16 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
750
840
|
throw error;
|
|
751
841
|
}
|
|
752
842
|
try {
|
|
753
|
-
const owner =
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
return { kind: "live", pid: ownerPid };
|
|
843
|
+
const owner = parseLockOwner(import_node_fs4.default.readFileSync(fd, "utf-8").trim());
|
|
844
|
+
if (owner && isLockOwnerLive(owner)) {
|
|
845
|
+
return { kind: "live", pid: owner.pid };
|
|
757
846
|
}
|
|
758
|
-
const staleStat =
|
|
847
|
+
const staleStat = import_node_fs4.default.fstatSync(fd);
|
|
759
848
|
if (staleStat.nlink !== 1) {
|
|
760
849
|
return { kind: "retry" };
|
|
761
850
|
}
|
|
762
851
|
try {
|
|
763
|
-
|
|
852
|
+
import_node_fs4.default.linkSync(lockPath, staleLinkPath);
|
|
764
853
|
} catch (error) {
|
|
765
854
|
if (error.code === "ENOENT") {
|
|
766
855
|
return { kind: "retry" };
|
|
@@ -770,7 +859,7 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
770
859
|
try {
|
|
771
860
|
let currentStat;
|
|
772
861
|
try {
|
|
773
|
-
currentStat =
|
|
862
|
+
currentStat = import_node_fs4.default.statSync(lockPath);
|
|
774
863
|
} catch (error) {
|
|
775
864
|
if (error.code === "ENOENT") {
|
|
776
865
|
return { kind: "retry" };
|
|
@@ -780,27 +869,32 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
780
869
|
if (!sameFile(staleStat, currentStat) || currentStat.nlink !== 2) {
|
|
781
870
|
return { kind: "retry" };
|
|
782
871
|
}
|
|
783
|
-
|
|
872
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
784
873
|
return { kind: "reclaimed" };
|
|
785
874
|
} finally {
|
|
786
|
-
|
|
875
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
787
876
|
}
|
|
788
877
|
} finally {
|
|
789
|
-
|
|
878
|
+
import_node_fs4.default.closeSync(fd);
|
|
790
879
|
}
|
|
791
880
|
}
|
|
792
881
|
function acquireFileLock(lockPath, options) {
|
|
793
|
-
const
|
|
794
|
-
|
|
882
|
+
const processBirth = processBirthIdentity(process.pid);
|
|
883
|
+
if (!processBirth) {
|
|
884
|
+
throw new Error(`could not determine process identity for ${options.activity} lock`);
|
|
885
|
+
}
|
|
886
|
+
const ownerId = (0, import_node_crypto2.randomUUID)();
|
|
887
|
+
const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
|
|
888
|
+
const candidatePath = `${lockPath}.${process.pid}.${ownerId}.candidate`;
|
|
795
889
|
const staleLinkPath = `${candidatePath}.stale`;
|
|
796
890
|
const deadline = Date.now() + (options.waitMs ?? 0);
|
|
797
891
|
let reclaimAttempts = 0;
|
|
798
|
-
|
|
892
|
+
import_node_fs4.default.writeFileSync(candidatePath, `${owner}
|
|
799
893
|
`, { encoding: "utf-8", flag: "wx" });
|
|
800
894
|
try {
|
|
801
895
|
for (; ; ) {
|
|
802
896
|
try {
|
|
803
|
-
|
|
897
|
+
import_node_fs4.default.linkSync(candidatePath, lockPath);
|
|
804
898
|
return owner;
|
|
805
899
|
} catch (error) {
|
|
806
900
|
if (error.code !== "EEXIST") {
|
|
@@ -828,14 +922,14 @@ function acquireFileLock(lockPath, options) {
|
|
|
828
922
|
}
|
|
829
923
|
}
|
|
830
924
|
} finally {
|
|
831
|
-
|
|
832
|
-
|
|
925
|
+
import_node_fs4.default.rmSync(candidatePath, { force: true });
|
|
926
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
833
927
|
}
|
|
834
928
|
}
|
|
835
929
|
function releaseFileLock(lockPath, owner) {
|
|
836
930
|
try {
|
|
837
|
-
if (
|
|
838
|
-
|
|
931
|
+
if (import_node_fs4.default.readFileSync(lockPath, "utf-8").trim() === owner) {
|
|
932
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
839
933
|
}
|
|
840
934
|
} catch (error) {
|
|
841
935
|
if (error.code !== "ENOENT") {
|
|
@@ -859,20 +953,21 @@ async function withFileLock(lockPath, options, operation) {
|
|
|
859
953
|
releaseFileLock(lockPath, owner);
|
|
860
954
|
}
|
|
861
955
|
}
|
|
862
|
-
var
|
|
956
|
+
var import_node_child_process2, import_node_crypto2, import_node_fs4;
|
|
863
957
|
var init_file_lock = __esm({
|
|
864
958
|
"src/core/file-lock.ts"() {
|
|
865
959
|
"use strict";
|
|
866
|
-
|
|
867
|
-
|
|
960
|
+
import_node_child_process2 = require("child_process");
|
|
961
|
+
import_node_crypto2 = require("crypto");
|
|
962
|
+
import_node_fs4 = __toESM(require("fs"));
|
|
868
963
|
}
|
|
869
964
|
});
|
|
870
965
|
|
|
871
966
|
// src/core/workspace.ts
|
|
872
967
|
function comparableWorkspacePath(filePath) {
|
|
873
|
-
const resolved =
|
|
968
|
+
const resolved = import_node_path4.default.resolve(filePath);
|
|
874
969
|
try {
|
|
875
|
-
return
|
|
970
|
+
return import_node_fs5.default.realpathSync.native(resolved);
|
|
876
971
|
} catch {
|
|
877
972
|
return resolved;
|
|
878
973
|
}
|
|
@@ -885,10 +980,10 @@ function wsFromBranch(branch) {
|
|
|
885
980
|
return slug.length > 0 ? slug : void 0;
|
|
886
981
|
}
|
|
887
982
|
function isLinkedWorktree(repoPath) {
|
|
888
|
-
const gitPath =
|
|
983
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
889
984
|
let stat;
|
|
890
985
|
try {
|
|
891
|
-
stat =
|
|
986
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
892
987
|
} catch {
|
|
893
988
|
return false;
|
|
894
989
|
}
|
|
@@ -897,7 +992,7 @@ function isLinkedWorktree(repoPath) {
|
|
|
897
992
|
}
|
|
898
993
|
let content;
|
|
899
994
|
try {
|
|
900
|
-
content =
|
|
995
|
+
content = import_node_fs5.default.readFileSync(gitPath, "utf-8");
|
|
901
996
|
} catch {
|
|
902
997
|
return false;
|
|
903
998
|
}
|
|
@@ -909,10 +1004,10 @@ function isLinkedWorktree(repoPath) {
|
|
|
909
1004
|
return /(^|\/)worktrees\//.test(gitdir);
|
|
910
1005
|
}
|
|
911
1006
|
function gitMetadataDir(repoPath) {
|
|
912
|
-
const gitPath =
|
|
1007
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
913
1008
|
let stat;
|
|
914
1009
|
try {
|
|
915
|
-
stat =
|
|
1010
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
916
1011
|
} catch {
|
|
917
1012
|
return void 0;
|
|
918
1013
|
}
|
|
@@ -922,11 +1017,11 @@ function gitMetadataDir(repoPath) {
|
|
|
922
1017
|
if (!stat.isFile()) {
|
|
923
1018
|
return void 0;
|
|
924
1019
|
}
|
|
925
|
-
const match =
|
|
1020
|
+
const match = import_node_fs5.default.readFileSync(gitPath, "utf-8").match(/^gitdir:\s*(.+)$/m);
|
|
926
1021
|
if (!match) {
|
|
927
1022
|
return void 0;
|
|
928
1023
|
}
|
|
929
|
-
return
|
|
1024
|
+
return import_node_path4.default.resolve(repoPath, match[1].trim());
|
|
930
1025
|
}
|
|
931
1026
|
function validatePersistedWorkspace(value) {
|
|
932
1027
|
const workspace = value.trim();
|
|
@@ -940,9 +1035,9 @@ function readPersistedWorkspace(repoPath) {
|
|
|
940
1035
|
if (!gitDir) {
|
|
941
1036
|
return void 0;
|
|
942
1037
|
}
|
|
943
|
-
const metadataPath =
|
|
1038
|
+
const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
|
|
944
1039
|
try {
|
|
945
|
-
return validatePersistedWorkspace(
|
|
1040
|
+
return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
|
|
946
1041
|
} catch (error) {
|
|
947
1042
|
if (error.code === "ENOENT") {
|
|
948
1043
|
return void 0;
|
|
@@ -965,7 +1060,7 @@ function persistWorkspace(repoPath, value) {
|
|
|
965
1060
|
if (!gitDir) {
|
|
966
1061
|
throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
|
|
967
1062
|
}
|
|
968
|
-
|
|
1063
|
+
import_node_fs5.default.writeFileSync(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
|
|
969
1064
|
`, "utf-8");
|
|
970
1065
|
return workspace;
|
|
971
1066
|
}
|
|
@@ -974,7 +1069,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
974
1069
|
if (!gitDir) {
|
|
975
1070
|
throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
|
|
976
1071
|
}
|
|
977
|
-
const lockPath =
|
|
1072
|
+
const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
|
|
978
1073
|
return withFileLock(
|
|
979
1074
|
lockPath,
|
|
980
1075
|
{ activity: "workspace lifecycle", target: `'${repoPath}'` },
|
|
@@ -982,7 +1077,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
982
1077
|
);
|
|
983
1078
|
}
|
|
984
1079
|
function currentBranch(repoPath) {
|
|
985
|
-
const result = (0,
|
|
1080
|
+
const result = (0, import_node_child_process3.spawnSync)("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
986
1081
|
encoding: "utf-8"
|
|
987
1082
|
});
|
|
988
1083
|
if (result.status !== 0) {
|
|
@@ -1002,7 +1097,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
|
1002
1097
|
return void 0;
|
|
1003
1098
|
}
|
|
1004
1099
|
const branch = currentBranch(repoPath);
|
|
1005
|
-
return wsFromBranch(branch ??
|
|
1100
|
+
return wsFromBranch(branch ?? import_node_path4.default.basename(import_node_path4.default.resolve(repoPath)));
|
|
1006
1101
|
}
|
|
1007
1102
|
function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
1008
1103
|
return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
|
|
@@ -1025,13 +1120,13 @@ function resolveWorkspace(repoPath, override) {
|
|
|
1025
1120
|
}
|
|
1026
1121
|
return deriveLinkedWorktreeWorkspace(repoPath);
|
|
1027
1122
|
}
|
|
1028
|
-
var
|
|
1123
|
+
var import_node_child_process3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
|
|
1029
1124
|
var init_workspace = __esm({
|
|
1030
1125
|
"src/core/workspace.ts"() {
|
|
1031
1126
|
"use strict";
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1127
|
+
import_node_child_process3 = require("child_process");
|
|
1128
|
+
import_node_fs5 = __toESM(require("fs"));
|
|
1129
|
+
import_node_path4 = __toESM(require("path"));
|
|
1035
1130
|
init_file_lock();
|
|
1036
1131
|
MAX_WORKSPACE_LENGTH = 32;
|
|
1037
1132
|
WORKSPACE_METADATA_FILE = "devrouter-workspace";
|
|
@@ -1151,64 +1246,218 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
|
|
|
1151
1246
|
}
|
|
1152
1247
|
return document;
|
|
1153
1248
|
}
|
|
1154
|
-
function
|
|
1249
|
+
function isRecord(value) {
|
|
1250
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1251
|
+
}
|
|
1252
|
+
function validateRouteState(value, source) {
|
|
1253
|
+
if (!Array.isArray(value)) {
|
|
1254
|
+
throw new Error(`${source} must contain a route array.`);
|
|
1255
|
+
}
|
|
1256
|
+
const routes = value.map((item, index) => {
|
|
1257
|
+
if (!isRecord(item)) {
|
|
1258
|
+
throw new Error(`${source} route ${index} must be an object.`);
|
|
1259
|
+
}
|
|
1260
|
+
for (const key of ["id", "name", "host", "repoPath", "createdAt", "updatedAt"]) {
|
|
1261
|
+
if (typeof item[key] !== "string" || item[key].length === 0) {
|
|
1262
|
+
throw new Error(`${source} route ${index} has an invalid '${key}'.`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (!Number.isInteger(item.port) || Number(item.port) < 1 || Number(item.port) > 65535) {
|
|
1266
|
+
throw new Error(`${source} route ${index} has an invalid 'port'.`);
|
|
1267
|
+
}
|
|
1268
|
+
if (item.mode !== "run" && item.mode !== "attach" && item.mode !== "proxy") {
|
|
1269
|
+
throw new Error(`${source} route ${index} has an invalid 'mode'.`);
|
|
1270
|
+
}
|
|
1271
|
+
if (item.protocol !== void 0 && item.protocol !== "http" && item.protocol !== "tcp") {
|
|
1272
|
+
throw new Error(`${source} route ${index} has an invalid 'protocol'.`);
|
|
1273
|
+
}
|
|
1274
|
+
for (const key of ["tcpProtocol", "upstreamHost", "command", "workspace"]) {
|
|
1275
|
+
if (item[key] !== void 0 && typeof item[key] !== "string") {
|
|
1276
|
+
throw new Error(`${source} route ${index} has an invalid '${key}'.`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (item.pid !== void 0 && (!Number.isInteger(item.pid) || Number(item.pid) <= 0)) {
|
|
1280
|
+
throw new Error(`${source} route ${index} has an invalid 'pid'.`);
|
|
1281
|
+
}
|
|
1282
|
+
const route = item;
|
|
1283
|
+
if (route.id !== buildHostRouteId(route.repoPath, route.name)) {
|
|
1284
|
+
throw new Error(`${source} route ${index} has an inconsistent 'id'.`);
|
|
1285
|
+
}
|
|
1286
|
+
return route;
|
|
1287
|
+
});
|
|
1288
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1289
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1290
|
+
for (const route of routes) {
|
|
1291
|
+
if (ids.has(route.id)) {
|
|
1292
|
+
throw new Error(`${source} contains duplicate route id '${route.id}'.`);
|
|
1293
|
+
}
|
|
1294
|
+
if (hosts.has(route.host)) {
|
|
1295
|
+
throw new Error(`${source} contains duplicate route host '${route.host}'.`);
|
|
1296
|
+
}
|
|
1297
|
+
ids.add(route.id);
|
|
1298
|
+
hosts.add(route.host);
|
|
1299
|
+
}
|
|
1300
|
+
return routes;
|
|
1301
|
+
}
|
|
1302
|
+
function renderCompatibilityState(routes) {
|
|
1303
|
+
return `${JSON.stringify(routes, null, 2)}
|
|
1304
|
+
`;
|
|
1305
|
+
}
|
|
1306
|
+
function renderCanonicalState(routes, tlsEnabled) {
|
|
1307
|
+
const metadata = { version: 1, tlsEnabled, routes };
|
|
1308
|
+
const encoded = Buffer.from(JSON.stringify(metadata), "utf-8").toString("base64url");
|
|
1155
1309
|
const document = buildHostRoutesDocument(routes, tlsEnabled);
|
|
1156
|
-
|
|
1310
|
+
return `${ROUTE_METADATA_PREFIX}${encoded}
|
|
1311
|
+
${import_yaml.default.stringify(document, { lineWidth: 0 })}`;
|
|
1157
1312
|
}
|
|
1158
|
-
function
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1313
|
+
function writeRouteGeneration(routes, tlsEnabled) {
|
|
1314
|
+
writeFileAtomically(HOST_ROUTES_STATE_FILE, renderCompatibilityState(routes));
|
|
1315
|
+
writeFileAtomically(TRAEFIK_HOST_ROUTES_FILE, renderCanonicalState(routes, tlsEnabled));
|
|
1316
|
+
}
|
|
1317
|
+
function readCompatibilityState() {
|
|
1318
|
+
let raw;
|
|
1319
|
+
try {
|
|
1320
|
+
raw = import_node_fs6.default.readFileSync(HOST_ROUTES_STATE_FILE, "utf-8");
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`Could not read the compatibility host-route state: ${error.message}`
|
|
1166
1324
|
);
|
|
1167
1325
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1326
|
+
try {
|
|
1327
|
+
return validateRouteState(JSON.parse(raw), "Compatibility host-route state");
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
throw new Error(`Invalid compatibility host-route state: ${error.message}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function parseCanonicalState(raw) {
|
|
1333
|
+
const firstNewline = raw.indexOf("\n");
|
|
1334
|
+
const firstLine2 = firstNewline === -1 ? raw : raw.slice(0, firstNewline);
|
|
1335
|
+
if (!firstLine2.startsWith(ROUTE_METADATA_PREFIX)) {
|
|
1336
|
+
if (firstLine2.startsWith(ROUTE_METADATA_FAMILY_PREFIX)) {
|
|
1337
|
+
throw new Error(`Unsupported or malformed host-route metadata header '${firstLine2}'.`);
|
|
1338
|
+
}
|
|
1339
|
+
return { kind: "legacy" };
|
|
1340
|
+
}
|
|
1341
|
+
const encoded = firstLine2.slice(ROUTE_METADATA_PREFIX.length);
|
|
1342
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(encoded)) {
|
|
1343
|
+
throw new Error("Host-route metadata header is not valid base64url.");
|
|
1344
|
+
}
|
|
1345
|
+
let metadataValue;
|
|
1346
|
+
try {
|
|
1347
|
+
const decoded = Buffer.from(encoded, "base64url");
|
|
1348
|
+
if (decoded.toString("base64url") !== encoded) {
|
|
1349
|
+
throw new Error("non-canonical base64url encoding");
|
|
1350
|
+
}
|
|
1351
|
+
metadataValue = JSON.parse(decoded.toString("utf-8"));
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
throw new Error(`Host-route metadata header is invalid: ${error.message}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (!isRecord(metadataValue) || metadataValue.version !== 1) {
|
|
1356
|
+
throw new Error("Host-route metadata header has an unsupported version.");
|
|
1357
|
+
}
|
|
1358
|
+
if (typeof metadataValue.tlsEnabled !== "boolean") {
|
|
1359
|
+
throw new Error("Host-route metadata header has an invalid TLS state.");
|
|
1360
|
+
}
|
|
1361
|
+
const routes = validateRouteState(metadataValue.routes, "Canonical host-route metadata");
|
|
1362
|
+
const metadata = {
|
|
1363
|
+
version: 1,
|
|
1364
|
+
tlsEnabled: metadataValue.tlsEnabled,
|
|
1365
|
+
routes
|
|
1366
|
+
};
|
|
1367
|
+
let document;
|
|
1368
|
+
try {
|
|
1369
|
+
document = import_yaml.default.parse(firstNewline === -1 ? "" : raw.slice(firstNewline + 1));
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
throw new Error(`Canonical host-route document is invalid: ${error.message}`);
|
|
1372
|
+
}
|
|
1373
|
+
const expectedDocument = buildHostRoutesDocument(routes, metadata.tlsEnabled);
|
|
1374
|
+
if (!(0, import_node_util.isDeepStrictEqual)(document, expectedDocument)) {
|
|
1375
|
+
throw new Error("Canonical host-route document does not match its metadata.");
|
|
1170
1376
|
}
|
|
1377
|
+
return { kind: "canonical", metadata };
|
|
1378
|
+
}
|
|
1379
|
+
function ensureHostRouteStorage() {
|
|
1380
|
+
import_node_fs6.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
1381
|
+
import_node_fs6.default.mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
|
|
1171
1382
|
}
|
|
1172
1383
|
function writeState(routes) {
|
|
1173
1384
|
ensureHostRouteStorage();
|
|
1174
|
-
|
|
1175
|
-
`, "utf-8");
|
|
1176
|
-
writeHostRoutesDynamicFile(routes, isTLSEnabled());
|
|
1385
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1177
1386
|
}
|
|
1178
1387
|
function withStateLock(fn) {
|
|
1179
1388
|
ensureHostRouteStorage();
|
|
1180
1389
|
return withFileLockSync(STATE_LOCK_FILE, { activity: "host route update", waitMs: 5e3 }, fn);
|
|
1181
1390
|
}
|
|
1182
1391
|
function refreshHostRoutesDynamicFile() {
|
|
1183
|
-
|
|
1184
|
-
|
|
1392
|
+
withStateLock(() => {
|
|
1393
|
+
const routes = readHostRouteStateLocked();
|
|
1394
|
+
writeState(routes);
|
|
1395
|
+
});
|
|
1185
1396
|
}
|
|
1186
|
-
function
|
|
1397
|
+
function readHostRouteStateLocked() {
|
|
1187
1398
|
ensureHostRouteStorage();
|
|
1188
|
-
if (!
|
|
1399
|
+
if (!import_node_fs6.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
|
|
1400
|
+
if (import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1401
|
+
const routes = readCompatibilityState();
|
|
1402
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1403
|
+
return routes;
|
|
1404
|
+
}
|
|
1405
|
+
writeRouteGeneration([], isTLSEnabled());
|
|
1189
1406
|
return [];
|
|
1190
1407
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if (!
|
|
1408
|
+
const raw = import_node_fs6.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
|
|
1409
|
+
const canonical = parseCanonicalState(raw);
|
|
1410
|
+
if (canonical.kind === "legacy") {
|
|
1411
|
+
if (!import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1412
|
+
throw new Error(
|
|
1413
|
+
"Headerless host-route document requires a valid compatibility state file for migration."
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
const routes = readCompatibilityState();
|
|
1417
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1418
|
+
return routes;
|
|
1419
|
+
}
|
|
1420
|
+
let mirrorMatches = false;
|
|
1421
|
+
if (import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1422
|
+
try {
|
|
1423
|
+
mirrorMatches = (0, import_node_util.isDeepStrictEqual)(readCompatibilityState(), canonical.metadata.routes);
|
|
1424
|
+
} catch {
|
|
1425
|
+
mirrorMatches = false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (!mirrorMatches) {
|
|
1429
|
+
writeFileAtomically(
|
|
1430
|
+
HOST_ROUTES_STATE_FILE,
|
|
1431
|
+
renderCompatibilityState(canonical.metadata.routes)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
return canonical.metadata.routes;
|
|
1435
|
+
}
|
|
1436
|
+
function readHostRouteStateReadOnly() {
|
|
1437
|
+
if (!import_node_fs6.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
|
|
1438
|
+
if (!import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1195
1439
|
return [];
|
|
1196
1440
|
}
|
|
1197
|
-
return
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1441
|
+
return readCompatibilityState();
|
|
1442
|
+
}
|
|
1443
|
+
const raw = import_node_fs6.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
|
|
1444
|
+
const canonical = parseCanonicalState(raw);
|
|
1445
|
+
if (canonical.kind === "legacy") {
|
|
1446
|
+
if (!import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
"Headerless host-route document requires a valid compatibility state file for inspection."
|
|
1204
1449
|
);
|
|
1205
1450
|
}
|
|
1206
|
-
return
|
|
1451
|
+
return readCompatibilityState();
|
|
1207
1452
|
}
|
|
1453
|
+
return canonical.metadata.routes;
|
|
1454
|
+
}
|
|
1455
|
+
function listHostRouteState() {
|
|
1456
|
+
return withStateLock(readHostRouteStateLocked);
|
|
1208
1457
|
}
|
|
1209
1458
|
function upsertHostRoute(input2) {
|
|
1210
1459
|
return withStateLock(() => {
|
|
1211
|
-
const routes =
|
|
1460
|
+
const routes = readHostRouteStateLocked();
|
|
1212
1461
|
const id = buildHostRouteId(input2.repoPath, input2.name);
|
|
1213
1462
|
const existing = routes.find((route) => route.id === id);
|
|
1214
1463
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1225,7 +1474,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1225
1474
|
if (inputs.some((input2) => !sameWorkspacePath(input2.repoPath, repoPath))) {
|
|
1226
1475
|
throw new Error(`Route replacement contains an entry outside '${repoPath}'.`);
|
|
1227
1476
|
}
|
|
1228
|
-
const routes =
|
|
1477
|
+
const routes = readHostRouteStateLocked();
|
|
1229
1478
|
const remaining = routes.filter((route) => !sameWorkspacePath(route.repoPath, repoPath));
|
|
1230
1479
|
const names = /* @__PURE__ */ new Set();
|
|
1231
1480
|
const hosts = /* @__PURE__ */ new Set();
|
|
@@ -1263,7 +1512,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1263
1512
|
}
|
|
1264
1513
|
function removeHostRouteById(id) {
|
|
1265
1514
|
return withStateLock(() => {
|
|
1266
|
-
const routes =
|
|
1515
|
+
const routes = readHostRouteStateLocked();
|
|
1267
1516
|
const next = routes.filter((route) => route.id !== id);
|
|
1268
1517
|
if (next.length === routes.length) {
|
|
1269
1518
|
return false;
|
|
@@ -1274,7 +1523,7 @@ function removeHostRouteById(id) {
|
|
|
1274
1523
|
}
|
|
1275
1524
|
function removeHostRoutesWhere(predicate) {
|
|
1276
1525
|
return withStateLock(() => {
|
|
1277
|
-
const routes =
|
|
1526
|
+
const routes = readHostRouteStateLocked();
|
|
1278
1527
|
const removed = routes.filter(predicate);
|
|
1279
1528
|
if (removed.length === 0) {
|
|
1280
1529
|
return [];
|
|
@@ -1296,7 +1545,7 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1296
1545
|
protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
|
|
1297
1546
|
appName: route.name,
|
|
1298
1547
|
serviceName: route.name,
|
|
1299
|
-
projectName:
|
|
1548
|
+
projectName: import_node_path5.default.basename(route.repoPath),
|
|
1300
1549
|
hosts: [route.host],
|
|
1301
1550
|
// TCP routes are reached via an SNI-aware client on the shared protocol
|
|
1302
1551
|
// port; surface a protocol-scheme URL rather than http(s)://.
|
|
@@ -1311,13 +1560,15 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1311
1560
|
};
|
|
1312
1561
|
});
|
|
1313
1562
|
}
|
|
1314
|
-
var
|
|
1563
|
+
var import_node_fs6, import_node_path5, import_node_util, import_yaml, LOOPBACK_HOSTS, UPSTREAM_RE, TCP_TLS_ALPN_PROTOCOLS, ROUTE_METADATA_PREFIX, ROUTE_METADATA_FAMILY_PREFIX, STATE_LOCK_FILE;
|
|
1315
1564
|
var init_host_routes = __esm({
|
|
1316
1565
|
"src/core/host-routes.ts"() {
|
|
1317
1566
|
"use strict";
|
|
1318
|
-
|
|
1319
|
-
|
|
1567
|
+
import_node_fs6 = __toESM(require("fs"));
|
|
1568
|
+
import_node_path5 = __toESM(require("path"));
|
|
1569
|
+
import_node_util = require("util");
|
|
1320
1570
|
import_yaml = __toESM(require("yaml"));
|
|
1571
|
+
init_atomic_file();
|
|
1321
1572
|
init_file_lock();
|
|
1322
1573
|
init_router();
|
|
1323
1574
|
init_workspace();
|
|
@@ -1326,6 +1577,8 @@ var init_host_routes = __esm({
|
|
|
1326
1577
|
TCP_TLS_ALPN_PROTOCOLS = {
|
|
1327
1578
|
postgres: ["postgresql"]
|
|
1328
1579
|
};
|
|
1580
|
+
ROUTE_METADATA_PREFIX = "# devrouter-routes-v1: ";
|
|
1581
|
+
ROUTE_METADATA_FAMILY_PREFIX = "# devrouter-routes-";
|
|
1329
1582
|
STATE_LOCK_FILE = `${HOST_ROUTES_STATE_FILE}.lock`;
|
|
1330
1583
|
}
|
|
1331
1584
|
});
|
|
@@ -1753,10 +2006,10 @@ function renderConfig(config) {
|
|
|
1753
2006
|
return import_yaml2.default.stringify(config, { lineWidth: 0 });
|
|
1754
2007
|
}
|
|
1755
2008
|
function readConfigDocument(configPath) {
|
|
1756
|
-
return (0, import_yaml2.parseDocument)(
|
|
2009
|
+
return (0, import_yaml2.parseDocument)(import_node_fs7.default.readFileSync(configPath, "utf-8"));
|
|
1757
2010
|
}
|
|
1758
2011
|
function writeConfigDocument(configPath, doc) {
|
|
1759
|
-
|
|
2012
|
+
import_node_fs7.default.writeFileSync(configPath, doc.toString({ lineWidth: 0 }), "utf-8");
|
|
1760
2013
|
}
|
|
1761
2014
|
function getOrCreateAppsSeq(doc) {
|
|
1762
2015
|
const apps = doc.get("apps", true);
|
|
@@ -1778,25 +2031,25 @@ function appToNodeValue(app) {
|
|
|
1778
2031
|
return value;
|
|
1779
2032
|
}
|
|
1780
2033
|
function resolveRepoPath(repoPath) {
|
|
1781
|
-
return
|
|
2034
|
+
return import_node_path6.default.resolve(repoPath ?? process.cwd());
|
|
1782
2035
|
}
|
|
1783
2036
|
function getRepoConfigPath(repoPath) {
|
|
1784
|
-
return
|
|
2037
|
+
return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
1785
2038
|
}
|
|
1786
2039
|
function loadRepoConfig(repoPath) {
|
|
1787
2040
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1788
2041
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1789
|
-
if (!
|
|
2042
|
+
if (!import_node_fs7.default.existsSync(configPath)) {
|
|
1790
2043
|
throw new Error(
|
|
1791
2044
|
`Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
|
|
1792
2045
|
);
|
|
1793
2046
|
}
|
|
1794
|
-
const raw =
|
|
2047
|
+
const raw = import_node_fs7.default.readFileSync(configPath, "utf-8");
|
|
1795
2048
|
const parsed = import_yaml2.default.parse(raw);
|
|
1796
2049
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
1797
2050
|
const requiredVersion = config.devrouter?.version;
|
|
1798
2051
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
1799
|
-
const cliVersion = true ? "0.0.
|
|
2052
|
+
const cliVersion = true ? "0.0.36" : "0.0.0-dev";
|
|
1800
2053
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
1801
2054
|
hasWarnedVersionMismatch = true;
|
|
1802
2055
|
process.stderr.write(
|
|
@@ -1813,7 +2066,7 @@ function loadRepoConfig(repoPath) {
|
|
|
1813
2066
|
function initRepoConfig(repoPath, options = {}) {
|
|
1814
2067
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1815
2068
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1816
|
-
if (
|
|
2069
|
+
if (import_node_fs7.default.existsSync(configPath)) {
|
|
1817
2070
|
return { repoPath: resolvedRepoPath, configPath, created: false };
|
|
1818
2071
|
}
|
|
1819
2072
|
if (options.devrouterVersion !== void 0 && !DEVROUTER_VERSION_RE.test(options.devrouterVersion)) {
|
|
@@ -1829,11 +2082,11 @@ function initRepoConfig(repoPath, options = {}) {
|
|
|
1829
2082
|
}
|
|
1830
2083
|
} : {},
|
|
1831
2084
|
project: {
|
|
1832
|
-
name:
|
|
2085
|
+
name: import_node_path6.default.basename(resolvedRepoPath)
|
|
1833
2086
|
},
|
|
1834
2087
|
apps: []
|
|
1835
2088
|
};
|
|
1836
|
-
|
|
2089
|
+
import_node_fs7.default.writeFileSync(configPath, renderConfig(initialConfig), "utf-8");
|
|
1837
2090
|
return { repoPath: resolvedRepoPath, configPath, created: true };
|
|
1838
2091
|
}
|
|
1839
2092
|
function buildAppFromOptions(options) {
|
|
@@ -2058,7 +2311,7 @@ function namespaceHost(host, workspace) {
|
|
|
2058
2311
|
return `${base}.${workspace}${suffix}`;
|
|
2059
2312
|
}
|
|
2060
2313
|
function applyWorkspace(config, workspace, repoPath) {
|
|
2061
|
-
const defaultToken = wsFromBranch(config.project?.name ??
|
|
2314
|
+
const defaultToken = wsFromBranch(config.project?.name ?? import_node_path6.default.basename(import_node_path6.default.resolve(repoPath))) ?? "app";
|
|
2062
2315
|
const substitutionToken = workspace ?? defaultToken;
|
|
2063
2316
|
const next = structuredClone(config);
|
|
2064
2317
|
for (const app of next.apps) {
|
|
@@ -2133,12 +2386,12 @@ function resolveAppDependencies(config, app) {
|
|
|
2133
2386
|
}
|
|
2134
2387
|
return results;
|
|
2135
2388
|
}
|
|
2136
|
-
var
|
|
2389
|
+
var import_node_fs7, import_node_path6, import_yaml2, hasWarnedVersionMismatch, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY;
|
|
2137
2390
|
var init_repo_config = __esm({
|
|
2138
2391
|
"src/core/repo-config.ts"() {
|
|
2139
2392
|
"use strict";
|
|
2140
|
-
|
|
2141
|
-
|
|
2393
|
+
import_node_fs7 = __toESM(require("fs"));
|
|
2394
|
+
import_node_path6 = __toESM(require("path"));
|
|
2142
2395
|
import_yaml2 = __toESM(require("yaml"));
|
|
2143
2396
|
init_capabilities();
|
|
2144
2397
|
init_host_routes();
|
|
@@ -2205,6 +2458,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2205
2458
|
"- Shared Traefik router owns host ports 80 (HTTP), 443 (HTTPS), and 5432 (Postgres TCP).",
|
|
2206
2459
|
"- Per-repo source of truth is REPO_PATH/.devrouter.yml only.",
|
|
2207
2460
|
"- Global generated/runtime artifacts are managed under ~/.config/devrouter (do not edit these manually).",
|
|
2461
|
+
"- Managed DevPod lifecycle uses devrouter ensure/stop/workspace commands; raw devpod mutations bypass exact checkout ownership checks.",
|
|
2208
2462
|
"",
|
|
2209
2463
|
"Inputs:",
|
|
2210
2464
|
`- REPO_PATH=${repoPath}`,
|
|
@@ -2248,6 +2502,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2248
2502
|
"- if kind=app and runtime=proxy:",
|
|
2249
2503
|
' - upstream: "host:port" (an already-running port, e.g. a devcontainer published on 127.0.0.1:3000, or a container reachable by name on a shared Docker network such as `derivatives-db:5432`)',
|
|
2250
2504
|
` - upstream may use the \`${WORKSPACE_PLACEHOLDER}\` placeholder (e.g. \`${WORKSPACE_PLACEHOLDER}-app:3000\`) to target a per-workspace devcontainer alias; it is substituted with the resolved workspace token at runtime and re-validated. Do NOT put \`${WORKSPACE_PLACEHOLDER}\` in \`host\` (rejected) \u2014 the host is auto-namespaced.`,
|
|
2505
|
+
" - managed `devrouter ensure` requires every HTTP/TCP proxy upstream to begin with the exact resolved workspace/project alias prefix before it mutates DevPod or routes",
|
|
2251
2506
|
" - protocol=http registers an HTTP route; protocol=tcp registers a TLS-SNI TCP route and additionally requires tcpProtocol",
|
|
2252
2507
|
" - do not set hostRun/docker/dependencies (proxy only registers a route to the upstream)",
|
|
2253
2508
|
" - loopback hosts (localhost/127.0.0.1/0.0.0.0) are rewritten to host.docker.internal for Traefik",
|
|
@@ -2298,10 +2553,13 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2298
2553
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
2299
2554
|
`- 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.`,
|
|
2300
2555
|
"- 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.",
|
|
2301
|
-
`- 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.`,
|
|
2556
|
+
`- 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. Managed ensure rejects every HTTP/TCP upstream outside that exact alias namespace before mutation. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
|
|
2302
2557
|
"- 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.",
|
|
2303
|
-
"- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause and `devrouter exec . -- <command...>` for container commands. `workspace up` creates linked worktrees; destructive
|
|
2558
|
+
"- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod mutations; they bypass the machine-global ownership lock. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
|
|
2559
|
+
"- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
|
|
2304
2560
|
"- 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.",
|
|
2561
|
+
"- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates DevPod, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, DevPod registration, runtime (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. Local DevPod list/status checks always run; `--check-merged` alone enables read-only origin and matching GitHub/GitLab checks. Treat `not-found` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit `gc`/`down` can remove exact stale registration only after expected-ID `NotFound` proof and ownership revalidation. DevPod `lastUsed` remains advisory.",
|
|
2562
|
+
'- `--measure-size` adds per-workspace storage consumption and stays read-only, but walks each worktree and runs `docker ps` / `docker inspect --size`, so leave it off for a quick report. Each row reports `worktree` and `containerWritable` bytes, which are reclaimable; `imageShared` bytes, which are not, because those image layers are shared with other containers and overlap across rows; and `reclaimable`, the sum of the reclaimable fields. Never add `imageShared` across rows. Any figure that cannot be trusted reports `{"status": "unknown", "reason": ...}` rather than zero, and a workspace with no container reports a measured `0`.',
|
|
2305
2563
|
"- 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.",
|
|
2306
2564
|
"- 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.",
|
|
2307
2565
|
"",
|
|
@@ -2465,8 +2723,8 @@ var init_ai_prompt = __esm({
|
|
|
2465
2723
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
2466
2724
|
},
|
|
2467
2725
|
{
|
|
2468
|
-
command: "devrouter stop [path] [--json]",
|
|
2469
|
-
purpose: "Stop the exact checkout DevPod and routes without
|
|
2726
|
+
command: "devrouter stop [path] [--delete] [--json]",
|
|
2727
|
+
purpose: "Stop the exact checkout DevPod and routes; --delete explicitly deletes the ownership-proven DevPod without removing the checkout."
|
|
2470
2728
|
},
|
|
2471
2729
|
{
|
|
2472
2730
|
command: "devrouter exec [path] -- <command...>",
|
|
@@ -2484,6 +2742,10 @@ var init_ai_prompt = __esm({
|
|
|
2484
2742
|
command: "devrouter workspace ls [--json]",
|
|
2485
2743
|
purpose: "List owner, Git, DevPod, route, path, and branch evidence for managed workspaces."
|
|
2486
2744
|
},
|
|
2745
|
+
{
|
|
2746
|
+
command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]",
|
|
2747
|
+
purpose: "Report-only evidence for managed linked workspaces; activity is advisory, --check-merged alone enables read-only origin/forge checks, and no --yes/apply path exists."
|
|
2748
|
+
},
|
|
2487
2749
|
{
|
|
2488
2750
|
command: "devrouter workspace stop <workspace|branch>",
|
|
2489
2751
|
purpose: "Stop the exact DevPod and remove exact routes while preserving the worktree, owner record, and data."
|
|
@@ -2567,6 +2829,90 @@ function printJSON(value) {
|
|
|
2567
2829
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
2568
2830
|
`);
|
|
2569
2831
|
}
|
|
2832
|
+
function printWorkspaceCleanupReport(report) {
|
|
2833
|
+
process.stdout.write("Workspace cleanup report (read-only)\n");
|
|
2834
|
+
process.stdout.write(
|
|
2835
|
+
`${renderTable(
|
|
2836
|
+
["FIELD", "VALUE"],
|
|
2837
|
+
[
|
|
2838
|
+
["Repo path", report.repoPath],
|
|
2839
|
+
["Generated", report.generatedAt],
|
|
2840
|
+
["Inactive threshold", report.inactiveFor],
|
|
2841
|
+
["Cutoff", report.cutoff],
|
|
2842
|
+
["Merged check", report.checkMerged ? "enabled" : "disabled"],
|
|
2843
|
+
["Size measurement", report.measureSize ? "enabled" : "disabled"],
|
|
2844
|
+
["Managed workspaces", String(report.workspaces.length)]
|
|
2845
|
+
]
|
|
2846
|
+
)}
|
|
2847
|
+
`
|
|
2848
|
+
);
|
|
2849
|
+
if (report.workspaces.length === 0) {
|
|
2850
|
+
process.stdout.write("\nNo managed linked workspaces found.\n");
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
for (const row of report.workspaces) {
|
|
2854
|
+
process.stdout.write(`
|
|
2855
|
+
Workspace: ${row.workspace}
|
|
2856
|
+
`);
|
|
2857
|
+
process.stdout.write(
|
|
2858
|
+
`${renderTable(
|
|
2859
|
+
["FIELD", "VALUE"],
|
|
2860
|
+
[
|
|
2861
|
+
["Branch", row.branch ?? "-"],
|
|
2862
|
+
["Repo", row.repo],
|
|
2863
|
+
["Worktree", row.worktreePath],
|
|
2864
|
+
["DevPod", `${row.devpodId} (${row.provider})`],
|
|
2865
|
+
["Runtime", row.runtime],
|
|
2866
|
+
["Ownership", row.ownership],
|
|
2867
|
+
["Checkout", row.checkout],
|
|
2868
|
+
["Route", row.route],
|
|
2869
|
+
["Activity", row.activity],
|
|
2870
|
+
["Cutoff", row.cutoff],
|
|
2871
|
+
["Latest activity", row.latestTimestamp ?? "-"],
|
|
2872
|
+
["Contributing evidence", row.contributingEvidence.join(", ") || "-"],
|
|
2873
|
+
[
|
|
2874
|
+
"Activity evidence",
|
|
2875
|
+
row.activityEvidence.map(
|
|
2876
|
+
(entry) => `${entry.source}=${entry.status}${entry.timestamp ? `@${entry.timestamp}` : ""}`
|
|
2877
|
+
).join(", ")
|
|
2878
|
+
],
|
|
2879
|
+
["Integration", row.integration],
|
|
2880
|
+
["Eligible actions", row.eligibleActions.join("; ") || "none"],
|
|
2881
|
+
[
|
|
2882
|
+
"Suggestions",
|
|
2883
|
+
row.suggestions.map((suggestion) => `${suggestion.command} (${suggestion.reason})`).join("; ") || "none"
|
|
2884
|
+
],
|
|
2885
|
+
["Reasons", row.reasons.join(" | ") || "none"],
|
|
2886
|
+
...row.consumption ? [
|
|
2887
|
+
["Reclaimable total", formatCleanupSize(row.consumption.reclaimable)],
|
|
2888
|
+
[" worktree files", formatCleanupSize(row.consumption.worktree)],
|
|
2889
|
+
[" container writable", formatCleanupSize(row.consumption.containerWritable)],
|
|
2890
|
+
[
|
|
2891
|
+
"Shared image layers",
|
|
2892
|
+
// The qualifier describes a size, so it is dropped when there
|
|
2893
|
+
// is no size to qualify: "unknown (reason) (not reclaimable)"
|
|
2894
|
+
// reads as two competing answers to the same question.
|
|
2895
|
+
row.consumption.imageShared.status === "measured" ? `${formatCleanupSize(row.consumption.imageShared)} (not reclaimable)` : formatCleanupSize(row.consumption.imageShared)
|
|
2896
|
+
]
|
|
2897
|
+
] : []
|
|
2898
|
+
]
|
|
2899
|
+
)}
|
|
2900
|
+
`
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
function formatCleanupSize(size) {
|
|
2905
|
+
if (size.status === "unknown") {
|
|
2906
|
+
return `unknown (${size.reason})`;
|
|
2907
|
+
}
|
|
2908
|
+
let value = size.bytes;
|
|
2909
|
+
let unit = 0;
|
|
2910
|
+
while (value >= 1024 && unit < BINARY_SIZE_UNITS.length - 1) {
|
|
2911
|
+
value /= 1024;
|
|
2912
|
+
unit += 1;
|
|
2913
|
+
}
|
|
2914
|
+
return unit === 0 ? `${value} B` : `${value.toFixed(1)} ${BINARY_SIZE_UNITS[unit]}`;
|
|
2915
|
+
}
|
|
2570
2916
|
function printStatus(status) {
|
|
2571
2917
|
const rows = [
|
|
2572
2918
|
["Docker context", status.dockerContext],
|
|
@@ -2770,11 +3116,13 @@ function printSetupReport(report) {
|
|
|
2770
3116
|
}
|
|
2771
3117
|
}
|
|
2772
3118
|
}
|
|
3119
|
+
var BINARY_SIZE_UNITS;
|
|
2773
3120
|
var init_output = __esm({
|
|
2774
3121
|
"src/core/output.ts"() {
|
|
2775
3122
|
"use strict";
|
|
2776
3123
|
init_table();
|
|
2777
3124
|
init_timeago();
|
|
3125
|
+
BINARY_SIZE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
2778
3126
|
}
|
|
2779
3127
|
});
|
|
2780
3128
|
|
|
@@ -2883,11 +3231,11 @@ function extractCurrentVersionFromRepoConfig(repoPath) {
|
|
|
2883
3231
|
};
|
|
2884
3232
|
}
|
|
2885
3233
|
function readPromptDirectory(promptDirPath) {
|
|
2886
|
-
if (!
|
|
3234
|
+
if (!import_node_fs8.default.existsSync(promptDirPath)) {
|
|
2887
3235
|
throw new Error(`Missing ${UPGRADE_PROMPTS_DIR} directory at ${promptDirPath}.`);
|
|
2888
3236
|
}
|
|
2889
3237
|
const releases = [];
|
|
2890
|
-
const files =
|
|
3238
|
+
const files = import_node_fs8.default.readdirSync(promptDirPath, { withFileTypes: true });
|
|
2891
3239
|
for (const file of files) {
|
|
2892
3240
|
if (!file.isFile() || !file.name.endsWith(".md")) {
|
|
2893
3241
|
continue;
|
|
@@ -2897,8 +3245,8 @@ function readPromptDirectory(promptDirPath) {
|
|
|
2897
3245
|
continue;
|
|
2898
3246
|
}
|
|
2899
3247
|
const version = normalizeVersion(basename);
|
|
2900
|
-
const promptPath =
|
|
2901
|
-
const prompt =
|
|
3248
|
+
const promptPath = import_node_path7.default.join(promptDirPath, file.name);
|
|
3249
|
+
const prompt = import_node_fs8.default.readFileSync(promptPath, "utf-8").trim();
|
|
2902
3250
|
releases.push({ version, prompt, promptPath });
|
|
2903
3251
|
}
|
|
2904
3252
|
const deduped = /* @__PURE__ */ new Map();
|
|
@@ -2919,19 +3267,19 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
|
|
|
2919
3267
|
}
|
|
2920
3268
|
function resolvePromptDirectory(explicitPath) {
|
|
2921
3269
|
if (explicitPath) {
|
|
2922
|
-
return
|
|
3270
|
+
return import_node_path7.default.resolve(explicitPath);
|
|
2923
3271
|
}
|
|
2924
|
-
const entryFile = process.argv[1] ?
|
|
2925
|
-
const entryDir =
|
|
2926
|
-
const resolvedEntryDir =
|
|
3272
|
+
const entryFile = process.argv[1] ? import_node_path7.default.resolve(process.argv[1]) : __filename;
|
|
3273
|
+
const entryDir = import_node_path7.default.dirname(entryFile);
|
|
3274
|
+
const resolvedEntryDir = import_node_fs8.default.existsSync(entryFile) ? import_node_path7.default.dirname(import_node_fs8.default.realpathSync(entryFile)) : entryDir;
|
|
2927
3275
|
const candidates = [
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3276
|
+
import_node_path7.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
3277
|
+
import_node_path7.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
3278
|
+
import_node_path7.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
|
|
3279
|
+
import_node_path7.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
|
|
3280
|
+
import_node_path7.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
|
|
2933
3281
|
];
|
|
2934
|
-
const existing = candidates.find((candidate) =>
|
|
3282
|
+
const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
|
|
2935
3283
|
return existing ?? candidates[0];
|
|
2936
3284
|
}
|
|
2937
3285
|
function loadUpgradeCatalog(options = {}) {
|
|
@@ -2947,12 +3295,12 @@ function loadUpgradeCatalog(options = {}) {
|
|
|
2947
3295
|
releases
|
|
2948
3296
|
};
|
|
2949
3297
|
}
|
|
2950
|
-
var
|
|
3298
|
+
var import_node_fs8, import_node_path7, SEMVER_RE, UPGRADE_PROMPTS_DIR;
|
|
2951
3299
|
var init_upgrade = __esm({
|
|
2952
3300
|
"src/core/upgrade.ts"() {
|
|
2953
3301
|
"use strict";
|
|
2954
|
-
|
|
2955
|
-
|
|
3302
|
+
import_node_fs8 = __toESM(require("fs"));
|
|
3303
|
+
import_node_path7 = __toESM(require("path"));
|
|
2956
3304
|
init_repo_config();
|
|
2957
3305
|
SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
2958
3306
|
UPGRADE_PROMPTS_DIR = "upgrade-prompts";
|
|
@@ -3052,7 +3400,7 @@ async function getDockerodeConstructor() {
|
|
|
3052
3400
|
return dockerodeConstructorPromise;
|
|
3053
3401
|
}
|
|
3054
3402
|
function runDockerContextCommand(args) {
|
|
3055
|
-
const result = (0,
|
|
3403
|
+
const result = (0, import_node_child_process4.spawnSync)("docker", ["context", ...args], {
|
|
3056
3404
|
encoding: "utf-8"
|
|
3057
3405
|
});
|
|
3058
3406
|
if (result.status !== 0) {
|
|
@@ -3065,7 +3413,7 @@ function getCurrentDockerContext() {
|
|
|
3065
3413
|
return runDockerContextCommand(["show"]);
|
|
3066
3414
|
}
|
|
3067
3415
|
function getDockerHostFromContext(context) {
|
|
3068
|
-
const result = (0,
|
|
3416
|
+
const result = (0, import_node_child_process4.spawnSync)(
|
|
3069
3417
|
"docker",
|
|
3070
3418
|
["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
|
|
3071
3419
|
{ encoding: "utf-8" }
|
|
@@ -3133,15 +3481,177 @@ async function networkExists(name) {
|
|
|
3133
3481
|
return false;
|
|
3134
3482
|
}
|
|
3135
3483
|
}
|
|
3136
|
-
var
|
|
3484
|
+
var import_node_child_process4, dockerodeConstructorPromise;
|
|
3137
3485
|
var init_docker = __esm({
|
|
3138
3486
|
"src/core/docker.ts"() {
|
|
3139
3487
|
"use strict";
|
|
3140
|
-
|
|
3488
|
+
import_node_child_process4 = require("child_process");
|
|
3141
3489
|
dockerodeConstructorPromise = null;
|
|
3142
3490
|
}
|
|
3143
3491
|
});
|
|
3144
3492
|
|
|
3493
|
+
// src/core/managed-post-start.ts
|
|
3494
|
+
function renderDeliveryScript(targetPath) {
|
|
3495
|
+
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
3496
|
+
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
3497
|
+
}
|
|
3498
|
+
const temporaryPrefix = import_node_path8.default.posix.basename(targetPath);
|
|
3499
|
+
return `set -eu
|
|
3500
|
+
umask 077
|
|
3501
|
+
runtime_root=/tmp/devrouter
|
|
3502
|
+
runtime_bin="$runtime_root/bin"
|
|
3503
|
+
target="${targetPath}"
|
|
3504
|
+
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
3505
|
+
echo "Refusing symlinked devrouter runtime path." >&2
|
|
3506
|
+
exit 1
|
|
3507
|
+
fi
|
|
3508
|
+
mkdir -p "$runtime_bin"
|
|
3509
|
+
chmod 700 "$runtime_root" "$runtime_bin"
|
|
3510
|
+
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
3511
|
+
trap 'rm -f "$temporary"' EXIT
|
|
3512
|
+
cat > "$temporary"
|
|
3513
|
+
chmod 700 "$temporary"
|
|
3514
|
+
mv -f "$temporary" "$target"
|
|
3515
|
+
trap - EXIT
|
|
3516
|
+
`;
|
|
3517
|
+
}
|
|
3518
|
+
function commandFailure(result) {
|
|
3519
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
3520
|
+
}
|
|
3521
|
+
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
3522
|
+
const result = (0, import_node_child_process5.spawnSync)(
|
|
3523
|
+
"docker",
|
|
3524
|
+
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
3525
|
+
{ input: contents, encoding: "utf-8" }
|
|
3526
|
+
);
|
|
3527
|
+
if (result.status !== 0) {
|
|
3528
|
+
const details = commandFailure(result);
|
|
3529
|
+
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
function readRegularFileBytes(filePath) {
|
|
3533
|
+
if (!import_node_fs9.default.existsSync(filePath) || !import_node_fs9.default.lstatSync(filePath).isFile()) return void 0;
|
|
3534
|
+
return import_node_fs9.default.readFileSync(filePath);
|
|
3535
|
+
}
|
|
3536
|
+
function readRegularFile(filePath) {
|
|
3537
|
+
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
3538
|
+
}
|
|
3539
|
+
function adapterFingerprint(adapter) {
|
|
3540
|
+
return (0, import_node_crypto3.createHash)("sha256").update(adapter).digest("hex");
|
|
3541
|
+
}
|
|
3542
|
+
function resolveProcessHelperPath() {
|
|
3543
|
+
const candidates = [
|
|
3544
|
+
import_node_path8.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
3545
|
+
import_node_path8.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
3546
|
+
];
|
|
3547
|
+
const helperPath = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
|
|
3548
|
+
if (!helperPath) {
|
|
3549
|
+
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
3550
|
+
}
|
|
3551
|
+
return helperPath;
|
|
3552
|
+
}
|
|
3553
|
+
function resolveManagedPostStartPlan(repoPath) {
|
|
3554
|
+
const adapterPath = import_node_path8.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
3555
|
+
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
3556
|
+
const adapterText = adapterBytes?.toString("utf-8");
|
|
3557
|
+
const dockerfilePath = import_node_path8.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
3558
|
+
const devcontainerPath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
3559
|
+
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
3560
|
+
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
3561
|
+
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
3562
|
+
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
3563
|
+
);
|
|
3564
|
+
if (adapterBytes === void 0) {
|
|
3565
|
+
if (devrouterPattern) {
|
|
3566
|
+
throw new Error(
|
|
3567
|
+
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
return { kind: "unmanaged" };
|
|
3571
|
+
}
|
|
3572
|
+
const adapter = adapterBytes.toString("utf-8");
|
|
3573
|
+
if (!adapter.includes(MANAGED_MARKER)) {
|
|
3574
|
+
if (devrouterPattern) {
|
|
3575
|
+
throw new Error(
|
|
3576
|
+
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
return { kind: "unmanaged" };
|
|
3580
|
+
}
|
|
3581
|
+
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
3582
|
+
return {
|
|
3583
|
+
kind: "runtime",
|
|
3584
|
+
adapterPath: MANAGED_ADAPTER_PATH,
|
|
3585
|
+
adapterSha256: adapterFingerprint(adapterBytes),
|
|
3586
|
+
adapterContents: adapterBytes
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
3590
|
+
return { kind: "legacy" };
|
|
3591
|
+
}
|
|
3592
|
+
throw new Error(
|
|
3593
|
+
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
function runManagedPostStart(options) {
|
|
3597
|
+
if (options.plan.kind !== "runtime") return;
|
|
3598
|
+
const helper = import_node_fs9.default.readFileSync(resolveProcessHelperPath());
|
|
3599
|
+
deliverRuntimeFile(
|
|
3600
|
+
options.container.id,
|
|
3601
|
+
RUNTIME_HELPER_PATH,
|
|
3602
|
+
helper,
|
|
3603
|
+
"the managed process helper"
|
|
3604
|
+
);
|
|
3605
|
+
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
3606
|
+
deliverRuntimeFile(
|
|
3607
|
+
options.container.id,
|
|
3608
|
+
runtimeAdapterPath,
|
|
3609
|
+
options.plan.adapterContents,
|
|
3610
|
+
"the managed post-start adapter"
|
|
3611
|
+
);
|
|
3612
|
+
const started = (0, import_node_child_process5.spawnSync)(
|
|
3613
|
+
"docker",
|
|
3614
|
+
[
|
|
3615
|
+
"exec",
|
|
3616
|
+
"--workdir",
|
|
3617
|
+
options.container.workspacePath,
|
|
3618
|
+
"--env",
|
|
3619
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
3620
|
+
"--env",
|
|
3621
|
+
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
3622
|
+
options.container.id,
|
|
3623
|
+
"bash",
|
|
3624
|
+
"-c",
|
|
3625
|
+
ADAPTER_WRAPPER,
|
|
3626
|
+
options.plan.adapterPath,
|
|
3627
|
+
runtimeAdapterPath
|
|
3628
|
+
],
|
|
3629
|
+
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
3630
|
+
);
|
|
3631
|
+
if (started.status !== 0) {
|
|
3632
|
+
const details = commandFailure(started);
|
|
3633
|
+
throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
var import_node_child_process5, import_node_crypto3, import_node_fs9, import_node_path8, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
|
|
3637
|
+
var init_managed_post_start = __esm({
|
|
3638
|
+
"src/core/managed-post-start.ts"() {
|
|
3639
|
+
"use strict";
|
|
3640
|
+
import_node_child_process5 = require("child_process");
|
|
3641
|
+
import_node_crypto3 = require("crypto");
|
|
3642
|
+
import_node_fs9 = __toESM(require("fs"));
|
|
3643
|
+
import_node_path8 = __toESM(require("path"));
|
|
3644
|
+
MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
3645
|
+
MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
3646
|
+
RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
3647
|
+
ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
3648
|
+
shift
|
|
3649
|
+
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
|
|
3650
|
+
source "$adapter_snapshot"
|
|
3651
|
+
`;
|
|
3652
|
+
}
|
|
3653
|
+
});
|
|
3654
|
+
|
|
3145
3655
|
// src/core/devcontainer-diagnostics.ts
|
|
3146
3656
|
function asRecord(value) {
|
|
3147
3657
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3196,7 +3706,7 @@ function isLoopbackHost(host) {
|
|
|
3196
3706
|
return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
|
|
3197
3707
|
}
|
|
3198
3708
|
function inspectCompose(composeFile, _workspace) {
|
|
3199
|
-
if (!
|
|
3709
|
+
if (!import_node_fs10.default.existsSync(composeFile)) {
|
|
3200
3710
|
return {
|
|
3201
3711
|
aliases: [],
|
|
3202
3712
|
publishedPorts: [],
|
|
@@ -3205,7 +3715,7 @@ function inspectCompose(composeFile, _workspace) {
|
|
|
3205
3715
|
};
|
|
3206
3716
|
}
|
|
3207
3717
|
try {
|
|
3208
|
-
const raw =
|
|
3718
|
+
const raw = import_node_fs10.default.readFileSync(composeFile, "utf-8");
|
|
3209
3719
|
const parsed = import_yaml3.default.parse(raw);
|
|
3210
3720
|
const root = asRecord(parsed);
|
|
3211
3721
|
const services = asRecord(root?.services);
|
|
@@ -3262,11 +3772,11 @@ function routedProxyApps(config) {
|
|
|
3262
3772
|
);
|
|
3263
3773
|
}
|
|
3264
3774
|
function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
3265
|
-
const devcontainerDir =
|
|
3266
|
-
if (!
|
|
3775
|
+
const devcontainerDir = import_node_path9.default.join(repoPath, ".devcontainer");
|
|
3776
|
+
if (!import_node_fs10.default.existsSync(devcontainerDir)) {
|
|
3267
3777
|
return [];
|
|
3268
3778
|
}
|
|
3269
|
-
const compose = inspectCompose(
|
|
3779
|
+
const compose = inspectCompose(import_node_path9.default.join(devcontainerDir, "docker-compose.yml"), workspace);
|
|
3270
3780
|
const checks = [];
|
|
3271
3781
|
if (compose.parseError) {
|
|
3272
3782
|
checks.push({
|
|
@@ -3293,9 +3803,9 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3293
3803
|
details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
|
|
3294
3804
|
suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
|
|
3295
3805
|
});
|
|
3296
|
-
const dockerfilePath =
|
|
3297
|
-
const dockerfileExists =
|
|
3298
|
-
const dockerfile = dockerfileExists ?
|
|
3806
|
+
const dockerfilePath = import_node_path9.default.join(devcontainerDir, "Dockerfile");
|
|
3807
|
+
const dockerfileExists = import_node_fs10.default.existsSync(dockerfilePath);
|
|
3808
|
+
const dockerfile = dockerfileExists ? import_node_fs10.default.readFileSync(dockerfilePath, "utf-8") : "";
|
|
3299
3809
|
const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
|
|
3300
3810
|
(artifact) => dockerfile.includes(artifact)
|
|
3301
3811
|
);
|
|
@@ -3306,6 +3816,23 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3306
3816
|
details: devrouterArtifacts.length > 0 ? `artifacts=${devrouterArtifacts.join(", ")}` : void 0,
|
|
3307
3817
|
suggestion: !dockerfileExists ? "Inspect the consumer image definition and keep devrouter package/helper installation out of it." : devrouterArtifacts.length > 0 ? "Remove devrouter package/helper installation from the Dockerfile; devrouter ensure delivers the helper at runtime." : void 0
|
|
3308
3818
|
});
|
|
3819
|
+
try {
|
|
3820
|
+
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
3821
|
+
checks.push({
|
|
3822
|
+
id: "repo.devcontainer.managed-post-start",
|
|
3823
|
+
level: managedPostStart.kind === "legacy" ? "warn" : "ok",
|
|
3824
|
+
summary: managedPostStart.kind === "runtime" ? "Managed post-start uses runtime helper delivery with an exact adapter fingerprint." : managedPostStart.kind === "legacy" ? "Managed post-start still uses the legacy image-installed helper contract." : "No devrouter-managed post-start contract is configured.",
|
|
3825
|
+
suggestion: managedPostStart.kind === "legacy" ? "Regenerate or migrate the devcontainer to runtime helper delivery." : void 0
|
|
3826
|
+
});
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
checks.push({
|
|
3829
|
+
id: "repo.devcontainer.managed-post-start",
|
|
3830
|
+
level: "error",
|
|
3831
|
+
summary: "Devrouter lifecycle wiring is incomplete or ambiguous.",
|
|
3832
|
+
details: error instanceof Error ? error.message : String(error),
|
|
3833
|
+
suggestion: "Regenerate or migrate the managed devcontainer, or remove devrouter-specific wiring from a truly custom devcontainer."
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3309
3836
|
if (!config) {
|
|
3310
3837
|
checks.push({
|
|
3311
3838
|
id: "repo.devcontainer.upstream-alias-match",
|
|
@@ -3333,30 +3860,31 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3333
3860
|
});
|
|
3334
3861
|
return checks;
|
|
3335
3862
|
}
|
|
3336
|
-
var
|
|
3863
|
+
var import_node_fs10, import_node_path9, import_yaml3;
|
|
3337
3864
|
var init_devcontainer_diagnostics = __esm({
|
|
3338
3865
|
"src/core/devcontainer-diagnostics.ts"() {
|
|
3339
3866
|
"use strict";
|
|
3340
|
-
|
|
3341
|
-
|
|
3867
|
+
import_node_fs10 = __toESM(require("fs"));
|
|
3868
|
+
import_node_path9 = __toESM(require("path"));
|
|
3342
3869
|
import_yaml3 = __toESM(require("yaml"));
|
|
3870
|
+
init_managed_post_start();
|
|
3343
3871
|
}
|
|
3344
3872
|
});
|
|
3345
3873
|
|
|
3346
3874
|
// src/core/paths.ts
|
|
3347
3875
|
function assertPathWithinRepo(filePath, repoRoot, label) {
|
|
3348
|
-
const resolvedRoot =
|
|
3349
|
-
const resolved =
|
|
3350
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot +
|
|
3876
|
+
const resolvedRoot = import_node_path10.default.resolve(repoRoot);
|
|
3877
|
+
const resolved = import_node_path10.default.resolve(repoRoot, filePath);
|
|
3878
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path10.default.sep)) {
|
|
3351
3879
|
throw new Error(`${label} path '${filePath}' escapes the repository root.`);
|
|
3352
3880
|
}
|
|
3353
3881
|
return resolved;
|
|
3354
3882
|
}
|
|
3355
|
-
var
|
|
3883
|
+
var import_node_path10;
|
|
3356
3884
|
var init_paths = __esm({
|
|
3357
3885
|
"src/core/paths.ts"() {
|
|
3358
3886
|
"use strict";
|
|
3359
|
-
|
|
3887
|
+
import_node_path10 = __toESM(require("path"));
|
|
3360
3888
|
}
|
|
3361
3889
|
});
|
|
3362
3890
|
|
|
@@ -3441,11 +3969,11 @@ function reconcileRouteRunConflict(repoPath, app) {
|
|
|
3441
3969
|
}
|
|
3442
3970
|
}
|
|
3443
3971
|
}
|
|
3444
|
-
var
|
|
3972
|
+
var import_node_fs11;
|
|
3445
3973
|
var init_route_state = __esm({
|
|
3446
3974
|
"src/core/route-state.ts"() {
|
|
3447
3975
|
"use strict";
|
|
3448
|
-
|
|
3976
|
+
import_node_fs11 = __toESM(require("fs"));
|
|
3449
3977
|
init_host_routes();
|
|
3450
3978
|
init_workspace();
|
|
3451
3979
|
}
|
|
@@ -3620,7 +4148,7 @@ function toRepoStatus(repoPath) {
|
|
|
3620
4148
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
3621
4149
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
3622
4150
|
const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
|
|
3623
|
-
const configExists =
|
|
4151
|
+
const configExists = import_node_fs12.default.existsSync(configPath);
|
|
3624
4152
|
if (!explicitRepo && !configExists) {
|
|
3625
4153
|
return void 0;
|
|
3626
4154
|
}
|
|
@@ -3747,11 +4275,11 @@ async function collectRouterStatus(repoPath) {
|
|
|
3747
4275
|
}
|
|
3748
4276
|
};
|
|
3749
4277
|
}
|
|
3750
|
-
var
|
|
4278
|
+
var import_node_fs12;
|
|
3751
4279
|
var init_status = __esm({
|
|
3752
4280
|
"src/core/status.ts"() {
|
|
3753
4281
|
"use strict";
|
|
3754
|
-
|
|
4282
|
+
import_node_fs12 = __toESM(require("fs"));
|
|
3755
4283
|
init_docker();
|
|
3756
4284
|
init_repo_config();
|
|
3757
4285
|
init_router();
|
|
@@ -3760,7 +4288,7 @@ var init_status = __esm({
|
|
|
3760
4288
|
|
|
3761
4289
|
// src/core/tls.ts
|
|
3762
4290
|
function runOrThrow(command, args) {
|
|
3763
|
-
const result = (0,
|
|
4291
|
+
const result = (0, import_node_child_process6.spawnSync)(command, args, {
|
|
3764
4292
|
encoding: "utf-8"
|
|
3765
4293
|
});
|
|
3766
4294
|
if (result.status !== 0) {
|
|
@@ -3770,7 +4298,7 @@ function runOrThrow(command, args) {
|
|
|
3770
4298
|
return result.stdout.trim();
|
|
3771
4299
|
}
|
|
3772
4300
|
function commandExists(command) {
|
|
3773
|
-
const result = (0,
|
|
4301
|
+
const result = (0, import_node_child_process6.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
|
|
3774
4302
|
return result.status === 0;
|
|
3775
4303
|
}
|
|
3776
4304
|
function ensureMkcert() {
|
|
@@ -3788,8 +4316,8 @@ function tlsSetupCommand(repoPath) {
|
|
|
3788
4316
|
}
|
|
3789
4317
|
function getMkcertRootCAPath(options = {}) {
|
|
3790
4318
|
ensureMkcert();
|
|
3791
|
-
const rootCAPath =
|
|
3792
|
-
if (!
|
|
4319
|
+
const rootCAPath = import_node_path11.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
|
|
4320
|
+
if (!import_node_fs13.default.existsSync(rootCAPath)) {
|
|
3793
4321
|
throw new Error(
|
|
3794
4322
|
`mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
3795
4323
|
);
|
|
@@ -3822,7 +4350,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
3822
4350
|
return normalizeUniqueHosts(names);
|
|
3823
4351
|
}
|
|
3824
4352
|
function parseCertificateDnsHosts(pem) {
|
|
3825
|
-
const certificate = new
|
|
4353
|
+
const certificate = new import_node_crypto4.X509Certificate(pem);
|
|
3826
4354
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
3827
4355
|
if (subjectAltName.length === 0) {
|
|
3828
4356
|
return [];
|
|
@@ -3855,10 +4383,10 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
|
3855
4383
|
);
|
|
3856
4384
|
}
|
|
3857
4385
|
function readCurrentCertificateHosts() {
|
|
3858
|
-
if (!
|
|
4386
|
+
if (!import_node_fs13.default.existsSync(CERT_FILE)) {
|
|
3859
4387
|
return [];
|
|
3860
4388
|
}
|
|
3861
|
-
const pem =
|
|
4389
|
+
const pem = import_node_fs13.default.readFileSync(CERT_FILE, "utf-8");
|
|
3862
4390
|
return parseCertificateDnsHosts(pem);
|
|
3863
4391
|
}
|
|
3864
4392
|
function currentCertificateHostsOrEmpty() {
|
|
@@ -3947,14 +4475,14 @@ Run: ${tlsSetupCommand(options.repoPath)}`
|
|
|
3947
4475
|
);
|
|
3948
4476
|
}
|
|
3949
4477
|
}
|
|
3950
|
-
var
|
|
4478
|
+
var import_node_child_process6, import_node_crypto4, import_node_fs13, import_node_path11, DEFAULT_TLS_CERT_HOSTS;
|
|
3951
4479
|
var init_tls = __esm({
|
|
3952
4480
|
"src/core/tls.ts"() {
|
|
3953
4481
|
"use strict";
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4482
|
+
import_node_child_process6 = require("child_process");
|
|
4483
|
+
import_node_crypto4 = require("crypto");
|
|
4484
|
+
import_node_fs13 = __toESM(require("fs"));
|
|
4485
|
+
import_node_path11 = __toESM(require("path"));
|
|
3958
4486
|
init_docker();
|
|
3959
4487
|
init_host_routes();
|
|
3960
4488
|
init_router();
|
|
@@ -3970,7 +4498,7 @@ function outputFromResult(result) {
|
|
|
3970
4498
|
return output2.length > 0 ? output2 : void 0;
|
|
3971
4499
|
}
|
|
3972
4500
|
function runTool(command, args = []) {
|
|
3973
|
-
const result = (0,
|
|
4501
|
+
const result = (0, import_node_child_process7.spawnSync)(command, args, {
|
|
3974
4502
|
encoding: "utf-8"
|
|
3975
4503
|
});
|
|
3976
4504
|
if (result.error) {
|
|
@@ -4027,12 +4555,12 @@ function parseMinimumNodeMajor(value) {
|
|
|
4027
4555
|
return Number(match[1]);
|
|
4028
4556
|
}
|
|
4029
4557
|
function readPackageJson(repoPath) {
|
|
4030
|
-
const packagePath =
|
|
4031
|
-
if (!
|
|
4558
|
+
const packagePath = import_node_path12.default.join(repoPath, "package.json");
|
|
4559
|
+
if (!import_node_fs14.default.existsSync(packagePath)) {
|
|
4032
4560
|
return void 0;
|
|
4033
4561
|
}
|
|
4034
4562
|
try {
|
|
4035
|
-
return JSON.parse(
|
|
4563
|
+
return JSON.parse(import_node_fs14.default.readFileSync(packagePath, "utf-8"));
|
|
4036
4564
|
} catch {
|
|
4037
4565
|
return void 0;
|
|
4038
4566
|
}
|
|
@@ -4127,19 +4655,21 @@ function buildGlobalToolChecks(repoPath) {
|
|
|
4127
4655
|
checks.push(nodeToolchainCheck(repoPath));
|
|
4128
4656
|
return checks;
|
|
4129
4657
|
}
|
|
4130
|
-
var
|
|
4658
|
+
var import_node_child_process7, import_node_fs14, import_node_path12;
|
|
4131
4659
|
var init_tool_diagnostics = __esm({
|
|
4132
4660
|
"src/core/tool-diagnostics.ts"() {
|
|
4133
4661
|
"use strict";
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4662
|
+
import_node_child_process7 = require("child_process");
|
|
4663
|
+
import_node_fs14 = __toESM(require("fs"));
|
|
4664
|
+
import_node_path12 = __toESM(require("path"));
|
|
4137
4665
|
}
|
|
4138
4666
|
});
|
|
4139
4667
|
|
|
4140
4668
|
// src/core/devpod-workspaces.ts
|
|
4141
4669
|
function listDevpodWorkspaces() {
|
|
4142
|
-
const result = (0,
|
|
4670
|
+
const result = (0, import_node_child_process8.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
|
|
4671
|
+
encoding: "utf-8"
|
|
4672
|
+
});
|
|
4143
4673
|
if (result.status !== 0) {
|
|
4144
4674
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4145
4675
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
@@ -4158,8 +4688,49 @@ function listDevpodWorkspaces() {
|
|
|
4158
4688
|
if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
|
|
4159
4689
|
throw new Error("devpod list returned a workspace without id/source.localFolder.");
|
|
4160
4690
|
}
|
|
4161
|
-
|
|
4691
|
+
const workspace = {
|
|
4692
|
+
id: candidate.id,
|
|
4693
|
+
source: { localFolder: candidate.source.localFolder }
|
|
4694
|
+
};
|
|
4695
|
+
if ("lastUsed" in candidate) {
|
|
4696
|
+
if (typeof candidate.lastUsed === "string") {
|
|
4697
|
+
workspace.lastUsed = candidate.lastUsed;
|
|
4698
|
+
} else {
|
|
4699
|
+
workspace.lastUsedMalformed = true;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return workspace;
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
function parseDevpodRuntimeStatus(output2, expectedId) {
|
|
4706
|
+
let parsed;
|
|
4707
|
+
try {
|
|
4708
|
+
parsed = JSON.parse(output2);
|
|
4709
|
+
} catch {
|
|
4710
|
+
return "unknown";
|
|
4711
|
+
}
|
|
4712
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.id !== expectedId) {
|
|
4713
|
+
return "unknown";
|
|
4714
|
+
}
|
|
4715
|
+
switch (parsed.state) {
|
|
4716
|
+
case "Running":
|
|
4717
|
+
return "running";
|
|
4718
|
+
case "Stopped":
|
|
4719
|
+
return "stopped";
|
|
4720
|
+
case "Busy":
|
|
4721
|
+
return "busy";
|
|
4722
|
+
case "NotFound":
|
|
4723
|
+
return "not-found";
|
|
4724
|
+
default:
|
|
4725
|
+
return "unknown";
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
function inspectDevpodRuntimeStatus(devpodId) {
|
|
4729
|
+
const result = (0, import_node_child_process8.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
|
|
4730
|
+
encoding: "utf-8"
|
|
4162
4731
|
});
|
|
4732
|
+
if (result.status !== 0 || result.error) return "unknown";
|
|
4733
|
+
return parseDevpodRuntimeStatus(result.stdout, devpodId);
|
|
4163
4734
|
}
|
|
4164
4735
|
function inspectDevpodWorkspaceOwnership(workspaces, devpodId, worktreePath) {
|
|
4165
4736
|
const idOwners = workspaces.filter((workspace) => workspace.id === devpodId);
|
|
@@ -4188,23 +4759,159 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
4188
4759
|
}
|
|
4189
4760
|
return matches[0];
|
|
4190
4761
|
}
|
|
4191
|
-
|
|
4192
|
-
const args = action2 === "delete" ? [action2, devpodId, "--ignore-not-found"] : [action2, devpodId];
|
|
4193
|
-
const result = (0, import_node_child_process6.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
4194
|
-
if (result.status !== 0) {
|
|
4195
|
-
const detail = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4196
|
-
throw new Error(`devpod ${action2} failed for '${devpodId}': ${detail || "unknown error"}`);
|
|
4197
|
-
}
|
|
4198
|
-
}
|
|
4199
|
-
var import_node_child_process6;
|
|
4762
|
+
var import_node_child_process8;
|
|
4200
4763
|
var init_devpod_workspaces = __esm({
|
|
4201
4764
|
"src/core/devpod-workspaces.ts"() {
|
|
4202
4765
|
"use strict";
|
|
4203
|
-
|
|
4766
|
+
import_node_child_process8 = require("child_process");
|
|
4204
4767
|
init_workspace();
|
|
4205
4768
|
}
|
|
4206
4769
|
});
|
|
4207
4770
|
|
|
4771
|
+
// src/core/devpod-mutation.ts
|
|
4772
|
+
function withMutationLock(activity, target, operation) {
|
|
4773
|
+
import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
4774
|
+
return withFileLockSync(
|
|
4775
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
4776
|
+
{ activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
|
|
4777
|
+
operation
|
|
4778
|
+
);
|
|
4779
|
+
}
|
|
4780
|
+
function commandFailure2(result) {
|
|
4781
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4782
|
+
}
|
|
4783
|
+
function runDevpodAction(action2, devpodId, force = false) {
|
|
4784
|
+
const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
|
|
4785
|
+
const result = (0, import_node_child_process9.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
4786
|
+
if (result.status !== 0) {
|
|
4787
|
+
throw new Error(
|
|
4788
|
+
`devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
function inspectExactOwnership(devpodId, worktreePath) {
|
|
4793
|
+
const ownership = inspectDevpodWorkspaceOwnership(listDevpodWorkspaces(), devpodId, worktreePath);
|
|
4794
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
4795
|
+
return ownership;
|
|
4796
|
+
}
|
|
4797
|
+
function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
|
|
4798
|
+
return withMutationLock(`DevPod ${action2}`, worktreePath, () => {
|
|
4799
|
+
const before = inspectExactOwnership(devpodId, worktreePath);
|
|
4800
|
+
if (before.status === "absent") return { status: "absent" };
|
|
4801
|
+
runDevpodAction(action2, devpodId);
|
|
4802
|
+
let after = inspectExactOwnership(devpodId, worktreePath);
|
|
4803
|
+
if (action2 === "stop" && after.status !== "owned") {
|
|
4804
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
4805
|
+
}
|
|
4806
|
+
if (action2 === "delete" && after.status === "owned") {
|
|
4807
|
+
const runtime = inspectDevpodRuntimeStatus(devpodId);
|
|
4808
|
+
if (runtime !== "not-found") {
|
|
4809
|
+
throw new Error(
|
|
4810
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
4811
|
+
);
|
|
4812
|
+
}
|
|
4813
|
+
after = inspectExactOwnership(devpodId, worktreePath);
|
|
4814
|
+
if (after.status === "owned") {
|
|
4815
|
+
runDevpodAction("delete", devpodId, true);
|
|
4816
|
+
after = inspectExactOwnership(devpodId, worktreePath);
|
|
4817
|
+
}
|
|
4818
|
+
if (after.status !== "absent") {
|
|
4819
|
+
throw new Error(
|
|
4820
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
4821
|
+
);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return { status: "changed" };
|
|
4825
|
+
});
|
|
4826
|
+
}
|
|
4827
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
4828
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
4829
|
+
}
|
|
4830
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
4831
|
+
return mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
4832
|
+
}
|
|
4833
|
+
function startDevpodWorkspace(options) {
|
|
4834
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
4835
|
+
return withMutationLock(activity, options.repoPath, () => {
|
|
4836
|
+
const workspaces = listDevpodWorkspaces();
|
|
4837
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
4838
|
+
if (devpodId) {
|
|
4839
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
4840
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
4841
|
+
if (options.recreate && before.status !== "owned") {
|
|
4842
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
4843
|
+
}
|
|
4844
|
+
} else if (options.recreate) {
|
|
4845
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
4846
|
+
}
|
|
4847
|
+
const args = ["up", options.repoPath];
|
|
4848
|
+
if (devpodId) args.push("--id", devpodId);
|
|
4849
|
+
args.push("--open-ide=false");
|
|
4850
|
+
if (options.workspace) {
|
|
4851
|
+
args.push(
|
|
4852
|
+
"--workspace-env",
|
|
4853
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
4854
|
+
"--workspace-env",
|
|
4855
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
4856
|
+
);
|
|
4857
|
+
}
|
|
4858
|
+
if (options.recreate) args.push("--recreate");
|
|
4859
|
+
const env = { ...process.env };
|
|
4860
|
+
if (options.workspace) {
|
|
4861
|
+
env.WORKSPACE = options.workspace.token;
|
|
4862
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
4863
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
4864
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
4865
|
+
} else {
|
|
4866
|
+
delete env.WORKSPACE;
|
|
4867
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
4868
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
4869
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
4870
|
+
}
|
|
4871
|
+
const result = (0, import_node_child_process9.spawnSync)("devpod", args, {
|
|
4872
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
4873
|
+
env
|
|
4874
|
+
});
|
|
4875
|
+
if (result.status !== 0) {
|
|
4876
|
+
throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
|
|
4877
|
+
}
|
|
4878
|
+
try {
|
|
4879
|
+
const attached = listDevpodWorkspaces();
|
|
4880
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
4881
|
+
if (!devpodId) {
|
|
4882
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
4883
|
+
}
|
|
4884
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
4885
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
4886
|
+
if (ownership.status !== "owned") {
|
|
4887
|
+
throw new Error(
|
|
4888
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
4891
|
+
return devpodId;
|
|
4892
|
+
} catch (error) {
|
|
4893
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4894
|
+
throw new DevpodStartPostconditionError(message);
|
|
4895
|
+
}
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
var import_node_child_process9, import_node_fs15, import_node_path13, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
4899
|
+
var init_devpod_mutation = __esm({
|
|
4900
|
+
"src/core/devpod-mutation.ts"() {
|
|
4901
|
+
"use strict";
|
|
4902
|
+
import_node_child_process9 = require("child_process");
|
|
4903
|
+
import_node_fs15 = __toESM(require("fs"));
|
|
4904
|
+
import_node_path13 = __toESM(require("path"));
|
|
4905
|
+
init_devpod_workspaces();
|
|
4906
|
+
init_file_lock();
|
|
4907
|
+
init_router();
|
|
4908
|
+
DEVPOD_MUTATION_LOCK_FILE = import_node_path13.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
4909
|
+
DEVPOD_MUTATION_WAIT_MS = 6e4;
|
|
4910
|
+
DevpodStartPostconditionError = class extends Error {
|
|
4911
|
+
};
|
|
4912
|
+
}
|
|
4913
|
+
});
|
|
4914
|
+
|
|
4208
4915
|
// src/core/workspace-ownership.ts
|
|
4209
4916
|
function commandError(command, repoPath, stderr) {
|
|
4210
4917
|
return new Error(
|
|
@@ -4212,18 +4919,20 @@ function commandError(command, repoPath, stderr) {
|
|
|
4212
4919
|
);
|
|
4213
4920
|
}
|
|
4214
4921
|
function resolveGitCommonDir(repoPath) {
|
|
4215
|
-
const result = (0,
|
|
4216
|
-
encoding: "utf-8"
|
|
4922
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
4923
|
+
encoding: "utf-8",
|
|
4924
|
+
env: READ_ONLY_GIT_ENV
|
|
4217
4925
|
});
|
|
4218
4926
|
const output2 = result.stdout.trim();
|
|
4219
4927
|
if (result.status !== 0 || !output2) {
|
|
4220
4928
|
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
4221
4929
|
}
|
|
4222
|
-
return comparableWorkspacePath(
|
|
4930
|
+
return comparableWorkspacePath(import_node_path14.default.isAbsolute(output2) ? output2 : import_node_path14.default.resolve(repoPath, output2));
|
|
4223
4931
|
}
|
|
4224
4932
|
function resolveGitTopLevel(repoPath) {
|
|
4225
|
-
const result = (0,
|
|
4226
|
-
encoding: "utf-8"
|
|
4933
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
4934
|
+
encoding: "utf-8",
|
|
4935
|
+
env: READ_ONLY_GIT_ENV
|
|
4227
4936
|
});
|
|
4228
4937
|
const output2 = result.stdout.trim();
|
|
4229
4938
|
if (result.status !== 0 || !output2) {
|
|
@@ -4232,8 +4941,9 @@ function resolveGitTopLevel(repoPath) {
|
|
|
4232
4941
|
return comparableWorkspacePath(output2);
|
|
4233
4942
|
}
|
|
4234
4943
|
function listGitWorktrees(repoPath) {
|
|
4235
|
-
const result = (0,
|
|
4236
|
-
encoding: "utf-8"
|
|
4944
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
4945
|
+
encoding: "utf-8",
|
|
4946
|
+
env: READ_ONLY_GIT_ENV
|
|
4237
4947
|
});
|
|
4238
4948
|
if (result.status !== 0) {
|
|
4239
4949
|
throw commandError("git worktree list", repoPath, result.stderr);
|
|
@@ -4268,7 +4978,7 @@ function listGitWorktrees(repoPath) {
|
|
|
4268
4978
|
return worktrees;
|
|
4269
4979
|
}
|
|
4270
4980
|
function ownershipDirectory(repoPath) {
|
|
4271
|
-
return
|
|
4981
|
+
return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
4272
4982
|
}
|
|
4273
4983
|
function validateWorkspace(value, label) {
|
|
4274
4984
|
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
@@ -4296,7 +5006,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4296
5006
|
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
4297
5007
|
);
|
|
4298
5008
|
}
|
|
4299
|
-
if (typeof candidate.worktreePath !== "string" || !
|
|
5009
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path14.default.isAbsolute(candidate.worktreePath)) {
|
|
4300
5010
|
throw new Error("invalid workspace ownership worktreePath");
|
|
4301
5011
|
}
|
|
4302
5012
|
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
@@ -4316,7 +5026,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4316
5026
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
4317
5027
|
let parsed;
|
|
4318
5028
|
try {
|
|
4319
|
-
parsed = JSON.parse(
|
|
5029
|
+
parsed = JSON.parse(import_node_fs16.default.readFileSync(filePath, "utf-8"));
|
|
4320
5030
|
} catch (error) {
|
|
4321
5031
|
if (error instanceof SyntaxError) {
|
|
4322
5032
|
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
@@ -4332,7 +5042,7 @@ function listWorkspaceOwnership(repoPath) {
|
|
|
4332
5042
|
function listWorkspaceOwnershipInDirectory(directory) {
|
|
4333
5043
|
let entries;
|
|
4334
5044
|
try {
|
|
4335
|
-
entries =
|
|
5045
|
+
entries = import_node_fs16.default.readdirSync(directory, { withFileTypes: true });
|
|
4336
5046
|
} catch (error) {
|
|
4337
5047
|
if (error.code === "ENOENT") return [];
|
|
4338
5048
|
throw error;
|
|
@@ -4340,14 +5050,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
|
|
|
4340
5050
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
4341
5051
|
const workspace = entry.name.slice(0, -".json".length);
|
|
4342
5052
|
validateWorkspace(workspace, "filename");
|
|
4343
|
-
return readRecordFile(
|
|
5053
|
+
return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
|
|
4344
5054
|
});
|
|
4345
5055
|
}
|
|
4346
5056
|
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
4347
5057
|
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
4348
5058
|
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
4349
5059
|
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
4350
|
-
const filePath =
|
|
5060
|
+
const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
|
|
4351
5061
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4352
5062
|
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
4353
5063
|
const existing = records.find((record2) => record2.workspace === workspace);
|
|
@@ -4378,23 +5088,14 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
|
4378
5088
|
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
4379
5089
|
updatedAt: validateTimestamp(now, "updatedAt")
|
|
4380
5090
|
};
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
import_node_fs13.default.writeFileSync(tempPath, `${JSON.stringify(record, null, 2)}
|
|
4384
|
-
`, {
|
|
4385
|
-
encoding: "utf-8",
|
|
4386
|
-
flag: "wx"
|
|
4387
|
-
});
|
|
4388
|
-
import_node_fs13.default.renameSync(tempPath, filePath);
|
|
4389
|
-
} finally {
|
|
4390
|
-
import_node_fs13.default.rmSync(tempPath, { force: true });
|
|
4391
|
-
}
|
|
5091
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
5092
|
+
`);
|
|
4392
5093
|
return record;
|
|
4393
5094
|
}
|
|
4394
5095
|
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
4395
|
-
const filePath =
|
|
5096
|
+
const filePath = import_node_path14.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
4396
5097
|
try {
|
|
4397
|
-
|
|
5098
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4398
5099
|
return true;
|
|
4399
5100
|
} catch (error) {
|
|
4400
5101
|
if (error.code === "ENOENT") return false;
|
|
@@ -4405,7 +5106,7 @@ function sameOwnershipRecord(left, right) {
|
|
|
4405
5106
|
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;
|
|
4406
5107
|
}
|
|
4407
5108
|
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
4408
|
-
const filePath =
|
|
5109
|
+
const filePath = import_node_path14.default.join(
|
|
4409
5110
|
directory,
|
|
4410
5111
|
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
4411
5112
|
);
|
|
@@ -4417,14 +5118,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
|
4417
5118
|
throw error;
|
|
4418
5119
|
}
|
|
4419
5120
|
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
4420
|
-
|
|
5121
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4421
5122
|
return "removed";
|
|
4422
5123
|
}
|
|
4423
5124
|
function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
4424
5125
|
const directory = ownershipDirectory(repoPath);
|
|
4425
|
-
|
|
5126
|
+
import_node_fs16.default.mkdirSync(directory, { recursive: true });
|
|
4426
5127
|
return withFileLockSync(
|
|
4427
|
-
|
|
5128
|
+
import_node_path14.default.join(directory, ".lock"),
|
|
4428
5129
|
{ activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
|
|
4429
5130
|
() => operation({
|
|
4430
5131
|
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
@@ -4455,7 +5156,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
|
4455
5156
|
if (worktree?.locked) {
|
|
4456
5157
|
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
4457
5158
|
}
|
|
4458
|
-
if ((!worktree || worktree.prunable) &&
|
|
5159
|
+
if ((!worktree || worktree.prunable) && import_node_fs16.default.existsSync(record.worktreePath)) {
|
|
4459
5160
|
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4460
5161
|
}
|
|
4461
5162
|
if (!worktree || worktree.prunable) {
|
|
@@ -4479,19 +5180,20 @@ function listMissingWorkspaceOwnership(repoPath) {
|
|
|
4479
5180
|
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
4480
5181
|
);
|
|
4481
5182
|
}
|
|
4482
|
-
var
|
|
5183
|
+
var import_node_child_process10, import_node_fs16, import_node_path14, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
4483
5184
|
var init_workspace_ownership = __esm({
|
|
4484
5185
|
"src/core/workspace-ownership.ts"() {
|
|
4485
5186
|
"use strict";
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
5187
|
+
import_node_child_process10 = require("child_process");
|
|
5188
|
+
import_node_fs16 = __toESM(require("fs"));
|
|
5189
|
+
import_node_path14 = __toESM(require("path"));
|
|
5190
|
+
init_atomic_file();
|
|
4490
5191
|
init_devpod_workspaces();
|
|
4491
5192
|
init_file_lock();
|
|
4492
5193
|
init_workspace();
|
|
5194
|
+
READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
4493
5195
|
OWNERSHIP_VERSION = 1;
|
|
4494
|
-
OWNERSHIP_DIR =
|
|
5196
|
+
OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
|
|
4495
5197
|
}
|
|
4496
5198
|
});
|
|
4497
5199
|
|
|
@@ -4505,10 +5207,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
4505
5207
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
4506
5208
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
4507
5209
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
4508
|
-
const localRoot =
|
|
5210
|
+
const localRoot = import_node_path15.default.join(comparableRepo, "trees") + import_node_path15.default.sep;
|
|
4509
5211
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
4510
|
-
const legacyPrefix = `${
|
|
4511
|
-
return
|
|
5212
|
+
const legacyPrefix = `${import_node_path15.default.basename(comparableRepo)}-`;
|
|
5213
|
+
return import_node_path15.default.dirname(comparableWorktree) === import_node_path15.default.dirname(comparableRepo) && import_node_path15.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
4512
5214
|
}
|
|
4513
5215
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
4514
5216
|
const actions = [
|
|
@@ -4636,17 +5338,6 @@ function revalidateCandidate(candidate, repoPath, transaction) {
|
|
|
4636
5338
|
}
|
|
4637
5339
|
};
|
|
4638
5340
|
}
|
|
4639
|
-
function recheckDevpodOwnership(record) {
|
|
4640
|
-
const ownership = inspectDevpodWorkspaceOwnership(
|
|
4641
|
-
listDevpodWorkspaces(),
|
|
4642
|
-
record.devpodId,
|
|
4643
|
-
record.worktreePath
|
|
4644
|
-
);
|
|
4645
|
-
if (ownership.status === "conflict") {
|
|
4646
|
-
throw new Error(ownership.reason);
|
|
4647
|
-
}
|
|
4648
|
-
return ownership.status;
|
|
4649
|
-
}
|
|
4650
5341
|
function failedAction(resource, error) {
|
|
4651
5342
|
return {
|
|
4652
5343
|
resource,
|
|
@@ -4676,8 +5367,8 @@ function applyCandidate(candidate, repoPath) {
|
|
|
4676
5367
|
const actions = [];
|
|
4677
5368
|
let devpodStatus;
|
|
4678
5369
|
try {
|
|
4679
|
-
|
|
4680
|
-
|
|
5370
|
+
const mutation = deleteOwnedDevpodWorkspace(record.devpodId, record.worktreePath);
|
|
5371
|
+
devpodStatus = mutation.status === "changed" ? "owned" : "absent";
|
|
4681
5372
|
} catch (error) {
|
|
4682
5373
|
return { ...fresh, actions: [failedAction("devpod", error)] };
|
|
4683
5374
|
}
|
|
@@ -4762,11 +5453,12 @@ function applyWorkspaceGc(plan) {
|
|
|
4762
5453
|
candidates
|
|
4763
5454
|
};
|
|
4764
5455
|
}
|
|
4765
|
-
var
|
|
5456
|
+
var import_node_path15;
|
|
4766
5457
|
var init_workspace_gc = __esm({
|
|
4767
5458
|
"src/core/workspace-gc.ts"() {
|
|
4768
5459
|
"use strict";
|
|
4769
|
-
|
|
5460
|
+
import_node_path15 = __toESM(require("path"));
|
|
5461
|
+
init_devpod_mutation();
|
|
4770
5462
|
init_devpod_workspaces();
|
|
4771
5463
|
init_host_routes();
|
|
4772
5464
|
init_repo_config();
|
|
@@ -4835,11 +5527,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
4835
5527
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
4836
5528
|
continue;
|
|
4837
5529
|
}
|
|
4838
|
-
if (!
|
|
5530
|
+
if (!import_node_fs17.default.existsSync(absolutePath)) {
|
|
4839
5531
|
continue;
|
|
4840
5532
|
}
|
|
4841
5533
|
try {
|
|
4842
|
-
const raw =
|
|
5534
|
+
const raw = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
|
|
4843
5535
|
const parsed = import_yaml4.default.parse(raw);
|
|
4844
5536
|
const root = asRecord2(parsed);
|
|
4845
5537
|
const services = asRecord2(root?.services);
|
|
@@ -5102,7 +5794,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
5102
5794
|
const config = runtimeConfig.config;
|
|
5103
5795
|
loadedConfig = config;
|
|
5104
5796
|
loadedWorkspace = runtimeConfig.workspace;
|
|
5105
|
-
const cliVersion = true ? "0.0.
|
|
5797
|
+
const cliVersion = true ? "0.0.36" : "0.0.0-dev";
|
|
5106
5798
|
const configVersion = config.devrouter?.version;
|
|
5107
5799
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
5108
5800
|
addCheck(checks, {
|
|
@@ -5132,9 +5824,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
5132
5824
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
5133
5825
|
app: app.name,
|
|
5134
5826
|
filePath,
|
|
5135
|
-
absolutePath:
|
|
5827
|
+
absolutePath: import_node_path16.default.resolve(repo.path, filePath)
|
|
5136
5828
|
}))
|
|
5137
|
-
).filter((entry) => !
|
|
5829
|
+
).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5138
5830
|
addCheck(checks, {
|
|
5139
5831
|
id: "repo.compose-files",
|
|
5140
5832
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -5177,8 +5869,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
5177
5869
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
5178
5870
|
app: app.name,
|
|
5179
5871
|
cwd: app.hostRun.cwd,
|
|
5180
|
-
absolutePath:
|
|
5181
|
-
})).filter((entry) => !
|
|
5872
|
+
absolutePath: import_node_path16.default.resolve(repo.path, app.hostRun.cwd)
|
|
5873
|
+
})).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5182
5874
|
addCheck(checks, {
|
|
5183
5875
|
id: "repo.host-cwd",
|
|
5184
5876
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -5322,12 +6014,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
5322
6014
|
nextSteps
|
|
5323
6015
|
};
|
|
5324
6016
|
}
|
|
5325
|
-
var
|
|
6017
|
+
var import_node_fs17, import_node_path16, import_yaml4, POSTGRES_DEFAULTS;
|
|
5326
6018
|
var init_doctor = __esm({
|
|
5327
6019
|
"src/core/doctor.ts"() {
|
|
5328
6020
|
"use strict";
|
|
5329
|
-
|
|
5330
|
-
|
|
6021
|
+
import_node_fs17 = __toESM(require("fs"));
|
|
6022
|
+
import_node_path16 = __toESM(require("path"));
|
|
5331
6023
|
import_yaml4 = __toESM(require("yaml"));
|
|
5332
6024
|
init_docker();
|
|
5333
6025
|
init_host_routes();
|
|
@@ -5585,12 +6277,12 @@ function parseSsPortListeners(stdout, port) {
|
|
|
5585
6277
|
return listeners;
|
|
5586
6278
|
}
|
|
5587
6279
|
function findPortListeners(port) {
|
|
5588
|
-
const result = (0,
|
|
6280
|
+
const result = (0, import_node_child_process11.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
5589
6281
|
encoding: "utf-8"
|
|
5590
6282
|
});
|
|
5591
6283
|
if (result.error && result.error.code === "ENOENT") {
|
|
5592
6284
|
if (process.platform === "linux") {
|
|
5593
|
-
const ssResult = (0,
|
|
6285
|
+
const ssResult = (0, import_node_child_process11.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
5594
6286
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
5595
6287
|
return parseSsPortListeners(ssResult.stdout, port);
|
|
5596
6288
|
}
|
|
@@ -5619,11 +6311,11 @@ function findPortListeners(port) {
|
|
|
5619
6311
|
};
|
|
5620
6312
|
});
|
|
5621
6313
|
}
|
|
5622
|
-
var
|
|
6314
|
+
var import_node_child_process11;
|
|
5623
6315
|
var init_ports = __esm({
|
|
5624
6316
|
"src/util/ports.ts"() {
|
|
5625
6317
|
"use strict";
|
|
5626
|
-
|
|
6318
|
+
import_node_child_process11 = require("child_process");
|
|
5627
6319
|
}
|
|
5628
6320
|
});
|
|
5629
6321
|
|
|
@@ -5679,20 +6371,26 @@ var init_up = __esm({
|
|
|
5679
6371
|
});
|
|
5680
6372
|
|
|
5681
6373
|
// src/core/devpod-environment.ts
|
|
5682
|
-
function inspectWorkspaceContainers() {
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
6374
|
+
function inspectWorkspaceContainers(options) {
|
|
6375
|
+
let ids = options?.ids;
|
|
6376
|
+
if (!ids) {
|
|
6377
|
+
const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
|
|
6378
|
+
encoding: "utf-8"
|
|
6379
|
+
});
|
|
6380
|
+
if (listed.status !== 0) {
|
|
6381
|
+
throw new Error(
|
|
6382
|
+
`docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
|
|
6383
|
+
);
|
|
6384
|
+
}
|
|
6385
|
+
ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
|
|
5690
6386
|
}
|
|
5691
|
-
const ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
|
|
5692
6387
|
if (ids.length === 0) return [];
|
|
5693
|
-
const
|
|
5694
|
-
|
|
5695
|
-
|
|
6388
|
+
const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
|
|
6389
|
+
const inspected = (0, import_node_child_process12.spawnSync)(
|
|
6390
|
+
"docker",
|
|
6391
|
+
["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
|
|
6392
|
+
{ encoding: "utf-8" }
|
|
6393
|
+
);
|
|
5696
6394
|
if (inspected.status !== 0) {
|
|
5697
6395
|
throw new Error(
|
|
5698
6396
|
`docker inspect failed: ${(inspected.stderr || inspected.stdout || "unknown error").trim()}`
|
|
@@ -5703,7 +6401,7 @@ function inspectWorkspaceContainers() {
|
|
|
5703
6401
|
function workspaceAppContainers(containers, repoPath) {
|
|
5704
6402
|
return containers.filter((container) => {
|
|
5705
6403
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5706
|
-
return Boolean(workingDir && sameWorkspacePath(workingDir,
|
|
6404
|
+
return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
|
|
5707
6405
|
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
5708
6406
|
);
|
|
5709
6407
|
});
|
|
@@ -5726,14 +6424,18 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5726
6424
|
}
|
|
5727
6425
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5728
6426
|
}
|
|
5729
|
-
var
|
|
6427
|
+
var import_node_child_process12, import_node_path17, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
|
|
5730
6428
|
var init_devpod_environment = __esm({
|
|
5731
6429
|
"src/core/devpod-environment.ts"() {
|
|
5732
6430
|
"use strict";
|
|
5733
|
-
|
|
5734
|
-
|
|
6431
|
+
import_node_child_process12 = require("child_process");
|
|
6432
|
+
import_node_path17 = __toESM(require("path"));
|
|
5735
6433
|
init_workspace();
|
|
5736
6434
|
SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
|
|
6435
|
+
SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
|
|
6436
|
+
/}$/,
|
|
6437
|
+
',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
|
|
6438
|
+
);
|
|
5737
6439
|
}
|
|
5738
6440
|
});
|
|
5739
6441
|
|
|
@@ -5758,124 +6460,23 @@ function probeHttpRoute(host, options = {}) {
|
|
|
5758
6460
|
args.push("--cacert", getMkcertRootCAPath({ repoPath: options.repoPath }));
|
|
5759
6461
|
}
|
|
5760
6462
|
args.push(url);
|
|
5761
|
-
const result = (0,
|
|
6463
|
+
const result = (0, import_node_child_process13.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
5762
6464
|
const stdout = result.stdout?.trim() ?? "";
|
|
5763
6465
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
5764
6466
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
5765
6467
|
const details = ok ? `HTTP ${status}` : [status === void 0 ? void 0 : `HTTP ${status}`, result.stderr?.trim()].filter(Boolean).join(": ") || `curl exited with status ${result.status ?? "unknown"}`;
|
|
5766
6468
|
return { ok, status, details };
|
|
5767
6469
|
}
|
|
5768
|
-
var
|
|
6470
|
+
var import_node_child_process13;
|
|
5769
6471
|
var init_http_route_probe = __esm({
|
|
5770
6472
|
"src/core/http-route-probe.ts"() {
|
|
5771
6473
|
"use strict";
|
|
5772
|
-
|
|
6474
|
+
import_node_child_process13 = require("child_process");
|
|
5773
6475
|
init_router();
|
|
5774
6476
|
init_tls();
|
|
5775
6477
|
}
|
|
5776
6478
|
});
|
|
5777
6479
|
|
|
5778
|
-
// src/core/managed-post-start.ts
|
|
5779
|
-
function commandFailure(result) {
|
|
5780
|
-
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
5781
|
-
}
|
|
5782
|
-
function readRegularFile(filePath) {
|
|
5783
|
-
if (!import_node_fs15.default.existsSync(filePath) || !import_node_fs15.default.lstatSync(filePath).isFile()) return void 0;
|
|
5784
|
-
return import_node_fs15.default.readFileSync(filePath, "utf-8");
|
|
5785
|
-
}
|
|
5786
|
-
function resolveProcessHelperPath() {
|
|
5787
|
-
const candidates = [
|
|
5788
|
-
import_node_path15.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
5789
|
-
import_node_path15.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
5790
|
-
];
|
|
5791
|
-
const helperPath = candidates.find((candidate) => import_node_fs15.default.existsSync(candidate));
|
|
5792
|
-
if (!helperPath) {
|
|
5793
|
-
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
5794
|
-
}
|
|
5795
|
-
return helperPath;
|
|
5796
|
-
}
|
|
5797
|
-
function resolveManagedPostStartPlan(repoPath) {
|
|
5798
|
-
const adapterPath = import_node_path15.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
5799
|
-
const adapter = readRegularFile(adapterPath);
|
|
5800
|
-
if (adapter === void 0) return { kind: "unmanaged" };
|
|
5801
|
-
if (!adapter.includes(MANAGED_MARKER)) return { kind: "unmanaged" };
|
|
5802
|
-
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
5803
|
-
return { kind: "runtime", adapterPath: MANAGED_ADAPTER_PATH };
|
|
5804
|
-
}
|
|
5805
|
-
const dockerfilePath = import_node_path15.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
5806
|
-
const devcontainerPath = import_node_path15.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
5807
|
-
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
5808
|
-
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
5809
|
-
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
5810
|
-
return { kind: "legacy" };
|
|
5811
|
-
}
|
|
5812
|
-
throw new Error(
|
|
5813
|
-
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
5814
|
-
);
|
|
5815
|
-
}
|
|
5816
|
-
function runManagedPostStart(options) {
|
|
5817
|
-
if (options.plan.kind !== "runtime") return;
|
|
5818
|
-
const helper = import_node_fs15.default.readFileSync(resolveProcessHelperPath());
|
|
5819
|
-
const delivered = (0, import_node_child_process11.spawnSync)(
|
|
5820
|
-
"docker",
|
|
5821
|
-
["exec", "-i", options.container.id, "sh", "-c", DELIVERY_SCRIPT],
|
|
5822
|
-
{ input: helper, encoding: "utf-8" }
|
|
5823
|
-
);
|
|
5824
|
-
if (delivered.status !== 0) {
|
|
5825
|
-
const details = commandFailure(delivered);
|
|
5826
|
-
throw new Error(
|
|
5827
|
-
`Could not deliver the managed process helper${details ? `: ${details}` : "."}`
|
|
5828
|
-
);
|
|
5829
|
-
}
|
|
5830
|
-
const started = (0, import_node_child_process11.spawnSync)(
|
|
5831
|
-
"docker",
|
|
5832
|
-
[
|
|
5833
|
-
"exec",
|
|
5834
|
-
"--workdir",
|
|
5835
|
-
options.container.workspacePath,
|
|
5836
|
-
"--env",
|
|
5837
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
5838
|
-
options.container.id,
|
|
5839
|
-
"bash",
|
|
5840
|
-
options.plan.adapterPath
|
|
5841
|
-
],
|
|
5842
|
-
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
5843
|
-
);
|
|
5844
|
-
if (started.status !== 0) {
|
|
5845
|
-
const details = commandFailure(started);
|
|
5846
|
-
throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
|
|
5847
|
-
}
|
|
5848
|
-
}
|
|
5849
|
-
var import_node_child_process11, import_node_fs15, import_node_path15, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, DELIVERY_SCRIPT;
|
|
5850
|
-
var init_managed_post_start = __esm({
|
|
5851
|
-
"src/core/managed-post-start.ts"() {
|
|
5852
|
-
"use strict";
|
|
5853
|
-
import_node_child_process11 = require("child_process");
|
|
5854
|
-
import_node_fs15 = __toESM(require("fs"));
|
|
5855
|
-
import_node_path15 = __toESM(require("path"));
|
|
5856
|
-
MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
5857
|
-
MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
5858
|
-
RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
5859
|
-
DELIVERY_SCRIPT = `set -eu
|
|
5860
|
-
umask 077
|
|
5861
|
-
runtime_root=/tmp/devrouter
|
|
5862
|
-
runtime_bin="$runtime_root/bin"
|
|
5863
|
-
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
5864
|
-
echo "Refusing symlinked devrouter runtime path." >&2
|
|
5865
|
-
exit 1
|
|
5866
|
-
fi
|
|
5867
|
-
mkdir -p "$runtime_bin"
|
|
5868
|
-
chmod 700 "$runtime_root" "$runtime_bin"
|
|
5869
|
-
temporary="$(mktemp "$runtime_bin/.devrouter-process.XXXXXX")"
|
|
5870
|
-
trap 'rm -f "$temporary"' EXIT
|
|
5871
|
-
cat > "$temporary"
|
|
5872
|
-
chmod 700 "$temporary"
|
|
5873
|
-
mv -f "$temporary" "${RUNTIME_HELPER_PATH}"
|
|
5874
|
-
trap - EXIT
|
|
5875
|
-
`;
|
|
5876
|
-
}
|
|
5877
|
-
});
|
|
5878
|
-
|
|
5879
6480
|
// src/core/route-publication.ts
|
|
5880
6481
|
function routedAppsFromConfig(config) {
|
|
5881
6482
|
return config.apps.filter((app) => app.kind !== "dependency");
|
|
@@ -5949,11 +6550,11 @@ var init_route_publication = __esm({
|
|
|
5949
6550
|
// src/core/workspace-ensure.ts
|
|
5950
6551
|
function assertOverlay(container, repoPath) {
|
|
5951
6552
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5952
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
6553
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path18.default.join(repoPath, ".devcontainer"))) {
|
|
5953
6554
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
5954
6555
|
}
|
|
5955
6556
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
5956
|
-
const expectedOverlay =
|
|
6557
|
+
const expectedOverlay = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
5957
6558
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
5958
6559
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
5959
6560
|
}
|
|
@@ -6027,7 +6628,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6027
6628
|
target
|
|
6028
6629
|
});
|
|
6029
6630
|
if (target.kind === "linked") {
|
|
6030
|
-
const workspaceEnv = (0,
|
|
6631
|
+
const workspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
6031
6632
|
"docker",
|
|
6032
6633
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
6033
6634
|
{ encoding: "utf-8" }
|
|
@@ -6037,7 +6638,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6037
6638
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
6038
6639
|
);
|
|
6039
6640
|
}
|
|
6040
|
-
const devrouterWorkspaceEnv = (0,
|
|
6641
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
6041
6642
|
"docker",
|
|
6042
6643
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
6043
6644
|
{ encoding: "utf-8" }
|
|
@@ -6048,7 +6649,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6048
6649
|
);
|
|
6049
6650
|
}
|
|
6050
6651
|
}
|
|
6051
|
-
const gitCheck = (0,
|
|
6652
|
+
const gitCheck = (0, import_node_child_process14.spawnSync)(
|
|
6052
6653
|
"docker",
|
|
6053
6654
|
[
|
|
6054
6655
|
"exec",
|
|
@@ -6135,63 +6736,14 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
6135
6736
|
}
|
|
6136
6737
|
function isPrimaryCheckout(repoPath) {
|
|
6137
6738
|
try {
|
|
6138
|
-
return
|
|
6739
|
+
return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
|
|
6139
6740
|
} catch {
|
|
6140
6741
|
return false;
|
|
6141
6742
|
}
|
|
6142
6743
|
}
|
|
6143
|
-
function startDevpod(repoPath, target, recreate = false, quiet = false) {
|
|
6144
|
-
const args = ["up", repoPath];
|
|
6145
|
-
if (target.devpodId) {
|
|
6146
|
-
args.push("--id", target.devpodId);
|
|
6147
|
-
}
|
|
6148
|
-
args.push("--open-ide=false");
|
|
6149
|
-
if (target.kind === "linked") {
|
|
6150
|
-
args.push(
|
|
6151
|
-
"--workspace-env",
|
|
6152
|
-
`WORKSPACE=${target.workspace}`,
|
|
6153
|
-
"--workspace-env",
|
|
6154
|
-
`DEVROUTER_WORKSPACE=${target.workspace}`
|
|
6155
|
-
);
|
|
6156
|
-
}
|
|
6157
|
-
if (recreate) {
|
|
6158
|
-
if (!target.devpodId) {
|
|
6159
|
-
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
6160
|
-
}
|
|
6161
|
-
args.push("--recreate");
|
|
6162
|
-
}
|
|
6163
|
-
const env = { ...process.env };
|
|
6164
|
-
if (target.kind === "linked") {
|
|
6165
|
-
env.WORKSPACE = target.workspace;
|
|
6166
|
-
env.DEVROUTER_WORKSPACE = target.workspace;
|
|
6167
|
-
env.DEVROUTER_GIT_COMMON_DIR = target.gitCommonDir;
|
|
6168
|
-
env.DEVCONTAINER_COMPOSE_OVERLAY = DEVCONTAINER_OVERLAY;
|
|
6169
|
-
} else {
|
|
6170
|
-
delete env.WORKSPACE;
|
|
6171
|
-
delete env.DEVROUTER_WORKSPACE;
|
|
6172
|
-
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
6173
|
-
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
6174
|
-
}
|
|
6175
|
-
const result = (0, import_node_child_process12.spawnSync)("devpod", args, {
|
|
6176
|
-
stdio: quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
6177
|
-
env
|
|
6178
|
-
});
|
|
6179
|
-
if (result.status !== 0) {
|
|
6180
|
-
throw new Error(`devpod up failed for '${target.devpodId ?? repoPath}'.`);
|
|
6181
|
-
}
|
|
6182
|
-
}
|
|
6183
|
-
function assertDevpodAttachment(repoPath, expectedId) {
|
|
6184
|
-
const attached = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
|
|
6185
|
-
if (!attached || expectedId && attached.id !== expectedId) {
|
|
6186
|
-
throw new Error(
|
|
6187
|
-
`DevPod did not attach '${repoPath}'${expectedId ? ` as '${expectedId}'` : ""} after startup.`
|
|
6188
|
-
);
|
|
6189
|
-
}
|
|
6190
|
-
return attached.id;
|
|
6191
|
-
}
|
|
6192
6744
|
function openUrls(urls) {
|
|
6193
6745
|
for (const url of urls) {
|
|
6194
|
-
const opened = (0,
|
|
6746
|
+
const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
6195
6747
|
if (opened.status !== 0) {
|
|
6196
6748
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
6197
6749
|
`);
|
|
@@ -6221,8 +6773,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6221
6773
|
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
6222
6774
|
let devpodId = target.devpodId;
|
|
6223
6775
|
if (target.kind === "linked") {
|
|
6224
|
-
const overlayPath =
|
|
6225
|
-
if (!
|
|
6776
|
+
const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
6777
|
+
if (!import_node_fs18.default.existsSync(overlayPath)) {
|
|
6226
6778
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
6227
6779
|
}
|
|
6228
6780
|
}
|
|
@@ -6233,16 +6785,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6233
6785
|
);
|
|
6234
6786
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
6235
6787
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
6236
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
6788
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path18.default.basename(repoPath)) ?? "app";
|
|
6237
6789
|
for (const [index, app] of apps.entries()) {
|
|
6238
|
-
if (
|
|
6790
|
+
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
6239
6791
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
6240
6792
|
throw new Error(
|
|
6241
|
-
`
|
|
6793
|
+
`Proxy app '${app.name}' must use a ${owner}-owned upstream beginning with '${aliasPrefix}-'.`
|
|
6242
6794
|
);
|
|
6243
6795
|
}
|
|
6244
6796
|
}
|
|
6245
|
-
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host)
|
|
6797
|
+
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
6246
6798
|
const ownership = target.kind === "linked" ? {
|
|
6247
6799
|
workspace: target.workspace,
|
|
6248
6800
|
worktreePath: repoPath,
|
|
@@ -6254,9 +6806,25 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6254
6806
|
}
|
|
6255
6807
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
6256
6808
|
const startAndProveAttachment = (recreate = false) => {
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6809
|
+
const requestedTarget = currentTarget();
|
|
6810
|
+
try {
|
|
6811
|
+
devpodId = startDevpodWorkspace({
|
|
6812
|
+
repoPath,
|
|
6813
|
+
devpodId: requestedTarget.devpodId,
|
|
6814
|
+
recreate,
|
|
6815
|
+
quiet: options.quiet,
|
|
6816
|
+
...requestedTarget.kind === "linked" ? {
|
|
6817
|
+
workspace: {
|
|
6818
|
+
token: requestedTarget.workspace,
|
|
6819
|
+
gitCommonDir: requestedTarget.gitCommonDir
|
|
6820
|
+
}
|
|
6821
|
+
} : {}
|
|
6822
|
+
});
|
|
6823
|
+
environmentStarted = true;
|
|
6824
|
+
} catch (error) {
|
|
6825
|
+
if (error instanceof DevpodStartPostconditionError) environmentStarted = true;
|
|
6826
|
+
throw error;
|
|
6827
|
+
}
|
|
6260
6828
|
if (ownership) {
|
|
6261
6829
|
writeWorkspaceOwnership(repoPath, ownership);
|
|
6262
6830
|
}
|
|
@@ -6357,14 +6925,15 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6357
6925
|
}
|
|
6358
6926
|
});
|
|
6359
6927
|
}
|
|
6360
|
-
var
|
|
6928
|
+
var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
6361
6929
|
var init_workspace_ensure = __esm({
|
|
6362
6930
|
"src/core/workspace-ensure.ts"() {
|
|
6363
6931
|
"use strict";
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6932
|
+
import_node_child_process14 = require("child_process");
|
|
6933
|
+
import_node_fs18 = __toESM(require("fs"));
|
|
6934
|
+
import_node_path18 = __toESM(require("path"));
|
|
6367
6935
|
init_devpod_environment();
|
|
6936
|
+
init_devpod_mutation();
|
|
6368
6937
|
init_devpod_workspaces();
|
|
6369
6938
|
init_host_routes();
|
|
6370
6939
|
init_http_route_probe();
|
|
@@ -6438,20 +7007,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
6438
7007
|
);
|
|
6439
7008
|
}
|
|
6440
7009
|
function defaultWorktreePath(mainRepo, ws) {
|
|
6441
|
-
return
|
|
7010
|
+
return import_node_path19.default.join(mainRepo, "trees", ws);
|
|
6442
7011
|
}
|
|
6443
7012
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
6444
|
-
const ignored = (0,
|
|
7013
|
+
const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
6445
7014
|
encoding: "utf-8"
|
|
6446
7015
|
});
|
|
6447
7016
|
if (ignored.status !== 0) {
|
|
6448
7017
|
throw new Error(
|
|
6449
|
-
`Default worktree root '${
|
|
7018
|
+
`Default worktree root '${import_node_path19.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path19.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
6450
7019
|
);
|
|
6451
7020
|
}
|
|
6452
7021
|
}
|
|
6453
7022
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
6454
|
-
return
|
|
7023
|
+
return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
|
|
6455
7024
|
}
|
|
6456
7025
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
6457
7026
|
const candidates = [
|
|
@@ -6543,7 +7112,7 @@ function resolveWorkspaceTarget(mainRepo, target, worktrees, records) {
|
|
|
6543
7112
|
function worktreeForRecord(worktrees, record) {
|
|
6544
7113
|
return worktrees.find((candidate) => sameWorkspacePath(candidate.path, record.worktreePath));
|
|
6545
7114
|
}
|
|
6546
|
-
function
|
|
7115
|
+
function assertDevpodTargetSafe(target, worktrees, devpods) {
|
|
6547
7116
|
if (target.record) {
|
|
6548
7117
|
const status = inspectWorkspaceOwnership(target.record, worktrees, devpods);
|
|
6549
7118
|
if (status.ownerStatus === "conflict") {
|
|
@@ -6551,26 +7120,25 @@ function devpodForTarget(target, worktrees, devpods) {
|
|
|
6551
7120
|
`Workspace '${target.workspace}' ownership conflicts with live Git or DevPod evidence; no resources were changed.`
|
|
6552
7121
|
);
|
|
6553
7122
|
}
|
|
6554
|
-
return
|
|
7123
|
+
return;
|
|
6555
7124
|
}
|
|
6556
7125
|
const devpodId = target.workspace;
|
|
6557
7126
|
const ownership = inspectDevpodWorkspaceOwnership(devpods, devpodId, target.worktreePath);
|
|
6558
7127
|
if (ownership.status === "conflict") {
|
|
6559
7128
|
throw new Error(ownership.reason);
|
|
6560
7129
|
}
|
|
6561
|
-
return ownership.status === "owned" ? ownership.workspace : void 0;
|
|
6562
7130
|
}
|
|
6563
7131
|
function assertFullDownPreflight(mainRepo, target) {
|
|
6564
7132
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
6565
7133
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
6566
7134
|
}
|
|
6567
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
7135
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
|
|
6568
7136
|
if (target.worktree.locked) {
|
|
6569
7137
|
throw new Error(
|
|
6570
7138
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
6571
7139
|
);
|
|
6572
7140
|
}
|
|
6573
|
-
const status = (0,
|
|
7141
|
+
const status = (0, import_node_child_process15.spawnSync)(
|
|
6574
7142
|
"git",
|
|
6575
7143
|
["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
|
|
6576
7144
|
{ encoding: "utf-8" }
|
|
@@ -6591,8 +7159,8 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6591
7159
|
if (!ws) {
|
|
6592
7160
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
6593
7161
|
}
|
|
6594
|
-
const worktreePath = opts.path ?
|
|
6595
|
-
if (
|
|
7162
|
+
const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
7163
|
+
if (import_node_fs19.default.existsSync(worktreePath)) {
|
|
6596
7164
|
const registered = listGitWorktrees(mainRepo).find(
|
|
6597
7165
|
(worktree) => sameWorkspacePath(worktree.path, worktreePath)
|
|
6598
7166
|
);
|
|
@@ -6610,11 +7178,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6610
7178
|
if (!opts.path) {
|
|
6611
7179
|
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
6612
7180
|
}
|
|
6613
|
-
const add = (0,
|
|
7181
|
+
const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
6614
7182
|
encoding: "utf-8"
|
|
6615
7183
|
});
|
|
6616
7184
|
if (add.status !== 0) {
|
|
6617
|
-
const addNew = (0,
|
|
7185
|
+
const addNew = (0, import_node_child_process15.spawnSync)(
|
|
6618
7186
|
"git",
|
|
6619
7187
|
["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
|
|
6620
7188
|
{
|
|
@@ -6685,10 +7253,9 @@ function workspaceLs(repoPath) {
|
|
|
6685
7253
|
}
|
|
6686
7254
|
function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
6687
7255
|
const devpods = listDevpodWorkspaces();
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
}
|
|
7256
|
+
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
7257
|
+
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
7258
|
+
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
6692
7259
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
6693
7260
|
if (!quiet) {
|
|
6694
7261
|
process.stdout.write(
|
|
@@ -6697,9 +7264,9 @@ function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
|
6697
7264
|
);
|
|
6698
7265
|
}
|
|
6699
7266
|
return {
|
|
6700
|
-
...
|
|
7267
|
+
...mutation.status === "changed" ? { devpodId } : {},
|
|
6701
7268
|
freedRoutes: routes.length,
|
|
6702
|
-
providerChanged:
|
|
7269
|
+
providerChanged: mutation.status === "changed",
|
|
6703
7270
|
workspace: resolved.workspace
|
|
6704
7271
|
};
|
|
6705
7272
|
}
|
|
@@ -6713,10 +7280,15 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6713
7280
|
if (removeWorktree) {
|
|
6714
7281
|
assertFullDownPreflight(mainRepo, resolved);
|
|
6715
7282
|
}
|
|
6716
|
-
const result = mutateWorkspaceRuntime(
|
|
7283
|
+
const result = mutateWorkspaceRuntime(
|
|
7284
|
+
action2 === "stop" ? "stop" : "delete",
|
|
7285
|
+
resolved,
|
|
7286
|
+
worktrees,
|
|
7287
|
+
opts.quiet
|
|
7288
|
+
);
|
|
6717
7289
|
if (removeWorktree) {
|
|
6718
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
6719
|
-
const rm = (0,
|
|
7290
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath)) {
|
|
7291
|
+
const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
6720
7292
|
encoding: "utf-8"
|
|
6721
7293
|
});
|
|
6722
7294
|
if (rm.status !== 0) {
|
|
@@ -6734,9 +7306,9 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6734
7306
|
}
|
|
6735
7307
|
return result;
|
|
6736
7308
|
};
|
|
6737
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
7309
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
6738
7310
|
}
|
|
6739
|
-
async function
|
|
7311
|
+
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
6740
7312
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
6741
7313
|
return withWorkspaceLifecycleLock(worktreePath, async () => {
|
|
6742
7314
|
const worktrees = identifyGitWorktrees(mainRepo);
|
|
@@ -6754,22 +7326,29 @@ async function workspaceStopOwnedPath(worktreePath, opts = {}) {
|
|
|
6754
7326
|
worktree: worktreeForRecord(worktrees, record),
|
|
6755
7327
|
record
|
|
6756
7328
|
};
|
|
6757
|
-
return mutateWorkspaceRuntime(
|
|
7329
|
+
return mutateWorkspaceRuntime(action2, resolved, worktrees, opts.quiet);
|
|
6758
7330
|
});
|
|
6759
7331
|
}
|
|
7332
|
+
async function workspaceStopOwnedPath(worktreePath, opts = {}) {
|
|
7333
|
+
return mutateWorkspaceOwnedPath("stop", worktreePath, opts);
|
|
7334
|
+
}
|
|
7335
|
+
async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
|
|
7336
|
+
return mutateWorkspaceOwnedPath("delete", worktreePath, opts);
|
|
7337
|
+
}
|
|
6760
7338
|
async function workspaceStop(target, opts = {}) {
|
|
6761
7339
|
return runWorkspaceLifecycle("stop", target, opts);
|
|
6762
7340
|
}
|
|
6763
7341
|
async function workspaceDown(target, opts = {}) {
|
|
6764
7342
|
return runWorkspaceLifecycle("down", target, opts);
|
|
6765
7343
|
}
|
|
6766
|
-
var
|
|
7344
|
+
var import_node_child_process15, import_node_fs19, import_node_path19;
|
|
6767
7345
|
var init_workspace_lifecycle = __esm({
|
|
6768
7346
|
"src/core/workspace-lifecycle.ts"() {
|
|
6769
7347
|
"use strict";
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
7348
|
+
import_node_child_process15 = require("child_process");
|
|
7349
|
+
import_node_fs19 = __toESM(require("fs"));
|
|
7350
|
+
import_node_path19 = __toESM(require("path"));
|
|
7351
|
+
init_devpod_mutation();
|
|
6773
7352
|
init_devpod_workspaces();
|
|
6774
7353
|
init_repo_config();
|
|
6775
7354
|
init_route_state();
|
|
@@ -6780,7 +7359,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
6780
7359
|
});
|
|
6781
7360
|
|
|
6782
7361
|
// src/core/environment-stop.ts
|
|
6783
|
-
async function environmentStop(repoPath) {
|
|
7362
|
+
async function environmentStop(repoPath, options = {}) {
|
|
6784
7363
|
const linked = isLinkedWorktree(repoPath);
|
|
6785
7364
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
6786
7365
|
if (linked && !workspace) {
|
|
@@ -6799,25 +7378,27 @@ async function environmentStop(repoPath) {
|
|
|
6799
7378
|
if (!mainRepo) {
|
|
6800
7379
|
throw new Error(`Could not resolve the primary checkout for '${repoPath}'.`);
|
|
6801
7380
|
}
|
|
6802
|
-
const result = await
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
7381
|
+
const result = await (options.delete ? workspaceDeleteOwnedPath : workspaceStopOwnedPath)(
|
|
7382
|
+
record.worktreePath,
|
|
7383
|
+
{
|
|
7384
|
+
quiet: true,
|
|
7385
|
+
repoPath: mainRepo
|
|
7386
|
+
}
|
|
7387
|
+
);
|
|
6806
7388
|
return {
|
|
6807
7389
|
kind: "linked",
|
|
6808
7390
|
repoPath,
|
|
6809
7391
|
workspace: result.workspace,
|
|
6810
7392
|
...result.devpodId ? { devpodId: result.devpodId } : {},
|
|
6811
|
-
stopped: result.providerChanged,
|
|
7393
|
+
stopped: !options.delete && result.providerChanged,
|
|
7394
|
+
...options.delete ? { deleted: result.providerChanged } : {},
|
|
6812
7395
|
freedRoutes: result.freedRoutes
|
|
6813
7396
|
};
|
|
6814
7397
|
}
|
|
6815
7398
|
}
|
|
6816
7399
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
6817
7400
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
|
|
6818
|
-
|
|
6819
|
-
runDevpodWorkspaceAction("stop", devpod.id);
|
|
6820
|
-
}
|
|
7401
|
+
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
6821
7402
|
const removedRoutes = removeHostRoutesWhere(
|
|
6822
7403
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
6823
7404
|
);
|
|
@@ -6825,8 +7406,9 @@ async function environmentStop(repoPath) {
|
|
|
6825
7406
|
kind: linked ? "linked" : "primary",
|
|
6826
7407
|
repoPath,
|
|
6827
7408
|
...workspace ? { workspace } : {},
|
|
6828
|
-
...devpod ? { devpodId: devpod.id } : {},
|
|
6829
|
-
stopped:
|
|
7409
|
+
...mutation.status === "changed" && devpod ? { devpodId: devpod.id } : {},
|
|
7410
|
+
stopped: !options.delete && mutation.status === "changed",
|
|
7411
|
+
...options.delete ? { deleted: mutation.status === "changed" } : {},
|
|
6830
7412
|
freedRoutes: removedRoutes.length
|
|
6831
7413
|
};
|
|
6832
7414
|
});
|
|
@@ -6834,6 +7416,7 @@ async function environmentStop(repoPath) {
|
|
|
6834
7416
|
var init_environment_stop = __esm({
|
|
6835
7417
|
"src/core/environment-stop.ts"() {
|
|
6836
7418
|
"use strict";
|
|
7419
|
+
init_devpod_mutation();
|
|
6837
7420
|
init_devpod_workspaces();
|
|
6838
7421
|
init_host_routes();
|
|
6839
7422
|
init_workspace();
|
|
@@ -6849,19 +7432,19 @@ __export(stop_exports, {
|
|
|
6849
7432
|
});
|
|
6850
7433
|
async function runStopCommand(options) {
|
|
6851
7434
|
const repoPath = resolveGitCheckoutPath(options.path);
|
|
6852
|
-
const result = await environmentStop(repoPath);
|
|
7435
|
+
const result = await environmentStop(repoPath, { delete: options.delete });
|
|
6853
7436
|
if (options.json) {
|
|
6854
7437
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6855
7438
|
`);
|
|
6856
7439
|
return;
|
|
6857
7440
|
}
|
|
6858
7441
|
const label = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
|
|
6859
|
-
if (!result.stopped && result.freedRoutes === 0) {
|
|
7442
|
+
if (!result.stopped && !result.deleted && result.freedRoutes === 0) {
|
|
6860
7443
|
process.stdout.write(`${label} is already stopped; no routes needed removal.
|
|
6861
7444
|
`);
|
|
6862
7445
|
return;
|
|
6863
7446
|
}
|
|
6864
|
-
const provider = result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
7447
|
+
const provider = result.deleted ? `Deleted DevPod '${result.devpodId}'. ` : result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
6865
7448
|
process.stdout.write(
|
|
6866
7449
|
`${provider}Freed ${result.freedRoutes} route(s) for ${label.toLowerCase()}.
|
|
6867
7450
|
`
|
|
@@ -6891,7 +7474,7 @@ function ensureGuidance(repoPath) {
|
|
|
6891
7474
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
6892
7475
|
}
|
|
6893
7476
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
6894
|
-
const result = (0,
|
|
7477
|
+
const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
6895
7478
|
encoding: "utf-8"
|
|
6896
7479
|
});
|
|
6897
7480
|
if (result.status !== 0) {
|
|
@@ -6929,7 +7512,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6929
7512
|
}
|
|
6930
7513
|
assertRunningDevpod(devpod.id, repoPath);
|
|
6931
7514
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
6932
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
7515
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
|
|
6933
7516
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
6934
7517
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
6935
7518
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -6947,7 +7530,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6947
7530
|
wrappedCommand
|
|
6948
7531
|
];
|
|
6949
7532
|
return new Promise((resolve, reject) => {
|
|
6950
|
-
const child = (0,
|
|
7533
|
+
const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
6951
7534
|
let pending = Buffer.alloc(0);
|
|
6952
7535
|
let statusBytes = Buffer.alloc(0);
|
|
6953
7536
|
let readingStatus = false;
|
|
@@ -7006,12 +7589,12 @@ async function devpodExec(repoPath, command) {
|
|
|
7006
7589
|
});
|
|
7007
7590
|
});
|
|
7008
7591
|
}
|
|
7009
|
-
var
|
|
7592
|
+
var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
|
|
7010
7593
|
var init_devpod_exec = __esm({
|
|
7011
7594
|
"src/core/devpod-exec.ts"() {
|
|
7012
7595
|
"use strict";
|
|
7013
|
-
|
|
7014
|
-
|
|
7596
|
+
import_node_child_process16 = require("child_process");
|
|
7597
|
+
import_node_crypto5 = require("crypto");
|
|
7015
7598
|
init_devpod_environment();
|
|
7016
7599
|
init_devpod_workspaces();
|
|
7017
7600
|
init_workspace();
|
|
@@ -7195,7 +7778,7 @@ async function runOpenCommand(name) {
|
|
|
7195
7778
|
);
|
|
7196
7779
|
return;
|
|
7197
7780
|
}
|
|
7198
|
-
const result = (0,
|
|
7781
|
+
const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
7199
7782
|
if (result.status !== 0) {
|
|
7200
7783
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7201
7784
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -7203,11 +7786,11 @@ async function runOpenCommand(name) {
|
|
|
7203
7786
|
process.stdout.write(`Opened ${url}
|
|
7204
7787
|
`);
|
|
7205
7788
|
}
|
|
7206
|
-
var
|
|
7789
|
+
var import_node_child_process17;
|
|
7207
7790
|
var init_open = __esm({
|
|
7208
7791
|
"src/commands/open.ts"() {
|
|
7209
7792
|
"use strict";
|
|
7210
|
-
|
|
7793
|
+
import_node_child_process17 = require("child_process");
|
|
7211
7794
|
init_docker();
|
|
7212
7795
|
init_host_routes();
|
|
7213
7796
|
init_repo_config();
|
|
@@ -7230,7 +7813,7 @@ async function runLogsCommand(options) {
|
|
|
7230
7813
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
7231
7814
|
if (options.follow) {
|
|
7232
7815
|
args.splice(1, 0, "-f");
|
|
7233
|
-
const child = (0,
|
|
7816
|
+
const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
|
|
7234
7817
|
const onSignal = () => {
|
|
7235
7818
|
child.kill("SIGTERM");
|
|
7236
7819
|
};
|
|
@@ -7244,17 +7827,17 @@ async function runLogsCommand(options) {
|
|
|
7244
7827
|
});
|
|
7245
7828
|
});
|
|
7246
7829
|
} else {
|
|
7247
|
-
const result = (0,
|
|
7830
|
+
const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
|
|
7248
7831
|
if (result.status !== 0) {
|
|
7249
7832
|
throw new Error("Failed to retrieve router logs.");
|
|
7250
7833
|
}
|
|
7251
7834
|
}
|
|
7252
7835
|
}
|
|
7253
|
-
var
|
|
7836
|
+
var import_node_child_process18;
|
|
7254
7837
|
var init_logs = __esm({
|
|
7255
7838
|
"src/commands/logs.ts"() {
|
|
7256
7839
|
"use strict";
|
|
7257
|
-
|
|
7840
|
+
import_node_child_process18 = require("child_process");
|
|
7258
7841
|
init_docker();
|
|
7259
7842
|
init_router();
|
|
7260
7843
|
}
|
|
@@ -7294,17 +7877,17 @@ function asRecord3(value) {
|
|
|
7294
7877
|
return value;
|
|
7295
7878
|
}
|
|
7296
7879
|
function readJson(filePath) {
|
|
7297
|
-
if (!
|
|
7880
|
+
if (!import_node_fs20.default.existsSync(filePath)) {
|
|
7298
7881
|
return void 0;
|
|
7299
7882
|
}
|
|
7300
7883
|
try {
|
|
7301
|
-
return JSON.parse(
|
|
7884
|
+
return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7302
7885
|
} catch {
|
|
7303
7886
|
return void 0;
|
|
7304
7887
|
}
|
|
7305
7888
|
}
|
|
7306
7889
|
function relative(repoPath, filePath) {
|
|
7307
|
-
return
|
|
7890
|
+
return import_node_path20.default.relative(repoPath, filePath) || ".";
|
|
7308
7891
|
}
|
|
7309
7892
|
function redactEnvAssignments(value) {
|
|
7310
7893
|
return value.replace(
|
|
@@ -7345,7 +7928,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
7345
7928
|
["bun.lock", "bun"]
|
|
7346
7929
|
];
|
|
7347
7930
|
for (const [fileName, name] of lockfiles) {
|
|
7348
|
-
if (
|
|
7931
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7349
7932
|
return { name, source: fileName };
|
|
7350
7933
|
}
|
|
7351
7934
|
}
|
|
@@ -7360,9 +7943,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
7360
7943
|
if (typeof engines?.node === "string") {
|
|
7361
7944
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
7362
7945
|
}
|
|
7363
|
-
const nvmrc =
|
|
7364
|
-
if (
|
|
7365
|
-
const version =
|
|
7946
|
+
const nvmrc = import_node_path20.default.join(repoPath, ".nvmrc");
|
|
7947
|
+
if (import_node_fs20.default.existsSync(nvmrc)) {
|
|
7948
|
+
const version = import_node_fs20.default.readFileSync(nvmrc, "utf-8").trim();
|
|
7366
7949
|
return { version, source: ".nvmrc" };
|
|
7367
7950
|
}
|
|
7368
7951
|
return void 0;
|
|
@@ -7423,7 +8006,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
7423
8006
|
const files = config.apps.filter(
|
|
7424
8007
|
(app) => app.runtime === "docker"
|
|
7425
8008
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
7426
|
-
(fileName) => !
|
|
8009
|
+
(fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
|
|
7427
8010
|
);
|
|
7428
8011
|
return Array.from(new Set(files));
|
|
7429
8012
|
} catch {
|
|
@@ -7441,7 +8024,7 @@ function composeFiles(repoPath) {
|
|
|
7441
8024
|
...configuredComposeFiles(repoPath)
|
|
7442
8025
|
];
|
|
7443
8026
|
return Array.from(new Set(candidates)).filter(
|
|
7444
|
-
(fileName) =>
|
|
8027
|
+
(fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
|
|
7445
8028
|
);
|
|
7446
8029
|
}
|
|
7447
8030
|
function stringArray(value) {
|
|
@@ -7479,7 +8062,7 @@ function inspectServices(repoPath) {
|
|
|
7479
8062
|
const services = [];
|
|
7480
8063
|
for (const fileName of composeFiles(repoPath)) {
|
|
7481
8064
|
try {
|
|
7482
|
-
const parsed = import_yaml5.default.parse(
|
|
8065
|
+
const parsed = import_yaml5.default.parse(import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8"));
|
|
7483
8066
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
7484
8067
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
7485
8068
|
const service = asRecord3(value);
|
|
@@ -7514,8 +8097,8 @@ function inspectServices(repoPath) {
|
|
|
7514
8097
|
return services;
|
|
7515
8098
|
}
|
|
7516
8099
|
function inspectEnvFiles(repoPath) {
|
|
7517
|
-
const files =
|
|
7518
|
-
const content =
|
|
8100
|
+
const files = import_node_fs20.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
8101
|
+
const content = import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8");
|
|
7519
8102
|
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();
|
|
7520
8103
|
return { path: fileName, names };
|
|
7521
8104
|
});
|
|
@@ -7529,18 +8112,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
7529
8112
|
};
|
|
7530
8113
|
}
|
|
7531
8114
|
function inspectDevcontainer(repoPath) {
|
|
7532
|
-
const dir =
|
|
7533
|
-
if (!
|
|
8115
|
+
const dir = import_node_path20.default.join(repoPath, ".devcontainer");
|
|
8116
|
+
if (!import_node_fs20.default.existsSync(dir)) {
|
|
7534
8117
|
return { exists: false, files: [] };
|
|
7535
8118
|
}
|
|
7536
8119
|
return {
|
|
7537
8120
|
exists: true,
|
|
7538
|
-
files:
|
|
8121
|
+
files: import_node_fs20.default.readdirSync(dir).filter((fileName) => import_node_fs20.default.statSync(import_node_path20.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
7539
8122
|
};
|
|
7540
8123
|
}
|
|
7541
8124
|
function inspectDevrouter(repoPath) {
|
|
7542
8125
|
const configPath = getRepoConfigPath(repoPath);
|
|
7543
|
-
if (!
|
|
8126
|
+
if (!import_node_fs20.default.existsSync(configPath)) {
|
|
7544
8127
|
return {
|
|
7545
8128
|
exists: false,
|
|
7546
8129
|
configPath,
|
|
@@ -7584,15 +8167,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
7584
8167
|
["AGENTS.md", "agents"],
|
|
7585
8168
|
["CLAUDE.md", "claude"]
|
|
7586
8169
|
]) {
|
|
7587
|
-
if (
|
|
8170
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7588
8171
|
results.push({ path: fileName, kind });
|
|
7589
8172
|
}
|
|
7590
8173
|
}
|
|
7591
|
-
const skillsDir =
|
|
7592
|
-
if (
|
|
7593
|
-
for (const name of
|
|
7594
|
-
const skillPath =
|
|
7595
|
-
if (
|
|
8174
|
+
const skillsDir = import_node_path20.default.join(repoPath, ".agents", "skills");
|
|
8175
|
+
if (import_node_fs20.default.existsSync(skillsDir)) {
|
|
8176
|
+
for (const name of import_node_fs20.default.readdirSync(skillsDir).sort()) {
|
|
8177
|
+
const skillPath = import_node_path20.default.join(skillsDir, name, "SKILL.md");
|
|
8178
|
+
if (import_node_fs20.default.existsSync(skillPath)) {
|
|
7596
8179
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
7597
8180
|
}
|
|
7598
8181
|
}
|
|
@@ -7635,7 +8218,7 @@ function buildIssues(report) {
|
|
|
7635
8218
|
}
|
|
7636
8219
|
function inspectRepo(options = {}) {
|
|
7637
8220
|
const repoPath = resolveRepoPath(options.repo);
|
|
7638
|
-
const pkg = readJson(
|
|
8221
|
+
const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
|
|
7639
8222
|
const scripts = inspectScripts(pkg);
|
|
7640
8223
|
const reportWithoutIssues = {
|
|
7641
8224
|
repoPath,
|
|
@@ -7654,12 +8237,12 @@ function inspectRepo(options = {}) {
|
|
|
7654
8237
|
issues: buildIssues(reportWithoutIssues)
|
|
7655
8238
|
};
|
|
7656
8239
|
}
|
|
7657
|
-
var
|
|
8240
|
+
var import_node_fs20, import_node_path20, import_yaml5;
|
|
7658
8241
|
var init_repo_inspect = __esm({
|
|
7659
8242
|
"src/core/repo-inspect.ts"() {
|
|
7660
8243
|
"use strict";
|
|
7661
|
-
|
|
7662
|
-
|
|
8244
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8245
|
+
import_node_path20 = __toESM(require("path"));
|
|
7663
8246
|
import_yaml5 = __toESM(require("yaml"));
|
|
7664
8247
|
init_repo_config();
|
|
7665
8248
|
}
|
|
@@ -7764,7 +8347,7 @@ function requiredFileChecks(repoPath) {
|
|
|
7764
8347
|
".devcontainer/docker-compose.yml",
|
|
7765
8348
|
".devrouter.yml"
|
|
7766
8349
|
];
|
|
7767
|
-
const missing = required.filter((fileName) => !
|
|
8350
|
+
const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
|
|
7768
8351
|
return {
|
|
7769
8352
|
id: "repo.devcontainer.verify-files",
|
|
7770
8353
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -7983,12 +8566,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
7983
8566
|
nextSteps: collectNextSteps3(checks)
|
|
7984
8567
|
};
|
|
7985
8568
|
}
|
|
7986
|
-
var
|
|
8569
|
+
var import_node_fs21, import_node_path21;
|
|
7987
8570
|
var init_devcontainer_verify = __esm({
|
|
7988
8571
|
"src/core/devcontainer-verify.ts"() {
|
|
7989
8572
|
"use strict";
|
|
7990
|
-
|
|
7991
|
-
|
|
8573
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8574
|
+
import_node_path21 = __toESM(require("path"));
|
|
7992
8575
|
init_capabilities();
|
|
7993
8576
|
init_doctor();
|
|
7994
8577
|
init_host_routes();
|
|
@@ -8284,7 +8867,7 @@ function packageManagerIssues(repo) {
|
|
|
8284
8867
|
}
|
|
8285
8868
|
function plannedFiles(repoPath, version) {
|
|
8286
8869
|
const repo = inspectRepo({ repo: repoPath });
|
|
8287
|
-
const projectName = sanitizeProjectName(
|
|
8870
|
+
const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
|
|
8288
8871
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
8289
8872
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
8290
8873
|
const port = inferPort2(repo);
|
|
@@ -8329,8 +8912,8 @@ function plannedFiles(repoPath, version) {
|
|
|
8329
8912
|
};
|
|
8330
8913
|
}
|
|
8331
8914
|
function classifyFile(repoPath, file) {
|
|
8332
|
-
const absolutePath =
|
|
8333
|
-
if (!
|
|
8915
|
+
const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
|
|
8916
|
+
if (!import_node_fs22.default.existsSync(absolutePath)) {
|
|
8334
8917
|
return {
|
|
8335
8918
|
path: file.relativePath,
|
|
8336
8919
|
action: "create",
|
|
@@ -8338,7 +8921,7 @@ function classifyFile(repoPath, file) {
|
|
|
8338
8921
|
bytes: Buffer.byteLength(file.content)
|
|
8339
8922
|
};
|
|
8340
8923
|
}
|
|
8341
|
-
const current =
|
|
8924
|
+
const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
|
|
8342
8925
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
8343
8926
|
return {
|
|
8344
8927
|
path: file.relativePath,
|
|
@@ -8395,11 +8978,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
8395
8978
|
};
|
|
8396
8979
|
}
|
|
8397
8980
|
function writeFile(repoPath, file) {
|
|
8398
|
-
const absolutePath =
|
|
8399
|
-
|
|
8400
|
-
|
|
8981
|
+
const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
|
|
8982
|
+
import_node_fs22.default.mkdirSync(import_node_path22.default.dirname(absolutePath), { recursive: true });
|
|
8983
|
+
import_node_fs22.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
8401
8984
|
if (file.executable) {
|
|
8402
|
-
|
|
8985
|
+
import_node_fs22.default.chmodSync(absolutePath, 493);
|
|
8403
8986
|
}
|
|
8404
8987
|
}
|
|
8405
8988
|
function writeDevcontainer(options = {}) {
|
|
@@ -8437,12 +9020,12 @@ function writeDevcontainer(options = {}) {
|
|
|
8437
9020
|
nextSteps: postWriteNextSteps(repoPath)
|
|
8438
9021
|
};
|
|
8439
9022
|
}
|
|
8440
|
-
var
|
|
9023
|
+
var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
8441
9024
|
var init_devcontainer_write = __esm({
|
|
8442
9025
|
"src/core/devcontainer-write.ts"() {
|
|
8443
9026
|
"use strict";
|
|
8444
|
-
|
|
8445
|
-
|
|
9027
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
9028
|
+
import_node_path22 = __toESM(require("path"));
|
|
8446
9029
|
init_repo_config();
|
|
8447
9030
|
init_repo_inspect();
|
|
8448
9031
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -8833,7 +9416,7 @@ function sanitizeRouterId(value) {
|
|
|
8833
9416
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
8834
9417
|
}
|
|
8835
9418
|
function repoHash(repoPath) {
|
|
8836
|
-
return (0,
|
|
9419
|
+
return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
8837
9420
|
}
|
|
8838
9421
|
function asDockerApp(app) {
|
|
8839
9422
|
return app.runtime === "docker";
|
|
@@ -8912,11 +9495,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
8912
9495
|
if (dockerApps.length === 0) {
|
|
8913
9496
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
8914
9497
|
}
|
|
8915
|
-
const cachePath =
|
|
8916
|
-
|
|
8917
|
-
const overlayPath =
|
|
9498
|
+
const cachePath = import_node_path23.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
9499
|
+
import_node_fs23.default.mkdirSync(cachePath, { recursive: true });
|
|
9500
|
+
const overlayPath = import_node_path23.default.join(cachePath, "compose.devrouter.yml");
|
|
8918
9501
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
8919
|
-
|
|
9502
|
+
import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
8920
9503
|
return {
|
|
8921
9504
|
overlayPath,
|
|
8922
9505
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -8930,7 +9513,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8930
9513
|
fileArgs.push("-f", resolved);
|
|
8931
9514
|
}
|
|
8932
9515
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
8933
|
-
const result = (0,
|
|
9516
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8934
9517
|
encoding: "utf-8",
|
|
8935
9518
|
cwd: repoPath
|
|
8936
9519
|
});
|
|
@@ -8958,7 +9541,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
8958
9541
|
"--services",
|
|
8959
9542
|
...services
|
|
8960
9543
|
];
|
|
8961
|
-
const result = (0,
|
|
9544
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8962
9545
|
encoding: "utf-8",
|
|
8963
9546
|
cwd: repoPath
|
|
8964
9547
|
});
|
|
@@ -8984,7 +9567,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8984
9567
|
fileArgs.push("-f", resolved);
|
|
8985
9568
|
}
|
|
8986
9569
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
8987
|
-
const result = (0,
|
|
9570
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8988
9571
|
encoding: "utf-8",
|
|
8989
9572
|
cwd: repoPath
|
|
8990
9573
|
});
|
|
@@ -9010,7 +9593,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
9010
9593
|
String(tail),
|
|
9011
9594
|
...services
|
|
9012
9595
|
];
|
|
9013
|
-
(0,
|
|
9596
|
+
(0, import_node_child_process19.spawnSync)("docker", args, {
|
|
9014
9597
|
stdio: "inherit",
|
|
9015
9598
|
cwd: repoPath
|
|
9016
9599
|
});
|
|
@@ -9022,7 +9605,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9022
9605
|
fileArgs.push("-f", resolved);
|
|
9023
9606
|
}
|
|
9024
9607
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
9025
|
-
const result = (0,
|
|
9608
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
9026
9609
|
encoding: "utf-8",
|
|
9027
9610
|
cwd: repoPath
|
|
9028
9611
|
});
|
|
@@ -9036,14 +9619,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9036
9619
|
const port = Number(match[1]);
|
|
9037
9620
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
9038
9621
|
}
|
|
9039
|
-
var
|
|
9622
|
+
var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
|
|
9040
9623
|
var init_docker_run = __esm({
|
|
9041
9624
|
"src/core/docker-run.ts"() {
|
|
9042
9625
|
"use strict";
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9626
|
+
import_node_child_process19 = require("child_process");
|
|
9627
|
+
import_node_crypto6 = require("crypto");
|
|
9628
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
9629
|
+
import_node_path23 = __toESM(require("path"));
|
|
9047
9630
|
import_yaml6 = __toESM(require("yaml"));
|
|
9048
9631
|
init_docker_error_guidance();
|
|
9049
9632
|
init_paths();
|
|
@@ -9133,7 +9716,7 @@ function isProcessRunning(pid) {
|
|
|
9133
9716
|
}
|
|
9134
9717
|
}
|
|
9135
9718
|
function readProcessTree(rootPid) {
|
|
9136
|
-
const result = (0,
|
|
9719
|
+
const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
9137
9720
|
if (result.status !== 0) {
|
|
9138
9721
|
return [rootPid];
|
|
9139
9722
|
}
|
|
@@ -9244,12 +9827,12 @@ function detectListeningPorts(pids) {
|
|
|
9244
9827
|
if (pids.length === 0) {
|
|
9245
9828
|
return [];
|
|
9246
9829
|
}
|
|
9247
|
-
const result = (0,
|
|
9830
|
+
const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
|
|
9248
9831
|
encoding: "utf-8"
|
|
9249
9832
|
});
|
|
9250
9833
|
if (result.error && result.error.code === "ENOENT") {
|
|
9251
9834
|
if (process.platform === "linux") {
|
|
9252
|
-
const ssResult = (0,
|
|
9835
|
+
const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
9253
9836
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
9254
9837
|
return parseSsListeningPorts(ssResult.stdout, new Set(pids));
|
|
9255
9838
|
}
|
|
@@ -9308,7 +9891,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
9308
9891
|
app.hostRun.command,
|
|
9309
9892
|
true
|
|
9310
9893
|
) : app.hostRun.command;
|
|
9311
|
-
const child = (0,
|
|
9894
|
+
const child = (0, import_node_child_process20.spawn)(spawnCommand, {
|
|
9312
9895
|
cwd: commandCwd,
|
|
9313
9896
|
stdio: "inherit",
|
|
9314
9897
|
shell: true,
|
|
@@ -9676,7 +10259,7 @@ async function execWithAppEnv(options) {
|
|
|
9676
10259
|
options.command[0],
|
|
9677
10260
|
true
|
|
9678
10261
|
);
|
|
9679
|
-
child = (0,
|
|
10262
|
+
child = (0, import_node_child_process20.spawn)(wrapped, {
|
|
9680
10263
|
cwd: deps.repoPath,
|
|
9681
10264
|
stdio: "inherit",
|
|
9682
10265
|
shell: true,
|
|
@@ -9690,7 +10273,7 @@ async function execWithAppEnv(options) {
|
|
|
9690
10273
|
false
|
|
9691
10274
|
);
|
|
9692
10275
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
9693
|
-
child = (0,
|
|
10276
|
+
child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
|
|
9694
10277
|
cwd: deps.repoPath,
|
|
9695
10278
|
stdio: "inherit",
|
|
9696
10279
|
shell: false,
|
|
@@ -9698,7 +10281,7 @@ async function execWithAppEnv(options) {
|
|
|
9698
10281
|
});
|
|
9699
10282
|
}
|
|
9700
10283
|
} else if (options.shell) {
|
|
9701
|
-
child = (0,
|
|
10284
|
+
child = (0, import_node_child_process20.spawn)(options.command[0], {
|
|
9702
10285
|
cwd: deps.repoPath,
|
|
9703
10286
|
stdio: "inherit",
|
|
9704
10287
|
shell: true,
|
|
@@ -9706,7 +10289,7 @@ async function execWithAppEnv(options) {
|
|
|
9706
10289
|
});
|
|
9707
10290
|
} else {
|
|
9708
10291
|
const [command, ...args] = options.command;
|
|
9709
|
-
child = (0,
|
|
10292
|
+
child = (0, import_node_child_process20.spawn)(command, args, {
|
|
9710
10293
|
cwd: deps.repoPath,
|
|
9711
10294
|
stdio: "inherit",
|
|
9712
10295
|
shell: false,
|
|
@@ -9727,11 +10310,11 @@ async function execWithAppEnv(options) {
|
|
|
9727
10310
|
deps.stopDeps();
|
|
9728
10311
|
}
|
|
9729
10312
|
}
|
|
9730
|
-
var
|
|
10313
|
+
var import_node_child_process20, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
|
|
9731
10314
|
var init_app_run = __esm({
|
|
9732
10315
|
"src/core/app-run.ts"() {
|
|
9733
10316
|
"use strict";
|
|
9734
|
-
|
|
10317
|
+
import_node_child_process20 = require("child_process");
|
|
9735
10318
|
import_node_net = __toESM(require("net"));
|
|
9736
10319
|
import_node_process = require("process");
|
|
9737
10320
|
import_promises = require("readline/promises");
|
|
@@ -9859,9 +10442,847 @@ var init_tls2 = __esm({
|
|
|
9859
10442
|
}
|
|
9860
10443
|
});
|
|
9861
10444
|
|
|
10445
|
+
// src/core/workspace-consumption.ts
|
|
10446
|
+
function measureWorktreeConsumption(worktreePath, options) {
|
|
10447
|
+
const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
10448
|
+
const startedAt = Date.now();
|
|
10449
|
+
let rootStat;
|
|
10450
|
+
try {
|
|
10451
|
+
rootStat = import_node_fs24.default.lstatSync(worktreePath);
|
|
10452
|
+
} catch (error) {
|
|
10453
|
+
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
10454
|
+
}
|
|
10455
|
+
let rootEntries;
|
|
10456
|
+
try {
|
|
10457
|
+
rootEntries = import_node_fs24.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
10458
|
+
} catch (error) {
|
|
10459
|
+
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
10460
|
+
}
|
|
10461
|
+
const seenInodes = /* @__PURE__ */ new Set();
|
|
10462
|
+
let bytes = 0;
|
|
10463
|
+
let timedOut = false;
|
|
10464
|
+
let unreadableReason = null;
|
|
10465
|
+
const accumulate = (stat) => {
|
|
10466
|
+
if (stat.nlink > 1) {
|
|
10467
|
+
const key = `${stat.dev}:${stat.ino}`;
|
|
10468
|
+
if (seenInodes.has(key)) return;
|
|
10469
|
+
seenInodes.add(key);
|
|
10470
|
+
}
|
|
10471
|
+
bytes += stat.blocks * BLOCK_SIZE_BYTES;
|
|
10472
|
+
};
|
|
10473
|
+
const deadlineExceeded = () => Date.now() - startedAt >= deadlineMs;
|
|
10474
|
+
accumulate(rootStat);
|
|
10475
|
+
const stack = [{ dirPath: worktreePath, entries: rootEntries }];
|
|
10476
|
+
while (stack.length > 0 && !timedOut && !unreadableReason) {
|
|
10477
|
+
const { dirPath, entries } = stack.pop();
|
|
10478
|
+
for (const entry of entries) {
|
|
10479
|
+
if (deadlineExceeded()) {
|
|
10480
|
+
timedOut = true;
|
|
10481
|
+
break;
|
|
10482
|
+
}
|
|
10483
|
+
const entryPath = import_node_path24.default.join(dirPath, entry.name);
|
|
10484
|
+
let entryStat;
|
|
10485
|
+
try {
|
|
10486
|
+
entryStat = import_node_fs24.default.lstatSync(entryPath);
|
|
10487
|
+
} catch (error) {
|
|
10488
|
+
if (error?.code === "ENOENT") continue;
|
|
10489
|
+
unreadableReason = describeIncompleteWalk(error);
|
|
10490
|
+
break;
|
|
10491
|
+
}
|
|
10492
|
+
accumulate(entryStat);
|
|
10493
|
+
if (!entryStat.isDirectory()) continue;
|
|
10494
|
+
let childEntries;
|
|
10495
|
+
try {
|
|
10496
|
+
childEntries = import_node_fs24.default.readdirSync(entryPath, { withFileTypes: true });
|
|
10497
|
+
} catch (error) {
|
|
10498
|
+
unreadableReason = describeIncompleteWalk(error);
|
|
10499
|
+
break;
|
|
10500
|
+
}
|
|
10501
|
+
stack.push({ dirPath: entryPath, entries: childEntries });
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
if (timedOut) {
|
|
10505
|
+
return { status: "unknown", reason: `exceeded deadline of ${deadlineMs}ms` };
|
|
10506
|
+
}
|
|
10507
|
+
if (unreadableReason) {
|
|
10508
|
+
return { status: "unknown", reason: unreadableReason };
|
|
10509
|
+
}
|
|
10510
|
+
return { status: "measured", bytes };
|
|
10511
|
+
}
|
|
10512
|
+
function describeIncompleteWalk(error) {
|
|
10513
|
+
return `could not read every path inside the worktree: ${error?.message ?? String(error)}`;
|
|
10514
|
+
}
|
|
10515
|
+
function measureContainerConsumption(worktreePaths, dependencies) {
|
|
10516
|
+
const byWorktree = /* @__PURE__ */ new Map();
|
|
10517
|
+
if (worktreePaths.length === 0) return byWorktree;
|
|
10518
|
+
const inspect = dependencies?.inspect ?? inspectWorkspaceContainers;
|
|
10519
|
+
const containers = inspect();
|
|
10520
|
+
const attributedIds = /* @__PURE__ */ new Set();
|
|
10521
|
+
for (const worktreePath of worktreePaths) {
|
|
10522
|
+
for (const container of workspaceAppContainers(containers, worktreePath)) {
|
|
10523
|
+
attributedIds.add(container.id);
|
|
10524
|
+
}
|
|
10525
|
+
}
|
|
10526
|
+
const sized = attributedIds.size === 0 ? [] : inspect({ withSize: true, ids: Array.from(attributedIds) });
|
|
10527
|
+
for (const worktreePath of worktreePaths) {
|
|
10528
|
+
byWorktree.set(worktreePath, summarizeContainers(workspaceAppContainers(sized, worktreePath)));
|
|
10529
|
+
}
|
|
10530
|
+
return byWorktree;
|
|
10531
|
+
}
|
|
10532
|
+
function summarizeContainers(containers) {
|
|
10533
|
+
let writable = 0;
|
|
10534
|
+
let shared = 0;
|
|
10535
|
+
for (const container of containers) {
|
|
10536
|
+
const { sizeRw, sizeRootFs } = container;
|
|
10537
|
+
if (typeof sizeRw !== "number" || typeof sizeRootFs !== "number" || !Number.isFinite(sizeRw) || !Number.isFinite(sizeRootFs) || sizeRootFs < sizeRw) {
|
|
10538
|
+
const unknown = {
|
|
10539
|
+
status: "unknown",
|
|
10540
|
+
reason: `container ${container.id.slice(0, 12)} reported no usable size`
|
|
10541
|
+
};
|
|
10542
|
+
return { containerWritable: unknown, imageShared: unknown };
|
|
10543
|
+
}
|
|
10544
|
+
writable += sizeRw;
|
|
10545
|
+
shared += sizeRootFs - sizeRw;
|
|
10546
|
+
}
|
|
10547
|
+
return {
|
|
10548
|
+
containerWritable: { status: "measured", bytes: writable },
|
|
10549
|
+
imageShared: { status: "measured", bytes: shared }
|
|
10550
|
+
};
|
|
10551
|
+
}
|
|
10552
|
+
function describeError(error, worktreePath) {
|
|
10553
|
+
const code = error?.code;
|
|
10554
|
+
if (code === "ENOENT") return `worktree path '${worktreePath}' does not exist`;
|
|
10555
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
10556
|
+
return `permission denied reading worktree path '${worktreePath}'`;
|
|
10557
|
+
}
|
|
10558
|
+
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
10559
|
+
}
|
|
10560
|
+
var import_node_fs24, import_node_path24, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
10561
|
+
var init_workspace_consumption = __esm({
|
|
10562
|
+
"src/core/workspace-consumption.ts"() {
|
|
10563
|
+
"use strict";
|
|
10564
|
+
import_node_fs24 = __toESM(require("fs"));
|
|
10565
|
+
import_node_path24 = __toESM(require("path"));
|
|
10566
|
+
init_devpod_environment();
|
|
10567
|
+
DEFAULT_DEADLINE_MS = 1e4;
|
|
10568
|
+
BLOCK_SIZE_BYTES = 512;
|
|
10569
|
+
}
|
|
10570
|
+
});
|
|
10571
|
+
|
|
10572
|
+
// src/core/workspace-cleanup.ts
|
|
10573
|
+
function isRecord2(value) {
|
|
10574
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10575
|
+
}
|
|
10576
|
+
function isSha(value) {
|
|
10577
|
+
return typeof value === "string" && /^[0-9a-f]{40,64}$/i.test(value);
|
|
10578
|
+
}
|
|
10579
|
+
function parseTimestamp(value) {
|
|
10580
|
+
if (typeof value !== "string") return void 0;
|
|
10581
|
+
const timestamp = Date.parse(value);
|
|
10582
|
+
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
10583
|
+
}
|
|
10584
|
+
function validTimestamp(value) {
|
|
10585
|
+
return parseTimestamp(value) !== void 0;
|
|
10586
|
+
}
|
|
10587
|
+
function successfulOutput(result) {
|
|
10588
|
+
if (result.status !== 0 || result.error) return void 0;
|
|
10589
|
+
const output2 = result.stdout.trim();
|
|
10590
|
+
return output2.length > 0 ? output2 : void 0;
|
|
10591
|
+
}
|
|
10592
|
+
function gitOutput(repoPath, args, commandRunner) {
|
|
10593
|
+
return successfulOutput(commandRunner("git", ["-C", repoPath, ...args]));
|
|
10594
|
+
}
|
|
10595
|
+
function quoteCommandArg(value) {
|
|
10596
|
+
return /^[a-zA-Z0-9_./:@+-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
10597
|
+
}
|
|
10598
|
+
function workspaceCommand(repoPath, args, beforeRepo = [], afterRepo = []) {
|
|
10599
|
+
return [
|
|
10600
|
+
"devrouter",
|
|
10601
|
+
"workspace",
|
|
10602
|
+
...args,
|
|
10603
|
+
...beforeRepo,
|
|
10604
|
+
"--repo",
|
|
10605
|
+
quoteCommandArg(repoPath),
|
|
10606
|
+
...afterRepo
|
|
10607
|
+
].join(" ");
|
|
10608
|
+
}
|
|
10609
|
+
function parseInactiveFor(value = DEFAULT_INACTIVE_FOR) {
|
|
10610
|
+
const match = /^(?<amount>[1-9]\d*)(?<unit>[smhdw])$/.exec(value);
|
|
10611
|
+
if (!match?.groups) {
|
|
10612
|
+
throw new Error(
|
|
10613
|
+
"--inactive-for must be a positive integer followed by s, m, h, d, or w (for example 30d)."
|
|
10614
|
+
);
|
|
10615
|
+
}
|
|
10616
|
+
const amount = Number(match.groups.amount);
|
|
10617
|
+
const multiplier = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[match.groups.unit];
|
|
10618
|
+
const seconds = amount * multiplier;
|
|
10619
|
+
if (!Number.isSafeInteger(seconds)) {
|
|
10620
|
+
throw new Error("--inactive-for is too large.");
|
|
10621
|
+
}
|
|
10622
|
+
return { input: value, seconds };
|
|
10623
|
+
}
|
|
10624
|
+
function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
10625
|
+
const cutoffTime = parseTimestamp(cutoff);
|
|
10626
|
+
if (cutoffTime === void 0) {
|
|
10627
|
+
throw new Error(`Invalid activity cutoff '${cutoff}'.`);
|
|
10628
|
+
}
|
|
10629
|
+
const validEvidence = evidence.filter(
|
|
10630
|
+
(entry) => entry.status === "valid" && validTimestamp(entry.timestamp)
|
|
10631
|
+
);
|
|
10632
|
+
const latestTime = validEvidence.reduce(
|
|
10633
|
+
(latest, entry) => Math.max(
|
|
10634
|
+
latest ?? Number.NEGATIVE_INFINITY,
|
|
10635
|
+
parseTimestamp(entry.timestamp) ?? Number.NEGATIVE_INFINITY
|
|
10636
|
+
),
|
|
10637
|
+
void 0
|
|
10638
|
+
);
|
|
10639
|
+
const latestTimestamp = latestTime === void 0 ? null : validEvidence.find((entry) => parseTimestamp(entry.timestamp) === latestTime)?.timestamp ?? null;
|
|
10640
|
+
const contributingEvidence = ACTIVITY_SOURCES.filter(
|
|
10641
|
+
(source) => validEvidence.some(
|
|
10642
|
+
(entry) => entry.source === source && parseTimestamp(entry.timestamp) === latestTime
|
|
10643
|
+
)
|
|
10644
|
+
);
|
|
10645
|
+
let status = "unknown";
|
|
10646
|
+
if (latestTime !== void 0) {
|
|
10647
|
+
if (latestTime >= cutoffTime) {
|
|
10648
|
+
status = "recent";
|
|
10649
|
+
} else if (validEvidence.length > 0 && evidence.every(
|
|
10650
|
+
(entry) => entry.status === "valid" || entry.status === "not-applicable" || entry.status === "missing"
|
|
10651
|
+
)) {
|
|
10652
|
+
status = "quiet";
|
|
10653
|
+
}
|
|
10654
|
+
}
|
|
10655
|
+
return {
|
|
10656
|
+
status,
|
|
10657
|
+
latestTimestamp,
|
|
10658
|
+
contributingEvidence,
|
|
10659
|
+
evidence: evidence.slice().sort(
|
|
10660
|
+
(left, right) => ACTIVITY_SOURCES.indexOf(left.source) - ACTIVITY_SOURCES.indexOf(right.source)
|
|
10661
|
+
)
|
|
10662
|
+
};
|
|
10663
|
+
}
|
|
10664
|
+
function readGitSnapshot(worktree, commandRunner) {
|
|
10665
|
+
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
10666
|
+
if (worktree.prunable || !import_node_fs25.default.existsSync(comparablePath)) {
|
|
10667
|
+
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
10668
|
+
}
|
|
10669
|
+
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
10670
|
+
if (!head) {
|
|
10671
|
+
return { worktree, checkout: "unknown", head: null, committerDate: null };
|
|
10672
|
+
}
|
|
10673
|
+
const committerDate = gitOutput(comparablePath, ["show", "-s", "--format=%cI", "HEAD"], commandRunner) ?? null;
|
|
10674
|
+
const statusResult = commandRunner("git", [
|
|
10675
|
+
"-C",
|
|
10676
|
+
comparablePath,
|
|
10677
|
+
"status",
|
|
10678
|
+
"--porcelain=v1",
|
|
10679
|
+
"--untracked-files=normal"
|
|
10680
|
+
]);
|
|
10681
|
+
const checkout = worktree.branch === void 0 ? "detached" : statusResult.status !== 0 || statusResult.error ? "unknown" : statusResult.stdout.trim().length > 0 ? "dirty" : "clean";
|
|
10682
|
+
return { worktree, checkout, head, committerDate };
|
|
10683
|
+
}
|
|
10684
|
+
function parseRemoteIdentity(value) {
|
|
10685
|
+
const normalized = value.trim();
|
|
10686
|
+
let host;
|
|
10687
|
+
let project;
|
|
10688
|
+
const scp = /^git@([^:]+):(.+)$/.exec(normalized);
|
|
10689
|
+
if (scp) {
|
|
10690
|
+
host = scp[1];
|
|
10691
|
+
project = scp[2];
|
|
10692
|
+
} else {
|
|
10693
|
+
try {
|
|
10694
|
+
const url = new URL(normalized);
|
|
10695
|
+
if (url.protocol !== "https:" && url.protocol !== "ssh:") return void 0;
|
|
10696
|
+
host = url.hostname;
|
|
10697
|
+
project = url.pathname.replace(/^\/+/, "");
|
|
10698
|
+
} catch {
|
|
10699
|
+
return void 0;
|
|
10700
|
+
}
|
|
10701
|
+
}
|
|
10702
|
+
project = project.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
10703
|
+
if (!host || !project || project.includes("..") || project.includes(" ")) return void 0;
|
|
10704
|
+
const lowerHost = host.toLowerCase();
|
|
10705
|
+
if (lowerHost === "github.com" && /^[^/]+\/[^/]+$/.test(project)) {
|
|
10706
|
+
return { provider: "github", host: lowerHost, project };
|
|
10707
|
+
}
|
|
10708
|
+
if (lowerHost === "gitlab.com") {
|
|
10709
|
+
return { provider: "gitlab", host: lowerHost, project };
|
|
10710
|
+
}
|
|
10711
|
+
return void 0;
|
|
10712
|
+
}
|
|
10713
|
+
function parseGitHubChanges(value, project, branch) {
|
|
10714
|
+
if (!Array.isArray(value)) return void 0;
|
|
10715
|
+
const changes = [];
|
|
10716
|
+
for (const item of value) {
|
|
10717
|
+
if (!isRecord2(item)) return void 0;
|
|
10718
|
+
const repository = isRecord2(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
10719
|
+
const headRepository = isRecord2(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
10720
|
+
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
10721
|
+
continue;
|
|
10722
|
+
}
|
|
10723
|
+
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
10724
|
+
const mergeCommit = isRecord2(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
10725
|
+
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
10726
|
+
changes.push({
|
|
10727
|
+
sourceBranch: branch,
|
|
10728
|
+
sourceHeadSha: item.headRefOid,
|
|
10729
|
+
targetBranch: item.baseRefName,
|
|
10730
|
+
...baseSha ? { baseSha } : {},
|
|
10731
|
+
...mergeCommit ? { mergeCommitSha: mergeCommit } : {},
|
|
10732
|
+
merged
|
|
10733
|
+
});
|
|
10734
|
+
}
|
|
10735
|
+
return changes;
|
|
10736
|
+
}
|
|
10737
|
+
function parseGitLabChanges(value, _project, branch) {
|
|
10738
|
+
if (!Array.isArray(value)) return void 0;
|
|
10739
|
+
const changes = [];
|
|
10740
|
+
for (const item of value) {
|
|
10741
|
+
if (!isRecord2(item)) return void 0;
|
|
10742
|
+
const sourceProjectId = item.source_project_id;
|
|
10743
|
+
const targetProjectId = item.target_project_id;
|
|
10744
|
+
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
10745
|
+
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
10746
|
+
continue;
|
|
10747
|
+
}
|
|
10748
|
+
const diffRefs = isRecord2(item.diff_refs) ? item.diff_refs : void 0;
|
|
10749
|
+
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
10750
|
+
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
10751
|
+
changes.push({
|
|
10752
|
+
sourceBranch: branch,
|
|
10753
|
+
sourceHeadSha: item.sha,
|
|
10754
|
+
targetBranch: item.target_branch,
|
|
10755
|
+
...baseSha ? { baseSha } : {},
|
|
10756
|
+
...mergeCommitSha ? { mergeCommitSha } : {},
|
|
10757
|
+
merged: item.state === "merged" && validTimestamp(item.merged_at)
|
|
10758
|
+
});
|
|
10759
|
+
}
|
|
10760
|
+
return changes;
|
|
10761
|
+
}
|
|
10762
|
+
function patchIdForRange(repoPath, baseSha, headSha, commandRunner) {
|
|
10763
|
+
const diff = commandRunner("git", [
|
|
10764
|
+
"-C",
|
|
10765
|
+
repoPath,
|
|
10766
|
+
"diff",
|
|
10767
|
+
"--no-ext-diff",
|
|
10768
|
+
"--binary",
|
|
10769
|
+
baseSha,
|
|
10770
|
+
headSha
|
|
10771
|
+
]);
|
|
10772
|
+
if (diff.status !== 0 || diff.error || diff.stdout.length === 0) return void 0;
|
|
10773
|
+
const result = commandRunner("git", ["patch-id", "--stable"], diff.stdout);
|
|
10774
|
+
if (result.status !== 0 || result.error) return void 0;
|
|
10775
|
+
const match = /^([0-9a-f]{40,64})\s+(?:-|[0-9a-f]{40,64})$/i.exec(result.stdout.trim());
|
|
10776
|
+
return match?.[1];
|
|
10777
|
+
}
|
|
10778
|
+
function hasUniqueSourceCommit(repoPath, baseSha, headSha, commandRunner) {
|
|
10779
|
+
const result = gitOutput(
|
|
10780
|
+
repoPath,
|
|
10781
|
+
["rev-list", "--no-merges", "--reverse", `${baseSha}..${headSha}`],
|
|
10782
|
+
commandRunner
|
|
10783
|
+
);
|
|
10784
|
+
if (!result) return false;
|
|
10785
|
+
const commits = result.split(/\r?\n/).map((sha) => sha.trim()).filter((sha) => sha.length > 0);
|
|
10786
|
+
return commits.length > 0 && commits.every(isSha) && new Set(commits).size > 0;
|
|
10787
|
+
}
|
|
10788
|
+
function hasVerifiedCommonBase(repoPath, baseSha, headSha, commandRunner) {
|
|
10789
|
+
const result = commandRunner("git", [
|
|
10790
|
+
"-C",
|
|
10791
|
+
repoPath,
|
|
10792
|
+
"merge-base",
|
|
10793
|
+
"--is-ancestor",
|
|
10794
|
+
baseSha,
|
|
10795
|
+
headSha
|
|
10796
|
+
]);
|
|
10797
|
+
return result.status === 0 && !result.error;
|
|
10798
|
+
}
|
|
10799
|
+
function parseTargetBranch(output2) {
|
|
10800
|
+
if (!output2) return void 0;
|
|
10801
|
+
const value = output2.trim();
|
|
10802
|
+
return value.startsWith("origin/") && value.length > "origin/".length ? value.slice("origin/".length) : void 0;
|
|
10803
|
+
}
|
|
10804
|
+
function parseRemoteDefaultTarget(output2) {
|
|
10805
|
+
if (!output2) return void 0;
|
|
10806
|
+
const match = /^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/m.exec(output2);
|
|
10807
|
+
return match?.[1];
|
|
10808
|
+
}
|
|
10809
|
+
function inspectWorkspaceIntegration(repoPath, snapshot, branch, checkMerged, commandRunner) {
|
|
10810
|
+
if (!checkMerged) return { status: "not-verified", reason: "Merged checks were not requested." };
|
|
10811
|
+
if (!snapshot.head || !branch || snapshot.checkout === "missing" || snapshot.checkout === "unknown") {
|
|
10812
|
+
return { status: "unknown", reason: "Current HEAD or branch evidence is unavailable." };
|
|
10813
|
+
}
|
|
10814
|
+
const originUrl = gitOutput(repoPath, ["config", "--get", "remote.origin.url"], commandRunner);
|
|
10815
|
+
const identity = originUrl ? parseRemoteIdentity(originUrl) : void 0;
|
|
10816
|
+
if (!identity) {
|
|
10817
|
+
return { status: "unknown", reason: "The origin is missing or uses an unsupported forge." };
|
|
10818
|
+
}
|
|
10819
|
+
const localTargetBranch = parseTargetBranch(
|
|
10820
|
+
gitOutput(
|
|
10821
|
+
repoPath,
|
|
10822
|
+
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
10823
|
+
commandRunner
|
|
10824
|
+
)
|
|
10825
|
+
);
|
|
10826
|
+
const remoteTargetBranch = parseRemoteDefaultTarget(
|
|
10827
|
+
successfulOutput(
|
|
10828
|
+
commandRunner("git", ["-C", repoPath, "ls-remote", "--symref", "origin", "HEAD"])
|
|
10829
|
+
)
|
|
10830
|
+
);
|
|
10831
|
+
if (!remoteTargetBranch || localTargetBranch && localTargetBranch !== remoteTargetBranch) {
|
|
10832
|
+
return {
|
|
10833
|
+
status: "unknown",
|
|
10834
|
+
reason: "The origin default target is missing, stale, or unavailable."
|
|
10835
|
+
};
|
|
10836
|
+
}
|
|
10837
|
+
const targetBranch = remoteTargetBranch;
|
|
10838
|
+
const localTargetSha = gitOutput(
|
|
10839
|
+
repoPath,
|
|
10840
|
+
["rev-parse", "--verify", `refs/remotes/origin/${targetBranch}`],
|
|
10841
|
+
commandRunner
|
|
10842
|
+
);
|
|
10843
|
+
const remoteTargetOutput = successfulOutput(
|
|
10844
|
+
commandRunner("git", ["-C", repoPath, "ls-remote", "origin", `refs/heads/${targetBranch}`])
|
|
10845
|
+
);
|
|
10846
|
+
const remoteTargetSha = remoteTargetOutput?.split(/\s+/)[0];
|
|
10847
|
+
if (!isSha(localTargetSha) || !isSha(remoteTargetSha) || localTargetSha !== remoteTargetSha) {
|
|
10848
|
+
return { status: "unknown", reason: "The origin target is missing, stale, or unavailable." };
|
|
10849
|
+
}
|
|
10850
|
+
const sourceRemoteOutput = successfulOutput(
|
|
10851
|
+
commandRunner("git", ["-C", repoPath, "ls-remote", "origin", `refs/heads/${branch}`])
|
|
10852
|
+
);
|
|
10853
|
+
const sourceRemoteSha = sourceRemoteOutput?.split(/\s+/)[0];
|
|
10854
|
+
if (!isSha(sourceRemoteSha)) {
|
|
10855
|
+
return {
|
|
10856
|
+
status: "unknown",
|
|
10857
|
+
reason: "The workspace source branch is missing or unavailable on origin."
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
const forgeChanges = [];
|
|
10861
|
+
const forgeCommand = identity.provider === "github" ? [
|
|
10862
|
+
"pr",
|
|
10863
|
+
"list",
|
|
10864
|
+
"--repo",
|
|
10865
|
+
identity.project,
|
|
10866
|
+
"--state",
|
|
10867
|
+
"all",
|
|
10868
|
+
"--head",
|
|
10869
|
+
branch,
|
|
10870
|
+
"--json",
|
|
10871
|
+
"headRefName,headRefOid,baseRefName,baseRefOid,state,mergedAt,repository,headRepository,mergeCommit"
|
|
10872
|
+
] : [
|
|
10873
|
+
"mr",
|
|
10874
|
+
"list",
|
|
10875
|
+
"--repo",
|
|
10876
|
+
identity.project,
|
|
10877
|
+
"--all",
|
|
10878
|
+
"--source-branch",
|
|
10879
|
+
branch,
|
|
10880
|
+
"--output",
|
|
10881
|
+
"json"
|
|
10882
|
+
];
|
|
10883
|
+
const forge = commandRunner(identity.provider === "github" ? "gh" : "glab", forgeCommand);
|
|
10884
|
+
if (forge.status !== 0 || forge.error) {
|
|
10885
|
+
return { status: "unknown", reason: "The forge query was unavailable or unauthenticated." };
|
|
10886
|
+
}
|
|
10887
|
+
try {
|
|
10888
|
+
const parsed = JSON.parse(forge.stdout);
|
|
10889
|
+
const changes = identity.provider === "github" ? parseGitHubChanges(parsed, identity.project, branch) : parseGitLabChanges(parsed, identity.project, branch);
|
|
10890
|
+
if (!changes) {
|
|
10891
|
+
return { status: "unknown", reason: "The forge response was malformed." };
|
|
10892
|
+
}
|
|
10893
|
+
forgeChanges.push(...changes);
|
|
10894
|
+
} catch {
|
|
10895
|
+
return { status: "unknown", reason: "The forge response was malformed." };
|
|
10896
|
+
}
|
|
10897
|
+
const mergedExact = forgeChanges.find(
|
|
10898
|
+
(change) => change.merged && change.targetBranch === targetBranch && change.sourceHeadSha.toLowerCase() === snapshot.head?.toLowerCase() && change.sourceHeadSha.toLowerCase() === sourceRemoteSha.toLowerCase()
|
|
10899
|
+
);
|
|
10900
|
+
if (mergedExact)
|
|
10901
|
+
return {
|
|
10902
|
+
status: "merged-exact",
|
|
10903
|
+
headSha: snapshot.head,
|
|
10904
|
+
reason: "Merged source head exactly matches current HEAD."
|
|
10905
|
+
};
|
|
10906
|
+
const worktreePath = snapshot.worktree.path;
|
|
10907
|
+
const ancestry = commandRunner("git", [
|
|
10908
|
+
"-C",
|
|
10909
|
+
worktreePath,
|
|
10910
|
+
"merge-base",
|
|
10911
|
+
"--is-ancestor",
|
|
10912
|
+
snapshot.head,
|
|
10913
|
+
remoteTargetSha
|
|
10914
|
+
]);
|
|
10915
|
+
if (ancestry.status === 0 && !ancestry.error) {
|
|
10916
|
+
return {
|
|
10917
|
+
status: "on-target",
|
|
10918
|
+
headSha: snapshot.head,
|
|
10919
|
+
reason: "Current HEAD is an ancestor of the verified-fresh origin target."
|
|
10920
|
+
};
|
|
10921
|
+
}
|
|
10922
|
+
if (ancestry.status !== 1 || ancestry.error) {
|
|
10923
|
+
return { status: "unknown", reason: "Target ancestry could not be verified." };
|
|
10924
|
+
}
|
|
10925
|
+
for (const change of forgeChanges.filter(
|
|
10926
|
+
(candidate) => candidate.merged && candidate.targetBranch === targetBranch && candidate.sourceHeadSha.toLowerCase() !== snapshot.head?.toLowerCase()
|
|
10927
|
+
)) {
|
|
10928
|
+
if (change.sourceHeadSha.toLowerCase() !== sourceRemoteSha?.toLowerCase()) continue;
|
|
10929
|
+
if (change.baseSha && change.mergeCommitSha && hasVerifiedCommonBase(worktreePath, change.baseSha, snapshot.head, commandRunner) && hasVerifiedCommonBase(worktreePath, change.baseSha, change.mergeCommitSha, commandRunner) && hasUniqueSourceCommit(worktreePath, change.baseSha, snapshot.head, commandRunner)) {
|
|
10930
|
+
const currentPatchId = patchIdForRange(
|
|
10931
|
+
worktreePath,
|
|
10932
|
+
change.baseSha,
|
|
10933
|
+
snapshot.head,
|
|
10934
|
+
commandRunner
|
|
10935
|
+
);
|
|
10936
|
+
const mergedPatchId = patchIdForRange(
|
|
10937
|
+
worktreePath,
|
|
10938
|
+
change.baseSha,
|
|
10939
|
+
change.mergeCommitSha,
|
|
10940
|
+
commandRunner
|
|
10941
|
+
);
|
|
10942
|
+
if (!currentPatchId || !mergedPatchId || currentPatchId !== mergedPatchId) continue;
|
|
10943
|
+
return {
|
|
10944
|
+
status: "patch-equivalent",
|
|
10945
|
+
headSha: snapshot.head,
|
|
10946
|
+
reason: "All source changes have an equivalent merged patch."
|
|
10947
|
+
};
|
|
10948
|
+
}
|
|
10949
|
+
}
|
|
10950
|
+
return {
|
|
10951
|
+
status: "not-verified",
|
|
10952
|
+
reason: "Fresh target and forge evidence did not prove integration."
|
|
10953
|
+
};
|
|
10954
|
+
}
|
|
10955
|
+
function routeStatus(record, routes) {
|
|
10956
|
+
if (!routes) return "unknown";
|
|
10957
|
+
const matchingPath = routes.filter(
|
|
10958
|
+
(route) => sameWorkspacePath(route.repoPath, record.worktreePath)
|
|
10959
|
+
);
|
|
10960
|
+
if (matchingPath.length === 0) return "absent";
|
|
10961
|
+
return matchingPath.every((route) => route.workspace === record.workspace) ? "owned" : "conflict";
|
|
10962
|
+
}
|
|
10963
|
+
function buildActivityEvidence(record, providerStatus, provider, routeEntries, git) {
|
|
10964
|
+
const providerEvidence = providerStatus === "unknown" || providerStatus === "conflict" ? { source: "devpod.lastUsed", status: "unavailable" } : providerStatus === "absent" ? { source: "devpod.lastUsed", status: "not-applicable" } : provider?.lastUsedMalformed ? { source: "devpod.lastUsed", status: "malformed" } : provider?.lastUsed ? validTimestamp(provider.lastUsed) ? { source: "devpod.lastUsed", status: "valid", timestamp: provider.lastUsed } : { source: "devpod.lastUsed", status: "malformed" } : { source: "devpod.lastUsed", status: "missing" };
|
|
10965
|
+
const routeTimestamps = routeEntries?.map((route) => route.updatedAt) ?? [];
|
|
10966
|
+
const malformedRoute = routeTimestamps.some((timestamp) => !validTimestamp(timestamp));
|
|
10967
|
+
const newestRoute = routeTimestamps.filter(validTimestamp).sort((left, right) => (parseTimestamp(right) ?? 0) - (parseTimestamp(left) ?? 0))[0];
|
|
10968
|
+
const routeEvidence = !routeEntries ? { source: "route.updatedAt", status: "unavailable" } : routeEntries.length === 0 ? { source: "route.updatedAt", status: "not-applicable" } : malformedRoute ? { source: "route.updatedAt", status: "malformed" } : newestRoute ? { source: "route.updatedAt", status: "valid", timestamp: newestRoute } : { source: "route.updatedAt", status: "malformed" };
|
|
10969
|
+
const ownerEvidence = validTimestamp(record.updatedAt) ? { source: "ownership.updatedAt", status: "valid", timestamp: record.updatedAt } : { source: "ownership.updatedAt", status: "malformed" };
|
|
10970
|
+
const gitEvidence = git.checkout === "missing" ? { source: "git.headCommitterDate", status: "not-applicable" } : git.committerDate && validTimestamp(git.committerDate) ? { source: "git.headCommitterDate", status: "valid", timestamp: git.committerDate } : git.checkout === "unknown" ? { source: "git.headCommitterDate", status: "unavailable" } : { source: "git.headCommitterDate", status: "malformed" };
|
|
10971
|
+
return [providerEvidence, routeEvidence, ownerEvidence, gitEvidence];
|
|
10972
|
+
}
|
|
10973
|
+
function buildSuggestions(repoPath, record, ownership, provider, runtime, checkout, route, activity, integration, worktree, checkMerged) {
|
|
10974
|
+
const reasons = [];
|
|
10975
|
+
const eligibleActions = [];
|
|
10976
|
+
const suggestions = [];
|
|
10977
|
+
const gcCommand = workspaceCommand(repoPath, ["gc"], [], ["--yes"]);
|
|
10978
|
+
const keepCommand = workspaceCommand(repoPath, ["down", record.workspace], ["--keep-worktree"]);
|
|
10979
|
+
const downCommand = workspaceCommand(repoPath, ["down", record.workspace]);
|
|
10980
|
+
const routeSafe = route === "owned" || route === "absent";
|
|
10981
|
+
const checkoutSafe = checkout === "clean" && !worktree?.locked;
|
|
10982
|
+
const providerSafe = provider === "owned";
|
|
10983
|
+
const runtimeSafe = runtime === "running" || runtime === "stopped" || runtime === "not-found";
|
|
10984
|
+
if (ownership === "missing") {
|
|
10985
|
+
if ((provider === "absent" || provider === "owned" && runtimeSafe) && routeSafe) {
|
|
10986
|
+
eligibleActions.push(gcCommand);
|
|
10987
|
+
suggestions.push({
|
|
10988
|
+
command: gcCommand,
|
|
10989
|
+
reason: "The exact ownership record is missing from live Git registration; GC will revalidate before deleting eligible runtime evidence."
|
|
10990
|
+
});
|
|
10991
|
+
} else {
|
|
10992
|
+
reasons.push(
|
|
10993
|
+
`GC suggestion suppressed because provider=${provider}, runtime=${runtime}, or route=${route} is not independently safe.`
|
|
10994
|
+
);
|
|
10995
|
+
}
|
|
10996
|
+
return { eligibleActions, suggestions, reasons };
|
|
10997
|
+
}
|
|
10998
|
+
if (ownership !== "present")
|
|
10999
|
+
reasons.push(`Destructive suggestions require ownership=present (found ${ownership}).`);
|
|
11000
|
+
if (!providerSafe)
|
|
11001
|
+
reasons.push(
|
|
11002
|
+
`Destructive suggestions require an exact owned DevPod (found provider=${provider}).`
|
|
11003
|
+
);
|
|
11004
|
+
if (!runtimeSafe)
|
|
11005
|
+
reasons.push(
|
|
11006
|
+
`Destructive suggestions require an actionable DevPod runtime (found runtime=${runtime}).`
|
|
11007
|
+
);
|
|
11008
|
+
if (!routeSafe)
|
|
11009
|
+
reasons.push(
|
|
11010
|
+
`Destructive suggestions require non-conflicting route evidence (found route=${route}).`
|
|
11011
|
+
);
|
|
11012
|
+
if (checkout === "dirty")
|
|
11013
|
+
reasons.push("Checkout is dirty; destructive workspace down is blocked.");
|
|
11014
|
+
if (checkout === "missing")
|
|
11015
|
+
reasons.push("Checkout is missing; the report cannot authorize full down.");
|
|
11016
|
+
if (checkout === "detached")
|
|
11017
|
+
reasons.push("Checkout is detached; branch identity is not safe for destructive advice.");
|
|
11018
|
+
if (checkout === "unknown")
|
|
11019
|
+
reasons.push("Checkout state is unknown; destructive advice is suppressed.");
|
|
11020
|
+
if (worktree?.locked)
|
|
11021
|
+
reasons.push("Git worktree is locked; destructive workspace down is blocked.");
|
|
11022
|
+
if (!providerSafe || !runtimeSafe || !routeSafe || !checkoutSafe || ownership !== "present") {
|
|
11023
|
+
return { eligibleActions, suggestions, reasons };
|
|
11024
|
+
}
|
|
11025
|
+
if (integration.status === "merged-exact" && integration.headSha) {
|
|
11026
|
+
eligibleActions.push(downCommand);
|
|
11027
|
+
suggestions.push({
|
|
11028
|
+
command: downCommand,
|
|
11029
|
+
reason: "Current HEAD is the exact source head of a same-repository merged change; workspace down still revalidates cleanliness, locks, and ownership before deleting runtime and the linked worktree."
|
|
11030
|
+
});
|
|
11031
|
+
return { eligibleActions, suggestions, reasons };
|
|
11032
|
+
}
|
|
11033
|
+
if (integration.status === "patch-equivalent")
|
|
11034
|
+
reasons.push("Patch-equivalent integration is advisory and never authorizes full removal.");
|
|
11035
|
+
if (integration.status === "unknown" || integration.status === "not-verified")
|
|
11036
|
+
reasons.push(`Integration is ${integration.status}; full removal is not suggested.`);
|
|
11037
|
+
const activityCleanupAllowed = !checkMerged || integration.status === "on-target" || integration.status === "merged-exact";
|
|
11038
|
+
if (!activityCleanupAllowed) {
|
|
11039
|
+
reasons.push(
|
|
11040
|
+
"Cleanup is not suggested because the requested integration check did not provide a verified target or exact merge."
|
|
11041
|
+
);
|
|
11042
|
+
return { eligibleActions, suggestions, reasons };
|
|
11043
|
+
}
|
|
11044
|
+
if (activity === "quiet") {
|
|
11045
|
+
eligibleActions.push(keepCommand);
|
|
11046
|
+
suggestions.push({
|
|
11047
|
+
command: keepCommand,
|
|
11048
|
+
reason: "Managed workspace has no recent trustworthy activity; this deletes DevPod/runtime data and preserves the worktree and owner record."
|
|
11049
|
+
});
|
|
11050
|
+
} else if (activity === "recent") {
|
|
11051
|
+
reasons.push("Recent trustworthy activity vetoes a quiet-workspace suggestion.");
|
|
11052
|
+
} else {
|
|
11053
|
+
reasons.push("Activity is unknown; a quiet-workspace suggestion is suppressed.");
|
|
11054
|
+
}
|
|
11055
|
+
return { eligibleActions, suggestions, reasons };
|
|
11056
|
+
}
|
|
11057
|
+
function buildRow(repoPath, record, worktrees, devpods, routes, snapshot, cutoff, checkMerged, commandRunner, inspectOwnershipFn, inspectIntegrationFn, inspectRuntimeFn) {
|
|
11058
|
+
const ownershipEvidence = inspectOwnershipFn(record, worktrees, devpods);
|
|
11059
|
+
const providerOwnership = devpods?.find(
|
|
11060
|
+
(devpod) => devpod.id === record.devpodId && sameWorkspacePath(devpod.source.localFolder, record.worktreePath)
|
|
11061
|
+
);
|
|
11062
|
+
const matchingRoutes = routes?.filter(
|
|
11063
|
+
(route) => sameWorkspacePath(route.repoPath, record.worktreePath)
|
|
11064
|
+
);
|
|
11065
|
+
const activity = evaluateWorkspaceActivity(
|
|
11066
|
+
buildActivityEvidence(
|
|
11067
|
+
record,
|
|
11068
|
+
ownershipEvidence.devpodStatus,
|
|
11069
|
+
providerOwnership,
|
|
11070
|
+
matchingRoutes,
|
|
11071
|
+
snapshot
|
|
11072
|
+
),
|
|
11073
|
+
cutoff
|
|
11074
|
+
);
|
|
11075
|
+
const branch = snapshot.checkout === "detached" ? null : snapshot.worktree.branch ?? record.branch;
|
|
11076
|
+
const integration = inspectIntegrationFn?.(repoPath, snapshot, branch, checkMerged) ?? inspectWorkspaceIntegration(repoPath, snapshot, branch, checkMerged, commandRunner);
|
|
11077
|
+
const runtime = ownershipEvidence.devpodStatus === "absent" ? "absent" : ownershipEvidence.devpodStatus === "owned" ? (() => {
|
|
11078
|
+
try {
|
|
11079
|
+
return inspectRuntimeFn(record.devpodId);
|
|
11080
|
+
} catch {
|
|
11081
|
+
return "unknown";
|
|
11082
|
+
}
|
|
11083
|
+
})() : "unknown";
|
|
11084
|
+
const suggestions = buildSuggestions(
|
|
11085
|
+
repoPath,
|
|
11086
|
+
record,
|
|
11087
|
+
ownershipEvidence.ownerStatus,
|
|
11088
|
+
ownershipEvidence.devpodStatus,
|
|
11089
|
+
runtime,
|
|
11090
|
+
snapshot.checkout,
|
|
11091
|
+
routeStatus(record, routes),
|
|
11092
|
+
activity.status,
|
|
11093
|
+
integration,
|
|
11094
|
+
snapshot.worktree,
|
|
11095
|
+
checkMerged
|
|
11096
|
+
);
|
|
11097
|
+
const reasons = [
|
|
11098
|
+
`ownership=${ownershipEvidence.ownerStatus}`,
|
|
11099
|
+
`provider=${ownershipEvidence.devpodStatus}`,
|
|
11100
|
+
`runtime=${runtime}`,
|
|
11101
|
+
`checkout=${snapshot.checkout}`,
|
|
11102
|
+
`route=${routeStatus(record, routes)}`,
|
|
11103
|
+
`activity=${activity.status}`,
|
|
11104
|
+
`integration=${integration.status}`,
|
|
11105
|
+
...integration.reason ? [integration.reason] : [],
|
|
11106
|
+
...suggestions.reasons
|
|
11107
|
+
];
|
|
11108
|
+
return {
|
|
11109
|
+
schemaVersion: 2,
|
|
11110
|
+
workspace: record.workspace,
|
|
11111
|
+
branch,
|
|
11112
|
+
repo: repoPath,
|
|
11113
|
+
worktreePath: record.worktreePath,
|
|
11114
|
+
devpodId: record.devpodId,
|
|
11115
|
+
ownership: ownershipEvidence.ownerStatus,
|
|
11116
|
+
provider: ownershipEvidence.devpodStatus,
|
|
11117
|
+
runtime,
|
|
11118
|
+
checkout: snapshot.checkout,
|
|
11119
|
+
route: routeStatus(record, routes),
|
|
11120
|
+
activity: activity.status,
|
|
11121
|
+
cutoff,
|
|
11122
|
+
latestTimestamp: activity.latestTimestamp,
|
|
11123
|
+
contributingEvidence: activity.contributingEvidence,
|
|
11124
|
+
activityEvidence: activity.evidence,
|
|
11125
|
+
integration: integration.status,
|
|
11126
|
+
eligibleActions: suggestions.eligibleActions,
|
|
11127
|
+
suggestions: suggestions.suggestions,
|
|
11128
|
+
reasons: Array.from(new Set(reasons))
|
|
11129
|
+
};
|
|
11130
|
+
}
|
|
11131
|
+
function deriveReclaimable(worktree, containerWritable) {
|
|
11132
|
+
if (worktree.status === "unknown") {
|
|
11133
|
+
return worktree;
|
|
11134
|
+
}
|
|
11135
|
+
if (containerWritable.status === "unknown") {
|
|
11136
|
+
return containerWritable;
|
|
11137
|
+
}
|
|
11138
|
+
return { status: "measured", bytes: worktree.bytes + containerWritable.bytes };
|
|
11139
|
+
}
|
|
11140
|
+
function collectConsumption(worktreePath, measureWorktreeFn, containers) {
|
|
11141
|
+
let worktree;
|
|
11142
|
+
try {
|
|
11143
|
+
worktree = measureWorktreeFn(worktreePath);
|
|
11144
|
+
} catch (error) {
|
|
11145
|
+
worktree = {
|
|
11146
|
+
status: "unknown",
|
|
11147
|
+
reason: `worktree measurement failed: ${describeCause(error)}`
|
|
11148
|
+
};
|
|
11149
|
+
}
|
|
11150
|
+
const docker = containers.get(worktreePath) ?? unknownContainers("attribution not collected");
|
|
11151
|
+
return {
|
|
11152
|
+
worktree,
|
|
11153
|
+
containerWritable: docker.containerWritable,
|
|
11154
|
+
imageShared: docker.imageShared,
|
|
11155
|
+
reclaimable: deriveReclaimable(worktree, docker.containerWritable)
|
|
11156
|
+
};
|
|
11157
|
+
}
|
|
11158
|
+
function unknownContainers(reason) {
|
|
11159
|
+
return {
|
|
11160
|
+
containerWritable: { status: "unknown", reason },
|
|
11161
|
+
imageShared: { status: "unknown", reason }
|
|
11162
|
+
};
|
|
11163
|
+
}
|
|
11164
|
+
function describeCause(error) {
|
|
11165
|
+
return error instanceof Error ? error.message : String(error);
|
|
11166
|
+
}
|
|
11167
|
+
function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
11168
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
11169
|
+
const commandRunner = dependencies.commandRunner ?? defaultCommandRunner;
|
|
11170
|
+
const duration = parseInactiveFor(options.inactiveFor);
|
|
11171
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
11172
|
+
const cutoff = new Date(now.getTime() - duration.seconds * 1e3).toISOString();
|
|
11173
|
+
const worktrees = (dependencies.listWorktrees ?? listGitWorktrees)(repoPath);
|
|
11174
|
+
const records = (dependencies.listOwnership ?? listWorkspaceOwnership)(repoPath);
|
|
11175
|
+
let devpods;
|
|
11176
|
+
try {
|
|
11177
|
+
devpods = (dependencies.listDevpods ?? listDevpodWorkspaces)();
|
|
11178
|
+
} catch {
|
|
11179
|
+
devpods = void 0;
|
|
11180
|
+
}
|
|
11181
|
+
let routes;
|
|
11182
|
+
try {
|
|
11183
|
+
routes = (dependencies.readRoutes ?? readHostRouteStateReadOnly)();
|
|
11184
|
+
} catch {
|
|
11185
|
+
routes = void 0;
|
|
11186
|
+
}
|
|
11187
|
+
const measureSize = Boolean(options.measureSize);
|
|
11188
|
+
const measureWorktreeFn = dependencies.measureWorktree ?? measureWorktreeConsumption;
|
|
11189
|
+
const measureContainersFn = dependencies.measureContainers ?? measureContainerConsumption;
|
|
11190
|
+
let containers = /* @__PURE__ */ new Map();
|
|
11191
|
+
if (measureSize) {
|
|
11192
|
+
const worktreePaths = records.map((record) => record.worktreePath);
|
|
11193
|
+
try {
|
|
11194
|
+
containers = measureContainersFn(worktreePaths);
|
|
11195
|
+
} catch (error) {
|
|
11196
|
+
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
11197
|
+
containers = new Map(worktreePaths.map((path24) => [path24, unknownContainers(reason)]));
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
const rows = records.map((record) => {
|
|
11201
|
+
const worktree = worktrees.find(
|
|
11202
|
+
(candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
|
|
11203
|
+
);
|
|
11204
|
+
const snapshot = (dependencies.readGitSnapshot ?? ((candidate) => readGitSnapshot(candidate, commandRunner)))(
|
|
11205
|
+
worktree ?? {
|
|
11206
|
+
path: record.worktreePath,
|
|
11207
|
+
branch: record.branch ?? void 0,
|
|
11208
|
+
locked: false,
|
|
11209
|
+
prunable: true
|
|
11210
|
+
}
|
|
11211
|
+
);
|
|
11212
|
+
const row = buildRow(
|
|
11213
|
+
repoPath,
|
|
11214
|
+
record,
|
|
11215
|
+
worktrees,
|
|
11216
|
+
devpods,
|
|
11217
|
+
routes,
|
|
11218
|
+
snapshot,
|
|
11219
|
+
cutoff,
|
|
11220
|
+
Boolean(options.checkMerged),
|
|
11221
|
+
commandRunner,
|
|
11222
|
+
dependencies.inspectOwnership ?? ((recordValue, worktreeValues, devpodValues) => {
|
|
11223
|
+
const status = inspectWorkspaceOwnership(recordValue, worktreeValues, devpodValues);
|
|
11224
|
+
return { ownerStatus: status.ownerStatus, devpodStatus: status.devpodStatus };
|
|
11225
|
+
}),
|
|
11226
|
+
dependencies.inspectIntegration,
|
|
11227
|
+
dependencies.inspectDevpodRuntime ?? inspectDevpodRuntimeStatus
|
|
11228
|
+
);
|
|
11229
|
+
return measureSize ? {
|
|
11230
|
+
...row,
|
|
11231
|
+
consumption: collectConsumption(record.worktreePath, measureWorktreeFn, containers)
|
|
11232
|
+
} : row;
|
|
11233
|
+
}).sort((left, right) => left.workspace.localeCompare(right.workspace));
|
|
11234
|
+
return {
|
|
11235
|
+
schemaVersion: 2,
|
|
11236
|
+
generatedAt: now.toISOString(),
|
|
11237
|
+
repoPath,
|
|
11238
|
+
inactiveFor: duration.input,
|
|
11239
|
+
cutoff,
|
|
11240
|
+
checkMerged: Boolean(options.checkMerged),
|
|
11241
|
+
measureSize,
|
|
11242
|
+
workspaces: rows
|
|
11243
|
+
};
|
|
11244
|
+
}
|
|
11245
|
+
var import_node_child_process21, import_node_fs25, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
11246
|
+
var init_workspace_cleanup = __esm({
|
|
11247
|
+
"src/core/workspace-cleanup.ts"() {
|
|
11248
|
+
"use strict";
|
|
11249
|
+
import_node_child_process21 = require("child_process");
|
|
11250
|
+
import_node_fs25 = __toESM(require("fs"));
|
|
11251
|
+
init_devpod_workspaces();
|
|
11252
|
+
init_host_routes();
|
|
11253
|
+
init_repo_config();
|
|
11254
|
+
init_workspace();
|
|
11255
|
+
init_workspace_consumption();
|
|
11256
|
+
init_workspace_ownership();
|
|
11257
|
+
DEFAULT_INACTIVE_FOR = "30d";
|
|
11258
|
+
READ_ONLY_GIT_ENV2 = { ...process.env, GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" };
|
|
11259
|
+
ACTIVITY_SOURCES = [
|
|
11260
|
+
"devpod.lastUsed",
|
|
11261
|
+
"route.updatedAt",
|
|
11262
|
+
"ownership.updatedAt",
|
|
11263
|
+
"git.headCommitterDate"
|
|
11264
|
+
];
|
|
11265
|
+
defaultCommandRunner = (command, args, input2) => {
|
|
11266
|
+
const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
|
|
11267
|
+
const result = (0, import_node_child_process21.spawnSync)(command, args, {
|
|
11268
|
+
encoding: "utf-8",
|
|
11269
|
+
env: environment,
|
|
11270
|
+
...input2 === void 0 ? {} : { input: input2 }
|
|
11271
|
+
});
|
|
11272
|
+
return {
|
|
11273
|
+
status: result.status,
|
|
11274
|
+
stdout: result.stdout ?? "",
|
|
11275
|
+
stderr: result.stderr ?? "",
|
|
11276
|
+
...result.error ? { error: result.error } : {}
|
|
11277
|
+
};
|
|
11278
|
+
};
|
|
11279
|
+
}
|
|
11280
|
+
});
|
|
11281
|
+
|
|
9862
11282
|
// src/commands/workspace.ts
|
|
9863
11283
|
var workspace_exports = {};
|
|
9864
11284
|
__export(workspace_exports, {
|
|
11285
|
+
runWorkspaceCleanupCommand: () => runWorkspaceCleanupCommand,
|
|
9865
11286
|
runWorkspaceDownCommand: () => runWorkspaceDownCommand,
|
|
9866
11287
|
runWorkspaceGcCommand: () => runWorkspaceGcCommand,
|
|
9867
11288
|
runWorkspaceLsCommand: () => runWorkspaceLsCommand,
|
|
@@ -9909,6 +11330,15 @@ function runWorkspaceLsCommand(options) {
|
|
|
9909
11330
|
);
|
|
9910
11331
|
}
|
|
9911
11332
|
}
|
|
11333
|
+
function runWorkspaceCleanupCommand(options) {
|
|
11334
|
+
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
11335
|
+
const report = buildWorkspaceCleanupReport({ ...options, repo: repoPath });
|
|
11336
|
+
if (options.json) {
|
|
11337
|
+
printJSON(report);
|
|
11338
|
+
return;
|
|
11339
|
+
}
|
|
11340
|
+
printWorkspaceCleanupReport(report);
|
|
11341
|
+
}
|
|
9912
11342
|
async function runWorkspaceDownCommand(target, options) {
|
|
9913
11343
|
const repoPath = resolveGitWorkspaceRepo(options.repo);
|
|
9914
11344
|
await workspaceDown(target, {
|
|
@@ -9947,7 +11377,9 @@ function runWorkspaceGcCommand(options) {
|
|
|
9947
11377
|
var init_workspace2 = __esm({
|
|
9948
11378
|
"src/commands/workspace.ts"() {
|
|
9949
11379
|
"use strict";
|
|
11380
|
+
init_output();
|
|
9950
11381
|
init_repo_config();
|
|
11382
|
+
init_workspace_cleanup();
|
|
9951
11383
|
init_workspace_gc();
|
|
9952
11384
|
init_workspace_lifecycle();
|
|
9953
11385
|
init_workspace_ownership();
|
|
@@ -9992,7 +11424,7 @@ var init_version = __esm({
|
|
|
9992
11424
|
|
|
9993
11425
|
// src/cli.ts
|
|
9994
11426
|
var import_commander = require("commander");
|
|
9995
|
-
var CLI_VERSION = true ? "0.0.
|
|
11427
|
+
var CLI_VERSION = true ? "0.0.36" : "0.0.0-dev";
|
|
9996
11428
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
9997
11429
|
function withErrorHandling(action2) {
|
|
9998
11430
|
return async (...args) => {
|
|
@@ -10053,11 +11485,15 @@ program.command("ensure").description("Start and prove a primary or linked check
|
|
|
10053
11485
|
});
|
|
10054
11486
|
})
|
|
10055
11487
|
);
|
|
10056
|
-
program.command("stop").description("Stop one checkout's exact DevPod and free its routes
|
|
11488
|
+
program.command("stop").description("Stop one checkout's exact DevPod and free its routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--json", "Output JSON").option("--delete", "Delete the exact DevPod instead of preserving its data").action(
|
|
10057
11489
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
10058
11490
|
const options = command.opts();
|
|
10059
11491
|
const { runStopCommand: runStopCommand2 } = await Promise.resolve().then(() => (init_stop(), stop_exports));
|
|
10060
|
-
await runStopCommand2({
|
|
11492
|
+
await runStopCommand2({
|
|
11493
|
+
path: repoPath,
|
|
11494
|
+
delete: Boolean(options.delete),
|
|
11495
|
+
json: Boolean(options.json)
|
|
11496
|
+
});
|
|
10061
11497
|
})
|
|
10062
11498
|
);
|
|
10063
11499
|
program.command("exec").description("Run one literal command inside the exact checkout's running DevPod").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
|
|
@@ -10227,8 +11663,8 @@ tlsCommand.command("install").description("Install mkcert certs and enable HTTPS
|
|
|
10227
11663
|
await runTLSInstallCommand2();
|
|
10228
11664
|
})
|
|
10229
11665
|
);
|
|
10230
|
-
var
|
|
10231
|
-
|
|
11666
|
+
var workspaceCommand2 = program.command("workspace").description("Spin up / list / tear down isolated worktree+devcontainer workspaces");
|
|
11667
|
+
workspaceCommand2.command("up").description(
|
|
10232
11668
|
"Create a worktree for <branch>, bring up its devpod, and register namespaced routes"
|
|
10233
11669
|
).argument("<branch>", "Git branch to base the workspace on").option("--path <dir>", "Worktree directory (default: <repo>/trees/<workspace>)").option("--no-devpod", "Only create the worktree; do not start the environment or change routes").option("--open", "Open the namespaced routes after registering").option("--repo <path>", "Main repository path (defaults to current directory)").action(
|
|
10234
11670
|
withErrorHandling(async (branch, _options, command) => {
|
|
@@ -10242,7 +11678,7 @@ workspaceCommand.command("up").description(
|
|
|
10242
11678
|
});
|
|
10243
11679
|
})
|
|
10244
11680
|
);
|
|
10245
|
-
|
|
11681
|
+
workspaceCommand2.command("ensure").description("Start and prove a primary or linked checkout's DevPod, upstreams, and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
|
|
10246
11682
|
withErrorHandling(
|
|
10247
11683
|
async (worktreePath, _options, command) => {
|
|
10248
11684
|
const options = command.opts();
|
|
@@ -10255,28 +11691,41 @@ workspaceCommand.command("ensure").description("Start and prove a primary or lin
|
|
|
10255
11691
|
}
|
|
10256
11692
|
)
|
|
10257
11693
|
);
|
|
10258
|
-
|
|
11694
|
+
workspaceCommand2.command("ls").description("List git worktrees with their workspace token and active route count").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").action(
|
|
10259
11695
|
withErrorHandling(async (_options, command) => {
|
|
10260
11696
|
const options = command.opts();
|
|
10261
11697
|
const { runWorkspaceLsCommand: runWorkspaceLsCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
10262
11698
|
runWorkspaceLsCommand2(options);
|
|
10263
11699
|
})
|
|
10264
11700
|
);
|
|
10265
|
-
|
|
11701
|
+
workspaceCommand2.command("cleanup").description("Report managed workspace activity, identity, checkout, and integration evidence").option("--repo <path>", "Main repository path (defaults to current directory)").option("--inactive-for <duration>", "Inactive threshold using Ns, Nm, Nh, Nd, or Nw", "30d").option("--check-merged", "Enable read-only origin and GitHub/GitLab integration checks").option("--measure-size", "Measure per-workspace storage consumption (slower, still read-only)").option("--json", "Output the stable cleanup report as JSON").action(
|
|
11702
|
+
withErrorHandling(async (_options, command) => {
|
|
11703
|
+
const options = command.opts();
|
|
11704
|
+
const { runWorkspaceCleanupCommand: runWorkspaceCleanupCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
11705
|
+
runWorkspaceCleanupCommand2({
|
|
11706
|
+
repo: options.repo,
|
|
11707
|
+
inactiveFor: options.inactiveFor,
|
|
11708
|
+
checkMerged: Boolean(options.checkMerged),
|
|
11709
|
+
measureSize: Boolean(options.measureSize),
|
|
11710
|
+
json: Boolean(options.json)
|
|
11711
|
+
});
|
|
11712
|
+
})
|
|
11713
|
+
);
|
|
11714
|
+
workspaceCommand2.command("gc").description("Report or clean missing ledger-owned workspace resources").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").option("--yes", "Delete eligible DevPod, route, and ownership resources").action(
|
|
10266
11715
|
withErrorHandling(async (_options, command) => {
|
|
10267
11716
|
const options = command.opts();
|
|
10268
11717
|
const { runWorkspaceGcCommand: runWorkspaceGcCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
10269
11718
|
runWorkspaceGcCommand2(options);
|
|
10270
11719
|
})
|
|
10271
11720
|
);
|
|
10272
|
-
|
|
11721
|
+
workspaceCommand2.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(
|
|
10273
11722
|
withErrorHandling(async (target, _options, command) => {
|
|
10274
11723
|
const options = command.opts();
|
|
10275
11724
|
const { runWorkspaceStopCommand: runWorkspaceStopCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
10276
11725
|
await runWorkspaceStopCommand2(target, options);
|
|
10277
11726
|
})
|
|
10278
11727
|
);
|
|
10279
|
-
|
|
11728
|
+
workspaceCommand2.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(
|
|
10280
11729
|
withErrorHandling(async (target, _options, command) => {
|
|
10281
11730
|
const options = command.opts();
|
|
10282
11731
|
const { runWorkspaceDownCommand: runWorkspaceDownCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|