@devrouter/cli 0.0.34 → 0.0.35
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 +945 -546
- package/package.json +2 -1
- package/upgrade-prompts/0.0.35.md +29 -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,10 +241,12 @@ 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
|
|
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.
|
|
246
250
|
- **Cleanup**: owner status is \`present\`, \`missing\`, \`locked\`, or \`conflict\`. \`workspace gc\` is a dry-run report; \`--yes\` deletes only exact ledger-owned missing/prunable resources and their records. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
|
|
247
251
|
- **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
|
|
248
252
|
|
|
@@ -295,7 +299,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
295
299
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
296
300
|
- \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
|
|
297
301
|
- \`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
|
|
302
|
+
- \`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
303
|
- \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
|
|
300
304
|
- \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
|
|
301
305
|
- \`devrouter status\`: router/container/network/TLS health
|
|
@@ -721,6 +725,51 @@ var init_capabilities = __esm({
|
|
|
721
725
|
}
|
|
722
726
|
});
|
|
723
727
|
|
|
728
|
+
// src/core/atomic-file.ts
|
|
729
|
+
function fsyncDirectory(directory) {
|
|
730
|
+
const handle = import_node_fs3.default.openSync(directory, "r");
|
|
731
|
+
try {
|
|
732
|
+
import_node_fs3.default.fsyncSync(handle);
|
|
733
|
+
} finally {
|
|
734
|
+
import_node_fs3.default.closeSync(handle);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function writeFileAtomically(filePath, contents) {
|
|
738
|
+
const directory = import_node_path3.default.dirname(filePath);
|
|
739
|
+
import_node_fs3.default.mkdirSync(directory, { recursive: true });
|
|
740
|
+
const temporaryPath = import_node_path3.default.join(
|
|
741
|
+
directory,
|
|
742
|
+
`.${import_node_path3.default.basename(filePath)}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`
|
|
743
|
+
);
|
|
744
|
+
let temporaryHandle;
|
|
745
|
+
try {
|
|
746
|
+
temporaryHandle = import_node_fs3.default.openSync(temporaryPath, "wx", 384);
|
|
747
|
+
import_node_fs3.default.writeFileSync(temporaryHandle, contents, "utf-8");
|
|
748
|
+
import_node_fs3.default.fsyncSync(temporaryHandle);
|
|
749
|
+
import_node_fs3.default.closeSync(temporaryHandle);
|
|
750
|
+
temporaryHandle = void 0;
|
|
751
|
+
import_node_fs3.default.renameSync(temporaryPath, filePath);
|
|
752
|
+
fsyncDirectory(directory);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (temporaryHandle !== void 0) import_node_fs3.default.closeSync(temporaryHandle);
|
|
755
|
+
try {
|
|
756
|
+
import_node_fs3.default.unlinkSync(temporaryPath);
|
|
757
|
+
} catch (cleanupError) {
|
|
758
|
+
if (cleanupError.code !== "ENOENT") throw cleanupError;
|
|
759
|
+
}
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
var import_node_crypto, import_node_fs3, import_node_path3;
|
|
764
|
+
var init_atomic_file = __esm({
|
|
765
|
+
"src/core/atomic-file.ts"() {
|
|
766
|
+
"use strict";
|
|
767
|
+
import_node_crypto = require("crypto");
|
|
768
|
+
import_node_fs3 = __toESM(require("fs"));
|
|
769
|
+
import_node_path3 = __toESM(require("path"));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
724
773
|
// src/core/file-lock.ts
|
|
725
774
|
function sleepSync(ms) {
|
|
726
775
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -732,17 +781,56 @@ function isProcessAlive(pid) {
|
|
|
732
781
|
try {
|
|
733
782
|
process.kill(pid, 0);
|
|
734
783
|
return true;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return error.code === "EPERM";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function processBirthIdentity(pid) {
|
|
789
|
+
try {
|
|
790
|
+
const stat = import_node_fs4.default.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
791
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
792
|
+
if (commandEnd >= 0) {
|
|
793
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
794
|
+
const startTime = fields[19];
|
|
795
|
+
if (startTime) return `proc:${startTime}`;
|
|
796
|
+
}
|
|
735
797
|
} catch {
|
|
736
|
-
|
|
798
|
+
}
|
|
799
|
+
const result = (0, import_node_child_process2.spawnSync)("ps", ["-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
800
|
+
encoding: "utf-8",
|
|
801
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
802
|
+
});
|
|
803
|
+
const startedAt = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
804
|
+
return startedAt ? `ps:${(0, import_node_crypto2.createHash)("sha256").update(startedAt).digest("hex")}` : void 0;
|
|
805
|
+
}
|
|
806
|
+
function parseLockOwner(value) {
|
|
807
|
+
const fields = value.split(":");
|
|
808
|
+
const pid = Number(fields[0]);
|
|
809
|
+
if (!Number.isInteger(pid) || pid <= 0) return void 0;
|
|
810
|
+
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])) {
|
|
811
|
+
return { pid };
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
const processBirth = Buffer.from(fields[1], "base64url").toString("utf-8");
|
|
815
|
+
const canonical = Buffer.from(processBirth).toString("base64url");
|
|
816
|
+
return canonical === fields[1] && /^(proc|ps):/.test(processBirth) ? { pid, processBirth } : { pid };
|
|
817
|
+
} catch {
|
|
818
|
+
return { pid };
|
|
737
819
|
}
|
|
738
820
|
}
|
|
821
|
+
function isLockOwnerLive(owner) {
|
|
822
|
+
if (!isProcessAlive(owner.pid)) return false;
|
|
823
|
+
if (!owner.processBirth) return true;
|
|
824
|
+
const currentBirth = processBirthIdentity(owner.pid);
|
|
825
|
+
return currentBirth === void 0 || currentBirth === owner.processBirth;
|
|
826
|
+
}
|
|
739
827
|
function sameFile(left, right) {
|
|
740
828
|
return left.dev === right.dev && left.ino === right.ino;
|
|
741
829
|
}
|
|
742
830
|
function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
743
831
|
let fd;
|
|
744
832
|
try {
|
|
745
|
-
fd =
|
|
833
|
+
fd = import_node_fs4.default.openSync(lockPath, "r");
|
|
746
834
|
} catch (error) {
|
|
747
835
|
if (error.code === "ENOENT") {
|
|
748
836
|
return { kind: "retry" };
|
|
@@ -750,17 +838,16 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
750
838
|
throw error;
|
|
751
839
|
}
|
|
752
840
|
try {
|
|
753
|
-
const owner =
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
return { kind: "live", pid: ownerPid };
|
|
841
|
+
const owner = parseLockOwner(import_node_fs4.default.readFileSync(fd, "utf-8").trim());
|
|
842
|
+
if (owner && isLockOwnerLive(owner)) {
|
|
843
|
+
return { kind: "live", pid: owner.pid };
|
|
757
844
|
}
|
|
758
|
-
const staleStat =
|
|
845
|
+
const staleStat = import_node_fs4.default.fstatSync(fd);
|
|
759
846
|
if (staleStat.nlink !== 1) {
|
|
760
847
|
return { kind: "retry" };
|
|
761
848
|
}
|
|
762
849
|
try {
|
|
763
|
-
|
|
850
|
+
import_node_fs4.default.linkSync(lockPath, staleLinkPath);
|
|
764
851
|
} catch (error) {
|
|
765
852
|
if (error.code === "ENOENT") {
|
|
766
853
|
return { kind: "retry" };
|
|
@@ -770,7 +857,7 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
770
857
|
try {
|
|
771
858
|
let currentStat;
|
|
772
859
|
try {
|
|
773
|
-
currentStat =
|
|
860
|
+
currentStat = import_node_fs4.default.statSync(lockPath);
|
|
774
861
|
} catch (error) {
|
|
775
862
|
if (error.code === "ENOENT") {
|
|
776
863
|
return { kind: "retry" };
|
|
@@ -780,27 +867,32 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
780
867
|
if (!sameFile(staleStat, currentStat) || currentStat.nlink !== 2) {
|
|
781
868
|
return { kind: "retry" };
|
|
782
869
|
}
|
|
783
|
-
|
|
870
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
784
871
|
return { kind: "reclaimed" };
|
|
785
872
|
} finally {
|
|
786
|
-
|
|
873
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
787
874
|
}
|
|
788
875
|
} finally {
|
|
789
|
-
|
|
876
|
+
import_node_fs4.default.closeSync(fd);
|
|
790
877
|
}
|
|
791
878
|
}
|
|
792
879
|
function acquireFileLock(lockPath, options) {
|
|
793
|
-
const
|
|
794
|
-
|
|
880
|
+
const processBirth = processBirthIdentity(process.pid);
|
|
881
|
+
if (!processBirth) {
|
|
882
|
+
throw new Error(`could not determine process identity for ${options.activity} lock`);
|
|
883
|
+
}
|
|
884
|
+
const ownerId = (0, import_node_crypto2.randomUUID)();
|
|
885
|
+
const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
|
|
886
|
+
const candidatePath = `${lockPath}.${process.pid}.${ownerId}.candidate`;
|
|
795
887
|
const staleLinkPath = `${candidatePath}.stale`;
|
|
796
888
|
const deadline = Date.now() + (options.waitMs ?? 0);
|
|
797
889
|
let reclaimAttempts = 0;
|
|
798
|
-
|
|
890
|
+
import_node_fs4.default.writeFileSync(candidatePath, `${owner}
|
|
799
891
|
`, { encoding: "utf-8", flag: "wx" });
|
|
800
892
|
try {
|
|
801
893
|
for (; ; ) {
|
|
802
894
|
try {
|
|
803
|
-
|
|
895
|
+
import_node_fs4.default.linkSync(candidatePath, lockPath);
|
|
804
896
|
return owner;
|
|
805
897
|
} catch (error) {
|
|
806
898
|
if (error.code !== "EEXIST") {
|
|
@@ -828,14 +920,14 @@ function acquireFileLock(lockPath, options) {
|
|
|
828
920
|
}
|
|
829
921
|
}
|
|
830
922
|
} finally {
|
|
831
|
-
|
|
832
|
-
|
|
923
|
+
import_node_fs4.default.rmSync(candidatePath, { force: true });
|
|
924
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
833
925
|
}
|
|
834
926
|
}
|
|
835
927
|
function releaseFileLock(lockPath, owner) {
|
|
836
928
|
try {
|
|
837
|
-
if (
|
|
838
|
-
|
|
929
|
+
if (import_node_fs4.default.readFileSync(lockPath, "utf-8").trim() === owner) {
|
|
930
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
839
931
|
}
|
|
840
932
|
} catch (error) {
|
|
841
933
|
if (error.code !== "ENOENT") {
|
|
@@ -859,20 +951,21 @@ async function withFileLock(lockPath, options, operation) {
|
|
|
859
951
|
releaseFileLock(lockPath, owner);
|
|
860
952
|
}
|
|
861
953
|
}
|
|
862
|
-
var
|
|
954
|
+
var import_node_child_process2, import_node_crypto2, import_node_fs4;
|
|
863
955
|
var init_file_lock = __esm({
|
|
864
956
|
"src/core/file-lock.ts"() {
|
|
865
957
|
"use strict";
|
|
866
|
-
|
|
867
|
-
|
|
958
|
+
import_node_child_process2 = require("child_process");
|
|
959
|
+
import_node_crypto2 = require("crypto");
|
|
960
|
+
import_node_fs4 = __toESM(require("fs"));
|
|
868
961
|
}
|
|
869
962
|
});
|
|
870
963
|
|
|
871
964
|
// src/core/workspace.ts
|
|
872
965
|
function comparableWorkspacePath(filePath) {
|
|
873
|
-
const resolved =
|
|
966
|
+
const resolved = import_node_path4.default.resolve(filePath);
|
|
874
967
|
try {
|
|
875
|
-
return
|
|
968
|
+
return import_node_fs5.default.realpathSync.native(resolved);
|
|
876
969
|
} catch {
|
|
877
970
|
return resolved;
|
|
878
971
|
}
|
|
@@ -885,10 +978,10 @@ function wsFromBranch(branch) {
|
|
|
885
978
|
return slug.length > 0 ? slug : void 0;
|
|
886
979
|
}
|
|
887
980
|
function isLinkedWorktree(repoPath) {
|
|
888
|
-
const gitPath =
|
|
981
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
889
982
|
let stat;
|
|
890
983
|
try {
|
|
891
|
-
stat =
|
|
984
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
892
985
|
} catch {
|
|
893
986
|
return false;
|
|
894
987
|
}
|
|
@@ -897,7 +990,7 @@ function isLinkedWorktree(repoPath) {
|
|
|
897
990
|
}
|
|
898
991
|
let content;
|
|
899
992
|
try {
|
|
900
|
-
content =
|
|
993
|
+
content = import_node_fs5.default.readFileSync(gitPath, "utf-8");
|
|
901
994
|
} catch {
|
|
902
995
|
return false;
|
|
903
996
|
}
|
|
@@ -909,10 +1002,10 @@ function isLinkedWorktree(repoPath) {
|
|
|
909
1002
|
return /(^|\/)worktrees\//.test(gitdir);
|
|
910
1003
|
}
|
|
911
1004
|
function gitMetadataDir(repoPath) {
|
|
912
|
-
const gitPath =
|
|
1005
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
913
1006
|
let stat;
|
|
914
1007
|
try {
|
|
915
|
-
stat =
|
|
1008
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
916
1009
|
} catch {
|
|
917
1010
|
return void 0;
|
|
918
1011
|
}
|
|
@@ -922,11 +1015,11 @@ function gitMetadataDir(repoPath) {
|
|
|
922
1015
|
if (!stat.isFile()) {
|
|
923
1016
|
return void 0;
|
|
924
1017
|
}
|
|
925
|
-
const match =
|
|
1018
|
+
const match = import_node_fs5.default.readFileSync(gitPath, "utf-8").match(/^gitdir:\s*(.+)$/m);
|
|
926
1019
|
if (!match) {
|
|
927
1020
|
return void 0;
|
|
928
1021
|
}
|
|
929
|
-
return
|
|
1022
|
+
return import_node_path4.default.resolve(repoPath, match[1].trim());
|
|
930
1023
|
}
|
|
931
1024
|
function validatePersistedWorkspace(value) {
|
|
932
1025
|
const workspace = value.trim();
|
|
@@ -940,9 +1033,9 @@ function readPersistedWorkspace(repoPath) {
|
|
|
940
1033
|
if (!gitDir) {
|
|
941
1034
|
return void 0;
|
|
942
1035
|
}
|
|
943
|
-
const metadataPath =
|
|
1036
|
+
const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
|
|
944
1037
|
try {
|
|
945
|
-
return validatePersistedWorkspace(
|
|
1038
|
+
return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
|
|
946
1039
|
} catch (error) {
|
|
947
1040
|
if (error.code === "ENOENT") {
|
|
948
1041
|
return void 0;
|
|
@@ -965,7 +1058,7 @@ function persistWorkspace(repoPath, value) {
|
|
|
965
1058
|
if (!gitDir) {
|
|
966
1059
|
throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
|
|
967
1060
|
}
|
|
968
|
-
|
|
1061
|
+
import_node_fs5.default.writeFileSync(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
|
|
969
1062
|
`, "utf-8");
|
|
970
1063
|
return workspace;
|
|
971
1064
|
}
|
|
@@ -974,7 +1067,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
974
1067
|
if (!gitDir) {
|
|
975
1068
|
throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
|
|
976
1069
|
}
|
|
977
|
-
const lockPath =
|
|
1070
|
+
const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
|
|
978
1071
|
return withFileLock(
|
|
979
1072
|
lockPath,
|
|
980
1073
|
{ activity: "workspace lifecycle", target: `'${repoPath}'` },
|
|
@@ -982,7 +1075,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
982
1075
|
);
|
|
983
1076
|
}
|
|
984
1077
|
function currentBranch(repoPath) {
|
|
985
|
-
const result = (0,
|
|
1078
|
+
const result = (0, import_node_child_process3.spawnSync)("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
986
1079
|
encoding: "utf-8"
|
|
987
1080
|
});
|
|
988
1081
|
if (result.status !== 0) {
|
|
@@ -1002,7 +1095,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
|
1002
1095
|
return void 0;
|
|
1003
1096
|
}
|
|
1004
1097
|
const branch = currentBranch(repoPath);
|
|
1005
|
-
return wsFromBranch(branch ??
|
|
1098
|
+
return wsFromBranch(branch ?? import_node_path4.default.basename(import_node_path4.default.resolve(repoPath)));
|
|
1006
1099
|
}
|
|
1007
1100
|
function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
1008
1101
|
return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
|
|
@@ -1025,13 +1118,13 @@ function resolveWorkspace(repoPath, override) {
|
|
|
1025
1118
|
}
|
|
1026
1119
|
return deriveLinkedWorktreeWorkspace(repoPath);
|
|
1027
1120
|
}
|
|
1028
|
-
var
|
|
1121
|
+
var import_node_child_process3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
|
|
1029
1122
|
var init_workspace = __esm({
|
|
1030
1123
|
"src/core/workspace.ts"() {
|
|
1031
1124
|
"use strict";
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1125
|
+
import_node_child_process3 = require("child_process");
|
|
1126
|
+
import_node_fs5 = __toESM(require("fs"));
|
|
1127
|
+
import_node_path4 = __toESM(require("path"));
|
|
1035
1128
|
init_file_lock();
|
|
1036
1129
|
MAX_WORKSPACE_LENGTH = 32;
|
|
1037
1130
|
WORKSPACE_METADATA_FILE = "devrouter-workspace";
|
|
@@ -1151,64 +1244,199 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
|
|
|
1151
1244
|
}
|
|
1152
1245
|
return document;
|
|
1153
1246
|
}
|
|
1154
|
-
function
|
|
1247
|
+
function isRecord(value) {
|
|
1248
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1249
|
+
}
|
|
1250
|
+
function validateRouteState(value, source) {
|
|
1251
|
+
if (!Array.isArray(value)) {
|
|
1252
|
+
throw new Error(`${source} must contain a route array.`);
|
|
1253
|
+
}
|
|
1254
|
+
const routes = value.map((item, index) => {
|
|
1255
|
+
if (!isRecord(item)) {
|
|
1256
|
+
throw new Error(`${source} route ${index} must be an object.`);
|
|
1257
|
+
}
|
|
1258
|
+
for (const key of ["id", "name", "host", "repoPath", "createdAt", "updatedAt"]) {
|
|
1259
|
+
if (typeof item[key] !== "string" || item[key].length === 0) {
|
|
1260
|
+
throw new Error(`${source} route ${index} has an invalid '${key}'.`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!Number.isInteger(item.port) || Number(item.port) < 1 || Number(item.port) > 65535) {
|
|
1264
|
+
throw new Error(`${source} route ${index} has an invalid 'port'.`);
|
|
1265
|
+
}
|
|
1266
|
+
if (item.mode !== "run" && item.mode !== "attach" && item.mode !== "proxy") {
|
|
1267
|
+
throw new Error(`${source} route ${index} has an invalid 'mode'.`);
|
|
1268
|
+
}
|
|
1269
|
+
if (item.protocol !== void 0 && item.protocol !== "http" && item.protocol !== "tcp") {
|
|
1270
|
+
throw new Error(`${source} route ${index} has an invalid 'protocol'.`);
|
|
1271
|
+
}
|
|
1272
|
+
for (const key of ["tcpProtocol", "upstreamHost", "command", "workspace"]) {
|
|
1273
|
+
if (item[key] !== void 0 && typeof item[key] !== "string") {
|
|
1274
|
+
throw new Error(`${source} route ${index} has an invalid '${key}'.`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (item.pid !== void 0 && (!Number.isInteger(item.pid) || Number(item.pid) <= 0)) {
|
|
1278
|
+
throw new Error(`${source} route ${index} has an invalid 'pid'.`);
|
|
1279
|
+
}
|
|
1280
|
+
const route = item;
|
|
1281
|
+
if (route.id !== buildHostRouteId(route.repoPath, route.name)) {
|
|
1282
|
+
throw new Error(`${source} route ${index} has an inconsistent 'id'.`);
|
|
1283
|
+
}
|
|
1284
|
+
return route;
|
|
1285
|
+
});
|
|
1286
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1287
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const route of routes) {
|
|
1289
|
+
if (ids.has(route.id)) {
|
|
1290
|
+
throw new Error(`${source} contains duplicate route id '${route.id}'.`);
|
|
1291
|
+
}
|
|
1292
|
+
if (hosts.has(route.host)) {
|
|
1293
|
+
throw new Error(`${source} contains duplicate route host '${route.host}'.`);
|
|
1294
|
+
}
|
|
1295
|
+
ids.add(route.id);
|
|
1296
|
+
hosts.add(route.host);
|
|
1297
|
+
}
|
|
1298
|
+
return routes;
|
|
1299
|
+
}
|
|
1300
|
+
function renderCompatibilityState(routes) {
|
|
1301
|
+
return `${JSON.stringify(routes, null, 2)}
|
|
1302
|
+
`;
|
|
1303
|
+
}
|
|
1304
|
+
function renderCanonicalState(routes, tlsEnabled) {
|
|
1305
|
+
const metadata = { version: 1, tlsEnabled, routes };
|
|
1306
|
+
const encoded = Buffer.from(JSON.stringify(metadata), "utf-8").toString("base64url");
|
|
1155
1307
|
const document = buildHostRoutesDocument(routes, tlsEnabled);
|
|
1156
|
-
|
|
1308
|
+
return `${ROUTE_METADATA_PREFIX}${encoded}
|
|
1309
|
+
${import_yaml.default.stringify(document, { lineWidth: 0 })}`;
|
|
1157
1310
|
}
|
|
1158
|
-
function
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1311
|
+
function writeRouteGeneration(routes, tlsEnabled) {
|
|
1312
|
+
writeFileAtomically(HOST_ROUTES_STATE_FILE, renderCompatibilityState(routes));
|
|
1313
|
+
writeFileAtomically(TRAEFIK_HOST_ROUTES_FILE, renderCanonicalState(routes, tlsEnabled));
|
|
1314
|
+
}
|
|
1315
|
+
function readCompatibilityState() {
|
|
1316
|
+
let raw;
|
|
1317
|
+
try {
|
|
1318
|
+
raw = import_node_fs6.default.readFileSync(HOST_ROUTES_STATE_FILE, "utf-8");
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`Could not read the compatibility host-route state: ${error.message}`
|
|
1166
1322
|
);
|
|
1167
1323
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1324
|
+
try {
|
|
1325
|
+
return validateRouteState(JSON.parse(raw), "Compatibility host-route state");
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
throw new Error(`Invalid compatibility host-route state: ${error.message}`);
|
|
1170
1328
|
}
|
|
1171
1329
|
}
|
|
1330
|
+
function parseCanonicalState(raw) {
|
|
1331
|
+
const firstNewline = raw.indexOf("\n");
|
|
1332
|
+
const firstLine2 = firstNewline === -1 ? raw : raw.slice(0, firstNewline);
|
|
1333
|
+
if (!firstLine2.startsWith(ROUTE_METADATA_PREFIX)) {
|
|
1334
|
+
if (firstLine2.startsWith(ROUTE_METADATA_FAMILY_PREFIX)) {
|
|
1335
|
+
throw new Error(`Unsupported or malformed host-route metadata header '${firstLine2}'.`);
|
|
1336
|
+
}
|
|
1337
|
+
return { kind: "legacy" };
|
|
1338
|
+
}
|
|
1339
|
+
const encoded = firstLine2.slice(ROUTE_METADATA_PREFIX.length);
|
|
1340
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(encoded)) {
|
|
1341
|
+
throw new Error("Host-route metadata header is not valid base64url.");
|
|
1342
|
+
}
|
|
1343
|
+
let metadataValue;
|
|
1344
|
+
try {
|
|
1345
|
+
const decoded = Buffer.from(encoded, "base64url");
|
|
1346
|
+
if (decoded.toString("base64url") !== encoded) {
|
|
1347
|
+
throw new Error("non-canonical base64url encoding");
|
|
1348
|
+
}
|
|
1349
|
+
metadataValue = JSON.parse(decoded.toString("utf-8"));
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
throw new Error(`Host-route metadata header is invalid: ${error.message}`);
|
|
1352
|
+
}
|
|
1353
|
+
if (!isRecord(metadataValue) || metadataValue.version !== 1) {
|
|
1354
|
+
throw new Error("Host-route metadata header has an unsupported version.");
|
|
1355
|
+
}
|
|
1356
|
+
if (typeof metadataValue.tlsEnabled !== "boolean") {
|
|
1357
|
+
throw new Error("Host-route metadata header has an invalid TLS state.");
|
|
1358
|
+
}
|
|
1359
|
+
const routes = validateRouteState(metadataValue.routes, "Canonical host-route metadata");
|
|
1360
|
+
const metadata = {
|
|
1361
|
+
version: 1,
|
|
1362
|
+
tlsEnabled: metadataValue.tlsEnabled,
|
|
1363
|
+
routes
|
|
1364
|
+
};
|
|
1365
|
+
let document;
|
|
1366
|
+
try {
|
|
1367
|
+
document = import_yaml.default.parse(firstNewline === -1 ? "" : raw.slice(firstNewline + 1));
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
throw new Error(`Canonical host-route document is invalid: ${error.message}`);
|
|
1370
|
+
}
|
|
1371
|
+
const expectedDocument = buildHostRoutesDocument(routes, metadata.tlsEnabled);
|
|
1372
|
+
if (!(0, import_node_util.isDeepStrictEqual)(document, expectedDocument)) {
|
|
1373
|
+
throw new Error("Canonical host-route document does not match its metadata.");
|
|
1374
|
+
}
|
|
1375
|
+
return { kind: "canonical", metadata };
|
|
1376
|
+
}
|
|
1377
|
+
function ensureHostRouteStorage() {
|
|
1378
|
+
import_node_fs6.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
1379
|
+
import_node_fs6.default.mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
|
|
1380
|
+
}
|
|
1172
1381
|
function writeState(routes) {
|
|
1173
1382
|
ensureHostRouteStorage();
|
|
1174
|
-
|
|
1175
|
-
`, "utf-8");
|
|
1176
|
-
writeHostRoutesDynamicFile(routes, isTLSEnabled());
|
|
1383
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1177
1384
|
}
|
|
1178
1385
|
function withStateLock(fn) {
|
|
1179
1386
|
ensureHostRouteStorage();
|
|
1180
1387
|
return withFileLockSync(STATE_LOCK_FILE, { activity: "host route update", waitMs: 5e3 }, fn);
|
|
1181
1388
|
}
|
|
1182
1389
|
function refreshHostRoutesDynamicFile() {
|
|
1183
|
-
|
|
1184
|
-
|
|
1390
|
+
withStateLock(() => {
|
|
1391
|
+
const routes = readHostRouteStateLocked();
|
|
1392
|
+
writeState(routes);
|
|
1393
|
+
});
|
|
1185
1394
|
}
|
|
1186
|
-
function
|
|
1395
|
+
function readHostRouteStateLocked() {
|
|
1187
1396
|
ensureHostRouteStorage();
|
|
1188
|
-
if (!
|
|
1397
|
+
if (!import_node_fs6.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
|
|
1398
|
+
if (import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1399
|
+
const routes = readCompatibilityState();
|
|
1400
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1401
|
+
return routes;
|
|
1402
|
+
}
|
|
1403
|
+
writeRouteGeneration([], isTLSEnabled());
|
|
1189
1404
|
return [];
|
|
1190
1405
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if (!
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
return parsed.filter((item) => item && typeof item === "object").map((item) => item);
|
|
1198
|
-
} catch (err) {
|
|
1199
|
-
const error = err;
|
|
1200
|
-
if (error.code !== "ENOENT") {
|
|
1201
|
-
process.stderr.write(
|
|
1202
|
-
`Warning: devrouter host routes state file is corrupted or unreadable (${error.message}). Recreating route state.
|
|
1203
|
-
`
|
|
1406
|
+
const raw = import_node_fs6.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
|
|
1407
|
+
const canonical = parseCanonicalState(raw);
|
|
1408
|
+
if (canonical.kind === "legacy") {
|
|
1409
|
+
if (!import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
"Headerless host-route document requires a valid compatibility state file for migration."
|
|
1204
1412
|
);
|
|
1205
1413
|
}
|
|
1206
|
-
|
|
1414
|
+
const routes = readCompatibilityState();
|
|
1415
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1416
|
+
return routes;
|
|
1417
|
+
}
|
|
1418
|
+
let mirrorMatches = false;
|
|
1419
|
+
if (import_node_fs6.default.existsSync(HOST_ROUTES_STATE_FILE)) {
|
|
1420
|
+
try {
|
|
1421
|
+
mirrorMatches = (0, import_node_util.isDeepStrictEqual)(readCompatibilityState(), canonical.metadata.routes);
|
|
1422
|
+
} catch {
|
|
1423
|
+
mirrorMatches = false;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (!mirrorMatches) {
|
|
1427
|
+
writeFileAtomically(
|
|
1428
|
+
HOST_ROUTES_STATE_FILE,
|
|
1429
|
+
renderCompatibilityState(canonical.metadata.routes)
|
|
1430
|
+
);
|
|
1207
1431
|
}
|
|
1432
|
+
return canonical.metadata.routes;
|
|
1433
|
+
}
|
|
1434
|
+
function listHostRouteState() {
|
|
1435
|
+
return withStateLock(readHostRouteStateLocked);
|
|
1208
1436
|
}
|
|
1209
1437
|
function upsertHostRoute(input2) {
|
|
1210
1438
|
return withStateLock(() => {
|
|
1211
|
-
const routes =
|
|
1439
|
+
const routes = readHostRouteStateLocked();
|
|
1212
1440
|
const id = buildHostRouteId(input2.repoPath, input2.name);
|
|
1213
1441
|
const existing = routes.find((route) => route.id === id);
|
|
1214
1442
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1225,7 +1453,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1225
1453
|
if (inputs.some((input2) => !sameWorkspacePath(input2.repoPath, repoPath))) {
|
|
1226
1454
|
throw new Error(`Route replacement contains an entry outside '${repoPath}'.`);
|
|
1227
1455
|
}
|
|
1228
|
-
const routes =
|
|
1456
|
+
const routes = readHostRouteStateLocked();
|
|
1229
1457
|
const remaining = routes.filter((route) => !sameWorkspacePath(route.repoPath, repoPath));
|
|
1230
1458
|
const names = /* @__PURE__ */ new Set();
|
|
1231
1459
|
const hosts = /* @__PURE__ */ new Set();
|
|
@@ -1263,7 +1491,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1263
1491
|
}
|
|
1264
1492
|
function removeHostRouteById(id) {
|
|
1265
1493
|
return withStateLock(() => {
|
|
1266
|
-
const routes =
|
|
1494
|
+
const routes = readHostRouteStateLocked();
|
|
1267
1495
|
const next = routes.filter((route) => route.id !== id);
|
|
1268
1496
|
if (next.length === routes.length) {
|
|
1269
1497
|
return false;
|
|
@@ -1274,7 +1502,7 @@ function removeHostRouteById(id) {
|
|
|
1274
1502
|
}
|
|
1275
1503
|
function removeHostRoutesWhere(predicate) {
|
|
1276
1504
|
return withStateLock(() => {
|
|
1277
|
-
const routes =
|
|
1505
|
+
const routes = readHostRouteStateLocked();
|
|
1278
1506
|
const removed = routes.filter(predicate);
|
|
1279
1507
|
if (removed.length === 0) {
|
|
1280
1508
|
return [];
|
|
@@ -1296,7 +1524,7 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1296
1524
|
protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
|
|
1297
1525
|
appName: route.name,
|
|
1298
1526
|
serviceName: route.name,
|
|
1299
|
-
projectName:
|
|
1527
|
+
projectName: import_node_path5.default.basename(route.repoPath),
|
|
1300
1528
|
hosts: [route.host],
|
|
1301
1529
|
// TCP routes are reached via an SNI-aware client on the shared protocol
|
|
1302
1530
|
// port; surface a protocol-scheme URL rather than http(s)://.
|
|
@@ -1311,13 +1539,15 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1311
1539
|
};
|
|
1312
1540
|
});
|
|
1313
1541
|
}
|
|
1314
|
-
var
|
|
1542
|
+
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
1543
|
var init_host_routes = __esm({
|
|
1316
1544
|
"src/core/host-routes.ts"() {
|
|
1317
1545
|
"use strict";
|
|
1318
|
-
|
|
1319
|
-
|
|
1546
|
+
import_node_fs6 = __toESM(require("fs"));
|
|
1547
|
+
import_node_path5 = __toESM(require("path"));
|
|
1548
|
+
import_node_util = require("util");
|
|
1320
1549
|
import_yaml = __toESM(require("yaml"));
|
|
1550
|
+
init_atomic_file();
|
|
1321
1551
|
init_file_lock();
|
|
1322
1552
|
init_router();
|
|
1323
1553
|
init_workspace();
|
|
@@ -1326,6 +1556,8 @@ var init_host_routes = __esm({
|
|
|
1326
1556
|
TCP_TLS_ALPN_PROTOCOLS = {
|
|
1327
1557
|
postgres: ["postgresql"]
|
|
1328
1558
|
};
|
|
1559
|
+
ROUTE_METADATA_PREFIX = "# devrouter-routes-v1: ";
|
|
1560
|
+
ROUTE_METADATA_FAMILY_PREFIX = "# devrouter-routes-";
|
|
1329
1561
|
STATE_LOCK_FILE = `${HOST_ROUTES_STATE_FILE}.lock`;
|
|
1330
1562
|
}
|
|
1331
1563
|
});
|
|
@@ -1753,10 +1985,10 @@ function renderConfig(config) {
|
|
|
1753
1985
|
return import_yaml2.default.stringify(config, { lineWidth: 0 });
|
|
1754
1986
|
}
|
|
1755
1987
|
function readConfigDocument(configPath) {
|
|
1756
|
-
return (0, import_yaml2.parseDocument)(
|
|
1988
|
+
return (0, import_yaml2.parseDocument)(import_node_fs7.default.readFileSync(configPath, "utf-8"));
|
|
1757
1989
|
}
|
|
1758
1990
|
function writeConfigDocument(configPath, doc) {
|
|
1759
|
-
|
|
1991
|
+
import_node_fs7.default.writeFileSync(configPath, doc.toString({ lineWidth: 0 }), "utf-8");
|
|
1760
1992
|
}
|
|
1761
1993
|
function getOrCreateAppsSeq(doc) {
|
|
1762
1994
|
const apps = doc.get("apps", true);
|
|
@@ -1778,25 +2010,25 @@ function appToNodeValue(app) {
|
|
|
1778
2010
|
return value;
|
|
1779
2011
|
}
|
|
1780
2012
|
function resolveRepoPath(repoPath) {
|
|
1781
|
-
return
|
|
2013
|
+
return import_node_path6.default.resolve(repoPath ?? process.cwd());
|
|
1782
2014
|
}
|
|
1783
2015
|
function getRepoConfigPath(repoPath) {
|
|
1784
|
-
return
|
|
2016
|
+
return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
1785
2017
|
}
|
|
1786
2018
|
function loadRepoConfig(repoPath) {
|
|
1787
2019
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1788
2020
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1789
|
-
if (!
|
|
2021
|
+
if (!import_node_fs7.default.existsSync(configPath)) {
|
|
1790
2022
|
throw new Error(
|
|
1791
2023
|
`Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
|
|
1792
2024
|
);
|
|
1793
2025
|
}
|
|
1794
|
-
const raw =
|
|
2026
|
+
const raw = import_node_fs7.default.readFileSync(configPath, "utf-8");
|
|
1795
2027
|
const parsed = import_yaml2.default.parse(raw);
|
|
1796
2028
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
1797
2029
|
const requiredVersion = config.devrouter?.version;
|
|
1798
2030
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
1799
|
-
const cliVersion = true ? "0.0.
|
|
2031
|
+
const cliVersion = true ? "0.0.35" : "0.0.0-dev";
|
|
1800
2032
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
1801
2033
|
hasWarnedVersionMismatch = true;
|
|
1802
2034
|
process.stderr.write(
|
|
@@ -1813,7 +2045,7 @@ function loadRepoConfig(repoPath) {
|
|
|
1813
2045
|
function initRepoConfig(repoPath, options = {}) {
|
|
1814
2046
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1815
2047
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1816
|
-
if (
|
|
2048
|
+
if (import_node_fs7.default.existsSync(configPath)) {
|
|
1817
2049
|
return { repoPath: resolvedRepoPath, configPath, created: false };
|
|
1818
2050
|
}
|
|
1819
2051
|
if (options.devrouterVersion !== void 0 && !DEVROUTER_VERSION_RE.test(options.devrouterVersion)) {
|
|
@@ -1829,11 +2061,11 @@ function initRepoConfig(repoPath, options = {}) {
|
|
|
1829
2061
|
}
|
|
1830
2062
|
} : {},
|
|
1831
2063
|
project: {
|
|
1832
|
-
name:
|
|
2064
|
+
name: import_node_path6.default.basename(resolvedRepoPath)
|
|
1833
2065
|
},
|
|
1834
2066
|
apps: []
|
|
1835
2067
|
};
|
|
1836
|
-
|
|
2068
|
+
import_node_fs7.default.writeFileSync(configPath, renderConfig(initialConfig), "utf-8");
|
|
1837
2069
|
return { repoPath: resolvedRepoPath, configPath, created: true };
|
|
1838
2070
|
}
|
|
1839
2071
|
function buildAppFromOptions(options) {
|
|
@@ -2058,7 +2290,7 @@ function namespaceHost(host, workspace) {
|
|
|
2058
2290
|
return `${base}.${workspace}${suffix}`;
|
|
2059
2291
|
}
|
|
2060
2292
|
function applyWorkspace(config, workspace, repoPath) {
|
|
2061
|
-
const defaultToken = wsFromBranch(config.project?.name ??
|
|
2293
|
+
const defaultToken = wsFromBranch(config.project?.name ?? import_node_path6.default.basename(import_node_path6.default.resolve(repoPath))) ?? "app";
|
|
2062
2294
|
const substitutionToken = workspace ?? defaultToken;
|
|
2063
2295
|
const next = structuredClone(config);
|
|
2064
2296
|
for (const app of next.apps) {
|
|
@@ -2133,12 +2365,12 @@ function resolveAppDependencies(config, app) {
|
|
|
2133
2365
|
}
|
|
2134
2366
|
return results;
|
|
2135
2367
|
}
|
|
2136
|
-
var
|
|
2368
|
+
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
2369
|
var init_repo_config = __esm({
|
|
2138
2370
|
"src/core/repo-config.ts"() {
|
|
2139
2371
|
"use strict";
|
|
2140
|
-
|
|
2141
|
-
|
|
2372
|
+
import_node_fs7 = __toESM(require("fs"));
|
|
2373
|
+
import_node_path6 = __toESM(require("path"));
|
|
2142
2374
|
import_yaml2 = __toESM(require("yaml"));
|
|
2143
2375
|
init_capabilities();
|
|
2144
2376
|
init_host_routes();
|
|
@@ -2205,6 +2437,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2205
2437
|
"- Shared Traefik router owns host ports 80 (HTTP), 443 (HTTPS), and 5432 (Postgres TCP).",
|
|
2206
2438
|
"- Per-repo source of truth is REPO_PATH/.devrouter.yml only.",
|
|
2207
2439
|
"- Global generated/runtime artifacts are managed under ~/.config/devrouter (do not edit these manually).",
|
|
2440
|
+
"- Managed DevPod lifecycle uses devrouter ensure/stop/workspace commands; raw devpod mutations bypass exact checkout ownership checks.",
|
|
2208
2441
|
"",
|
|
2209
2442
|
"Inputs:",
|
|
2210
2443
|
`- REPO_PATH=${repoPath}`,
|
|
@@ -2248,6 +2481,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2248
2481
|
"- if kind=app and runtime=proxy:",
|
|
2249
2482
|
' - 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
2483
|
` - 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.`,
|
|
2484
|
+
" - 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
2485
|
" - protocol=http registers an HTTP route; protocol=tcp registers a TLS-SNI TCP route and additionally requires tcpProtocol",
|
|
2252
2486
|
" - do not set hostRun/docker/dependencies (proxy only registers a route to the upstream)",
|
|
2253
2487
|
" - loopback hosts (localhost/127.0.0.1/0.0.0.0) are rewritten to host.docker.internal for Traefik",
|
|
@@ -2298,9 +2532,10 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2298
2532
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
2299
2533
|
`- 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
2534
|
"- 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.`,
|
|
2535
|
+
`- 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
2536
|
"- 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
|
|
2537
|
+
"- 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.",
|
|
2538
|
+
"- 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
2539
|
"- 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.",
|
|
2305
2540
|
"- 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
2541
|
"- 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.",
|
|
@@ -2465,8 +2700,8 @@ var init_ai_prompt = __esm({
|
|
|
2465
2700
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
2466
2701
|
},
|
|
2467
2702
|
{
|
|
2468
|
-
command: "devrouter stop [path] [--json]",
|
|
2469
|
-
purpose: "Stop the exact checkout DevPod and routes without
|
|
2703
|
+
command: "devrouter stop [path] [--delete] [--json]",
|
|
2704
|
+
purpose: "Stop the exact checkout DevPod and routes; --delete explicitly deletes the ownership-proven DevPod without removing the checkout."
|
|
2470
2705
|
},
|
|
2471
2706
|
{
|
|
2472
2707
|
command: "devrouter exec [path] -- <command...>",
|
|
@@ -2883,11 +3118,11 @@ function extractCurrentVersionFromRepoConfig(repoPath) {
|
|
|
2883
3118
|
};
|
|
2884
3119
|
}
|
|
2885
3120
|
function readPromptDirectory(promptDirPath) {
|
|
2886
|
-
if (!
|
|
3121
|
+
if (!import_node_fs8.default.existsSync(promptDirPath)) {
|
|
2887
3122
|
throw new Error(`Missing ${UPGRADE_PROMPTS_DIR} directory at ${promptDirPath}.`);
|
|
2888
3123
|
}
|
|
2889
3124
|
const releases = [];
|
|
2890
|
-
const files =
|
|
3125
|
+
const files = import_node_fs8.default.readdirSync(promptDirPath, { withFileTypes: true });
|
|
2891
3126
|
for (const file of files) {
|
|
2892
3127
|
if (!file.isFile() || !file.name.endsWith(".md")) {
|
|
2893
3128
|
continue;
|
|
@@ -2897,8 +3132,8 @@ function readPromptDirectory(promptDirPath) {
|
|
|
2897
3132
|
continue;
|
|
2898
3133
|
}
|
|
2899
3134
|
const version = normalizeVersion(basename);
|
|
2900
|
-
const promptPath =
|
|
2901
|
-
const prompt =
|
|
3135
|
+
const promptPath = import_node_path7.default.join(promptDirPath, file.name);
|
|
3136
|
+
const prompt = import_node_fs8.default.readFileSync(promptPath, "utf-8").trim();
|
|
2902
3137
|
releases.push({ version, prompt, promptPath });
|
|
2903
3138
|
}
|
|
2904
3139
|
const deduped = /* @__PURE__ */ new Map();
|
|
@@ -2919,19 +3154,19 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
|
|
|
2919
3154
|
}
|
|
2920
3155
|
function resolvePromptDirectory(explicitPath) {
|
|
2921
3156
|
if (explicitPath) {
|
|
2922
|
-
return
|
|
3157
|
+
return import_node_path7.default.resolve(explicitPath);
|
|
2923
3158
|
}
|
|
2924
|
-
const entryFile = process.argv[1] ?
|
|
2925
|
-
const entryDir =
|
|
2926
|
-
const resolvedEntryDir =
|
|
3159
|
+
const entryFile = process.argv[1] ? import_node_path7.default.resolve(process.argv[1]) : __filename;
|
|
3160
|
+
const entryDir = import_node_path7.default.dirname(entryFile);
|
|
3161
|
+
const resolvedEntryDir = import_node_fs8.default.existsSync(entryFile) ? import_node_path7.default.dirname(import_node_fs8.default.realpathSync(entryFile)) : entryDir;
|
|
2927
3162
|
const candidates = [
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3163
|
+
import_node_path7.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
3164
|
+
import_node_path7.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
3165
|
+
import_node_path7.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
|
|
3166
|
+
import_node_path7.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
|
|
3167
|
+
import_node_path7.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
|
|
2933
3168
|
];
|
|
2934
|
-
const existing = candidates.find((candidate) =>
|
|
3169
|
+
const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
|
|
2935
3170
|
return existing ?? candidates[0];
|
|
2936
3171
|
}
|
|
2937
3172
|
function loadUpgradeCatalog(options = {}) {
|
|
@@ -2947,12 +3182,12 @@ function loadUpgradeCatalog(options = {}) {
|
|
|
2947
3182
|
releases
|
|
2948
3183
|
};
|
|
2949
3184
|
}
|
|
2950
|
-
var
|
|
3185
|
+
var import_node_fs8, import_node_path7, SEMVER_RE, UPGRADE_PROMPTS_DIR;
|
|
2951
3186
|
var init_upgrade = __esm({
|
|
2952
3187
|
"src/core/upgrade.ts"() {
|
|
2953
3188
|
"use strict";
|
|
2954
|
-
|
|
2955
|
-
|
|
3189
|
+
import_node_fs8 = __toESM(require("fs"));
|
|
3190
|
+
import_node_path7 = __toESM(require("path"));
|
|
2956
3191
|
init_repo_config();
|
|
2957
3192
|
SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
2958
3193
|
UPGRADE_PROMPTS_DIR = "upgrade-prompts";
|
|
@@ -3052,7 +3287,7 @@ async function getDockerodeConstructor() {
|
|
|
3052
3287
|
return dockerodeConstructorPromise;
|
|
3053
3288
|
}
|
|
3054
3289
|
function runDockerContextCommand(args) {
|
|
3055
|
-
const result = (0,
|
|
3290
|
+
const result = (0, import_node_child_process4.spawnSync)("docker", ["context", ...args], {
|
|
3056
3291
|
encoding: "utf-8"
|
|
3057
3292
|
});
|
|
3058
3293
|
if (result.status !== 0) {
|
|
@@ -3065,7 +3300,7 @@ function getCurrentDockerContext() {
|
|
|
3065
3300
|
return runDockerContextCommand(["show"]);
|
|
3066
3301
|
}
|
|
3067
3302
|
function getDockerHostFromContext(context) {
|
|
3068
|
-
const result = (0,
|
|
3303
|
+
const result = (0, import_node_child_process4.spawnSync)(
|
|
3069
3304
|
"docker",
|
|
3070
3305
|
["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
|
|
3071
3306
|
{ encoding: "utf-8" }
|
|
@@ -3133,15 +3368,177 @@ async function networkExists(name) {
|
|
|
3133
3368
|
return false;
|
|
3134
3369
|
}
|
|
3135
3370
|
}
|
|
3136
|
-
var
|
|
3371
|
+
var import_node_child_process4, dockerodeConstructorPromise;
|
|
3137
3372
|
var init_docker = __esm({
|
|
3138
3373
|
"src/core/docker.ts"() {
|
|
3139
3374
|
"use strict";
|
|
3140
|
-
|
|
3375
|
+
import_node_child_process4 = require("child_process");
|
|
3141
3376
|
dockerodeConstructorPromise = null;
|
|
3142
3377
|
}
|
|
3143
3378
|
});
|
|
3144
3379
|
|
|
3380
|
+
// src/core/managed-post-start.ts
|
|
3381
|
+
function renderDeliveryScript(targetPath) {
|
|
3382
|
+
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
3383
|
+
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
3384
|
+
}
|
|
3385
|
+
const temporaryPrefix = import_node_path8.default.posix.basename(targetPath);
|
|
3386
|
+
return `set -eu
|
|
3387
|
+
umask 077
|
|
3388
|
+
runtime_root=/tmp/devrouter
|
|
3389
|
+
runtime_bin="$runtime_root/bin"
|
|
3390
|
+
target="${targetPath}"
|
|
3391
|
+
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
3392
|
+
echo "Refusing symlinked devrouter runtime path." >&2
|
|
3393
|
+
exit 1
|
|
3394
|
+
fi
|
|
3395
|
+
mkdir -p "$runtime_bin"
|
|
3396
|
+
chmod 700 "$runtime_root" "$runtime_bin"
|
|
3397
|
+
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
3398
|
+
trap 'rm -f "$temporary"' EXIT
|
|
3399
|
+
cat > "$temporary"
|
|
3400
|
+
chmod 700 "$temporary"
|
|
3401
|
+
mv -f "$temporary" "$target"
|
|
3402
|
+
trap - EXIT
|
|
3403
|
+
`;
|
|
3404
|
+
}
|
|
3405
|
+
function commandFailure(result) {
|
|
3406
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
3407
|
+
}
|
|
3408
|
+
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
3409
|
+
const result = (0, import_node_child_process5.spawnSync)(
|
|
3410
|
+
"docker",
|
|
3411
|
+
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
3412
|
+
{ input: contents, encoding: "utf-8" }
|
|
3413
|
+
);
|
|
3414
|
+
if (result.status !== 0) {
|
|
3415
|
+
const details = commandFailure(result);
|
|
3416
|
+
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function readRegularFileBytes(filePath) {
|
|
3420
|
+
if (!import_node_fs9.default.existsSync(filePath) || !import_node_fs9.default.lstatSync(filePath).isFile()) return void 0;
|
|
3421
|
+
return import_node_fs9.default.readFileSync(filePath);
|
|
3422
|
+
}
|
|
3423
|
+
function readRegularFile(filePath) {
|
|
3424
|
+
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
3425
|
+
}
|
|
3426
|
+
function adapterFingerprint(adapter) {
|
|
3427
|
+
return (0, import_node_crypto3.createHash)("sha256").update(adapter).digest("hex");
|
|
3428
|
+
}
|
|
3429
|
+
function resolveProcessHelperPath() {
|
|
3430
|
+
const candidates = [
|
|
3431
|
+
import_node_path8.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
3432
|
+
import_node_path8.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
3433
|
+
];
|
|
3434
|
+
const helperPath = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
|
|
3435
|
+
if (!helperPath) {
|
|
3436
|
+
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
3437
|
+
}
|
|
3438
|
+
return helperPath;
|
|
3439
|
+
}
|
|
3440
|
+
function resolveManagedPostStartPlan(repoPath) {
|
|
3441
|
+
const adapterPath = import_node_path8.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
3442
|
+
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
3443
|
+
const adapterText = adapterBytes?.toString("utf-8");
|
|
3444
|
+
const dockerfilePath = import_node_path8.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
3445
|
+
const devcontainerPath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
3446
|
+
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
3447
|
+
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
3448
|
+
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
3449
|
+
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
3450
|
+
);
|
|
3451
|
+
if (adapterBytes === void 0) {
|
|
3452
|
+
if (devrouterPattern) {
|
|
3453
|
+
throw new Error(
|
|
3454
|
+
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
return { kind: "unmanaged" };
|
|
3458
|
+
}
|
|
3459
|
+
const adapter = adapterBytes.toString("utf-8");
|
|
3460
|
+
if (!adapter.includes(MANAGED_MARKER)) {
|
|
3461
|
+
if (devrouterPattern) {
|
|
3462
|
+
throw new Error(
|
|
3463
|
+
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
return { kind: "unmanaged" };
|
|
3467
|
+
}
|
|
3468
|
+
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
3469
|
+
return {
|
|
3470
|
+
kind: "runtime",
|
|
3471
|
+
adapterPath: MANAGED_ADAPTER_PATH,
|
|
3472
|
+
adapterSha256: adapterFingerprint(adapterBytes),
|
|
3473
|
+
adapterContents: adapterBytes
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
3477
|
+
return { kind: "legacy" };
|
|
3478
|
+
}
|
|
3479
|
+
throw new Error(
|
|
3480
|
+
"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."
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
function runManagedPostStart(options) {
|
|
3484
|
+
if (options.plan.kind !== "runtime") return;
|
|
3485
|
+
const helper = import_node_fs9.default.readFileSync(resolveProcessHelperPath());
|
|
3486
|
+
deliverRuntimeFile(
|
|
3487
|
+
options.container.id,
|
|
3488
|
+
RUNTIME_HELPER_PATH,
|
|
3489
|
+
helper,
|
|
3490
|
+
"the managed process helper"
|
|
3491
|
+
);
|
|
3492
|
+
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
3493
|
+
deliverRuntimeFile(
|
|
3494
|
+
options.container.id,
|
|
3495
|
+
runtimeAdapterPath,
|
|
3496
|
+
options.plan.adapterContents,
|
|
3497
|
+
"the managed post-start adapter"
|
|
3498
|
+
);
|
|
3499
|
+
const started = (0, import_node_child_process5.spawnSync)(
|
|
3500
|
+
"docker",
|
|
3501
|
+
[
|
|
3502
|
+
"exec",
|
|
3503
|
+
"--workdir",
|
|
3504
|
+
options.container.workspacePath,
|
|
3505
|
+
"--env",
|
|
3506
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
3507
|
+
"--env",
|
|
3508
|
+
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
3509
|
+
options.container.id,
|
|
3510
|
+
"bash",
|
|
3511
|
+
"-c",
|
|
3512
|
+
ADAPTER_WRAPPER,
|
|
3513
|
+
options.plan.adapterPath,
|
|
3514
|
+
runtimeAdapterPath
|
|
3515
|
+
],
|
|
3516
|
+
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
3517
|
+
);
|
|
3518
|
+
if (started.status !== 0) {
|
|
3519
|
+
const details = commandFailure(started);
|
|
3520
|
+
throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
var import_node_child_process5, import_node_crypto3, import_node_fs9, import_node_path8, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
|
|
3524
|
+
var init_managed_post_start = __esm({
|
|
3525
|
+
"src/core/managed-post-start.ts"() {
|
|
3526
|
+
"use strict";
|
|
3527
|
+
import_node_child_process5 = require("child_process");
|
|
3528
|
+
import_node_crypto3 = require("crypto");
|
|
3529
|
+
import_node_fs9 = __toESM(require("fs"));
|
|
3530
|
+
import_node_path8 = __toESM(require("path"));
|
|
3531
|
+
MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
3532
|
+
MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
3533
|
+
RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
3534
|
+
ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
3535
|
+
shift
|
|
3536
|
+
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
|
|
3537
|
+
source "$adapter_snapshot"
|
|
3538
|
+
`;
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3145
3542
|
// src/core/devcontainer-diagnostics.ts
|
|
3146
3543
|
function asRecord(value) {
|
|
3147
3544
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3196,7 +3593,7 @@ function isLoopbackHost(host) {
|
|
|
3196
3593
|
return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
|
|
3197
3594
|
}
|
|
3198
3595
|
function inspectCompose(composeFile, _workspace) {
|
|
3199
|
-
if (!
|
|
3596
|
+
if (!import_node_fs10.default.existsSync(composeFile)) {
|
|
3200
3597
|
return {
|
|
3201
3598
|
aliases: [],
|
|
3202
3599
|
publishedPorts: [],
|
|
@@ -3205,7 +3602,7 @@ function inspectCompose(composeFile, _workspace) {
|
|
|
3205
3602
|
};
|
|
3206
3603
|
}
|
|
3207
3604
|
try {
|
|
3208
|
-
const raw =
|
|
3605
|
+
const raw = import_node_fs10.default.readFileSync(composeFile, "utf-8");
|
|
3209
3606
|
const parsed = import_yaml3.default.parse(raw);
|
|
3210
3607
|
const root = asRecord(parsed);
|
|
3211
3608
|
const services = asRecord(root?.services);
|
|
@@ -3262,11 +3659,11 @@ function routedProxyApps(config) {
|
|
|
3262
3659
|
);
|
|
3263
3660
|
}
|
|
3264
3661
|
function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
3265
|
-
const devcontainerDir =
|
|
3266
|
-
if (!
|
|
3662
|
+
const devcontainerDir = import_node_path9.default.join(repoPath, ".devcontainer");
|
|
3663
|
+
if (!import_node_fs10.default.existsSync(devcontainerDir)) {
|
|
3267
3664
|
return [];
|
|
3268
3665
|
}
|
|
3269
|
-
const compose = inspectCompose(
|
|
3666
|
+
const compose = inspectCompose(import_node_path9.default.join(devcontainerDir, "docker-compose.yml"), workspace);
|
|
3270
3667
|
const checks = [];
|
|
3271
3668
|
if (compose.parseError) {
|
|
3272
3669
|
checks.push({
|
|
@@ -3293,9 +3690,9 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3293
3690
|
details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
|
|
3294
3691
|
suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
|
|
3295
3692
|
});
|
|
3296
|
-
const dockerfilePath =
|
|
3297
|
-
const dockerfileExists =
|
|
3298
|
-
const dockerfile = dockerfileExists ?
|
|
3693
|
+
const dockerfilePath = import_node_path9.default.join(devcontainerDir, "Dockerfile");
|
|
3694
|
+
const dockerfileExists = import_node_fs10.default.existsSync(dockerfilePath);
|
|
3695
|
+
const dockerfile = dockerfileExists ? import_node_fs10.default.readFileSync(dockerfilePath, "utf-8") : "";
|
|
3299
3696
|
const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
|
|
3300
3697
|
(artifact) => dockerfile.includes(artifact)
|
|
3301
3698
|
);
|
|
@@ -3306,6 +3703,23 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3306
3703
|
details: devrouterArtifacts.length > 0 ? `artifacts=${devrouterArtifacts.join(", ")}` : void 0,
|
|
3307
3704
|
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
3705
|
});
|
|
3706
|
+
try {
|
|
3707
|
+
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
3708
|
+
checks.push({
|
|
3709
|
+
id: "repo.devcontainer.managed-post-start",
|
|
3710
|
+
level: managedPostStart.kind === "legacy" ? "warn" : "ok",
|
|
3711
|
+
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.",
|
|
3712
|
+
suggestion: managedPostStart.kind === "legacy" ? "Regenerate or migrate the devcontainer to runtime helper delivery." : void 0
|
|
3713
|
+
});
|
|
3714
|
+
} catch (error) {
|
|
3715
|
+
checks.push({
|
|
3716
|
+
id: "repo.devcontainer.managed-post-start",
|
|
3717
|
+
level: "error",
|
|
3718
|
+
summary: "Devrouter lifecycle wiring is incomplete or ambiguous.",
|
|
3719
|
+
details: error instanceof Error ? error.message : String(error),
|
|
3720
|
+
suggestion: "Regenerate or migrate the managed devcontainer, or remove devrouter-specific wiring from a truly custom devcontainer."
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3309
3723
|
if (!config) {
|
|
3310
3724
|
checks.push({
|
|
3311
3725
|
id: "repo.devcontainer.upstream-alias-match",
|
|
@@ -3333,30 +3747,31 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3333
3747
|
});
|
|
3334
3748
|
return checks;
|
|
3335
3749
|
}
|
|
3336
|
-
var
|
|
3750
|
+
var import_node_fs10, import_node_path9, import_yaml3;
|
|
3337
3751
|
var init_devcontainer_diagnostics = __esm({
|
|
3338
3752
|
"src/core/devcontainer-diagnostics.ts"() {
|
|
3339
3753
|
"use strict";
|
|
3340
|
-
|
|
3341
|
-
|
|
3754
|
+
import_node_fs10 = __toESM(require("fs"));
|
|
3755
|
+
import_node_path9 = __toESM(require("path"));
|
|
3342
3756
|
import_yaml3 = __toESM(require("yaml"));
|
|
3757
|
+
init_managed_post_start();
|
|
3343
3758
|
}
|
|
3344
3759
|
});
|
|
3345
3760
|
|
|
3346
3761
|
// src/core/paths.ts
|
|
3347
3762
|
function assertPathWithinRepo(filePath, repoRoot, label) {
|
|
3348
|
-
const resolvedRoot =
|
|
3349
|
-
const resolved =
|
|
3350
|
-
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot +
|
|
3763
|
+
const resolvedRoot = import_node_path10.default.resolve(repoRoot);
|
|
3764
|
+
const resolved = import_node_path10.default.resolve(repoRoot, filePath);
|
|
3765
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path10.default.sep)) {
|
|
3351
3766
|
throw new Error(`${label} path '${filePath}' escapes the repository root.`);
|
|
3352
3767
|
}
|
|
3353
3768
|
return resolved;
|
|
3354
3769
|
}
|
|
3355
|
-
var
|
|
3770
|
+
var import_node_path10;
|
|
3356
3771
|
var init_paths = __esm({
|
|
3357
3772
|
"src/core/paths.ts"() {
|
|
3358
3773
|
"use strict";
|
|
3359
|
-
|
|
3774
|
+
import_node_path10 = __toESM(require("path"));
|
|
3360
3775
|
}
|
|
3361
3776
|
});
|
|
3362
3777
|
|
|
@@ -3441,11 +3856,11 @@ function reconcileRouteRunConflict(repoPath, app) {
|
|
|
3441
3856
|
}
|
|
3442
3857
|
}
|
|
3443
3858
|
}
|
|
3444
|
-
var
|
|
3859
|
+
var import_node_fs11;
|
|
3445
3860
|
var init_route_state = __esm({
|
|
3446
3861
|
"src/core/route-state.ts"() {
|
|
3447
3862
|
"use strict";
|
|
3448
|
-
|
|
3863
|
+
import_node_fs11 = __toESM(require("fs"));
|
|
3449
3864
|
init_host_routes();
|
|
3450
3865
|
init_workspace();
|
|
3451
3866
|
}
|
|
@@ -3620,7 +4035,7 @@ function toRepoStatus(repoPath) {
|
|
|
3620
4035
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
3621
4036
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
3622
4037
|
const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
|
|
3623
|
-
const configExists =
|
|
4038
|
+
const configExists = import_node_fs12.default.existsSync(configPath);
|
|
3624
4039
|
if (!explicitRepo && !configExists) {
|
|
3625
4040
|
return void 0;
|
|
3626
4041
|
}
|
|
@@ -3747,11 +4162,11 @@ async function collectRouterStatus(repoPath) {
|
|
|
3747
4162
|
}
|
|
3748
4163
|
};
|
|
3749
4164
|
}
|
|
3750
|
-
var
|
|
4165
|
+
var import_node_fs12;
|
|
3751
4166
|
var init_status = __esm({
|
|
3752
4167
|
"src/core/status.ts"() {
|
|
3753
4168
|
"use strict";
|
|
3754
|
-
|
|
4169
|
+
import_node_fs12 = __toESM(require("fs"));
|
|
3755
4170
|
init_docker();
|
|
3756
4171
|
init_repo_config();
|
|
3757
4172
|
init_router();
|
|
@@ -3760,7 +4175,7 @@ var init_status = __esm({
|
|
|
3760
4175
|
|
|
3761
4176
|
// src/core/tls.ts
|
|
3762
4177
|
function runOrThrow(command, args) {
|
|
3763
|
-
const result = (0,
|
|
4178
|
+
const result = (0, import_node_child_process6.spawnSync)(command, args, {
|
|
3764
4179
|
encoding: "utf-8"
|
|
3765
4180
|
});
|
|
3766
4181
|
if (result.status !== 0) {
|
|
@@ -3770,7 +4185,7 @@ function runOrThrow(command, args) {
|
|
|
3770
4185
|
return result.stdout.trim();
|
|
3771
4186
|
}
|
|
3772
4187
|
function commandExists(command) {
|
|
3773
|
-
const result = (0,
|
|
4188
|
+
const result = (0, import_node_child_process6.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
|
|
3774
4189
|
return result.status === 0;
|
|
3775
4190
|
}
|
|
3776
4191
|
function ensureMkcert() {
|
|
@@ -3788,8 +4203,8 @@ function tlsSetupCommand(repoPath) {
|
|
|
3788
4203
|
}
|
|
3789
4204
|
function getMkcertRootCAPath(options = {}) {
|
|
3790
4205
|
ensureMkcert();
|
|
3791
|
-
const rootCAPath =
|
|
3792
|
-
if (!
|
|
4206
|
+
const rootCAPath = import_node_path11.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
|
|
4207
|
+
if (!import_node_fs13.default.existsSync(rootCAPath)) {
|
|
3793
4208
|
throw new Error(
|
|
3794
4209
|
`mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
3795
4210
|
);
|
|
@@ -3822,7 +4237,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
3822
4237
|
return normalizeUniqueHosts(names);
|
|
3823
4238
|
}
|
|
3824
4239
|
function parseCertificateDnsHosts(pem) {
|
|
3825
|
-
const certificate = new
|
|
4240
|
+
const certificate = new import_node_crypto4.X509Certificate(pem);
|
|
3826
4241
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
3827
4242
|
if (subjectAltName.length === 0) {
|
|
3828
4243
|
return [];
|
|
@@ -3855,10 +4270,10 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
|
3855
4270
|
);
|
|
3856
4271
|
}
|
|
3857
4272
|
function readCurrentCertificateHosts() {
|
|
3858
|
-
if (!
|
|
4273
|
+
if (!import_node_fs13.default.existsSync(CERT_FILE)) {
|
|
3859
4274
|
return [];
|
|
3860
4275
|
}
|
|
3861
|
-
const pem =
|
|
4276
|
+
const pem = import_node_fs13.default.readFileSync(CERT_FILE, "utf-8");
|
|
3862
4277
|
return parseCertificateDnsHosts(pem);
|
|
3863
4278
|
}
|
|
3864
4279
|
function currentCertificateHostsOrEmpty() {
|
|
@@ -3947,14 +4362,14 @@ Run: ${tlsSetupCommand(options.repoPath)}`
|
|
|
3947
4362
|
);
|
|
3948
4363
|
}
|
|
3949
4364
|
}
|
|
3950
|
-
var
|
|
4365
|
+
var import_node_child_process6, import_node_crypto4, import_node_fs13, import_node_path11, DEFAULT_TLS_CERT_HOSTS;
|
|
3951
4366
|
var init_tls = __esm({
|
|
3952
4367
|
"src/core/tls.ts"() {
|
|
3953
4368
|
"use strict";
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4369
|
+
import_node_child_process6 = require("child_process");
|
|
4370
|
+
import_node_crypto4 = require("crypto");
|
|
4371
|
+
import_node_fs13 = __toESM(require("fs"));
|
|
4372
|
+
import_node_path11 = __toESM(require("path"));
|
|
3958
4373
|
init_docker();
|
|
3959
4374
|
init_host_routes();
|
|
3960
4375
|
init_router();
|
|
@@ -3970,7 +4385,7 @@ function outputFromResult(result) {
|
|
|
3970
4385
|
return output2.length > 0 ? output2 : void 0;
|
|
3971
4386
|
}
|
|
3972
4387
|
function runTool(command, args = []) {
|
|
3973
|
-
const result = (0,
|
|
4388
|
+
const result = (0, import_node_child_process7.spawnSync)(command, args, {
|
|
3974
4389
|
encoding: "utf-8"
|
|
3975
4390
|
});
|
|
3976
4391
|
if (result.error) {
|
|
@@ -4027,12 +4442,12 @@ function parseMinimumNodeMajor(value) {
|
|
|
4027
4442
|
return Number(match[1]);
|
|
4028
4443
|
}
|
|
4029
4444
|
function readPackageJson(repoPath) {
|
|
4030
|
-
const packagePath =
|
|
4031
|
-
if (!
|
|
4445
|
+
const packagePath = import_node_path12.default.join(repoPath, "package.json");
|
|
4446
|
+
if (!import_node_fs14.default.existsSync(packagePath)) {
|
|
4032
4447
|
return void 0;
|
|
4033
4448
|
}
|
|
4034
4449
|
try {
|
|
4035
|
-
return JSON.parse(
|
|
4450
|
+
return JSON.parse(import_node_fs14.default.readFileSync(packagePath, "utf-8"));
|
|
4036
4451
|
} catch {
|
|
4037
4452
|
return void 0;
|
|
4038
4453
|
}
|
|
@@ -4127,19 +4542,19 @@ function buildGlobalToolChecks(repoPath) {
|
|
|
4127
4542
|
checks.push(nodeToolchainCheck(repoPath));
|
|
4128
4543
|
return checks;
|
|
4129
4544
|
}
|
|
4130
|
-
var
|
|
4545
|
+
var import_node_child_process7, import_node_fs14, import_node_path12;
|
|
4131
4546
|
var init_tool_diagnostics = __esm({
|
|
4132
4547
|
"src/core/tool-diagnostics.ts"() {
|
|
4133
4548
|
"use strict";
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4549
|
+
import_node_child_process7 = require("child_process");
|
|
4550
|
+
import_node_fs14 = __toESM(require("fs"));
|
|
4551
|
+
import_node_path12 = __toESM(require("path"));
|
|
4137
4552
|
}
|
|
4138
4553
|
});
|
|
4139
4554
|
|
|
4140
4555
|
// src/core/devpod-workspaces.ts
|
|
4141
4556
|
function listDevpodWorkspaces() {
|
|
4142
|
-
const result = (0,
|
|
4557
|
+
const result = (0, import_node_child_process8.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
|
|
4143
4558
|
if (result.status !== 0) {
|
|
4144
4559
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4145
4560
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
@@ -4188,20 +4603,138 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
4188
4603
|
}
|
|
4189
4604
|
return matches[0];
|
|
4190
4605
|
}
|
|
4191
|
-
|
|
4606
|
+
var import_node_child_process8;
|
|
4607
|
+
var init_devpod_workspaces = __esm({
|
|
4608
|
+
"src/core/devpod-workspaces.ts"() {
|
|
4609
|
+
"use strict";
|
|
4610
|
+
import_node_child_process8 = require("child_process");
|
|
4611
|
+
init_workspace();
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
|
|
4615
|
+
// src/core/devpod-mutation.ts
|
|
4616
|
+
function withMutationLock(activity, target, operation) {
|
|
4617
|
+
import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
4618
|
+
return withFileLockSync(
|
|
4619
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
4620
|
+
{ activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
|
|
4621
|
+
operation
|
|
4622
|
+
);
|
|
4623
|
+
}
|
|
4624
|
+
function commandFailure2(result) {
|
|
4625
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4626
|
+
}
|
|
4627
|
+
function runDevpodAction(action2, devpodId) {
|
|
4192
4628
|
const args = action2 === "delete" ? [action2, devpodId, "--ignore-not-found"] : [action2, devpodId];
|
|
4193
|
-
const result = (0,
|
|
4629
|
+
const result = (0, import_node_child_process9.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
4194
4630
|
if (result.status !== 0) {
|
|
4195
|
-
|
|
4196
|
-
|
|
4631
|
+
throw new Error(
|
|
4632
|
+
`devpod ${action2} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
|
|
4633
|
+
);
|
|
4197
4634
|
}
|
|
4198
4635
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4636
|
+
function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
|
|
4637
|
+
return withMutationLock(`DevPod ${action2}`, worktreePath, () => {
|
|
4638
|
+
const before = inspectDevpodWorkspaceOwnership(listDevpodWorkspaces(), devpodId, worktreePath);
|
|
4639
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
4640
|
+
if (before.status === "absent") return { status: "absent" };
|
|
4641
|
+
runDevpodAction(action2, devpodId);
|
|
4642
|
+
const after = inspectDevpodWorkspaceOwnership(listDevpodWorkspaces(), devpodId, worktreePath);
|
|
4643
|
+
if (after.status === "conflict") throw new Error(after.reason);
|
|
4644
|
+
if (action2 === "stop" && after.status !== "owned") {
|
|
4645
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
4646
|
+
}
|
|
4647
|
+
if (action2 === "delete" && after.status !== "absent") {
|
|
4648
|
+
throw new Error(`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete.`);
|
|
4649
|
+
}
|
|
4650
|
+
return { status: "changed" };
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
4654
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
4655
|
+
}
|
|
4656
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
4657
|
+
return mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
4658
|
+
}
|
|
4659
|
+
function startDevpodWorkspace(options) {
|
|
4660
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
4661
|
+
return withMutationLock(activity, options.repoPath, () => {
|
|
4662
|
+
const workspaces = listDevpodWorkspaces();
|
|
4663
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
4664
|
+
if (devpodId) {
|
|
4665
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
4666
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
4667
|
+
if (options.recreate && before.status !== "owned") {
|
|
4668
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
4669
|
+
}
|
|
4670
|
+
} else if (options.recreate) {
|
|
4671
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
4672
|
+
}
|
|
4673
|
+
const args = ["up", options.repoPath];
|
|
4674
|
+
if (devpodId) args.push("--id", devpodId);
|
|
4675
|
+
args.push("--open-ide=false");
|
|
4676
|
+
if (options.workspace) {
|
|
4677
|
+
args.push(
|
|
4678
|
+
"--workspace-env",
|
|
4679
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
4680
|
+
"--workspace-env",
|
|
4681
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
if (options.recreate) args.push("--recreate");
|
|
4685
|
+
const env = { ...process.env };
|
|
4686
|
+
if (options.workspace) {
|
|
4687
|
+
env.WORKSPACE = options.workspace.token;
|
|
4688
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
4689
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
4690
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
4691
|
+
} else {
|
|
4692
|
+
delete env.WORKSPACE;
|
|
4693
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
4694
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
4695
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
4696
|
+
}
|
|
4697
|
+
const result = (0, import_node_child_process9.spawnSync)("devpod", args, {
|
|
4698
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
4699
|
+
env
|
|
4700
|
+
});
|
|
4701
|
+
if (result.status !== 0) {
|
|
4702
|
+
throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
|
|
4703
|
+
}
|
|
4704
|
+
try {
|
|
4705
|
+
const attached = listDevpodWorkspaces();
|
|
4706
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
4707
|
+
if (!devpodId) {
|
|
4708
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
4709
|
+
}
|
|
4710
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
4711
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
4712
|
+
if (ownership.status !== "owned") {
|
|
4713
|
+
throw new Error(
|
|
4714
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
4715
|
+
);
|
|
4716
|
+
}
|
|
4717
|
+
return devpodId;
|
|
4718
|
+
} catch (error) {
|
|
4719
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4720
|
+
throw new DevpodStartPostconditionError(message);
|
|
4721
|
+
}
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
var import_node_child_process9, import_node_fs15, import_node_path13, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
|
|
4725
|
+
var init_devpod_mutation = __esm({
|
|
4726
|
+
"src/core/devpod-mutation.ts"() {
|
|
4202
4727
|
"use strict";
|
|
4203
|
-
|
|
4204
|
-
|
|
4728
|
+
import_node_child_process9 = require("child_process");
|
|
4729
|
+
import_node_fs15 = __toESM(require("fs"));
|
|
4730
|
+
import_node_path13 = __toESM(require("path"));
|
|
4731
|
+
init_devpod_workspaces();
|
|
4732
|
+
init_file_lock();
|
|
4733
|
+
init_router();
|
|
4734
|
+
DEVPOD_MUTATION_LOCK_FILE = import_node_path13.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
4735
|
+
DEVPOD_MUTATION_WAIT_MS = 6e4;
|
|
4736
|
+
DevpodStartPostconditionError = class extends Error {
|
|
4737
|
+
};
|
|
4205
4738
|
}
|
|
4206
4739
|
});
|
|
4207
4740
|
|
|
@@ -4212,17 +4745,17 @@ function commandError(command, repoPath, stderr) {
|
|
|
4212
4745
|
);
|
|
4213
4746
|
}
|
|
4214
4747
|
function resolveGitCommonDir(repoPath) {
|
|
4215
|
-
const result = (0,
|
|
4748
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
4216
4749
|
encoding: "utf-8"
|
|
4217
4750
|
});
|
|
4218
4751
|
const output2 = result.stdout.trim();
|
|
4219
4752
|
if (result.status !== 0 || !output2) {
|
|
4220
4753
|
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
4221
4754
|
}
|
|
4222
|
-
return comparableWorkspacePath(
|
|
4755
|
+
return comparableWorkspacePath(import_node_path14.default.isAbsolute(output2) ? output2 : import_node_path14.default.resolve(repoPath, output2));
|
|
4223
4756
|
}
|
|
4224
4757
|
function resolveGitTopLevel(repoPath) {
|
|
4225
|
-
const result = (0,
|
|
4758
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
4226
4759
|
encoding: "utf-8"
|
|
4227
4760
|
});
|
|
4228
4761
|
const output2 = result.stdout.trim();
|
|
@@ -4232,7 +4765,7 @@ function resolveGitTopLevel(repoPath) {
|
|
|
4232
4765
|
return comparableWorkspacePath(output2);
|
|
4233
4766
|
}
|
|
4234
4767
|
function listGitWorktrees(repoPath) {
|
|
4235
|
-
const result = (0,
|
|
4768
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
4236
4769
|
encoding: "utf-8"
|
|
4237
4770
|
});
|
|
4238
4771
|
if (result.status !== 0) {
|
|
@@ -4268,7 +4801,7 @@ function listGitWorktrees(repoPath) {
|
|
|
4268
4801
|
return worktrees;
|
|
4269
4802
|
}
|
|
4270
4803
|
function ownershipDirectory(repoPath) {
|
|
4271
|
-
return
|
|
4804
|
+
return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
4272
4805
|
}
|
|
4273
4806
|
function validateWorkspace(value, label) {
|
|
4274
4807
|
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
@@ -4296,7 +4829,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4296
4829
|
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
4297
4830
|
);
|
|
4298
4831
|
}
|
|
4299
|
-
if (typeof candidate.worktreePath !== "string" || !
|
|
4832
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path14.default.isAbsolute(candidate.worktreePath)) {
|
|
4300
4833
|
throw new Error("invalid workspace ownership worktreePath");
|
|
4301
4834
|
}
|
|
4302
4835
|
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
@@ -4316,7 +4849,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4316
4849
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
4317
4850
|
let parsed;
|
|
4318
4851
|
try {
|
|
4319
|
-
parsed = JSON.parse(
|
|
4852
|
+
parsed = JSON.parse(import_node_fs16.default.readFileSync(filePath, "utf-8"));
|
|
4320
4853
|
} catch (error) {
|
|
4321
4854
|
if (error instanceof SyntaxError) {
|
|
4322
4855
|
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
@@ -4332,7 +4865,7 @@ function listWorkspaceOwnership(repoPath) {
|
|
|
4332
4865
|
function listWorkspaceOwnershipInDirectory(directory) {
|
|
4333
4866
|
let entries;
|
|
4334
4867
|
try {
|
|
4335
|
-
entries =
|
|
4868
|
+
entries = import_node_fs16.default.readdirSync(directory, { withFileTypes: true });
|
|
4336
4869
|
} catch (error) {
|
|
4337
4870
|
if (error.code === "ENOENT") return [];
|
|
4338
4871
|
throw error;
|
|
@@ -4340,14 +4873,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
|
|
|
4340
4873
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
4341
4874
|
const workspace = entry.name.slice(0, -".json".length);
|
|
4342
4875
|
validateWorkspace(workspace, "filename");
|
|
4343
|
-
return readRecordFile(
|
|
4876
|
+
return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
|
|
4344
4877
|
});
|
|
4345
4878
|
}
|
|
4346
4879
|
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
4347
4880
|
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
4348
4881
|
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
4349
4882
|
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
4350
|
-
const filePath =
|
|
4883
|
+
const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
|
|
4351
4884
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4352
4885
|
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
4353
4886
|
const existing = records.find((record2) => record2.workspace === workspace);
|
|
@@ -4378,23 +4911,14 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
|
4378
4911
|
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
4379
4912
|
updatedAt: validateTimestamp(now, "updatedAt")
|
|
4380
4913
|
};
|
|
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
|
-
}
|
|
4914
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
4915
|
+
`);
|
|
4392
4916
|
return record;
|
|
4393
4917
|
}
|
|
4394
4918
|
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
4395
|
-
const filePath =
|
|
4919
|
+
const filePath = import_node_path14.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
4396
4920
|
try {
|
|
4397
|
-
|
|
4921
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4398
4922
|
return true;
|
|
4399
4923
|
} catch (error) {
|
|
4400
4924
|
if (error.code === "ENOENT") return false;
|
|
@@ -4405,7 +4929,7 @@ function sameOwnershipRecord(left, right) {
|
|
|
4405
4929
|
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
4930
|
}
|
|
4407
4931
|
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
4408
|
-
const filePath =
|
|
4932
|
+
const filePath = import_node_path14.default.join(
|
|
4409
4933
|
directory,
|
|
4410
4934
|
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
4411
4935
|
);
|
|
@@ -4417,14 +4941,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
|
4417
4941
|
throw error;
|
|
4418
4942
|
}
|
|
4419
4943
|
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
4420
|
-
|
|
4944
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4421
4945
|
return "removed";
|
|
4422
4946
|
}
|
|
4423
4947
|
function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
4424
4948
|
const directory = ownershipDirectory(repoPath);
|
|
4425
|
-
|
|
4949
|
+
import_node_fs16.default.mkdirSync(directory, { recursive: true });
|
|
4426
4950
|
return withFileLockSync(
|
|
4427
|
-
|
|
4951
|
+
import_node_path14.default.join(directory, ".lock"),
|
|
4428
4952
|
{ activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
|
|
4429
4953
|
() => operation({
|
|
4430
4954
|
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
@@ -4455,7 +4979,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
|
4455
4979
|
if (worktree?.locked) {
|
|
4456
4980
|
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
4457
4981
|
}
|
|
4458
|
-
if ((!worktree || worktree.prunable) &&
|
|
4982
|
+
if ((!worktree || worktree.prunable) && import_node_fs16.default.existsSync(record.worktreePath)) {
|
|
4459
4983
|
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4460
4984
|
}
|
|
4461
4985
|
if (!worktree || worktree.prunable) {
|
|
@@ -4479,19 +5003,19 @@ function listMissingWorkspaceOwnership(repoPath) {
|
|
|
4479
5003
|
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
4480
5004
|
);
|
|
4481
5005
|
}
|
|
4482
|
-
var
|
|
5006
|
+
var import_node_child_process10, import_node_fs16, import_node_path14, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
4483
5007
|
var init_workspace_ownership = __esm({
|
|
4484
5008
|
"src/core/workspace-ownership.ts"() {
|
|
4485
5009
|
"use strict";
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
5010
|
+
import_node_child_process10 = require("child_process");
|
|
5011
|
+
import_node_fs16 = __toESM(require("fs"));
|
|
5012
|
+
import_node_path14 = __toESM(require("path"));
|
|
5013
|
+
init_atomic_file();
|
|
4490
5014
|
init_devpod_workspaces();
|
|
4491
5015
|
init_file_lock();
|
|
4492
5016
|
init_workspace();
|
|
4493
5017
|
OWNERSHIP_VERSION = 1;
|
|
4494
|
-
OWNERSHIP_DIR =
|
|
5018
|
+
OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
|
|
4495
5019
|
}
|
|
4496
5020
|
});
|
|
4497
5021
|
|
|
@@ -4505,10 +5029,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
4505
5029
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
4506
5030
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
4507
5031
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
4508
|
-
const localRoot =
|
|
5032
|
+
const localRoot = import_node_path15.default.join(comparableRepo, "trees") + import_node_path15.default.sep;
|
|
4509
5033
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
4510
|
-
const legacyPrefix = `${
|
|
4511
|
-
return
|
|
5034
|
+
const legacyPrefix = `${import_node_path15.default.basename(comparableRepo)}-`;
|
|
5035
|
+
return import_node_path15.default.dirname(comparableWorktree) === import_node_path15.default.dirname(comparableRepo) && import_node_path15.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
4512
5036
|
}
|
|
4513
5037
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
4514
5038
|
const actions = [
|
|
@@ -4636,17 +5160,6 @@ function revalidateCandidate(candidate, repoPath, transaction) {
|
|
|
4636
5160
|
}
|
|
4637
5161
|
};
|
|
4638
5162
|
}
|
|
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
5163
|
function failedAction(resource, error) {
|
|
4651
5164
|
return {
|
|
4652
5165
|
resource,
|
|
@@ -4676,8 +5189,8 @@ function applyCandidate(candidate, repoPath) {
|
|
|
4676
5189
|
const actions = [];
|
|
4677
5190
|
let devpodStatus;
|
|
4678
5191
|
try {
|
|
4679
|
-
|
|
4680
|
-
|
|
5192
|
+
const mutation = deleteOwnedDevpodWorkspace(record.devpodId, record.worktreePath);
|
|
5193
|
+
devpodStatus = mutation.status === "changed" ? "owned" : "absent";
|
|
4681
5194
|
} catch (error) {
|
|
4682
5195
|
return { ...fresh, actions: [failedAction("devpod", error)] };
|
|
4683
5196
|
}
|
|
@@ -4762,11 +5275,12 @@ function applyWorkspaceGc(plan) {
|
|
|
4762
5275
|
candidates
|
|
4763
5276
|
};
|
|
4764
5277
|
}
|
|
4765
|
-
var
|
|
5278
|
+
var import_node_path15;
|
|
4766
5279
|
var init_workspace_gc = __esm({
|
|
4767
5280
|
"src/core/workspace-gc.ts"() {
|
|
4768
5281
|
"use strict";
|
|
4769
|
-
|
|
5282
|
+
import_node_path15 = __toESM(require("path"));
|
|
5283
|
+
init_devpod_mutation();
|
|
4770
5284
|
init_devpod_workspaces();
|
|
4771
5285
|
init_host_routes();
|
|
4772
5286
|
init_repo_config();
|
|
@@ -4835,11 +5349,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
4835
5349
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
4836
5350
|
continue;
|
|
4837
5351
|
}
|
|
4838
|
-
if (!
|
|
5352
|
+
if (!import_node_fs17.default.existsSync(absolutePath)) {
|
|
4839
5353
|
continue;
|
|
4840
5354
|
}
|
|
4841
5355
|
try {
|
|
4842
|
-
const raw =
|
|
5356
|
+
const raw = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
|
|
4843
5357
|
const parsed = import_yaml4.default.parse(raw);
|
|
4844
5358
|
const root = asRecord2(parsed);
|
|
4845
5359
|
const services = asRecord2(root?.services);
|
|
@@ -5102,7 +5616,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
5102
5616
|
const config = runtimeConfig.config;
|
|
5103
5617
|
loadedConfig = config;
|
|
5104
5618
|
loadedWorkspace = runtimeConfig.workspace;
|
|
5105
|
-
const cliVersion = true ? "0.0.
|
|
5619
|
+
const cliVersion = true ? "0.0.35" : "0.0.0-dev";
|
|
5106
5620
|
const configVersion = config.devrouter?.version;
|
|
5107
5621
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
5108
5622
|
addCheck(checks, {
|
|
@@ -5132,9 +5646,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
5132
5646
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
5133
5647
|
app: app.name,
|
|
5134
5648
|
filePath,
|
|
5135
|
-
absolutePath:
|
|
5649
|
+
absolutePath: import_node_path16.default.resolve(repo.path, filePath)
|
|
5136
5650
|
}))
|
|
5137
|
-
).filter((entry) => !
|
|
5651
|
+
).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5138
5652
|
addCheck(checks, {
|
|
5139
5653
|
id: "repo.compose-files",
|
|
5140
5654
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -5177,8 +5691,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
5177
5691
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
5178
5692
|
app: app.name,
|
|
5179
5693
|
cwd: app.hostRun.cwd,
|
|
5180
|
-
absolutePath:
|
|
5181
|
-
})).filter((entry) => !
|
|
5694
|
+
absolutePath: import_node_path16.default.resolve(repo.path, app.hostRun.cwd)
|
|
5695
|
+
})).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5182
5696
|
addCheck(checks, {
|
|
5183
5697
|
id: "repo.host-cwd",
|
|
5184
5698
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -5322,12 +5836,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
5322
5836
|
nextSteps
|
|
5323
5837
|
};
|
|
5324
5838
|
}
|
|
5325
|
-
var
|
|
5839
|
+
var import_node_fs17, import_node_path16, import_yaml4, POSTGRES_DEFAULTS;
|
|
5326
5840
|
var init_doctor = __esm({
|
|
5327
5841
|
"src/core/doctor.ts"() {
|
|
5328
5842
|
"use strict";
|
|
5329
|
-
|
|
5330
|
-
|
|
5843
|
+
import_node_fs17 = __toESM(require("fs"));
|
|
5844
|
+
import_node_path16 = __toESM(require("path"));
|
|
5331
5845
|
import_yaml4 = __toESM(require("yaml"));
|
|
5332
5846
|
init_docker();
|
|
5333
5847
|
init_host_routes();
|
|
@@ -5585,12 +6099,12 @@ function parseSsPortListeners(stdout, port) {
|
|
|
5585
6099
|
return listeners;
|
|
5586
6100
|
}
|
|
5587
6101
|
function findPortListeners(port) {
|
|
5588
|
-
const result = (0,
|
|
6102
|
+
const result = (0, import_node_child_process11.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
5589
6103
|
encoding: "utf-8"
|
|
5590
6104
|
});
|
|
5591
6105
|
if (result.error && result.error.code === "ENOENT") {
|
|
5592
6106
|
if (process.platform === "linux") {
|
|
5593
|
-
const ssResult = (0,
|
|
6107
|
+
const ssResult = (0, import_node_child_process11.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
5594
6108
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
5595
6109
|
return parseSsPortListeners(ssResult.stdout, port);
|
|
5596
6110
|
}
|
|
@@ -5619,11 +6133,11 @@ function findPortListeners(port) {
|
|
|
5619
6133
|
};
|
|
5620
6134
|
});
|
|
5621
6135
|
}
|
|
5622
|
-
var
|
|
6136
|
+
var import_node_child_process11;
|
|
5623
6137
|
var init_ports = __esm({
|
|
5624
6138
|
"src/util/ports.ts"() {
|
|
5625
6139
|
"use strict";
|
|
5626
|
-
|
|
6140
|
+
import_node_child_process11 = require("child_process");
|
|
5627
6141
|
}
|
|
5628
6142
|
});
|
|
5629
6143
|
|
|
@@ -5680,7 +6194,7 @@ var init_up = __esm({
|
|
|
5680
6194
|
|
|
5681
6195
|
// src/core/devpod-environment.ts
|
|
5682
6196
|
function inspectWorkspaceContainers() {
|
|
5683
|
-
const listed = (0,
|
|
6197
|
+
const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
|
|
5684
6198
|
encoding: "utf-8"
|
|
5685
6199
|
});
|
|
5686
6200
|
if (listed.status !== 0) {
|
|
@@ -5690,7 +6204,7 @@ function inspectWorkspaceContainers() {
|
|
|
5690
6204
|
}
|
|
5691
6205
|
const ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
|
|
5692
6206
|
if (ids.length === 0) return [];
|
|
5693
|
-
const inspected = (0,
|
|
6207
|
+
const inspected = (0, import_node_child_process12.spawnSync)("docker", ["inspect", "--format", SAFE_INSPECT_TEMPLATE, ...ids], {
|
|
5694
6208
|
encoding: "utf-8"
|
|
5695
6209
|
});
|
|
5696
6210
|
if (inspected.status !== 0) {
|
|
@@ -5703,7 +6217,7 @@ function inspectWorkspaceContainers() {
|
|
|
5703
6217
|
function workspaceAppContainers(containers, repoPath) {
|
|
5704
6218
|
return containers.filter((container) => {
|
|
5705
6219
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5706
|
-
return Boolean(workingDir && sameWorkspacePath(workingDir,
|
|
6220
|
+
return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
|
|
5707
6221
|
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
5708
6222
|
);
|
|
5709
6223
|
});
|
|
@@ -5726,12 +6240,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5726
6240
|
}
|
|
5727
6241
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5728
6242
|
}
|
|
5729
|
-
var
|
|
6243
|
+
var import_node_child_process12, import_node_path17, SAFE_INSPECT_TEMPLATE;
|
|
5730
6244
|
var init_devpod_environment = __esm({
|
|
5731
6245
|
"src/core/devpod-environment.ts"() {
|
|
5732
6246
|
"use strict";
|
|
5733
|
-
|
|
5734
|
-
|
|
6247
|
+
import_node_child_process12 = require("child_process");
|
|
6248
|
+
import_node_path17 = __toESM(require("path"));
|
|
5735
6249
|
init_workspace();
|
|
5736
6250
|
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}}}';
|
|
5737
6251
|
}
|
|
@@ -5758,124 +6272,23 @@ function probeHttpRoute(host, options = {}) {
|
|
|
5758
6272
|
args.push("--cacert", getMkcertRootCAPath({ repoPath: options.repoPath }));
|
|
5759
6273
|
}
|
|
5760
6274
|
args.push(url);
|
|
5761
|
-
const result = (0,
|
|
6275
|
+
const result = (0, import_node_child_process13.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
5762
6276
|
const stdout = result.stdout?.trim() ?? "";
|
|
5763
6277
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
5764
6278
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
5765
6279
|
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
6280
|
return { ok, status, details };
|
|
5767
6281
|
}
|
|
5768
|
-
var
|
|
6282
|
+
var import_node_child_process13;
|
|
5769
6283
|
var init_http_route_probe = __esm({
|
|
5770
6284
|
"src/core/http-route-probe.ts"() {
|
|
5771
6285
|
"use strict";
|
|
5772
|
-
|
|
6286
|
+
import_node_child_process13 = require("child_process");
|
|
5773
6287
|
init_router();
|
|
5774
6288
|
init_tls();
|
|
5775
6289
|
}
|
|
5776
6290
|
});
|
|
5777
6291
|
|
|
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
6292
|
// src/core/route-publication.ts
|
|
5880
6293
|
function routedAppsFromConfig(config) {
|
|
5881
6294
|
return config.apps.filter((app) => app.kind !== "dependency");
|
|
@@ -5949,11 +6362,11 @@ var init_route_publication = __esm({
|
|
|
5949
6362
|
// src/core/workspace-ensure.ts
|
|
5950
6363
|
function assertOverlay(container, repoPath) {
|
|
5951
6364
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5952
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
6365
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path18.default.join(repoPath, ".devcontainer"))) {
|
|
5953
6366
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
5954
6367
|
}
|
|
5955
6368
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
5956
|
-
const expectedOverlay =
|
|
6369
|
+
const expectedOverlay = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
5957
6370
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
5958
6371
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
5959
6372
|
}
|
|
@@ -6027,7 +6440,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6027
6440
|
target
|
|
6028
6441
|
});
|
|
6029
6442
|
if (target.kind === "linked") {
|
|
6030
|
-
const workspaceEnv = (0,
|
|
6443
|
+
const workspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
6031
6444
|
"docker",
|
|
6032
6445
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
6033
6446
|
{ encoding: "utf-8" }
|
|
@@ -6037,7 +6450,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6037
6450
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
6038
6451
|
);
|
|
6039
6452
|
}
|
|
6040
|
-
const devrouterWorkspaceEnv = (0,
|
|
6453
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
6041
6454
|
"docker",
|
|
6042
6455
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
6043
6456
|
{ encoding: "utf-8" }
|
|
@@ -6048,7 +6461,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
6048
6461
|
);
|
|
6049
6462
|
}
|
|
6050
6463
|
}
|
|
6051
|
-
const gitCheck = (0,
|
|
6464
|
+
const gitCheck = (0, import_node_child_process14.spawnSync)(
|
|
6052
6465
|
"docker",
|
|
6053
6466
|
[
|
|
6054
6467
|
"exec",
|
|
@@ -6135,63 +6548,14 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
6135
6548
|
}
|
|
6136
6549
|
function isPrimaryCheckout(repoPath) {
|
|
6137
6550
|
try {
|
|
6138
|
-
return
|
|
6551
|
+
return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
|
|
6139
6552
|
} catch {
|
|
6140
6553
|
return false;
|
|
6141
6554
|
}
|
|
6142
6555
|
}
|
|
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
6556
|
function openUrls(urls) {
|
|
6193
6557
|
for (const url of urls) {
|
|
6194
|
-
const opened = (0,
|
|
6558
|
+
const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
6195
6559
|
if (opened.status !== 0) {
|
|
6196
6560
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
6197
6561
|
`);
|
|
@@ -6221,8 +6585,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6221
6585
|
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
6222
6586
|
let devpodId = target.devpodId;
|
|
6223
6587
|
if (target.kind === "linked") {
|
|
6224
|
-
const overlayPath =
|
|
6225
|
-
if (!
|
|
6588
|
+
const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
6589
|
+
if (!import_node_fs18.default.existsSync(overlayPath)) {
|
|
6226
6590
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
6227
6591
|
}
|
|
6228
6592
|
}
|
|
@@ -6233,16 +6597,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6233
6597
|
);
|
|
6234
6598
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
6235
6599
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
6236
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
6600
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path18.default.basename(repoPath)) ?? "app";
|
|
6237
6601
|
for (const [index, app] of apps.entries()) {
|
|
6238
|
-
if (
|
|
6602
|
+
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
6239
6603
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
6240
6604
|
throw new Error(
|
|
6241
|
-
`
|
|
6605
|
+
`Proxy app '${app.name}' must use a ${owner}-owned upstream beginning with '${aliasPrefix}-'.`
|
|
6242
6606
|
);
|
|
6243
6607
|
}
|
|
6244
6608
|
}
|
|
6245
|
-
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host)
|
|
6609
|
+
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
6246
6610
|
const ownership = target.kind === "linked" ? {
|
|
6247
6611
|
workspace: target.workspace,
|
|
6248
6612
|
worktreePath: repoPath,
|
|
@@ -6254,9 +6618,25 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6254
6618
|
}
|
|
6255
6619
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
6256
6620
|
const startAndProveAttachment = (recreate = false) => {
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6621
|
+
const requestedTarget = currentTarget();
|
|
6622
|
+
try {
|
|
6623
|
+
devpodId = startDevpodWorkspace({
|
|
6624
|
+
repoPath,
|
|
6625
|
+
devpodId: requestedTarget.devpodId,
|
|
6626
|
+
recreate,
|
|
6627
|
+
quiet: options.quiet,
|
|
6628
|
+
...requestedTarget.kind === "linked" ? {
|
|
6629
|
+
workspace: {
|
|
6630
|
+
token: requestedTarget.workspace,
|
|
6631
|
+
gitCommonDir: requestedTarget.gitCommonDir
|
|
6632
|
+
}
|
|
6633
|
+
} : {}
|
|
6634
|
+
});
|
|
6635
|
+
environmentStarted = true;
|
|
6636
|
+
} catch (error) {
|
|
6637
|
+
if (error instanceof DevpodStartPostconditionError) environmentStarted = true;
|
|
6638
|
+
throw error;
|
|
6639
|
+
}
|
|
6260
6640
|
if (ownership) {
|
|
6261
6641
|
writeWorkspaceOwnership(repoPath, ownership);
|
|
6262
6642
|
}
|
|
@@ -6357,14 +6737,15 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6357
6737
|
}
|
|
6358
6738
|
});
|
|
6359
6739
|
}
|
|
6360
|
-
var
|
|
6740
|
+
var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
6361
6741
|
var init_workspace_ensure = __esm({
|
|
6362
6742
|
"src/core/workspace-ensure.ts"() {
|
|
6363
6743
|
"use strict";
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6744
|
+
import_node_child_process14 = require("child_process");
|
|
6745
|
+
import_node_fs18 = __toESM(require("fs"));
|
|
6746
|
+
import_node_path18 = __toESM(require("path"));
|
|
6367
6747
|
init_devpod_environment();
|
|
6748
|
+
init_devpod_mutation();
|
|
6368
6749
|
init_devpod_workspaces();
|
|
6369
6750
|
init_host_routes();
|
|
6370
6751
|
init_http_route_probe();
|
|
@@ -6438,20 +6819,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
6438
6819
|
);
|
|
6439
6820
|
}
|
|
6440
6821
|
function defaultWorktreePath(mainRepo, ws) {
|
|
6441
|
-
return
|
|
6822
|
+
return import_node_path19.default.join(mainRepo, "trees", ws);
|
|
6442
6823
|
}
|
|
6443
6824
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
6444
|
-
const ignored = (0,
|
|
6825
|
+
const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
6445
6826
|
encoding: "utf-8"
|
|
6446
6827
|
});
|
|
6447
6828
|
if (ignored.status !== 0) {
|
|
6448
6829
|
throw new Error(
|
|
6449
|
-
`Default worktree root '${
|
|
6830
|
+
`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
6831
|
);
|
|
6451
6832
|
}
|
|
6452
6833
|
}
|
|
6453
6834
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
6454
|
-
return
|
|
6835
|
+
return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
|
|
6455
6836
|
}
|
|
6456
6837
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
6457
6838
|
const candidates = [
|
|
@@ -6543,7 +6924,7 @@ function resolveWorkspaceTarget(mainRepo, target, worktrees, records) {
|
|
|
6543
6924
|
function worktreeForRecord(worktrees, record) {
|
|
6544
6925
|
return worktrees.find((candidate) => sameWorkspacePath(candidate.path, record.worktreePath));
|
|
6545
6926
|
}
|
|
6546
|
-
function
|
|
6927
|
+
function assertDevpodTargetSafe(target, worktrees, devpods) {
|
|
6547
6928
|
if (target.record) {
|
|
6548
6929
|
const status = inspectWorkspaceOwnership(target.record, worktrees, devpods);
|
|
6549
6930
|
if (status.ownerStatus === "conflict") {
|
|
@@ -6551,26 +6932,25 @@ function devpodForTarget(target, worktrees, devpods) {
|
|
|
6551
6932
|
`Workspace '${target.workspace}' ownership conflicts with live Git or DevPod evidence; no resources were changed.`
|
|
6552
6933
|
);
|
|
6553
6934
|
}
|
|
6554
|
-
return
|
|
6935
|
+
return;
|
|
6555
6936
|
}
|
|
6556
6937
|
const devpodId = target.workspace;
|
|
6557
6938
|
const ownership = inspectDevpodWorkspaceOwnership(devpods, devpodId, target.worktreePath);
|
|
6558
6939
|
if (ownership.status === "conflict") {
|
|
6559
6940
|
throw new Error(ownership.reason);
|
|
6560
6941
|
}
|
|
6561
|
-
return ownership.status === "owned" ? ownership.workspace : void 0;
|
|
6562
6942
|
}
|
|
6563
6943
|
function assertFullDownPreflight(mainRepo, target) {
|
|
6564
6944
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
6565
6945
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
6566
6946
|
}
|
|
6567
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
6947
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
|
|
6568
6948
|
if (target.worktree.locked) {
|
|
6569
6949
|
throw new Error(
|
|
6570
6950
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
6571
6951
|
);
|
|
6572
6952
|
}
|
|
6573
|
-
const status = (0,
|
|
6953
|
+
const status = (0, import_node_child_process15.spawnSync)(
|
|
6574
6954
|
"git",
|
|
6575
6955
|
["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
|
|
6576
6956
|
{ encoding: "utf-8" }
|
|
@@ -6591,8 +6971,8 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6591
6971
|
if (!ws) {
|
|
6592
6972
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
6593
6973
|
}
|
|
6594
|
-
const worktreePath = opts.path ?
|
|
6595
|
-
if (
|
|
6974
|
+
const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
6975
|
+
if (import_node_fs19.default.existsSync(worktreePath)) {
|
|
6596
6976
|
const registered = listGitWorktrees(mainRepo).find(
|
|
6597
6977
|
(worktree) => sameWorkspacePath(worktree.path, worktreePath)
|
|
6598
6978
|
);
|
|
@@ -6610,11 +6990,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6610
6990
|
if (!opts.path) {
|
|
6611
6991
|
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
6612
6992
|
}
|
|
6613
|
-
const add = (0,
|
|
6993
|
+
const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
6614
6994
|
encoding: "utf-8"
|
|
6615
6995
|
});
|
|
6616
6996
|
if (add.status !== 0) {
|
|
6617
|
-
const addNew = (0,
|
|
6997
|
+
const addNew = (0, import_node_child_process15.spawnSync)(
|
|
6618
6998
|
"git",
|
|
6619
6999
|
["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
|
|
6620
7000
|
{
|
|
@@ -6685,10 +7065,9 @@ function workspaceLs(repoPath) {
|
|
|
6685
7065
|
}
|
|
6686
7066
|
function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
6687
7067
|
const devpods = listDevpodWorkspaces();
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
}
|
|
7068
|
+
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
7069
|
+
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
7070
|
+
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
6692
7071
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
6693
7072
|
if (!quiet) {
|
|
6694
7073
|
process.stdout.write(
|
|
@@ -6697,9 +7076,9 @@ function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
|
6697
7076
|
);
|
|
6698
7077
|
}
|
|
6699
7078
|
return {
|
|
6700
|
-
...
|
|
7079
|
+
...mutation.status === "changed" ? { devpodId } : {},
|
|
6701
7080
|
freedRoutes: routes.length,
|
|
6702
|
-
providerChanged:
|
|
7081
|
+
providerChanged: mutation.status === "changed",
|
|
6703
7082
|
workspace: resolved.workspace
|
|
6704
7083
|
};
|
|
6705
7084
|
}
|
|
@@ -6713,10 +7092,15 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6713
7092
|
if (removeWorktree) {
|
|
6714
7093
|
assertFullDownPreflight(mainRepo, resolved);
|
|
6715
7094
|
}
|
|
6716
|
-
const result = mutateWorkspaceRuntime(
|
|
7095
|
+
const result = mutateWorkspaceRuntime(
|
|
7096
|
+
action2 === "stop" ? "stop" : "delete",
|
|
7097
|
+
resolved,
|
|
7098
|
+
worktrees,
|
|
7099
|
+
opts.quiet
|
|
7100
|
+
);
|
|
6717
7101
|
if (removeWorktree) {
|
|
6718
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
6719
|
-
const rm = (0,
|
|
7102
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath)) {
|
|
7103
|
+
const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
6720
7104
|
encoding: "utf-8"
|
|
6721
7105
|
});
|
|
6722
7106
|
if (rm.status !== 0) {
|
|
@@ -6734,9 +7118,9 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6734
7118
|
}
|
|
6735
7119
|
return result;
|
|
6736
7120
|
};
|
|
6737
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
7121
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs19.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
|
|
6738
7122
|
}
|
|
6739
|
-
async function
|
|
7123
|
+
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
6740
7124
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
6741
7125
|
return withWorkspaceLifecycleLock(worktreePath, async () => {
|
|
6742
7126
|
const worktrees = identifyGitWorktrees(mainRepo);
|
|
@@ -6754,22 +7138,29 @@ async function workspaceStopOwnedPath(worktreePath, opts = {}) {
|
|
|
6754
7138
|
worktree: worktreeForRecord(worktrees, record),
|
|
6755
7139
|
record
|
|
6756
7140
|
};
|
|
6757
|
-
return mutateWorkspaceRuntime(
|
|
7141
|
+
return mutateWorkspaceRuntime(action2, resolved, worktrees, opts.quiet);
|
|
6758
7142
|
});
|
|
6759
7143
|
}
|
|
7144
|
+
async function workspaceStopOwnedPath(worktreePath, opts = {}) {
|
|
7145
|
+
return mutateWorkspaceOwnedPath("stop", worktreePath, opts);
|
|
7146
|
+
}
|
|
7147
|
+
async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
|
|
7148
|
+
return mutateWorkspaceOwnedPath("delete", worktreePath, opts);
|
|
7149
|
+
}
|
|
6760
7150
|
async function workspaceStop(target, opts = {}) {
|
|
6761
7151
|
return runWorkspaceLifecycle("stop", target, opts);
|
|
6762
7152
|
}
|
|
6763
7153
|
async function workspaceDown(target, opts = {}) {
|
|
6764
7154
|
return runWorkspaceLifecycle("down", target, opts);
|
|
6765
7155
|
}
|
|
6766
|
-
var
|
|
7156
|
+
var import_node_child_process15, import_node_fs19, import_node_path19;
|
|
6767
7157
|
var init_workspace_lifecycle = __esm({
|
|
6768
7158
|
"src/core/workspace-lifecycle.ts"() {
|
|
6769
7159
|
"use strict";
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
7160
|
+
import_node_child_process15 = require("child_process");
|
|
7161
|
+
import_node_fs19 = __toESM(require("fs"));
|
|
7162
|
+
import_node_path19 = __toESM(require("path"));
|
|
7163
|
+
init_devpod_mutation();
|
|
6773
7164
|
init_devpod_workspaces();
|
|
6774
7165
|
init_repo_config();
|
|
6775
7166
|
init_route_state();
|
|
@@ -6780,7 +7171,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
6780
7171
|
});
|
|
6781
7172
|
|
|
6782
7173
|
// src/core/environment-stop.ts
|
|
6783
|
-
async function environmentStop(repoPath) {
|
|
7174
|
+
async function environmentStop(repoPath, options = {}) {
|
|
6784
7175
|
const linked = isLinkedWorktree(repoPath);
|
|
6785
7176
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
6786
7177
|
if (linked && !workspace) {
|
|
@@ -6799,25 +7190,27 @@ async function environmentStop(repoPath) {
|
|
|
6799
7190
|
if (!mainRepo) {
|
|
6800
7191
|
throw new Error(`Could not resolve the primary checkout for '${repoPath}'.`);
|
|
6801
7192
|
}
|
|
6802
|
-
const result = await
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
7193
|
+
const result = await (options.delete ? workspaceDeleteOwnedPath : workspaceStopOwnedPath)(
|
|
7194
|
+
record.worktreePath,
|
|
7195
|
+
{
|
|
7196
|
+
quiet: true,
|
|
7197
|
+
repoPath: mainRepo
|
|
7198
|
+
}
|
|
7199
|
+
);
|
|
6806
7200
|
return {
|
|
6807
7201
|
kind: "linked",
|
|
6808
7202
|
repoPath,
|
|
6809
7203
|
workspace: result.workspace,
|
|
6810
7204
|
...result.devpodId ? { devpodId: result.devpodId } : {},
|
|
6811
|
-
stopped: result.providerChanged,
|
|
7205
|
+
stopped: !options.delete && result.providerChanged,
|
|
7206
|
+
...options.delete ? { deleted: result.providerChanged } : {},
|
|
6812
7207
|
freedRoutes: result.freedRoutes
|
|
6813
7208
|
};
|
|
6814
7209
|
}
|
|
6815
7210
|
}
|
|
6816
7211
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
6817
7212
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
|
|
6818
|
-
|
|
6819
|
-
runDevpodWorkspaceAction("stop", devpod.id);
|
|
6820
|
-
}
|
|
7213
|
+
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
6821
7214
|
const removedRoutes = removeHostRoutesWhere(
|
|
6822
7215
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
6823
7216
|
);
|
|
@@ -6825,8 +7218,9 @@ async function environmentStop(repoPath) {
|
|
|
6825
7218
|
kind: linked ? "linked" : "primary",
|
|
6826
7219
|
repoPath,
|
|
6827
7220
|
...workspace ? { workspace } : {},
|
|
6828
|
-
...devpod ? { devpodId: devpod.id } : {},
|
|
6829
|
-
stopped:
|
|
7221
|
+
...mutation.status === "changed" && devpod ? { devpodId: devpod.id } : {},
|
|
7222
|
+
stopped: !options.delete && mutation.status === "changed",
|
|
7223
|
+
...options.delete ? { deleted: mutation.status === "changed" } : {},
|
|
6830
7224
|
freedRoutes: removedRoutes.length
|
|
6831
7225
|
};
|
|
6832
7226
|
});
|
|
@@ -6834,6 +7228,7 @@ async function environmentStop(repoPath) {
|
|
|
6834
7228
|
var init_environment_stop = __esm({
|
|
6835
7229
|
"src/core/environment-stop.ts"() {
|
|
6836
7230
|
"use strict";
|
|
7231
|
+
init_devpod_mutation();
|
|
6837
7232
|
init_devpod_workspaces();
|
|
6838
7233
|
init_host_routes();
|
|
6839
7234
|
init_workspace();
|
|
@@ -6849,19 +7244,19 @@ __export(stop_exports, {
|
|
|
6849
7244
|
});
|
|
6850
7245
|
async function runStopCommand(options) {
|
|
6851
7246
|
const repoPath = resolveGitCheckoutPath(options.path);
|
|
6852
|
-
const result = await environmentStop(repoPath);
|
|
7247
|
+
const result = await environmentStop(repoPath, { delete: options.delete });
|
|
6853
7248
|
if (options.json) {
|
|
6854
7249
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6855
7250
|
`);
|
|
6856
7251
|
return;
|
|
6857
7252
|
}
|
|
6858
7253
|
const label = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
|
|
6859
|
-
if (!result.stopped && result.freedRoutes === 0) {
|
|
7254
|
+
if (!result.stopped && !result.deleted && result.freedRoutes === 0) {
|
|
6860
7255
|
process.stdout.write(`${label} is already stopped; no routes needed removal.
|
|
6861
7256
|
`);
|
|
6862
7257
|
return;
|
|
6863
7258
|
}
|
|
6864
|
-
const provider = result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
7259
|
+
const provider = result.deleted ? `Deleted DevPod '${result.devpodId}'. ` : result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
6865
7260
|
process.stdout.write(
|
|
6866
7261
|
`${provider}Freed ${result.freedRoutes} route(s) for ${label.toLowerCase()}.
|
|
6867
7262
|
`
|
|
@@ -6891,7 +7286,7 @@ function ensureGuidance(repoPath) {
|
|
|
6891
7286
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
6892
7287
|
}
|
|
6893
7288
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
6894
|
-
const result = (0,
|
|
7289
|
+
const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
6895
7290
|
encoding: "utf-8"
|
|
6896
7291
|
});
|
|
6897
7292
|
if (result.status !== 0) {
|
|
@@ -6929,7 +7324,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6929
7324
|
}
|
|
6930
7325
|
assertRunningDevpod(devpod.id, repoPath);
|
|
6931
7326
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
6932
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
7327
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
|
|
6933
7328
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
6934
7329
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
6935
7330
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -6947,7 +7342,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6947
7342
|
wrappedCommand
|
|
6948
7343
|
];
|
|
6949
7344
|
return new Promise((resolve, reject) => {
|
|
6950
|
-
const child = (0,
|
|
7345
|
+
const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
6951
7346
|
let pending = Buffer.alloc(0);
|
|
6952
7347
|
let statusBytes = Buffer.alloc(0);
|
|
6953
7348
|
let readingStatus = false;
|
|
@@ -7006,12 +7401,12 @@ async function devpodExec(repoPath, command) {
|
|
|
7006
7401
|
});
|
|
7007
7402
|
});
|
|
7008
7403
|
}
|
|
7009
|
-
var
|
|
7404
|
+
var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
|
|
7010
7405
|
var init_devpod_exec = __esm({
|
|
7011
7406
|
"src/core/devpod-exec.ts"() {
|
|
7012
7407
|
"use strict";
|
|
7013
|
-
|
|
7014
|
-
|
|
7408
|
+
import_node_child_process16 = require("child_process");
|
|
7409
|
+
import_node_crypto5 = require("crypto");
|
|
7015
7410
|
init_devpod_environment();
|
|
7016
7411
|
init_devpod_workspaces();
|
|
7017
7412
|
init_workspace();
|
|
@@ -7195,7 +7590,7 @@ async function runOpenCommand(name) {
|
|
|
7195
7590
|
);
|
|
7196
7591
|
return;
|
|
7197
7592
|
}
|
|
7198
|
-
const result = (0,
|
|
7593
|
+
const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
7199
7594
|
if (result.status !== 0) {
|
|
7200
7595
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7201
7596
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -7203,11 +7598,11 @@ async function runOpenCommand(name) {
|
|
|
7203
7598
|
process.stdout.write(`Opened ${url}
|
|
7204
7599
|
`);
|
|
7205
7600
|
}
|
|
7206
|
-
var
|
|
7601
|
+
var import_node_child_process17;
|
|
7207
7602
|
var init_open = __esm({
|
|
7208
7603
|
"src/commands/open.ts"() {
|
|
7209
7604
|
"use strict";
|
|
7210
|
-
|
|
7605
|
+
import_node_child_process17 = require("child_process");
|
|
7211
7606
|
init_docker();
|
|
7212
7607
|
init_host_routes();
|
|
7213
7608
|
init_repo_config();
|
|
@@ -7230,7 +7625,7 @@ async function runLogsCommand(options) {
|
|
|
7230
7625
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
7231
7626
|
if (options.follow) {
|
|
7232
7627
|
args.splice(1, 0, "-f");
|
|
7233
|
-
const child = (0,
|
|
7628
|
+
const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
|
|
7234
7629
|
const onSignal = () => {
|
|
7235
7630
|
child.kill("SIGTERM");
|
|
7236
7631
|
};
|
|
@@ -7244,17 +7639,17 @@ async function runLogsCommand(options) {
|
|
|
7244
7639
|
});
|
|
7245
7640
|
});
|
|
7246
7641
|
} else {
|
|
7247
|
-
const result = (0,
|
|
7642
|
+
const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
|
|
7248
7643
|
if (result.status !== 0) {
|
|
7249
7644
|
throw new Error("Failed to retrieve router logs.");
|
|
7250
7645
|
}
|
|
7251
7646
|
}
|
|
7252
7647
|
}
|
|
7253
|
-
var
|
|
7648
|
+
var import_node_child_process18;
|
|
7254
7649
|
var init_logs = __esm({
|
|
7255
7650
|
"src/commands/logs.ts"() {
|
|
7256
7651
|
"use strict";
|
|
7257
|
-
|
|
7652
|
+
import_node_child_process18 = require("child_process");
|
|
7258
7653
|
init_docker();
|
|
7259
7654
|
init_router();
|
|
7260
7655
|
}
|
|
@@ -7294,17 +7689,17 @@ function asRecord3(value) {
|
|
|
7294
7689
|
return value;
|
|
7295
7690
|
}
|
|
7296
7691
|
function readJson(filePath) {
|
|
7297
|
-
if (!
|
|
7692
|
+
if (!import_node_fs20.default.existsSync(filePath)) {
|
|
7298
7693
|
return void 0;
|
|
7299
7694
|
}
|
|
7300
7695
|
try {
|
|
7301
|
-
return JSON.parse(
|
|
7696
|
+
return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7302
7697
|
} catch {
|
|
7303
7698
|
return void 0;
|
|
7304
7699
|
}
|
|
7305
7700
|
}
|
|
7306
7701
|
function relative(repoPath, filePath) {
|
|
7307
|
-
return
|
|
7702
|
+
return import_node_path20.default.relative(repoPath, filePath) || ".";
|
|
7308
7703
|
}
|
|
7309
7704
|
function redactEnvAssignments(value) {
|
|
7310
7705
|
return value.replace(
|
|
@@ -7345,7 +7740,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
7345
7740
|
["bun.lock", "bun"]
|
|
7346
7741
|
];
|
|
7347
7742
|
for (const [fileName, name] of lockfiles) {
|
|
7348
|
-
if (
|
|
7743
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7349
7744
|
return { name, source: fileName };
|
|
7350
7745
|
}
|
|
7351
7746
|
}
|
|
@@ -7360,9 +7755,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
7360
7755
|
if (typeof engines?.node === "string") {
|
|
7361
7756
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
7362
7757
|
}
|
|
7363
|
-
const nvmrc =
|
|
7364
|
-
if (
|
|
7365
|
-
const version =
|
|
7758
|
+
const nvmrc = import_node_path20.default.join(repoPath, ".nvmrc");
|
|
7759
|
+
if (import_node_fs20.default.existsSync(nvmrc)) {
|
|
7760
|
+
const version = import_node_fs20.default.readFileSync(nvmrc, "utf-8").trim();
|
|
7366
7761
|
return { version, source: ".nvmrc" };
|
|
7367
7762
|
}
|
|
7368
7763
|
return void 0;
|
|
@@ -7423,7 +7818,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
7423
7818
|
const files = config.apps.filter(
|
|
7424
7819
|
(app) => app.runtime === "docker"
|
|
7425
7820
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
7426
|
-
(fileName) => !
|
|
7821
|
+
(fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
|
|
7427
7822
|
);
|
|
7428
7823
|
return Array.from(new Set(files));
|
|
7429
7824
|
} catch {
|
|
@@ -7441,7 +7836,7 @@ function composeFiles(repoPath) {
|
|
|
7441
7836
|
...configuredComposeFiles(repoPath)
|
|
7442
7837
|
];
|
|
7443
7838
|
return Array.from(new Set(candidates)).filter(
|
|
7444
|
-
(fileName) =>
|
|
7839
|
+
(fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
|
|
7445
7840
|
);
|
|
7446
7841
|
}
|
|
7447
7842
|
function stringArray(value) {
|
|
@@ -7479,7 +7874,7 @@ function inspectServices(repoPath) {
|
|
|
7479
7874
|
const services = [];
|
|
7480
7875
|
for (const fileName of composeFiles(repoPath)) {
|
|
7481
7876
|
try {
|
|
7482
|
-
const parsed = import_yaml5.default.parse(
|
|
7877
|
+
const parsed = import_yaml5.default.parse(import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8"));
|
|
7483
7878
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
7484
7879
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
7485
7880
|
const service = asRecord3(value);
|
|
@@ -7514,8 +7909,8 @@ function inspectServices(repoPath) {
|
|
|
7514
7909
|
return services;
|
|
7515
7910
|
}
|
|
7516
7911
|
function inspectEnvFiles(repoPath) {
|
|
7517
|
-
const files =
|
|
7518
|
-
const content =
|
|
7912
|
+
const files = import_node_fs20.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
7913
|
+
const content = import_node_fs20.default.readFileSync(import_node_path20.default.join(repoPath, fileName), "utf-8");
|
|
7519
7914
|
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
7915
|
return { path: fileName, names };
|
|
7521
7916
|
});
|
|
@@ -7529,18 +7924,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
7529
7924
|
};
|
|
7530
7925
|
}
|
|
7531
7926
|
function inspectDevcontainer(repoPath) {
|
|
7532
|
-
const dir =
|
|
7533
|
-
if (!
|
|
7927
|
+
const dir = import_node_path20.default.join(repoPath, ".devcontainer");
|
|
7928
|
+
if (!import_node_fs20.default.existsSync(dir)) {
|
|
7534
7929
|
return { exists: false, files: [] };
|
|
7535
7930
|
}
|
|
7536
7931
|
return {
|
|
7537
7932
|
exists: true,
|
|
7538
|
-
files:
|
|
7933
|
+
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
7934
|
};
|
|
7540
7935
|
}
|
|
7541
7936
|
function inspectDevrouter(repoPath) {
|
|
7542
7937
|
const configPath = getRepoConfigPath(repoPath);
|
|
7543
|
-
if (!
|
|
7938
|
+
if (!import_node_fs20.default.existsSync(configPath)) {
|
|
7544
7939
|
return {
|
|
7545
7940
|
exists: false,
|
|
7546
7941
|
configPath,
|
|
@@ -7584,15 +7979,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
7584
7979
|
["AGENTS.md", "agents"],
|
|
7585
7980
|
["CLAUDE.md", "claude"]
|
|
7586
7981
|
]) {
|
|
7587
|
-
if (
|
|
7982
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7588
7983
|
results.push({ path: fileName, kind });
|
|
7589
7984
|
}
|
|
7590
7985
|
}
|
|
7591
|
-
const skillsDir =
|
|
7592
|
-
if (
|
|
7593
|
-
for (const name of
|
|
7594
|
-
const skillPath =
|
|
7595
|
-
if (
|
|
7986
|
+
const skillsDir = import_node_path20.default.join(repoPath, ".agents", "skills");
|
|
7987
|
+
if (import_node_fs20.default.existsSync(skillsDir)) {
|
|
7988
|
+
for (const name of import_node_fs20.default.readdirSync(skillsDir).sort()) {
|
|
7989
|
+
const skillPath = import_node_path20.default.join(skillsDir, name, "SKILL.md");
|
|
7990
|
+
if (import_node_fs20.default.existsSync(skillPath)) {
|
|
7596
7991
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
7597
7992
|
}
|
|
7598
7993
|
}
|
|
@@ -7635,7 +8030,7 @@ function buildIssues(report) {
|
|
|
7635
8030
|
}
|
|
7636
8031
|
function inspectRepo(options = {}) {
|
|
7637
8032
|
const repoPath = resolveRepoPath(options.repo);
|
|
7638
|
-
const pkg = readJson(
|
|
8033
|
+
const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
|
|
7639
8034
|
const scripts = inspectScripts(pkg);
|
|
7640
8035
|
const reportWithoutIssues = {
|
|
7641
8036
|
repoPath,
|
|
@@ -7654,12 +8049,12 @@ function inspectRepo(options = {}) {
|
|
|
7654
8049
|
issues: buildIssues(reportWithoutIssues)
|
|
7655
8050
|
};
|
|
7656
8051
|
}
|
|
7657
|
-
var
|
|
8052
|
+
var import_node_fs20, import_node_path20, import_yaml5;
|
|
7658
8053
|
var init_repo_inspect = __esm({
|
|
7659
8054
|
"src/core/repo-inspect.ts"() {
|
|
7660
8055
|
"use strict";
|
|
7661
|
-
|
|
7662
|
-
|
|
8056
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8057
|
+
import_node_path20 = __toESM(require("path"));
|
|
7663
8058
|
import_yaml5 = __toESM(require("yaml"));
|
|
7664
8059
|
init_repo_config();
|
|
7665
8060
|
}
|
|
@@ -7764,7 +8159,7 @@ function requiredFileChecks(repoPath) {
|
|
|
7764
8159
|
".devcontainer/docker-compose.yml",
|
|
7765
8160
|
".devrouter.yml"
|
|
7766
8161
|
];
|
|
7767
|
-
const missing = required.filter((fileName) => !
|
|
8162
|
+
const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
|
|
7768
8163
|
return {
|
|
7769
8164
|
id: "repo.devcontainer.verify-files",
|
|
7770
8165
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -7983,12 +8378,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
7983
8378
|
nextSteps: collectNextSteps3(checks)
|
|
7984
8379
|
};
|
|
7985
8380
|
}
|
|
7986
|
-
var
|
|
8381
|
+
var import_node_fs21, import_node_path21;
|
|
7987
8382
|
var init_devcontainer_verify = __esm({
|
|
7988
8383
|
"src/core/devcontainer-verify.ts"() {
|
|
7989
8384
|
"use strict";
|
|
7990
|
-
|
|
7991
|
-
|
|
8385
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8386
|
+
import_node_path21 = __toESM(require("path"));
|
|
7992
8387
|
init_capabilities();
|
|
7993
8388
|
init_doctor();
|
|
7994
8389
|
init_host_routes();
|
|
@@ -8284,7 +8679,7 @@ function packageManagerIssues(repo) {
|
|
|
8284
8679
|
}
|
|
8285
8680
|
function plannedFiles(repoPath, version) {
|
|
8286
8681
|
const repo = inspectRepo({ repo: repoPath });
|
|
8287
|
-
const projectName = sanitizeProjectName(
|
|
8682
|
+
const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
|
|
8288
8683
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
8289
8684
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
8290
8685
|
const port = inferPort2(repo);
|
|
@@ -8329,8 +8724,8 @@ function plannedFiles(repoPath, version) {
|
|
|
8329
8724
|
};
|
|
8330
8725
|
}
|
|
8331
8726
|
function classifyFile(repoPath, file) {
|
|
8332
|
-
const absolutePath =
|
|
8333
|
-
if (!
|
|
8727
|
+
const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
|
|
8728
|
+
if (!import_node_fs22.default.existsSync(absolutePath)) {
|
|
8334
8729
|
return {
|
|
8335
8730
|
path: file.relativePath,
|
|
8336
8731
|
action: "create",
|
|
@@ -8338,7 +8733,7 @@ function classifyFile(repoPath, file) {
|
|
|
8338
8733
|
bytes: Buffer.byteLength(file.content)
|
|
8339
8734
|
};
|
|
8340
8735
|
}
|
|
8341
|
-
const current =
|
|
8736
|
+
const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
|
|
8342
8737
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
8343
8738
|
return {
|
|
8344
8739
|
path: file.relativePath,
|
|
@@ -8395,11 +8790,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
8395
8790
|
};
|
|
8396
8791
|
}
|
|
8397
8792
|
function writeFile(repoPath, file) {
|
|
8398
|
-
const absolutePath =
|
|
8399
|
-
|
|
8400
|
-
|
|
8793
|
+
const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
|
|
8794
|
+
import_node_fs22.default.mkdirSync(import_node_path22.default.dirname(absolutePath), { recursive: true });
|
|
8795
|
+
import_node_fs22.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
8401
8796
|
if (file.executable) {
|
|
8402
|
-
|
|
8797
|
+
import_node_fs22.default.chmodSync(absolutePath, 493);
|
|
8403
8798
|
}
|
|
8404
8799
|
}
|
|
8405
8800
|
function writeDevcontainer(options = {}) {
|
|
@@ -8437,12 +8832,12 @@ function writeDevcontainer(options = {}) {
|
|
|
8437
8832
|
nextSteps: postWriteNextSteps(repoPath)
|
|
8438
8833
|
};
|
|
8439
8834
|
}
|
|
8440
|
-
var
|
|
8835
|
+
var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
8441
8836
|
var init_devcontainer_write = __esm({
|
|
8442
8837
|
"src/core/devcontainer-write.ts"() {
|
|
8443
8838
|
"use strict";
|
|
8444
|
-
|
|
8445
|
-
|
|
8839
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
8840
|
+
import_node_path22 = __toESM(require("path"));
|
|
8446
8841
|
init_repo_config();
|
|
8447
8842
|
init_repo_inspect();
|
|
8448
8843
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -8833,7 +9228,7 @@ function sanitizeRouterId(value) {
|
|
|
8833
9228
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
8834
9229
|
}
|
|
8835
9230
|
function repoHash(repoPath) {
|
|
8836
|
-
return (0,
|
|
9231
|
+
return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
8837
9232
|
}
|
|
8838
9233
|
function asDockerApp(app) {
|
|
8839
9234
|
return app.runtime === "docker";
|
|
@@ -8912,11 +9307,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
8912
9307
|
if (dockerApps.length === 0) {
|
|
8913
9308
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
8914
9309
|
}
|
|
8915
|
-
const cachePath =
|
|
8916
|
-
|
|
8917
|
-
const overlayPath =
|
|
9310
|
+
const cachePath = import_node_path23.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
9311
|
+
import_node_fs23.default.mkdirSync(cachePath, { recursive: true });
|
|
9312
|
+
const overlayPath = import_node_path23.default.join(cachePath, "compose.devrouter.yml");
|
|
8918
9313
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
8919
|
-
|
|
9314
|
+
import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
8920
9315
|
return {
|
|
8921
9316
|
overlayPath,
|
|
8922
9317
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -8930,7 +9325,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8930
9325
|
fileArgs.push("-f", resolved);
|
|
8931
9326
|
}
|
|
8932
9327
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
8933
|
-
const result = (0,
|
|
9328
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8934
9329
|
encoding: "utf-8",
|
|
8935
9330
|
cwd: repoPath
|
|
8936
9331
|
});
|
|
@@ -8958,7 +9353,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
8958
9353
|
"--services",
|
|
8959
9354
|
...services
|
|
8960
9355
|
];
|
|
8961
|
-
const result = (0,
|
|
9356
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8962
9357
|
encoding: "utf-8",
|
|
8963
9358
|
cwd: repoPath
|
|
8964
9359
|
});
|
|
@@ -8984,7 +9379,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8984
9379
|
fileArgs.push("-f", resolved);
|
|
8985
9380
|
}
|
|
8986
9381
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
8987
|
-
const result = (0,
|
|
9382
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8988
9383
|
encoding: "utf-8",
|
|
8989
9384
|
cwd: repoPath
|
|
8990
9385
|
});
|
|
@@ -9010,7 +9405,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
9010
9405
|
String(tail),
|
|
9011
9406
|
...services
|
|
9012
9407
|
];
|
|
9013
|
-
(0,
|
|
9408
|
+
(0, import_node_child_process19.spawnSync)("docker", args, {
|
|
9014
9409
|
stdio: "inherit",
|
|
9015
9410
|
cwd: repoPath
|
|
9016
9411
|
});
|
|
@@ -9022,7 +9417,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9022
9417
|
fileArgs.push("-f", resolved);
|
|
9023
9418
|
}
|
|
9024
9419
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
9025
|
-
const result = (0,
|
|
9420
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
9026
9421
|
encoding: "utf-8",
|
|
9027
9422
|
cwd: repoPath
|
|
9028
9423
|
});
|
|
@@ -9036,14 +9431,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
9036
9431
|
const port = Number(match[1]);
|
|
9037
9432
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
9038
9433
|
}
|
|
9039
|
-
var
|
|
9434
|
+
var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
|
|
9040
9435
|
var init_docker_run = __esm({
|
|
9041
9436
|
"src/core/docker-run.ts"() {
|
|
9042
9437
|
"use strict";
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9438
|
+
import_node_child_process19 = require("child_process");
|
|
9439
|
+
import_node_crypto6 = require("crypto");
|
|
9440
|
+
import_node_fs23 = __toESM(require("fs"));
|
|
9441
|
+
import_node_path23 = __toESM(require("path"));
|
|
9047
9442
|
import_yaml6 = __toESM(require("yaml"));
|
|
9048
9443
|
init_docker_error_guidance();
|
|
9049
9444
|
init_paths();
|
|
@@ -9133,7 +9528,7 @@ function isProcessRunning(pid) {
|
|
|
9133
9528
|
}
|
|
9134
9529
|
}
|
|
9135
9530
|
function readProcessTree(rootPid) {
|
|
9136
|
-
const result = (0,
|
|
9531
|
+
const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
9137
9532
|
if (result.status !== 0) {
|
|
9138
9533
|
return [rootPid];
|
|
9139
9534
|
}
|
|
@@ -9244,12 +9639,12 @@ function detectListeningPorts(pids) {
|
|
|
9244
9639
|
if (pids.length === 0) {
|
|
9245
9640
|
return [];
|
|
9246
9641
|
}
|
|
9247
|
-
const result = (0,
|
|
9642
|
+
const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
|
|
9248
9643
|
encoding: "utf-8"
|
|
9249
9644
|
});
|
|
9250
9645
|
if (result.error && result.error.code === "ENOENT") {
|
|
9251
9646
|
if (process.platform === "linux") {
|
|
9252
|
-
const ssResult = (0,
|
|
9647
|
+
const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
9253
9648
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
9254
9649
|
return parseSsListeningPorts(ssResult.stdout, new Set(pids));
|
|
9255
9650
|
}
|
|
@@ -9308,7 +9703,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
9308
9703
|
app.hostRun.command,
|
|
9309
9704
|
true
|
|
9310
9705
|
) : app.hostRun.command;
|
|
9311
|
-
const child = (0,
|
|
9706
|
+
const child = (0, import_node_child_process20.spawn)(spawnCommand, {
|
|
9312
9707
|
cwd: commandCwd,
|
|
9313
9708
|
stdio: "inherit",
|
|
9314
9709
|
shell: true,
|
|
@@ -9676,7 +10071,7 @@ async function execWithAppEnv(options) {
|
|
|
9676
10071
|
options.command[0],
|
|
9677
10072
|
true
|
|
9678
10073
|
);
|
|
9679
|
-
child = (0,
|
|
10074
|
+
child = (0, import_node_child_process20.spawn)(wrapped, {
|
|
9680
10075
|
cwd: deps.repoPath,
|
|
9681
10076
|
stdio: "inherit",
|
|
9682
10077
|
shell: true,
|
|
@@ -9690,7 +10085,7 @@ async function execWithAppEnv(options) {
|
|
|
9690
10085
|
false
|
|
9691
10086
|
);
|
|
9692
10087
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
9693
|
-
child = (0,
|
|
10088
|
+
child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
|
|
9694
10089
|
cwd: deps.repoPath,
|
|
9695
10090
|
stdio: "inherit",
|
|
9696
10091
|
shell: false,
|
|
@@ -9698,7 +10093,7 @@ async function execWithAppEnv(options) {
|
|
|
9698
10093
|
});
|
|
9699
10094
|
}
|
|
9700
10095
|
} else if (options.shell) {
|
|
9701
|
-
child = (0,
|
|
10096
|
+
child = (0, import_node_child_process20.spawn)(options.command[0], {
|
|
9702
10097
|
cwd: deps.repoPath,
|
|
9703
10098
|
stdio: "inherit",
|
|
9704
10099
|
shell: true,
|
|
@@ -9706,7 +10101,7 @@ async function execWithAppEnv(options) {
|
|
|
9706
10101
|
});
|
|
9707
10102
|
} else {
|
|
9708
10103
|
const [command, ...args] = options.command;
|
|
9709
|
-
child = (0,
|
|
10104
|
+
child = (0, import_node_child_process20.spawn)(command, args, {
|
|
9710
10105
|
cwd: deps.repoPath,
|
|
9711
10106
|
stdio: "inherit",
|
|
9712
10107
|
shell: false,
|
|
@@ -9727,11 +10122,11 @@ async function execWithAppEnv(options) {
|
|
|
9727
10122
|
deps.stopDeps();
|
|
9728
10123
|
}
|
|
9729
10124
|
}
|
|
9730
|
-
var
|
|
10125
|
+
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
10126
|
var init_app_run = __esm({
|
|
9732
10127
|
"src/core/app-run.ts"() {
|
|
9733
10128
|
"use strict";
|
|
9734
|
-
|
|
10129
|
+
import_node_child_process20 = require("child_process");
|
|
9735
10130
|
import_node_net = __toESM(require("net"));
|
|
9736
10131
|
import_node_process = require("process");
|
|
9737
10132
|
import_promises = require("readline/promises");
|
|
@@ -9992,7 +10387,7 @@ var init_version = __esm({
|
|
|
9992
10387
|
|
|
9993
10388
|
// src/cli.ts
|
|
9994
10389
|
var import_commander = require("commander");
|
|
9995
|
-
var CLI_VERSION = true ? "0.0.
|
|
10390
|
+
var CLI_VERSION = true ? "0.0.35" : "0.0.0-dev";
|
|
9996
10391
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
9997
10392
|
function withErrorHandling(action2) {
|
|
9998
10393
|
return async (...args) => {
|
|
@@ -10053,11 +10448,15 @@ program.command("ensure").description("Start and prove a primary or linked check
|
|
|
10053
10448
|
});
|
|
10054
10449
|
})
|
|
10055
10450
|
);
|
|
10056
|
-
program.command("stop").description("Stop one checkout's exact DevPod and free its routes
|
|
10451
|
+
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
10452
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
10058
10453
|
const options = command.opts();
|
|
10059
10454
|
const { runStopCommand: runStopCommand2 } = await Promise.resolve().then(() => (init_stop(), stop_exports));
|
|
10060
|
-
await runStopCommand2({
|
|
10455
|
+
await runStopCommand2({
|
|
10456
|
+
path: repoPath,
|
|
10457
|
+
delete: Boolean(options.delete),
|
|
10458
|
+
json: Boolean(options.json)
|
|
10459
|
+
});
|
|
10061
10460
|
})
|
|
10062
10461
|
);
|
|
10063
10462
|
program.command("exec").description("Run one literal command inside the exact checkout's running DevPod").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
|