@devrouter/cli 0.0.33 → 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 +1001 -502
- package/package.json +2 -1
- package/upgrade-prompts/0.0.34.md +28 -0
- package/upgrade-prompts/0.0.35.md +29 -0
package/dist/devrouter.js
CHANGED
|
@@ -43,6 +43,7 @@ function buildDevrouterSection() {
|
|
|
43
43
|
`\`${DEVROUTER_SKILL_REL_PATH}\``,
|
|
44
44
|
"",
|
|
45
45
|
"Quick validation sequence:",
|
|
46
|
+
"- Managed devcontainer consumer images contain no devrouter package or helper; `devrouter ensure` delivers the matching helper at runtime.",
|
|
46
47
|
"- `devrouter up`",
|
|
47
48
|
"- `devrouter tls install` (required when repo defines tcp/postgres apps)",
|
|
48
49
|
"- `devrouter app ls --repo .`",
|
|
@@ -155,6 +156,8 @@ apps:
|
|
|
155
156
|
# to target a per-workspace devcontainer alias \u2014 substituted with the resolved
|
|
156
157
|
# workspace token at runtime. See "Workspace isolation" below. Do NOT put
|
|
157
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.
|
|
158
161
|
#
|
|
159
162
|
# proxy + tcp (front a DB in an externally-managed container, e.g. a
|
|
160
163
|
# devcontainer's Postgres on devnet) \u2014 no per-DB host port:
|
|
@@ -238,10 +241,12 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
|
|
|
238
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.
|
|
239
242
|
|
|
240
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.
|
|
241
|
-
- **When active**: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`\${WORKSPACE}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
|
|
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.
|
|
242
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.
|
|
243
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>\`.
|
|
244
|
-
- **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.
|
|
245
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.
|
|
246
251
|
- **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
|
|
247
252
|
|
|
@@ -294,7 +299,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
294
299
|
- \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
295
300
|
- \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
|
|
296
301
|
- \`devrouter ensure [path] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts
|
|
297
|
-
- \`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
|
|
298
303
|
- \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
|
|
299
304
|
- \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
|
|
300
305
|
- \`devrouter status\`: router/container/network/TLS health
|
|
@@ -347,6 +352,7 @@ For host/docker runtime apps only:
|
|
|
347
352
|
|
|
348
353
|
## Runtime behavior notes
|
|
349
354
|
|
|
355
|
+
- Managed devcontainer images contain no devrouter package or helper. \`devrouter ensure\` delivers its matching process helper to the exact running container and invokes the repository-owned \`.devcontainer/post-start.sh\`; keep \`.devrouter.yml\` as the only consumer-side devrouter version pin.
|
|
350
356
|
- \`devrouter app run\` auto-starts Docker dependencies and waits for health. Host app runs stop auto-started docker deps on exit; docker app runs leave target services running until explicit cleanup.
|
|
351
357
|
- Host-runtime dependencies are NOT auto-started (v1).
|
|
352
358
|
- \`kind=dependency\` entries do not create routes and cannot be direct targets for \`devrouter app run\`, \`devrouter app exec\`, or \`devrouter open\`.
|
|
@@ -719,6 +725,51 @@ var init_capabilities = __esm({
|
|
|
719
725
|
}
|
|
720
726
|
});
|
|
721
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
|
+
|
|
722
773
|
// src/core/file-lock.ts
|
|
723
774
|
function sleepSync(ms) {
|
|
724
775
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -730,9 +781,48 @@ function isProcessAlive(pid) {
|
|
|
730
781
|
try {
|
|
731
782
|
process.kill(pid, 0);
|
|
732
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
|
+
}
|
|
733
797
|
} catch {
|
|
734
|
-
return false;
|
|
735
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 };
|
|
819
|
+
}
|
|
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;
|
|
736
826
|
}
|
|
737
827
|
function sameFile(left, right) {
|
|
738
828
|
return left.dev === right.dev && left.ino === right.ino;
|
|
@@ -740,7 +830,7 @@ function sameFile(left, right) {
|
|
|
740
830
|
function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
741
831
|
let fd;
|
|
742
832
|
try {
|
|
743
|
-
fd =
|
|
833
|
+
fd = import_node_fs4.default.openSync(lockPath, "r");
|
|
744
834
|
} catch (error) {
|
|
745
835
|
if (error.code === "ENOENT") {
|
|
746
836
|
return { kind: "retry" };
|
|
@@ -748,17 +838,16 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
748
838
|
throw error;
|
|
749
839
|
}
|
|
750
840
|
try {
|
|
751
|
-
const owner =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
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 };
|
|
755
844
|
}
|
|
756
|
-
const staleStat =
|
|
845
|
+
const staleStat = import_node_fs4.default.fstatSync(fd);
|
|
757
846
|
if (staleStat.nlink !== 1) {
|
|
758
847
|
return { kind: "retry" };
|
|
759
848
|
}
|
|
760
849
|
try {
|
|
761
|
-
|
|
850
|
+
import_node_fs4.default.linkSync(lockPath, staleLinkPath);
|
|
762
851
|
} catch (error) {
|
|
763
852
|
if (error.code === "ENOENT") {
|
|
764
853
|
return { kind: "retry" };
|
|
@@ -768,7 +857,7 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
768
857
|
try {
|
|
769
858
|
let currentStat;
|
|
770
859
|
try {
|
|
771
|
-
currentStat =
|
|
860
|
+
currentStat = import_node_fs4.default.statSync(lockPath);
|
|
772
861
|
} catch (error) {
|
|
773
862
|
if (error.code === "ENOENT") {
|
|
774
863
|
return { kind: "retry" };
|
|
@@ -778,27 +867,32 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
|
|
|
778
867
|
if (!sameFile(staleStat, currentStat) || currentStat.nlink !== 2) {
|
|
779
868
|
return { kind: "retry" };
|
|
780
869
|
}
|
|
781
|
-
|
|
870
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
782
871
|
return { kind: "reclaimed" };
|
|
783
872
|
} finally {
|
|
784
|
-
|
|
873
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
785
874
|
}
|
|
786
875
|
} finally {
|
|
787
|
-
|
|
876
|
+
import_node_fs4.default.closeSync(fd);
|
|
788
877
|
}
|
|
789
878
|
}
|
|
790
879
|
function acquireFileLock(lockPath, options) {
|
|
791
|
-
const
|
|
792
|
-
|
|
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`;
|
|
793
887
|
const staleLinkPath = `${candidatePath}.stale`;
|
|
794
888
|
const deadline = Date.now() + (options.waitMs ?? 0);
|
|
795
889
|
let reclaimAttempts = 0;
|
|
796
|
-
|
|
890
|
+
import_node_fs4.default.writeFileSync(candidatePath, `${owner}
|
|
797
891
|
`, { encoding: "utf-8", flag: "wx" });
|
|
798
892
|
try {
|
|
799
893
|
for (; ; ) {
|
|
800
894
|
try {
|
|
801
|
-
|
|
895
|
+
import_node_fs4.default.linkSync(candidatePath, lockPath);
|
|
802
896
|
return owner;
|
|
803
897
|
} catch (error) {
|
|
804
898
|
if (error.code !== "EEXIST") {
|
|
@@ -826,14 +920,14 @@ function acquireFileLock(lockPath, options) {
|
|
|
826
920
|
}
|
|
827
921
|
}
|
|
828
922
|
} finally {
|
|
829
|
-
|
|
830
|
-
|
|
923
|
+
import_node_fs4.default.rmSync(candidatePath, { force: true });
|
|
924
|
+
import_node_fs4.default.rmSync(staleLinkPath, { force: true });
|
|
831
925
|
}
|
|
832
926
|
}
|
|
833
927
|
function releaseFileLock(lockPath, owner) {
|
|
834
928
|
try {
|
|
835
|
-
if (
|
|
836
|
-
|
|
929
|
+
if (import_node_fs4.default.readFileSync(lockPath, "utf-8").trim() === owner) {
|
|
930
|
+
import_node_fs4.default.rmSync(lockPath);
|
|
837
931
|
}
|
|
838
932
|
} catch (error) {
|
|
839
933
|
if (error.code !== "ENOENT") {
|
|
@@ -857,20 +951,21 @@ async function withFileLock(lockPath, options, operation) {
|
|
|
857
951
|
releaseFileLock(lockPath, owner);
|
|
858
952
|
}
|
|
859
953
|
}
|
|
860
|
-
var
|
|
954
|
+
var import_node_child_process2, import_node_crypto2, import_node_fs4;
|
|
861
955
|
var init_file_lock = __esm({
|
|
862
956
|
"src/core/file-lock.ts"() {
|
|
863
957
|
"use strict";
|
|
864
|
-
|
|
865
|
-
|
|
958
|
+
import_node_child_process2 = require("child_process");
|
|
959
|
+
import_node_crypto2 = require("crypto");
|
|
960
|
+
import_node_fs4 = __toESM(require("fs"));
|
|
866
961
|
}
|
|
867
962
|
});
|
|
868
963
|
|
|
869
964
|
// src/core/workspace.ts
|
|
870
965
|
function comparableWorkspacePath(filePath) {
|
|
871
|
-
const resolved =
|
|
966
|
+
const resolved = import_node_path4.default.resolve(filePath);
|
|
872
967
|
try {
|
|
873
|
-
return
|
|
968
|
+
return import_node_fs5.default.realpathSync.native(resolved);
|
|
874
969
|
} catch {
|
|
875
970
|
return resolved;
|
|
876
971
|
}
|
|
@@ -883,10 +978,10 @@ function wsFromBranch(branch) {
|
|
|
883
978
|
return slug.length > 0 ? slug : void 0;
|
|
884
979
|
}
|
|
885
980
|
function isLinkedWorktree(repoPath) {
|
|
886
|
-
const gitPath =
|
|
981
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
887
982
|
let stat;
|
|
888
983
|
try {
|
|
889
|
-
stat =
|
|
984
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
890
985
|
} catch {
|
|
891
986
|
return false;
|
|
892
987
|
}
|
|
@@ -895,7 +990,7 @@ function isLinkedWorktree(repoPath) {
|
|
|
895
990
|
}
|
|
896
991
|
let content;
|
|
897
992
|
try {
|
|
898
|
-
content =
|
|
993
|
+
content = import_node_fs5.default.readFileSync(gitPath, "utf-8");
|
|
899
994
|
} catch {
|
|
900
995
|
return false;
|
|
901
996
|
}
|
|
@@ -907,10 +1002,10 @@ function isLinkedWorktree(repoPath) {
|
|
|
907
1002
|
return /(^|\/)worktrees\//.test(gitdir);
|
|
908
1003
|
}
|
|
909
1004
|
function gitMetadataDir(repoPath) {
|
|
910
|
-
const gitPath =
|
|
1005
|
+
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
911
1006
|
let stat;
|
|
912
1007
|
try {
|
|
913
|
-
stat =
|
|
1008
|
+
stat = import_node_fs5.default.statSync(gitPath);
|
|
914
1009
|
} catch {
|
|
915
1010
|
return void 0;
|
|
916
1011
|
}
|
|
@@ -920,11 +1015,11 @@ function gitMetadataDir(repoPath) {
|
|
|
920
1015
|
if (!stat.isFile()) {
|
|
921
1016
|
return void 0;
|
|
922
1017
|
}
|
|
923
|
-
const match =
|
|
1018
|
+
const match = import_node_fs5.default.readFileSync(gitPath, "utf-8").match(/^gitdir:\s*(.+)$/m);
|
|
924
1019
|
if (!match) {
|
|
925
1020
|
return void 0;
|
|
926
1021
|
}
|
|
927
|
-
return
|
|
1022
|
+
return import_node_path4.default.resolve(repoPath, match[1].trim());
|
|
928
1023
|
}
|
|
929
1024
|
function validatePersistedWorkspace(value) {
|
|
930
1025
|
const workspace = value.trim();
|
|
@@ -938,9 +1033,9 @@ function readPersistedWorkspace(repoPath) {
|
|
|
938
1033
|
if (!gitDir) {
|
|
939
1034
|
return void 0;
|
|
940
1035
|
}
|
|
941
|
-
const metadataPath =
|
|
1036
|
+
const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
|
|
942
1037
|
try {
|
|
943
|
-
return validatePersistedWorkspace(
|
|
1038
|
+
return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
|
|
944
1039
|
} catch (error) {
|
|
945
1040
|
if (error.code === "ENOENT") {
|
|
946
1041
|
return void 0;
|
|
@@ -963,7 +1058,7 @@ function persistWorkspace(repoPath, value) {
|
|
|
963
1058
|
if (!gitDir) {
|
|
964
1059
|
throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
|
|
965
1060
|
}
|
|
966
|
-
|
|
1061
|
+
import_node_fs5.default.writeFileSync(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
|
|
967
1062
|
`, "utf-8");
|
|
968
1063
|
return workspace;
|
|
969
1064
|
}
|
|
@@ -972,7 +1067,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
972
1067
|
if (!gitDir) {
|
|
973
1068
|
throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
|
|
974
1069
|
}
|
|
975
|
-
const lockPath =
|
|
1070
|
+
const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
|
|
976
1071
|
return withFileLock(
|
|
977
1072
|
lockPath,
|
|
978
1073
|
{ activity: "workspace lifecycle", target: `'${repoPath}'` },
|
|
@@ -980,7 +1075,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
|
980
1075
|
);
|
|
981
1076
|
}
|
|
982
1077
|
function currentBranch(repoPath) {
|
|
983
|
-
const result = (0,
|
|
1078
|
+
const result = (0, import_node_child_process3.spawnSync)("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
984
1079
|
encoding: "utf-8"
|
|
985
1080
|
});
|
|
986
1081
|
if (result.status !== 0) {
|
|
@@ -1000,7 +1095,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
|
1000
1095
|
return void 0;
|
|
1001
1096
|
}
|
|
1002
1097
|
const branch = currentBranch(repoPath);
|
|
1003
|
-
return wsFromBranch(branch ??
|
|
1098
|
+
return wsFromBranch(branch ?? import_node_path4.default.basename(import_node_path4.default.resolve(repoPath)));
|
|
1004
1099
|
}
|
|
1005
1100
|
function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
|
|
1006
1101
|
return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
|
|
@@ -1023,13 +1118,13 @@ function resolveWorkspace(repoPath, override) {
|
|
|
1023
1118
|
}
|
|
1024
1119
|
return deriveLinkedWorktreeWorkspace(repoPath);
|
|
1025
1120
|
}
|
|
1026
|
-
var
|
|
1121
|
+
var import_node_child_process3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
|
|
1027
1122
|
var init_workspace = __esm({
|
|
1028
1123
|
"src/core/workspace.ts"() {
|
|
1029
1124
|
"use strict";
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1125
|
+
import_node_child_process3 = require("child_process");
|
|
1126
|
+
import_node_fs5 = __toESM(require("fs"));
|
|
1127
|
+
import_node_path4 = __toESM(require("path"));
|
|
1033
1128
|
init_file_lock();
|
|
1034
1129
|
MAX_WORKSPACE_LENGTH = 32;
|
|
1035
1130
|
WORKSPACE_METADATA_FILE = "devrouter-workspace";
|
|
@@ -1149,64 +1244,199 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
|
|
|
1149
1244
|
}
|
|
1150
1245
|
return document;
|
|
1151
1246
|
}
|
|
1152
|
-
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");
|
|
1153
1307
|
const document = buildHostRoutesDocument(routes, tlsEnabled);
|
|
1154
|
-
|
|
1308
|
+
return `${ROUTE_METADATA_PREFIX}${encoded}
|
|
1309
|
+
${import_yaml.default.stringify(document, { lineWidth: 0 })}`;
|
|
1155
1310
|
}
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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}`
|
|
1164
1322
|
);
|
|
1165
1323
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
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}`);
|
|
1328
|
+
}
|
|
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.");
|
|
1168
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 });
|
|
1169
1380
|
}
|
|
1170
1381
|
function writeState(routes) {
|
|
1171
1382
|
ensureHostRouteStorage();
|
|
1172
|
-
|
|
1173
|
-
`, "utf-8");
|
|
1174
|
-
writeHostRoutesDynamicFile(routes, isTLSEnabled());
|
|
1383
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1175
1384
|
}
|
|
1176
1385
|
function withStateLock(fn) {
|
|
1177
1386
|
ensureHostRouteStorage();
|
|
1178
1387
|
return withFileLockSync(STATE_LOCK_FILE, { activity: "host route update", waitMs: 5e3 }, fn);
|
|
1179
1388
|
}
|
|
1180
1389
|
function refreshHostRoutesDynamicFile() {
|
|
1181
|
-
|
|
1182
|
-
|
|
1390
|
+
withStateLock(() => {
|
|
1391
|
+
const routes = readHostRouteStateLocked();
|
|
1392
|
+
writeState(routes);
|
|
1393
|
+
});
|
|
1183
1394
|
}
|
|
1184
|
-
function
|
|
1395
|
+
function readHostRouteStateLocked() {
|
|
1185
1396
|
ensureHostRouteStorage();
|
|
1186
|
-
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());
|
|
1187
1404
|
return [];
|
|
1188
1405
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (!
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
return parsed.filter((item) => item && typeof item === "object").map((item) => item);
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
const error = err;
|
|
1198
|
-
if (error.code !== "ENOENT") {
|
|
1199
|
-
process.stderr.write(
|
|
1200
|
-
`Warning: devrouter host routes state file is corrupted or unreadable (${error.message}). Recreating route state.
|
|
1201
|
-
`
|
|
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."
|
|
1202
1412
|
);
|
|
1203
1413
|
}
|
|
1204
|
-
|
|
1414
|
+
const routes = readCompatibilityState();
|
|
1415
|
+
writeRouteGeneration(routes, isTLSEnabled());
|
|
1416
|
+
return routes;
|
|
1205
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
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
return canonical.metadata.routes;
|
|
1433
|
+
}
|
|
1434
|
+
function listHostRouteState() {
|
|
1435
|
+
return withStateLock(readHostRouteStateLocked);
|
|
1206
1436
|
}
|
|
1207
1437
|
function upsertHostRoute(input2) {
|
|
1208
1438
|
return withStateLock(() => {
|
|
1209
|
-
const routes =
|
|
1439
|
+
const routes = readHostRouteStateLocked();
|
|
1210
1440
|
const id = buildHostRouteId(input2.repoPath, input2.name);
|
|
1211
1441
|
const existing = routes.find((route) => route.id === id);
|
|
1212
1442
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1223,7 +1453,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1223
1453
|
if (inputs.some((input2) => !sameWorkspacePath(input2.repoPath, repoPath))) {
|
|
1224
1454
|
throw new Error(`Route replacement contains an entry outside '${repoPath}'.`);
|
|
1225
1455
|
}
|
|
1226
|
-
const routes =
|
|
1456
|
+
const routes = readHostRouteStateLocked();
|
|
1227
1457
|
const remaining = routes.filter((route) => !sameWorkspacePath(route.repoPath, repoPath));
|
|
1228
1458
|
const names = /* @__PURE__ */ new Set();
|
|
1229
1459
|
const hosts = /* @__PURE__ */ new Set();
|
|
@@ -1261,7 +1491,7 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
1261
1491
|
}
|
|
1262
1492
|
function removeHostRouteById(id) {
|
|
1263
1493
|
return withStateLock(() => {
|
|
1264
|
-
const routes =
|
|
1494
|
+
const routes = readHostRouteStateLocked();
|
|
1265
1495
|
const next = routes.filter((route) => route.id !== id);
|
|
1266
1496
|
if (next.length === routes.length) {
|
|
1267
1497
|
return false;
|
|
@@ -1272,7 +1502,7 @@ function removeHostRouteById(id) {
|
|
|
1272
1502
|
}
|
|
1273
1503
|
function removeHostRoutesWhere(predicate) {
|
|
1274
1504
|
return withStateLock(() => {
|
|
1275
|
-
const routes =
|
|
1505
|
+
const routes = readHostRouteStateLocked();
|
|
1276
1506
|
const removed = routes.filter(predicate);
|
|
1277
1507
|
if (removed.length === 0) {
|
|
1278
1508
|
return [];
|
|
@@ -1294,7 +1524,7 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1294
1524
|
protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
|
|
1295
1525
|
appName: route.name,
|
|
1296
1526
|
serviceName: route.name,
|
|
1297
|
-
projectName:
|
|
1527
|
+
projectName: import_node_path5.default.basename(route.repoPath),
|
|
1298
1528
|
hosts: [route.host],
|
|
1299
1529
|
// TCP routes are reached via an SNI-aware client on the shared protocol
|
|
1300
1530
|
// port; surface a protocol-scheme URL rather than http(s)://.
|
|
@@ -1309,13 +1539,15 @@ function listHostRoutes(tlsEnabled) {
|
|
|
1309
1539
|
};
|
|
1310
1540
|
});
|
|
1311
1541
|
}
|
|
1312
|
-
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;
|
|
1313
1543
|
var init_host_routes = __esm({
|
|
1314
1544
|
"src/core/host-routes.ts"() {
|
|
1315
1545
|
"use strict";
|
|
1316
|
-
|
|
1317
|
-
|
|
1546
|
+
import_node_fs6 = __toESM(require("fs"));
|
|
1547
|
+
import_node_path5 = __toESM(require("path"));
|
|
1548
|
+
import_node_util = require("util");
|
|
1318
1549
|
import_yaml = __toESM(require("yaml"));
|
|
1550
|
+
init_atomic_file();
|
|
1319
1551
|
init_file_lock();
|
|
1320
1552
|
init_router();
|
|
1321
1553
|
init_workspace();
|
|
@@ -1324,6 +1556,8 @@ var init_host_routes = __esm({
|
|
|
1324
1556
|
TCP_TLS_ALPN_PROTOCOLS = {
|
|
1325
1557
|
postgres: ["postgresql"]
|
|
1326
1558
|
};
|
|
1559
|
+
ROUTE_METADATA_PREFIX = "# devrouter-routes-v1: ";
|
|
1560
|
+
ROUTE_METADATA_FAMILY_PREFIX = "# devrouter-routes-";
|
|
1327
1561
|
STATE_LOCK_FILE = `${HOST_ROUTES_STATE_FILE}.lock`;
|
|
1328
1562
|
}
|
|
1329
1563
|
});
|
|
@@ -1751,10 +1985,10 @@ function renderConfig(config) {
|
|
|
1751
1985
|
return import_yaml2.default.stringify(config, { lineWidth: 0 });
|
|
1752
1986
|
}
|
|
1753
1987
|
function readConfigDocument(configPath) {
|
|
1754
|
-
return (0, import_yaml2.parseDocument)(
|
|
1988
|
+
return (0, import_yaml2.parseDocument)(import_node_fs7.default.readFileSync(configPath, "utf-8"));
|
|
1755
1989
|
}
|
|
1756
1990
|
function writeConfigDocument(configPath, doc) {
|
|
1757
|
-
|
|
1991
|
+
import_node_fs7.default.writeFileSync(configPath, doc.toString({ lineWidth: 0 }), "utf-8");
|
|
1758
1992
|
}
|
|
1759
1993
|
function getOrCreateAppsSeq(doc) {
|
|
1760
1994
|
const apps = doc.get("apps", true);
|
|
@@ -1776,25 +2010,25 @@ function appToNodeValue(app) {
|
|
|
1776
2010
|
return value;
|
|
1777
2011
|
}
|
|
1778
2012
|
function resolveRepoPath(repoPath) {
|
|
1779
|
-
return
|
|
2013
|
+
return import_node_path6.default.resolve(repoPath ?? process.cwd());
|
|
1780
2014
|
}
|
|
1781
2015
|
function getRepoConfigPath(repoPath) {
|
|
1782
|
-
return
|
|
2016
|
+
return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
1783
2017
|
}
|
|
1784
2018
|
function loadRepoConfig(repoPath) {
|
|
1785
2019
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1786
2020
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1787
|
-
if (!
|
|
2021
|
+
if (!import_node_fs7.default.existsSync(configPath)) {
|
|
1788
2022
|
throw new Error(
|
|
1789
2023
|
`Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
|
|
1790
2024
|
);
|
|
1791
2025
|
}
|
|
1792
|
-
const raw =
|
|
2026
|
+
const raw = import_node_fs7.default.readFileSync(configPath, "utf-8");
|
|
1793
2027
|
const parsed = import_yaml2.default.parse(raw);
|
|
1794
2028
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
1795
2029
|
const requiredVersion = config.devrouter?.version;
|
|
1796
2030
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
1797
|
-
const cliVersion = true ? "0.0.
|
|
2031
|
+
const cliVersion = true ? "0.0.35" : "0.0.0-dev";
|
|
1798
2032
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
1799
2033
|
hasWarnedVersionMismatch = true;
|
|
1800
2034
|
process.stderr.write(
|
|
@@ -1811,7 +2045,7 @@ function loadRepoConfig(repoPath) {
|
|
|
1811
2045
|
function initRepoConfig(repoPath, options = {}) {
|
|
1812
2046
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
1813
2047
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
1814
|
-
if (
|
|
2048
|
+
if (import_node_fs7.default.existsSync(configPath)) {
|
|
1815
2049
|
return { repoPath: resolvedRepoPath, configPath, created: false };
|
|
1816
2050
|
}
|
|
1817
2051
|
if (options.devrouterVersion !== void 0 && !DEVROUTER_VERSION_RE.test(options.devrouterVersion)) {
|
|
@@ -1827,11 +2061,11 @@ function initRepoConfig(repoPath, options = {}) {
|
|
|
1827
2061
|
}
|
|
1828
2062
|
} : {},
|
|
1829
2063
|
project: {
|
|
1830
|
-
name:
|
|
2064
|
+
name: import_node_path6.default.basename(resolvedRepoPath)
|
|
1831
2065
|
},
|
|
1832
2066
|
apps: []
|
|
1833
2067
|
};
|
|
1834
|
-
|
|
2068
|
+
import_node_fs7.default.writeFileSync(configPath, renderConfig(initialConfig), "utf-8");
|
|
1835
2069
|
return { repoPath: resolvedRepoPath, configPath, created: true };
|
|
1836
2070
|
}
|
|
1837
2071
|
function buildAppFromOptions(options) {
|
|
@@ -2056,7 +2290,7 @@ function namespaceHost(host, workspace) {
|
|
|
2056
2290
|
return `${base}.${workspace}${suffix}`;
|
|
2057
2291
|
}
|
|
2058
2292
|
function applyWorkspace(config, workspace, repoPath) {
|
|
2059
|
-
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";
|
|
2060
2294
|
const substitutionToken = workspace ?? defaultToken;
|
|
2061
2295
|
const next = structuredClone(config);
|
|
2062
2296
|
for (const app of next.apps) {
|
|
@@ -2131,12 +2365,12 @@ function resolveAppDependencies(config, app) {
|
|
|
2131
2365
|
}
|
|
2132
2366
|
return results;
|
|
2133
2367
|
}
|
|
2134
|
-
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;
|
|
2135
2369
|
var init_repo_config = __esm({
|
|
2136
2370
|
"src/core/repo-config.ts"() {
|
|
2137
2371
|
"use strict";
|
|
2138
|
-
|
|
2139
|
-
|
|
2372
|
+
import_node_fs7 = __toESM(require("fs"));
|
|
2373
|
+
import_node_path6 = __toESM(require("path"));
|
|
2140
2374
|
import_yaml2 = __toESM(require("yaml"));
|
|
2141
2375
|
init_capabilities();
|
|
2142
2376
|
init_host_routes();
|
|
@@ -2203,6 +2437,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2203
2437
|
"- Shared Traefik router owns host ports 80 (HTTP), 443 (HTTPS), and 5432 (Postgres TCP).",
|
|
2204
2438
|
"- Per-repo source of truth is REPO_PATH/.devrouter.yml only.",
|
|
2205
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.",
|
|
2206
2441
|
"",
|
|
2207
2442
|
"Inputs:",
|
|
2208
2443
|
`- REPO_PATH=${repoPath}`,
|
|
@@ -2246,6 +2481,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2246
2481
|
"- if kind=app and runtime=proxy:",
|
|
2247
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`)',
|
|
2248
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",
|
|
2249
2485
|
" - protocol=http registers an HTTP route; protocol=tcp registers a TLS-SNI TCP route and additionally requires tcpProtocol",
|
|
2250
2486
|
" - do not set hostRun/docker/dependencies (proxy only registers a route to the upstream)",
|
|
2251
2487
|
" - loopback hosts (localhost/127.0.0.1/0.0.0.0) are rewritten to host.docker.internal for Traefik",
|
|
@@ -2296,9 +2532,10 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
2296
2532
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
2297
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.`,
|
|
2298
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.",
|
|
2299
|
-
`- 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.`,
|
|
2300
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.",
|
|
2301
|
-
"- 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. 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.",
|
|
2302
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.",
|
|
2303
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.",
|
|
2304
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.",
|
|
@@ -2463,8 +2700,8 @@ var init_ai_prompt = __esm({
|
|
|
2463
2700
|
purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
|
|
2464
2701
|
},
|
|
2465
2702
|
{
|
|
2466
|
-
command: "devrouter stop [path] [--json]",
|
|
2467
|
-
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."
|
|
2468
2705
|
},
|
|
2469
2706
|
{
|
|
2470
2707
|
command: "devrouter exec [path] -- <command...>",
|
|
@@ -2881,11 +3118,11 @@ function extractCurrentVersionFromRepoConfig(repoPath) {
|
|
|
2881
3118
|
};
|
|
2882
3119
|
}
|
|
2883
3120
|
function readPromptDirectory(promptDirPath) {
|
|
2884
|
-
if (!
|
|
3121
|
+
if (!import_node_fs8.default.existsSync(promptDirPath)) {
|
|
2885
3122
|
throw new Error(`Missing ${UPGRADE_PROMPTS_DIR} directory at ${promptDirPath}.`);
|
|
2886
3123
|
}
|
|
2887
3124
|
const releases = [];
|
|
2888
|
-
const files =
|
|
3125
|
+
const files = import_node_fs8.default.readdirSync(promptDirPath, { withFileTypes: true });
|
|
2889
3126
|
for (const file of files) {
|
|
2890
3127
|
if (!file.isFile() || !file.name.endsWith(".md")) {
|
|
2891
3128
|
continue;
|
|
@@ -2895,8 +3132,8 @@ function readPromptDirectory(promptDirPath) {
|
|
|
2895
3132
|
continue;
|
|
2896
3133
|
}
|
|
2897
3134
|
const version = normalizeVersion(basename);
|
|
2898
|
-
const promptPath =
|
|
2899
|
-
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();
|
|
2900
3137
|
releases.push({ version, prompt, promptPath });
|
|
2901
3138
|
}
|
|
2902
3139
|
const deduped = /* @__PURE__ */ new Map();
|
|
@@ -2917,19 +3154,19 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
|
|
|
2917
3154
|
}
|
|
2918
3155
|
function resolvePromptDirectory(explicitPath) {
|
|
2919
3156
|
if (explicitPath) {
|
|
2920
|
-
return
|
|
3157
|
+
return import_node_path7.default.resolve(explicitPath);
|
|
2921
3158
|
}
|
|
2922
|
-
const entryFile = process.argv[1] ?
|
|
2923
|
-
const entryDir =
|
|
2924
|
-
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;
|
|
2925
3162
|
const candidates = [
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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)
|
|
2931
3168
|
];
|
|
2932
|
-
const existing = candidates.find((candidate) =>
|
|
3169
|
+
const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
|
|
2933
3170
|
return existing ?? candidates[0];
|
|
2934
3171
|
}
|
|
2935
3172
|
function loadUpgradeCatalog(options = {}) {
|
|
@@ -2945,12 +3182,12 @@ function loadUpgradeCatalog(options = {}) {
|
|
|
2945
3182
|
releases
|
|
2946
3183
|
};
|
|
2947
3184
|
}
|
|
2948
|
-
var
|
|
3185
|
+
var import_node_fs8, import_node_path7, SEMVER_RE, UPGRADE_PROMPTS_DIR;
|
|
2949
3186
|
var init_upgrade = __esm({
|
|
2950
3187
|
"src/core/upgrade.ts"() {
|
|
2951
3188
|
"use strict";
|
|
2952
|
-
|
|
2953
|
-
|
|
3189
|
+
import_node_fs8 = __toESM(require("fs"));
|
|
3190
|
+
import_node_path7 = __toESM(require("path"));
|
|
2954
3191
|
init_repo_config();
|
|
2955
3192
|
SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
2956
3193
|
UPGRADE_PROMPTS_DIR = "upgrade-prompts";
|
|
@@ -3050,7 +3287,7 @@ async function getDockerodeConstructor() {
|
|
|
3050
3287
|
return dockerodeConstructorPromise;
|
|
3051
3288
|
}
|
|
3052
3289
|
function runDockerContextCommand(args) {
|
|
3053
|
-
const result = (0,
|
|
3290
|
+
const result = (0, import_node_child_process4.spawnSync)("docker", ["context", ...args], {
|
|
3054
3291
|
encoding: "utf-8"
|
|
3055
3292
|
});
|
|
3056
3293
|
if (result.status !== 0) {
|
|
@@ -3063,7 +3300,7 @@ function getCurrentDockerContext() {
|
|
|
3063
3300
|
return runDockerContextCommand(["show"]);
|
|
3064
3301
|
}
|
|
3065
3302
|
function getDockerHostFromContext(context) {
|
|
3066
|
-
const result = (0,
|
|
3303
|
+
const result = (0, import_node_child_process4.spawnSync)(
|
|
3067
3304
|
"docker",
|
|
3068
3305
|
["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
|
|
3069
3306
|
{ encoding: "utf-8" }
|
|
@@ -3131,15 +3368,177 @@ async function networkExists(name) {
|
|
|
3131
3368
|
return false;
|
|
3132
3369
|
}
|
|
3133
3370
|
}
|
|
3134
|
-
var
|
|
3371
|
+
var import_node_child_process4, dockerodeConstructorPromise;
|
|
3135
3372
|
var init_docker = __esm({
|
|
3136
3373
|
"src/core/docker.ts"() {
|
|
3137
3374
|
"use strict";
|
|
3138
|
-
|
|
3375
|
+
import_node_child_process4 = require("child_process");
|
|
3139
3376
|
dockerodeConstructorPromise = null;
|
|
3140
3377
|
}
|
|
3141
3378
|
});
|
|
3142
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
|
+
|
|
3143
3542
|
// src/core/devcontainer-diagnostics.ts
|
|
3144
3543
|
function asRecord(value) {
|
|
3145
3544
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3194,7 +3593,7 @@ function isLoopbackHost(host) {
|
|
|
3194
3593
|
return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
|
|
3195
3594
|
}
|
|
3196
3595
|
function inspectCompose(composeFile, _workspace) {
|
|
3197
|
-
if (!
|
|
3596
|
+
if (!import_node_fs10.default.existsSync(composeFile)) {
|
|
3198
3597
|
return {
|
|
3199
3598
|
aliases: [],
|
|
3200
3599
|
publishedPorts: [],
|
|
@@ -3203,7 +3602,7 @@ function inspectCompose(composeFile, _workspace) {
|
|
|
3203
3602
|
};
|
|
3204
3603
|
}
|
|
3205
3604
|
try {
|
|
3206
|
-
const raw =
|
|
3605
|
+
const raw = import_node_fs10.default.readFileSync(composeFile, "utf-8");
|
|
3207
3606
|
const parsed = import_yaml3.default.parse(raw);
|
|
3208
3607
|
const root = asRecord(parsed);
|
|
3209
3608
|
const services = asRecord(root?.services);
|
|
@@ -3260,11 +3659,11 @@ function routedProxyApps(config) {
|
|
|
3260
3659
|
);
|
|
3261
3660
|
}
|
|
3262
3661
|
function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
3263
|
-
const devcontainerDir =
|
|
3264
|
-
if (!
|
|
3662
|
+
const devcontainerDir = import_node_path9.default.join(repoPath, ".devcontainer");
|
|
3663
|
+
if (!import_node_fs10.default.existsSync(devcontainerDir)) {
|
|
3265
3664
|
return [];
|
|
3266
3665
|
}
|
|
3267
|
-
const compose = inspectCompose(
|
|
3666
|
+
const compose = inspectCompose(import_node_path9.default.join(devcontainerDir, "docker-compose.yml"), workspace);
|
|
3268
3667
|
const checks = [];
|
|
3269
3668
|
if (compose.parseError) {
|
|
3270
3669
|
checks.push({
|
|
@@ -3291,6 +3690,36 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3291
3690
|
details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
|
|
3292
3691
|
suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
|
|
3293
3692
|
});
|
|
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") : "";
|
|
3696
|
+
const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
|
|
3697
|
+
(artifact) => dockerfile.includes(artifact)
|
|
3698
|
+
);
|
|
3699
|
+
checks.push({
|
|
3700
|
+
id: "repo.devcontainer.no-devrouter-image-install",
|
|
3701
|
+
level: !dockerfileExists ? "warn" : devrouterArtifacts.length === 0 ? "ok" : "error",
|
|
3702
|
+
summary: !dockerfileExists ? "No .devcontainer/Dockerfile found to inspect for devrouter artifacts." : devrouterArtifacts.length === 0 ? "Consumer image does not install or extract devrouter artifacts." : "Consumer image installs or extracts devrouter artifacts.",
|
|
3703
|
+
details: devrouterArtifacts.length > 0 ? `artifacts=${devrouterArtifacts.join(", ")}` : void 0,
|
|
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
|
|
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
|
+
}
|
|
3294
3723
|
if (!config) {
|
|
3295
3724
|
checks.push({
|
|
3296
3725
|
id: "repo.devcontainer.upstream-alias-match",
|
|
@@ -3318,30 +3747,31 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
3318
3747
|
});
|
|
3319
3748
|
return checks;
|
|
3320
3749
|
}
|
|
3321
|
-
var
|
|
3750
|
+
var import_node_fs10, import_node_path9, import_yaml3;
|
|
3322
3751
|
var init_devcontainer_diagnostics = __esm({
|
|
3323
3752
|
"src/core/devcontainer-diagnostics.ts"() {
|
|
3324
3753
|
"use strict";
|
|
3325
|
-
|
|
3326
|
-
|
|
3754
|
+
import_node_fs10 = __toESM(require("fs"));
|
|
3755
|
+
import_node_path9 = __toESM(require("path"));
|
|
3327
3756
|
import_yaml3 = __toESM(require("yaml"));
|
|
3757
|
+
init_managed_post_start();
|
|
3328
3758
|
}
|
|
3329
3759
|
});
|
|
3330
3760
|
|
|
3331
3761
|
// src/core/paths.ts
|
|
3332
3762
|
function assertPathWithinRepo(filePath, repoRoot, label) {
|
|
3333
|
-
const resolvedRoot =
|
|
3334
|
-
const resolved =
|
|
3335
|
-
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)) {
|
|
3336
3766
|
throw new Error(`${label} path '${filePath}' escapes the repository root.`);
|
|
3337
3767
|
}
|
|
3338
3768
|
return resolved;
|
|
3339
3769
|
}
|
|
3340
|
-
var
|
|
3770
|
+
var import_node_path10;
|
|
3341
3771
|
var init_paths = __esm({
|
|
3342
3772
|
"src/core/paths.ts"() {
|
|
3343
3773
|
"use strict";
|
|
3344
|
-
|
|
3774
|
+
import_node_path10 = __toESM(require("path"));
|
|
3345
3775
|
}
|
|
3346
3776
|
});
|
|
3347
3777
|
|
|
@@ -3426,11 +3856,11 @@ function reconcileRouteRunConflict(repoPath, app) {
|
|
|
3426
3856
|
}
|
|
3427
3857
|
}
|
|
3428
3858
|
}
|
|
3429
|
-
var
|
|
3859
|
+
var import_node_fs11;
|
|
3430
3860
|
var init_route_state = __esm({
|
|
3431
3861
|
"src/core/route-state.ts"() {
|
|
3432
3862
|
"use strict";
|
|
3433
|
-
|
|
3863
|
+
import_node_fs11 = __toESM(require("fs"));
|
|
3434
3864
|
init_host_routes();
|
|
3435
3865
|
init_workspace();
|
|
3436
3866
|
}
|
|
@@ -3605,7 +4035,7 @@ function toRepoStatus(repoPath) {
|
|
|
3605
4035
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
3606
4036
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
3607
4037
|
const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
|
|
3608
|
-
const configExists =
|
|
4038
|
+
const configExists = import_node_fs12.default.existsSync(configPath);
|
|
3609
4039
|
if (!explicitRepo && !configExists) {
|
|
3610
4040
|
return void 0;
|
|
3611
4041
|
}
|
|
@@ -3732,11 +4162,11 @@ async function collectRouterStatus(repoPath) {
|
|
|
3732
4162
|
}
|
|
3733
4163
|
};
|
|
3734
4164
|
}
|
|
3735
|
-
var
|
|
4165
|
+
var import_node_fs12;
|
|
3736
4166
|
var init_status = __esm({
|
|
3737
4167
|
"src/core/status.ts"() {
|
|
3738
4168
|
"use strict";
|
|
3739
|
-
|
|
4169
|
+
import_node_fs12 = __toESM(require("fs"));
|
|
3740
4170
|
init_docker();
|
|
3741
4171
|
init_repo_config();
|
|
3742
4172
|
init_router();
|
|
@@ -3745,7 +4175,7 @@ var init_status = __esm({
|
|
|
3745
4175
|
|
|
3746
4176
|
// src/core/tls.ts
|
|
3747
4177
|
function runOrThrow(command, args) {
|
|
3748
|
-
const result = (0,
|
|
4178
|
+
const result = (0, import_node_child_process6.spawnSync)(command, args, {
|
|
3749
4179
|
encoding: "utf-8"
|
|
3750
4180
|
});
|
|
3751
4181
|
if (result.status !== 0) {
|
|
@@ -3755,7 +4185,7 @@ function runOrThrow(command, args) {
|
|
|
3755
4185
|
return result.stdout.trim();
|
|
3756
4186
|
}
|
|
3757
4187
|
function commandExists(command) {
|
|
3758
|
-
const result = (0,
|
|
4188
|
+
const result = (0, import_node_child_process6.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
|
|
3759
4189
|
return result.status === 0;
|
|
3760
4190
|
}
|
|
3761
4191
|
function ensureMkcert() {
|
|
@@ -3773,8 +4203,8 @@ function tlsSetupCommand(repoPath) {
|
|
|
3773
4203
|
}
|
|
3774
4204
|
function getMkcertRootCAPath(options = {}) {
|
|
3775
4205
|
ensureMkcert();
|
|
3776
|
-
const rootCAPath =
|
|
3777
|
-
if (!
|
|
4206
|
+
const rootCAPath = import_node_path11.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
|
|
4207
|
+
if (!import_node_fs13.default.existsSync(rootCAPath)) {
|
|
3778
4208
|
throw new Error(
|
|
3779
4209
|
`mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
|
|
3780
4210
|
);
|
|
@@ -3807,7 +4237,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
3807
4237
|
return normalizeUniqueHosts(names);
|
|
3808
4238
|
}
|
|
3809
4239
|
function parseCertificateDnsHosts(pem) {
|
|
3810
|
-
const certificate = new
|
|
4240
|
+
const certificate = new import_node_crypto4.X509Certificate(pem);
|
|
3811
4241
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
3812
4242
|
if (subjectAltName.length === 0) {
|
|
3813
4243
|
return [];
|
|
@@ -3840,10 +4270,10 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
|
3840
4270
|
);
|
|
3841
4271
|
}
|
|
3842
4272
|
function readCurrentCertificateHosts() {
|
|
3843
|
-
if (!
|
|
4273
|
+
if (!import_node_fs13.default.existsSync(CERT_FILE)) {
|
|
3844
4274
|
return [];
|
|
3845
4275
|
}
|
|
3846
|
-
const pem =
|
|
4276
|
+
const pem = import_node_fs13.default.readFileSync(CERT_FILE, "utf-8");
|
|
3847
4277
|
return parseCertificateDnsHosts(pem);
|
|
3848
4278
|
}
|
|
3849
4279
|
function currentCertificateHostsOrEmpty() {
|
|
@@ -3932,14 +4362,14 @@ Run: ${tlsSetupCommand(options.repoPath)}`
|
|
|
3932
4362
|
);
|
|
3933
4363
|
}
|
|
3934
4364
|
}
|
|
3935
|
-
var
|
|
4365
|
+
var import_node_child_process6, import_node_crypto4, import_node_fs13, import_node_path11, DEFAULT_TLS_CERT_HOSTS;
|
|
3936
4366
|
var init_tls = __esm({
|
|
3937
4367
|
"src/core/tls.ts"() {
|
|
3938
4368
|
"use strict";
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
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"));
|
|
3943
4373
|
init_docker();
|
|
3944
4374
|
init_host_routes();
|
|
3945
4375
|
init_router();
|
|
@@ -3955,7 +4385,7 @@ function outputFromResult(result) {
|
|
|
3955
4385
|
return output2.length > 0 ? output2 : void 0;
|
|
3956
4386
|
}
|
|
3957
4387
|
function runTool(command, args = []) {
|
|
3958
|
-
const result = (0,
|
|
4388
|
+
const result = (0, import_node_child_process7.spawnSync)(command, args, {
|
|
3959
4389
|
encoding: "utf-8"
|
|
3960
4390
|
});
|
|
3961
4391
|
if (result.error) {
|
|
@@ -4012,12 +4442,12 @@ function parseMinimumNodeMajor(value) {
|
|
|
4012
4442
|
return Number(match[1]);
|
|
4013
4443
|
}
|
|
4014
4444
|
function readPackageJson(repoPath) {
|
|
4015
|
-
const packagePath =
|
|
4016
|
-
if (!
|
|
4445
|
+
const packagePath = import_node_path12.default.join(repoPath, "package.json");
|
|
4446
|
+
if (!import_node_fs14.default.existsSync(packagePath)) {
|
|
4017
4447
|
return void 0;
|
|
4018
4448
|
}
|
|
4019
4449
|
try {
|
|
4020
|
-
return JSON.parse(
|
|
4450
|
+
return JSON.parse(import_node_fs14.default.readFileSync(packagePath, "utf-8"));
|
|
4021
4451
|
} catch {
|
|
4022
4452
|
return void 0;
|
|
4023
4453
|
}
|
|
@@ -4112,19 +4542,19 @@ function buildGlobalToolChecks(repoPath) {
|
|
|
4112
4542
|
checks.push(nodeToolchainCheck(repoPath));
|
|
4113
4543
|
return checks;
|
|
4114
4544
|
}
|
|
4115
|
-
var
|
|
4545
|
+
var import_node_child_process7, import_node_fs14, import_node_path12;
|
|
4116
4546
|
var init_tool_diagnostics = __esm({
|
|
4117
4547
|
"src/core/tool-diagnostics.ts"() {
|
|
4118
4548
|
"use strict";
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4549
|
+
import_node_child_process7 = require("child_process");
|
|
4550
|
+
import_node_fs14 = __toESM(require("fs"));
|
|
4551
|
+
import_node_path12 = __toESM(require("path"));
|
|
4122
4552
|
}
|
|
4123
4553
|
});
|
|
4124
4554
|
|
|
4125
4555
|
// src/core/devpod-workspaces.ts
|
|
4126
4556
|
function listDevpodWorkspaces() {
|
|
4127
|
-
const result = (0,
|
|
4557
|
+
const result = (0, import_node_child_process8.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
|
|
4128
4558
|
if (result.status !== 0) {
|
|
4129
4559
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4130
4560
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
@@ -4173,20 +4603,138 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
4173
4603
|
}
|
|
4174
4604
|
return matches[0];
|
|
4175
4605
|
}
|
|
4176
|
-
|
|
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) {
|
|
4177
4628
|
const args = action2 === "delete" ? [action2, devpodId, "--ignore-not-found"] : [action2, devpodId];
|
|
4178
|
-
const result = (0,
|
|
4629
|
+
const result = (0, import_node_child_process9.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
4179
4630
|
if (result.status !== 0) {
|
|
4180
|
-
|
|
4181
|
-
|
|
4631
|
+
throw new Error(
|
|
4632
|
+
`devpod ${action2} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
|
|
4633
|
+
);
|
|
4182
4634
|
}
|
|
4183
4635
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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"() {
|
|
4187
4727
|
"use strict";
|
|
4188
|
-
|
|
4189
|
-
|
|
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
|
+
};
|
|
4190
4738
|
}
|
|
4191
4739
|
});
|
|
4192
4740
|
|
|
@@ -4197,17 +4745,17 @@ function commandError(command, repoPath, stderr) {
|
|
|
4197
4745
|
);
|
|
4198
4746
|
}
|
|
4199
4747
|
function resolveGitCommonDir(repoPath) {
|
|
4200
|
-
const result = (0,
|
|
4748
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
|
|
4201
4749
|
encoding: "utf-8"
|
|
4202
4750
|
});
|
|
4203
4751
|
const output2 = result.stdout.trim();
|
|
4204
4752
|
if (result.status !== 0 || !output2) {
|
|
4205
4753
|
throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
|
|
4206
4754
|
}
|
|
4207
|
-
return comparableWorkspacePath(
|
|
4755
|
+
return comparableWorkspacePath(import_node_path14.default.isAbsolute(output2) ? output2 : import_node_path14.default.resolve(repoPath, output2));
|
|
4208
4756
|
}
|
|
4209
4757
|
function resolveGitTopLevel(repoPath) {
|
|
4210
|
-
const result = (0,
|
|
4758
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
|
|
4211
4759
|
encoding: "utf-8"
|
|
4212
4760
|
});
|
|
4213
4761
|
const output2 = result.stdout.trim();
|
|
@@ -4217,7 +4765,7 @@ function resolveGitTopLevel(repoPath) {
|
|
|
4217
4765
|
return comparableWorkspacePath(output2);
|
|
4218
4766
|
}
|
|
4219
4767
|
function listGitWorktrees(repoPath) {
|
|
4220
|
-
const result = (0,
|
|
4768
|
+
const result = (0, import_node_child_process10.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
4221
4769
|
encoding: "utf-8"
|
|
4222
4770
|
});
|
|
4223
4771
|
if (result.status !== 0) {
|
|
@@ -4253,7 +4801,7 @@ function listGitWorktrees(repoPath) {
|
|
|
4253
4801
|
return worktrees;
|
|
4254
4802
|
}
|
|
4255
4803
|
function ownershipDirectory(repoPath) {
|
|
4256
|
-
return
|
|
4804
|
+
return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
|
|
4257
4805
|
}
|
|
4258
4806
|
function validateWorkspace(value, label) {
|
|
4259
4807
|
if (typeof value !== "string" || wsFromBranch(value) !== value) {
|
|
@@ -4281,7 +4829,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4281
4829
|
`workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
|
|
4282
4830
|
);
|
|
4283
4831
|
}
|
|
4284
|
-
if (typeof candidate.worktreePath !== "string" || !
|
|
4832
|
+
if (typeof candidate.worktreePath !== "string" || !import_node_path14.default.isAbsolute(candidate.worktreePath)) {
|
|
4285
4833
|
throw new Error("invalid workspace ownership worktreePath");
|
|
4286
4834
|
}
|
|
4287
4835
|
if (candidate.branch !== null && typeof candidate.branch !== "string") {
|
|
@@ -4301,7 +4849,7 @@ function validateRecord(value, expectedWorkspace) {
|
|
|
4301
4849
|
function readRecordFile(filePath, expectedWorkspace) {
|
|
4302
4850
|
let parsed;
|
|
4303
4851
|
try {
|
|
4304
|
-
parsed = JSON.parse(
|
|
4852
|
+
parsed = JSON.parse(import_node_fs16.default.readFileSync(filePath, "utf-8"));
|
|
4305
4853
|
} catch (error) {
|
|
4306
4854
|
if (error instanceof SyntaxError) {
|
|
4307
4855
|
throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
|
|
@@ -4317,7 +4865,7 @@ function listWorkspaceOwnership(repoPath) {
|
|
|
4317
4865
|
function listWorkspaceOwnershipInDirectory(directory) {
|
|
4318
4866
|
let entries;
|
|
4319
4867
|
try {
|
|
4320
|
-
entries =
|
|
4868
|
+
entries = import_node_fs16.default.readdirSync(directory, { withFileTypes: true });
|
|
4321
4869
|
} catch (error) {
|
|
4322
4870
|
if (error.code === "ENOENT") return [];
|
|
4323
4871
|
throw error;
|
|
@@ -4325,14 +4873,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
|
|
|
4325
4873
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
4326
4874
|
const workspace = entry.name.slice(0, -".json".length);
|
|
4327
4875
|
validateWorkspace(workspace, "filename");
|
|
4328
|
-
return readRecordFile(
|
|
4876
|
+
return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
|
|
4329
4877
|
});
|
|
4330
4878
|
}
|
|
4331
4879
|
function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
4332
4880
|
const workspace = validateWorkspace(input2.workspace, "workspace");
|
|
4333
4881
|
const devpodId = validateWorkspace(input2.devpodId, "devpodId");
|
|
4334
4882
|
const worktreePath = comparableWorkspacePath(input2.worktreePath);
|
|
4335
|
-
const filePath =
|
|
4883
|
+
const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
|
|
4336
4884
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4337
4885
|
const records = listWorkspaceOwnershipInDirectory(directory);
|
|
4338
4886
|
const existing = records.find((record2) => record2.workspace === workspace);
|
|
@@ -4363,23 +4911,14 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
|
|
|
4363
4911
|
createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
|
|
4364
4912
|
updatedAt: validateTimestamp(now, "updatedAt")
|
|
4365
4913
|
};
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
import_node_fs13.default.writeFileSync(tempPath, `${JSON.stringify(record, null, 2)}
|
|
4369
|
-
`, {
|
|
4370
|
-
encoding: "utf-8",
|
|
4371
|
-
flag: "wx"
|
|
4372
|
-
});
|
|
4373
|
-
import_node_fs13.default.renameSync(tempPath, filePath);
|
|
4374
|
-
} finally {
|
|
4375
|
-
import_node_fs13.default.rmSync(tempPath, { force: true });
|
|
4376
|
-
}
|
|
4914
|
+
writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
|
|
4915
|
+
`);
|
|
4377
4916
|
return record;
|
|
4378
4917
|
}
|
|
4379
4918
|
function removeWorkspaceOwnershipInDirectory(directory, workspace) {
|
|
4380
|
-
const filePath =
|
|
4919
|
+
const filePath = import_node_path14.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
|
|
4381
4920
|
try {
|
|
4382
|
-
|
|
4921
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4383
4922
|
return true;
|
|
4384
4923
|
} catch (error) {
|
|
4385
4924
|
if (error.code === "ENOENT") return false;
|
|
@@ -4390,7 +4929,7 @@ function sameOwnershipRecord(left, right) {
|
|
|
4390
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;
|
|
4391
4930
|
}
|
|
4392
4931
|
function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
4393
|
-
const filePath =
|
|
4932
|
+
const filePath = import_node_path14.default.join(
|
|
4394
4933
|
directory,
|
|
4395
4934
|
`${validateWorkspace(expected.workspace, "workspace")}.json`
|
|
4396
4935
|
);
|
|
@@ -4402,14 +4941,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
|
4402
4941
|
throw error;
|
|
4403
4942
|
}
|
|
4404
4943
|
if (!sameOwnershipRecord(current, expected)) return "changed";
|
|
4405
|
-
|
|
4944
|
+
import_node_fs16.default.rmSync(filePath);
|
|
4406
4945
|
return "removed";
|
|
4407
4946
|
}
|
|
4408
4947
|
function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
4409
4948
|
const directory = ownershipDirectory(repoPath);
|
|
4410
|
-
|
|
4949
|
+
import_node_fs16.default.mkdirSync(directory, { recursive: true });
|
|
4411
4950
|
return withFileLockSync(
|
|
4412
|
-
|
|
4951
|
+
import_node_path14.default.join(directory, ".lock"),
|
|
4413
4952
|
{ activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
|
|
4414
4953
|
() => operation({
|
|
4415
4954
|
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
@@ -4440,7 +4979,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
|
|
|
4440
4979
|
if (worktree?.locked) {
|
|
4441
4980
|
return { ownerStatus: "locked", devpodStatus, worktree };
|
|
4442
4981
|
}
|
|
4443
|
-
if ((!worktree || worktree.prunable) &&
|
|
4982
|
+
if ((!worktree || worktree.prunable) && import_node_fs16.default.existsSync(record.worktreePath)) {
|
|
4444
4983
|
return { ownerStatus: "conflict", devpodStatus, worktree };
|
|
4445
4984
|
}
|
|
4446
4985
|
if (!worktree || worktree.prunable) {
|
|
@@ -4464,19 +5003,19 @@ function listMissingWorkspaceOwnership(repoPath) {
|
|
|
4464
5003
|
(record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
|
|
4465
5004
|
);
|
|
4466
5005
|
}
|
|
4467
|
-
var
|
|
5006
|
+
var import_node_child_process10, import_node_fs16, import_node_path14, OWNERSHIP_VERSION, OWNERSHIP_DIR;
|
|
4468
5007
|
var init_workspace_ownership = __esm({
|
|
4469
5008
|
"src/core/workspace-ownership.ts"() {
|
|
4470
5009
|
"use strict";
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
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();
|
|
4475
5014
|
init_devpod_workspaces();
|
|
4476
5015
|
init_file_lock();
|
|
4477
5016
|
init_workspace();
|
|
4478
5017
|
OWNERSHIP_VERSION = 1;
|
|
4479
|
-
OWNERSHIP_DIR =
|
|
5018
|
+
OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
|
|
4480
5019
|
}
|
|
4481
5020
|
});
|
|
4482
5021
|
|
|
@@ -4490,10 +5029,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
4490
5029
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
4491
5030
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
4492
5031
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
4493
|
-
const localRoot =
|
|
5032
|
+
const localRoot = import_node_path15.default.join(comparableRepo, "trees") + import_node_path15.default.sep;
|
|
4494
5033
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
4495
|
-
const legacyPrefix = `${
|
|
4496
|
-
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);
|
|
4497
5036
|
}
|
|
4498
5037
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
4499
5038
|
const actions = [
|
|
@@ -4621,17 +5160,6 @@ function revalidateCandidate(candidate, repoPath, transaction) {
|
|
|
4621
5160
|
}
|
|
4622
5161
|
};
|
|
4623
5162
|
}
|
|
4624
|
-
function recheckDevpodOwnership(record) {
|
|
4625
|
-
const ownership = inspectDevpodWorkspaceOwnership(
|
|
4626
|
-
listDevpodWorkspaces(),
|
|
4627
|
-
record.devpodId,
|
|
4628
|
-
record.worktreePath
|
|
4629
|
-
);
|
|
4630
|
-
if (ownership.status === "conflict") {
|
|
4631
|
-
throw new Error(ownership.reason);
|
|
4632
|
-
}
|
|
4633
|
-
return ownership.status;
|
|
4634
|
-
}
|
|
4635
5163
|
function failedAction(resource, error) {
|
|
4636
5164
|
return {
|
|
4637
5165
|
resource,
|
|
@@ -4661,8 +5189,8 @@ function applyCandidate(candidate, repoPath) {
|
|
|
4661
5189
|
const actions = [];
|
|
4662
5190
|
let devpodStatus;
|
|
4663
5191
|
try {
|
|
4664
|
-
|
|
4665
|
-
|
|
5192
|
+
const mutation = deleteOwnedDevpodWorkspace(record.devpodId, record.worktreePath);
|
|
5193
|
+
devpodStatus = mutation.status === "changed" ? "owned" : "absent";
|
|
4666
5194
|
} catch (error) {
|
|
4667
5195
|
return { ...fresh, actions: [failedAction("devpod", error)] };
|
|
4668
5196
|
}
|
|
@@ -4747,11 +5275,12 @@ function applyWorkspaceGc(plan) {
|
|
|
4747
5275
|
candidates
|
|
4748
5276
|
};
|
|
4749
5277
|
}
|
|
4750
|
-
var
|
|
5278
|
+
var import_node_path15;
|
|
4751
5279
|
var init_workspace_gc = __esm({
|
|
4752
5280
|
"src/core/workspace-gc.ts"() {
|
|
4753
5281
|
"use strict";
|
|
4754
|
-
|
|
5282
|
+
import_node_path15 = __toESM(require("path"));
|
|
5283
|
+
init_devpod_mutation();
|
|
4755
5284
|
init_devpod_workspaces();
|
|
4756
5285
|
init_host_routes();
|
|
4757
5286
|
init_repo_config();
|
|
@@ -4820,11 +5349,11 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
4820
5349
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
4821
5350
|
continue;
|
|
4822
5351
|
}
|
|
4823
|
-
if (!
|
|
5352
|
+
if (!import_node_fs17.default.existsSync(absolutePath)) {
|
|
4824
5353
|
continue;
|
|
4825
5354
|
}
|
|
4826
5355
|
try {
|
|
4827
|
-
const raw =
|
|
5356
|
+
const raw = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
|
|
4828
5357
|
const parsed = import_yaml4.default.parse(raw);
|
|
4829
5358
|
const root = asRecord2(parsed);
|
|
4830
5359
|
const services = asRecord2(root?.services);
|
|
@@ -5087,7 +5616,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
5087
5616
|
const config = runtimeConfig.config;
|
|
5088
5617
|
loadedConfig = config;
|
|
5089
5618
|
loadedWorkspace = runtimeConfig.workspace;
|
|
5090
|
-
const cliVersion = true ? "0.0.
|
|
5619
|
+
const cliVersion = true ? "0.0.35" : "0.0.0-dev";
|
|
5091
5620
|
const configVersion = config.devrouter?.version;
|
|
5092
5621
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
5093
5622
|
addCheck(checks, {
|
|
@@ -5117,9 +5646,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
5117
5646
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
5118
5647
|
app: app.name,
|
|
5119
5648
|
filePath,
|
|
5120
|
-
absolutePath:
|
|
5649
|
+
absolutePath: import_node_path16.default.resolve(repo.path, filePath)
|
|
5121
5650
|
}))
|
|
5122
|
-
).filter((entry) => !
|
|
5651
|
+
).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5123
5652
|
addCheck(checks, {
|
|
5124
5653
|
id: "repo.compose-files",
|
|
5125
5654
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -5162,8 +5691,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
5162
5691
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
5163
5692
|
app: app.name,
|
|
5164
5693
|
cwd: app.hostRun.cwd,
|
|
5165
|
-
absolutePath:
|
|
5166
|
-
})).filter((entry) => !
|
|
5694
|
+
absolutePath: import_node_path16.default.resolve(repo.path, app.hostRun.cwd)
|
|
5695
|
+
})).filter((entry) => !import_node_fs17.default.existsSync(entry.absolutePath));
|
|
5167
5696
|
addCheck(checks, {
|
|
5168
5697
|
id: "repo.host-cwd",
|
|
5169
5698
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -5307,12 +5836,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
5307
5836
|
nextSteps
|
|
5308
5837
|
};
|
|
5309
5838
|
}
|
|
5310
|
-
var
|
|
5839
|
+
var import_node_fs17, import_node_path16, import_yaml4, POSTGRES_DEFAULTS;
|
|
5311
5840
|
var init_doctor = __esm({
|
|
5312
5841
|
"src/core/doctor.ts"() {
|
|
5313
5842
|
"use strict";
|
|
5314
|
-
|
|
5315
|
-
|
|
5843
|
+
import_node_fs17 = __toESM(require("fs"));
|
|
5844
|
+
import_node_path16 = __toESM(require("path"));
|
|
5316
5845
|
import_yaml4 = __toESM(require("yaml"));
|
|
5317
5846
|
init_docker();
|
|
5318
5847
|
init_host_routes();
|
|
@@ -5570,12 +6099,12 @@ function parseSsPortListeners(stdout, port) {
|
|
|
5570
6099
|
return listeners;
|
|
5571
6100
|
}
|
|
5572
6101
|
function findPortListeners(port) {
|
|
5573
|
-
const result = (0,
|
|
6102
|
+
const result = (0, import_node_child_process11.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
5574
6103
|
encoding: "utf-8"
|
|
5575
6104
|
});
|
|
5576
6105
|
if (result.error && result.error.code === "ENOENT") {
|
|
5577
6106
|
if (process.platform === "linux") {
|
|
5578
|
-
const ssResult = (0,
|
|
6107
|
+
const ssResult = (0, import_node_child_process11.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
5579
6108
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
5580
6109
|
return parseSsPortListeners(ssResult.stdout, port);
|
|
5581
6110
|
}
|
|
@@ -5604,11 +6133,11 @@ function findPortListeners(port) {
|
|
|
5604
6133
|
};
|
|
5605
6134
|
});
|
|
5606
6135
|
}
|
|
5607
|
-
var
|
|
6136
|
+
var import_node_child_process11;
|
|
5608
6137
|
var init_ports = __esm({
|
|
5609
6138
|
"src/util/ports.ts"() {
|
|
5610
6139
|
"use strict";
|
|
5611
|
-
|
|
6140
|
+
import_node_child_process11 = require("child_process");
|
|
5612
6141
|
}
|
|
5613
6142
|
});
|
|
5614
6143
|
|
|
@@ -5665,7 +6194,7 @@ var init_up = __esm({
|
|
|
5665
6194
|
|
|
5666
6195
|
// src/core/devpod-environment.ts
|
|
5667
6196
|
function inspectWorkspaceContainers() {
|
|
5668
|
-
const listed = (0,
|
|
6197
|
+
const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
|
|
5669
6198
|
encoding: "utf-8"
|
|
5670
6199
|
});
|
|
5671
6200
|
if (listed.status !== 0) {
|
|
@@ -5675,7 +6204,7 @@ function inspectWorkspaceContainers() {
|
|
|
5675
6204
|
}
|
|
5676
6205
|
const ids = listed.stdout.split(/\r?\n/).map((id) => id.trim()).filter(Boolean);
|
|
5677
6206
|
if (ids.length === 0) return [];
|
|
5678
|
-
const inspected = (0,
|
|
6207
|
+
const inspected = (0, import_node_child_process12.spawnSync)("docker", ["inspect", "--format", SAFE_INSPECT_TEMPLATE, ...ids], {
|
|
5679
6208
|
encoding: "utf-8"
|
|
5680
6209
|
});
|
|
5681
6210
|
if (inspected.status !== 0) {
|
|
@@ -5688,7 +6217,7 @@ function inspectWorkspaceContainers() {
|
|
|
5688
6217
|
function workspaceAppContainers(containers, repoPath) {
|
|
5689
6218
|
return containers.filter((container) => {
|
|
5690
6219
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5691
|
-
return Boolean(workingDir && sameWorkspacePath(workingDir,
|
|
6220
|
+
return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
|
|
5692
6221
|
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
5693
6222
|
);
|
|
5694
6223
|
});
|
|
@@ -5711,12 +6240,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
|
|
|
5711
6240
|
}
|
|
5712
6241
|
return { id: container.id, workspacePath: repoMount.Destination };
|
|
5713
6242
|
}
|
|
5714
|
-
var
|
|
6243
|
+
var import_node_child_process12, import_node_path17, SAFE_INSPECT_TEMPLATE;
|
|
5715
6244
|
var init_devpod_environment = __esm({
|
|
5716
6245
|
"src/core/devpod-environment.ts"() {
|
|
5717
6246
|
"use strict";
|
|
5718
|
-
|
|
5719
|
-
|
|
6247
|
+
import_node_child_process12 = require("child_process");
|
|
6248
|
+
import_node_path17 = __toESM(require("path"));
|
|
5720
6249
|
init_workspace();
|
|
5721
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}}}';
|
|
5722
6251
|
}
|
|
@@ -5743,18 +6272,18 @@ function probeHttpRoute(host, options = {}) {
|
|
|
5743
6272
|
args.push("--cacert", getMkcertRootCAPath({ repoPath: options.repoPath }));
|
|
5744
6273
|
}
|
|
5745
6274
|
args.push(url);
|
|
5746
|
-
const result = (0,
|
|
6275
|
+
const result = (0, import_node_child_process13.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
5747
6276
|
const stdout = result.stdout?.trim() ?? "";
|
|
5748
6277
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
5749
6278
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
5750
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"}`;
|
|
5751
6280
|
return { ok, status, details };
|
|
5752
6281
|
}
|
|
5753
|
-
var
|
|
6282
|
+
var import_node_child_process13;
|
|
5754
6283
|
var init_http_route_probe = __esm({
|
|
5755
6284
|
"src/core/http-route-probe.ts"() {
|
|
5756
6285
|
"use strict";
|
|
5757
|
-
|
|
6286
|
+
import_node_child_process13 = require("child_process");
|
|
5758
6287
|
init_router();
|
|
5759
6288
|
init_tls();
|
|
5760
6289
|
}
|
|
@@ -5833,11 +6362,11 @@ var init_route_publication = __esm({
|
|
|
5833
6362
|
// src/core/workspace-ensure.ts
|
|
5834
6363
|
function assertOverlay(container, repoPath) {
|
|
5835
6364
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
5836
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
6365
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path18.default.join(repoPath, ".devcontainer"))) {
|
|
5837
6366
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
5838
6367
|
}
|
|
5839
6368
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
5840
|
-
const expectedOverlay =
|
|
6369
|
+
const expectedOverlay = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
5841
6370
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
5842
6371
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
5843
6372
|
}
|
|
@@ -5911,7 +6440,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
5911
6440
|
target
|
|
5912
6441
|
});
|
|
5913
6442
|
if (target.kind === "linked") {
|
|
5914
|
-
const workspaceEnv = (0,
|
|
6443
|
+
const workspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
5915
6444
|
"docker",
|
|
5916
6445
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
5917
6446
|
{ encoding: "utf-8" }
|
|
@@ -5921,7 +6450,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
5921
6450
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
5922
6451
|
);
|
|
5923
6452
|
}
|
|
5924
|
-
const devrouterWorkspaceEnv = (0,
|
|
6453
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
|
|
5925
6454
|
"docker",
|
|
5926
6455
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
5927
6456
|
{ encoding: "utf-8" }
|
|
@@ -5932,7 +6461,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
5932
6461
|
);
|
|
5933
6462
|
}
|
|
5934
6463
|
}
|
|
5935
|
-
const gitCheck = (0,
|
|
6464
|
+
const gitCheck = (0, import_node_child_process14.spawnSync)(
|
|
5936
6465
|
"docker",
|
|
5937
6466
|
[
|
|
5938
6467
|
"exec",
|
|
@@ -5948,7 +6477,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
5948
6477
|
if (gitCheck.status !== 0 || !sameWorkspacePath(gitCheck.stdout.trim(), appContainer.workspacePath)) {
|
|
5949
6478
|
throw new Error("Git does not resolve the expected checkout inside the app container.");
|
|
5950
6479
|
}
|
|
5951
|
-
return;
|
|
6480
|
+
return appContainer;
|
|
5952
6481
|
} catch (error) {
|
|
5953
6482
|
lastError = error;
|
|
5954
6483
|
}
|
|
@@ -6019,63 +6548,14 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
6019
6548
|
}
|
|
6020
6549
|
function isPrimaryCheckout(repoPath) {
|
|
6021
6550
|
try {
|
|
6022
|
-
return
|
|
6551
|
+
return import_node_fs18.default.statSync(import_node_path18.default.join(repoPath, ".git")).isDirectory();
|
|
6023
6552
|
} catch {
|
|
6024
6553
|
return false;
|
|
6025
6554
|
}
|
|
6026
6555
|
}
|
|
6027
|
-
function startDevpod(repoPath, target, recreate = false, quiet = false) {
|
|
6028
|
-
const args = ["up", repoPath];
|
|
6029
|
-
if (target.devpodId) {
|
|
6030
|
-
args.push("--id", target.devpodId);
|
|
6031
|
-
}
|
|
6032
|
-
args.push("--open-ide=false");
|
|
6033
|
-
if (target.kind === "linked") {
|
|
6034
|
-
args.push(
|
|
6035
|
-
"--workspace-env",
|
|
6036
|
-
`WORKSPACE=${target.workspace}`,
|
|
6037
|
-
"--workspace-env",
|
|
6038
|
-
`DEVROUTER_WORKSPACE=${target.workspace}`
|
|
6039
|
-
);
|
|
6040
|
-
}
|
|
6041
|
-
if (recreate) {
|
|
6042
|
-
if (!target.devpodId) {
|
|
6043
|
-
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
6044
|
-
}
|
|
6045
|
-
args.push("--recreate");
|
|
6046
|
-
}
|
|
6047
|
-
const env = { ...process.env };
|
|
6048
|
-
if (target.kind === "linked") {
|
|
6049
|
-
env.WORKSPACE = target.workspace;
|
|
6050
|
-
env.DEVROUTER_WORKSPACE = target.workspace;
|
|
6051
|
-
env.DEVROUTER_GIT_COMMON_DIR = target.gitCommonDir;
|
|
6052
|
-
env.DEVCONTAINER_COMPOSE_OVERLAY = DEVCONTAINER_OVERLAY;
|
|
6053
|
-
} else {
|
|
6054
|
-
delete env.WORKSPACE;
|
|
6055
|
-
delete env.DEVROUTER_WORKSPACE;
|
|
6056
|
-
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
6057
|
-
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
6058
|
-
}
|
|
6059
|
-
const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
|
|
6060
|
-
stdio: quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
6061
|
-
env
|
|
6062
|
-
});
|
|
6063
|
-
if (result.status !== 0) {
|
|
6064
|
-
throw new Error(`devpod up failed for '${target.devpodId ?? repoPath}'.`);
|
|
6065
|
-
}
|
|
6066
|
-
}
|
|
6067
|
-
function assertDevpodAttachment(repoPath, expectedId) {
|
|
6068
|
-
const attached = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
|
|
6069
|
-
if (!attached || expectedId && attached.id !== expectedId) {
|
|
6070
|
-
throw new Error(
|
|
6071
|
-
`DevPod did not attach '${repoPath}'${expectedId ? ` as '${expectedId}'` : ""} after startup.`
|
|
6072
|
-
);
|
|
6073
|
-
}
|
|
6074
|
-
return attached.id;
|
|
6075
|
-
}
|
|
6076
6556
|
function openUrls(urls) {
|
|
6077
6557
|
for (const url of urls) {
|
|
6078
|
-
const opened = (0,
|
|
6558
|
+
const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
6079
6559
|
if (opened.status !== 0) {
|
|
6080
6560
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
6081
6561
|
`);
|
|
@@ -6105,27 +6585,28 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6105
6585
|
const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
6106
6586
|
let devpodId = target.devpodId;
|
|
6107
6587
|
if (target.kind === "linked") {
|
|
6108
|
-
const overlayPath =
|
|
6109
|
-
if (!
|
|
6588
|
+
const overlayPath = import_node_path18.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
6589
|
+
if (!import_node_fs18.default.existsSync(overlayPath)) {
|
|
6110
6590
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
6111
6591
|
}
|
|
6112
6592
|
}
|
|
6593
|
+
const managedPostStart = resolveManagedPostStartPlan(repoPath);
|
|
6113
6594
|
const runtime = loadRuntimeConfig(
|
|
6114
6595
|
repoPath,
|
|
6115
6596
|
target.kind === "primary" ? "" : target.workspace
|
|
6116
6597
|
);
|
|
6117
6598
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
6118
6599
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
6119
|
-
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";
|
|
6120
6601
|
for (const [index, app] of apps.entries()) {
|
|
6121
|
-
if (
|
|
6602
|
+
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
6122
6603
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
6123
6604
|
throw new Error(
|
|
6124
|
-
`
|
|
6605
|
+
`Proxy app '${app.name}' must use a ${owner}-owned upstream beginning with '${aliasPrefix}-'.`
|
|
6125
6606
|
);
|
|
6126
6607
|
}
|
|
6127
6608
|
}
|
|
6128
|
-
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host)
|
|
6609
|
+
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
6129
6610
|
const ownership = target.kind === "linked" ? {
|
|
6130
6611
|
workspace: target.workspace,
|
|
6131
6612
|
worktreePath: repoPath,
|
|
@@ -6137,43 +6618,61 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6137
6618
|
}
|
|
6138
6619
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
6139
6620
|
const startAndProveAttachment = (recreate = false) => {
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
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
|
+
}
|
|
6143
6640
|
if (ownership) {
|
|
6144
6641
|
writeWorkspaceOwnership(repoPath, ownership);
|
|
6145
6642
|
}
|
|
6146
6643
|
};
|
|
6147
|
-
const
|
|
6644
|
+
const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
|
|
6645
|
+
const recreateAndPreflight = async () => {
|
|
6148
6646
|
startAndProveAttachment(true);
|
|
6149
|
-
|
|
6150
|
-
repoPath,
|
|
6151
|
-
currentTarget(),
|
|
6152
|
-
upstreamHosts,
|
|
6153
|
-
options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
|
|
6154
|
-
);
|
|
6647
|
+
return preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
6155
6648
|
};
|
|
6156
6649
|
let recreated = false;
|
|
6650
|
+
let container;
|
|
6157
6651
|
try {
|
|
6158
6652
|
startAndProveAttachment();
|
|
6159
6653
|
} catch (error) {
|
|
6160
6654
|
if (!target.hadExactDevpod) {
|
|
6161
6655
|
throw error;
|
|
6162
6656
|
}
|
|
6163
|
-
await
|
|
6657
|
+
container = await recreateAndPreflight();
|
|
6164
6658
|
recreated = true;
|
|
6165
6659
|
}
|
|
6166
|
-
if (!
|
|
6660
|
+
if (!container) {
|
|
6167
6661
|
try {
|
|
6168
|
-
await
|
|
6662
|
+
container = await preflight(0);
|
|
6169
6663
|
} catch (error) {
|
|
6170
6664
|
if (!target.hadExactDevpod) {
|
|
6171
6665
|
throw error;
|
|
6172
6666
|
}
|
|
6173
|
-
await
|
|
6667
|
+
container = await recreateAndPreflight();
|
|
6174
6668
|
recreated = true;
|
|
6175
6669
|
}
|
|
6176
6670
|
}
|
|
6671
|
+
runManagedPostStart({
|
|
6672
|
+
plan: managedPostStart,
|
|
6673
|
+
container,
|
|
6674
|
+
quiet: options.quiet
|
|
6675
|
+
});
|
|
6177
6676
|
const publication = await replacePublishedProxyRoutes(
|
|
6178
6677
|
repoPath,
|
|
6179
6678
|
runtime.config,
|
|
@@ -6190,7 +6689,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6190
6689
|
if (!target.hadExactDevpod || recreated) {
|
|
6191
6690
|
throw error;
|
|
6192
6691
|
}
|
|
6193
|
-
await
|
|
6692
|
+
const recoveredContainer = await recreateAndPreflight();
|
|
6693
|
+
runManagedPostStart({
|
|
6694
|
+
plan: managedPostStart,
|
|
6695
|
+
container: recoveredContainer,
|
|
6696
|
+
quiet: options.quiet
|
|
6697
|
+
});
|
|
6194
6698
|
recreated = true;
|
|
6195
6699
|
replaceHostRoutesForRepo(repoPath, publication.routes);
|
|
6196
6700
|
await waitForHttpRoutes(
|
|
@@ -6233,17 +6737,19 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
6233
6737
|
}
|
|
6234
6738
|
});
|
|
6235
6739
|
}
|
|
6236
|
-
var
|
|
6740
|
+
var import_node_child_process14, import_node_fs18, import_node_path18, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
|
|
6237
6741
|
var init_workspace_ensure = __esm({
|
|
6238
6742
|
"src/core/workspace-ensure.ts"() {
|
|
6239
6743
|
"use strict";
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6744
|
+
import_node_child_process14 = require("child_process");
|
|
6745
|
+
import_node_fs18 = __toESM(require("fs"));
|
|
6746
|
+
import_node_path18 = __toESM(require("path"));
|
|
6243
6747
|
init_devpod_environment();
|
|
6748
|
+
init_devpod_mutation();
|
|
6244
6749
|
init_devpod_workspaces();
|
|
6245
6750
|
init_host_routes();
|
|
6246
6751
|
init_http_route_probe();
|
|
6752
|
+
init_managed_post_start();
|
|
6247
6753
|
init_repo_config();
|
|
6248
6754
|
init_route_publication();
|
|
6249
6755
|
init_router();
|
|
@@ -6313,20 +6819,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
6313
6819
|
);
|
|
6314
6820
|
}
|
|
6315
6821
|
function defaultWorktreePath(mainRepo, ws) {
|
|
6316
|
-
return
|
|
6822
|
+
return import_node_path19.default.join(mainRepo, "trees", ws);
|
|
6317
6823
|
}
|
|
6318
6824
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
6319
|
-
const ignored = (0,
|
|
6825
|
+
const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
6320
6826
|
encoding: "utf-8"
|
|
6321
6827
|
});
|
|
6322
6828
|
if (ignored.status !== 0) {
|
|
6323
6829
|
throw new Error(
|
|
6324
|
-
`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.`
|
|
6325
6831
|
);
|
|
6326
6832
|
}
|
|
6327
6833
|
}
|
|
6328
6834
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
6329
|
-
return
|
|
6835
|
+
return import_node_path19.default.join(import_node_path19.default.dirname(mainRepo), `${import_node_path19.default.basename(mainRepo)}-${ws}`);
|
|
6330
6836
|
}
|
|
6331
6837
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
6332
6838
|
const candidates = [
|
|
@@ -6418,7 +6924,7 @@ function resolveWorkspaceTarget(mainRepo, target, worktrees, records) {
|
|
|
6418
6924
|
function worktreeForRecord(worktrees, record) {
|
|
6419
6925
|
return worktrees.find((candidate) => sameWorkspacePath(candidate.path, record.worktreePath));
|
|
6420
6926
|
}
|
|
6421
|
-
function
|
|
6927
|
+
function assertDevpodTargetSafe(target, worktrees, devpods) {
|
|
6422
6928
|
if (target.record) {
|
|
6423
6929
|
const status = inspectWorkspaceOwnership(target.record, worktrees, devpods);
|
|
6424
6930
|
if (status.ownerStatus === "conflict") {
|
|
@@ -6426,26 +6932,25 @@ function devpodForTarget(target, worktrees, devpods) {
|
|
|
6426
6932
|
`Workspace '${target.workspace}' ownership conflicts with live Git or DevPod evidence; no resources were changed.`
|
|
6427
6933
|
);
|
|
6428
6934
|
}
|
|
6429
|
-
return
|
|
6935
|
+
return;
|
|
6430
6936
|
}
|
|
6431
6937
|
const devpodId = target.workspace;
|
|
6432
6938
|
const ownership = inspectDevpodWorkspaceOwnership(devpods, devpodId, target.worktreePath);
|
|
6433
6939
|
if (ownership.status === "conflict") {
|
|
6434
6940
|
throw new Error(ownership.reason);
|
|
6435
6941
|
}
|
|
6436
|
-
return ownership.status === "owned" ? ownership.workspace : void 0;
|
|
6437
6942
|
}
|
|
6438
6943
|
function assertFullDownPreflight(mainRepo, target) {
|
|
6439
6944
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
6440
6945
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
6441
6946
|
}
|
|
6442
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
6947
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs19.default.existsSync(target.worktreePath)) return;
|
|
6443
6948
|
if (target.worktree.locked) {
|
|
6444
6949
|
throw new Error(
|
|
6445
6950
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
6446
6951
|
);
|
|
6447
6952
|
}
|
|
6448
|
-
const status = (0,
|
|
6953
|
+
const status = (0, import_node_child_process15.spawnSync)(
|
|
6449
6954
|
"git",
|
|
6450
6955
|
["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
|
|
6451
6956
|
{ encoding: "utf-8" }
|
|
@@ -6466,8 +6971,8 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6466
6971
|
if (!ws) {
|
|
6467
6972
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
6468
6973
|
}
|
|
6469
|
-
const worktreePath = opts.path ?
|
|
6470
|
-
if (
|
|
6974
|
+
const worktreePath = opts.path ? import_node_path19.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
6975
|
+
if (import_node_fs19.default.existsSync(worktreePath)) {
|
|
6471
6976
|
const registered = listGitWorktrees(mainRepo).find(
|
|
6472
6977
|
(worktree) => sameWorkspacePath(worktree.path, worktreePath)
|
|
6473
6978
|
);
|
|
@@ -6485,11 +6990,11 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
6485
6990
|
if (!opts.path) {
|
|
6486
6991
|
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
6487
6992
|
}
|
|
6488
|
-
const add = (0,
|
|
6993
|
+
const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
6489
6994
|
encoding: "utf-8"
|
|
6490
6995
|
});
|
|
6491
6996
|
if (add.status !== 0) {
|
|
6492
|
-
const addNew = (0,
|
|
6997
|
+
const addNew = (0, import_node_child_process15.spawnSync)(
|
|
6493
6998
|
"git",
|
|
6494
6999
|
["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
|
|
6495
7000
|
{
|
|
@@ -6560,10 +7065,9 @@ function workspaceLs(repoPath) {
|
|
|
6560
7065
|
}
|
|
6561
7066
|
function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
6562
7067
|
const devpods = listDevpodWorkspaces();
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
}
|
|
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);
|
|
6567
7071
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
6568
7072
|
if (!quiet) {
|
|
6569
7073
|
process.stdout.write(
|
|
@@ -6572,9 +7076,9 @@ function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
|
6572
7076
|
);
|
|
6573
7077
|
}
|
|
6574
7078
|
return {
|
|
6575
|
-
...
|
|
7079
|
+
...mutation.status === "changed" ? { devpodId } : {},
|
|
6576
7080
|
freedRoutes: routes.length,
|
|
6577
|
-
providerChanged:
|
|
7081
|
+
providerChanged: mutation.status === "changed",
|
|
6578
7082
|
workspace: resolved.workspace
|
|
6579
7083
|
};
|
|
6580
7084
|
}
|
|
@@ -6588,10 +7092,15 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6588
7092
|
if (removeWorktree) {
|
|
6589
7093
|
assertFullDownPreflight(mainRepo, resolved);
|
|
6590
7094
|
}
|
|
6591
|
-
const result = mutateWorkspaceRuntime(
|
|
7095
|
+
const result = mutateWorkspaceRuntime(
|
|
7096
|
+
action2 === "stop" ? "stop" : "delete",
|
|
7097
|
+
resolved,
|
|
7098
|
+
worktrees,
|
|
7099
|
+
opts.quiet
|
|
7100
|
+
);
|
|
6592
7101
|
if (removeWorktree) {
|
|
6593
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
6594
|
-
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], {
|
|
6595
7104
|
encoding: "utf-8"
|
|
6596
7105
|
});
|
|
6597
7106
|
if (rm.status !== 0) {
|
|
@@ -6609,9 +7118,9 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
6609
7118
|
}
|
|
6610
7119
|
return result;
|
|
6611
7120
|
};
|
|
6612
|
-
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();
|
|
6613
7122
|
}
|
|
6614
|
-
async function
|
|
7123
|
+
async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
|
|
6615
7124
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
6616
7125
|
return withWorkspaceLifecycleLock(worktreePath, async () => {
|
|
6617
7126
|
const worktrees = identifyGitWorktrees(mainRepo);
|
|
@@ -6629,22 +7138,29 @@ async function workspaceStopOwnedPath(worktreePath, opts = {}) {
|
|
|
6629
7138
|
worktree: worktreeForRecord(worktrees, record),
|
|
6630
7139
|
record
|
|
6631
7140
|
};
|
|
6632
|
-
return mutateWorkspaceRuntime(
|
|
7141
|
+
return mutateWorkspaceRuntime(action2, resolved, worktrees, opts.quiet);
|
|
6633
7142
|
});
|
|
6634
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
|
+
}
|
|
6635
7150
|
async function workspaceStop(target, opts = {}) {
|
|
6636
7151
|
return runWorkspaceLifecycle("stop", target, opts);
|
|
6637
7152
|
}
|
|
6638
7153
|
async function workspaceDown(target, opts = {}) {
|
|
6639
7154
|
return runWorkspaceLifecycle("down", target, opts);
|
|
6640
7155
|
}
|
|
6641
|
-
var
|
|
7156
|
+
var import_node_child_process15, import_node_fs19, import_node_path19;
|
|
6642
7157
|
var init_workspace_lifecycle = __esm({
|
|
6643
7158
|
"src/core/workspace-lifecycle.ts"() {
|
|
6644
7159
|
"use strict";
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
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();
|
|
6648
7164
|
init_devpod_workspaces();
|
|
6649
7165
|
init_repo_config();
|
|
6650
7166
|
init_route_state();
|
|
@@ -6655,7 +7171,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
6655
7171
|
});
|
|
6656
7172
|
|
|
6657
7173
|
// src/core/environment-stop.ts
|
|
6658
|
-
async function environmentStop(repoPath) {
|
|
7174
|
+
async function environmentStop(repoPath, options = {}) {
|
|
6659
7175
|
const linked = isLinkedWorktree(repoPath);
|
|
6660
7176
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
6661
7177
|
if (linked && !workspace) {
|
|
@@ -6674,25 +7190,27 @@ async function environmentStop(repoPath) {
|
|
|
6674
7190
|
if (!mainRepo) {
|
|
6675
7191
|
throw new Error(`Could not resolve the primary checkout for '${repoPath}'.`);
|
|
6676
7192
|
}
|
|
6677
|
-
const result = await
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
7193
|
+
const result = await (options.delete ? workspaceDeleteOwnedPath : workspaceStopOwnedPath)(
|
|
7194
|
+
record.worktreePath,
|
|
7195
|
+
{
|
|
7196
|
+
quiet: true,
|
|
7197
|
+
repoPath: mainRepo
|
|
7198
|
+
}
|
|
7199
|
+
);
|
|
6681
7200
|
return {
|
|
6682
7201
|
kind: "linked",
|
|
6683
7202
|
repoPath,
|
|
6684
7203
|
workspace: result.workspace,
|
|
6685
7204
|
...result.devpodId ? { devpodId: result.devpodId } : {},
|
|
6686
|
-
stopped: result.providerChanged,
|
|
7205
|
+
stopped: !options.delete && result.providerChanged,
|
|
7206
|
+
...options.delete ? { deleted: result.providerChanged } : {},
|
|
6687
7207
|
freedRoutes: result.freedRoutes
|
|
6688
7208
|
};
|
|
6689
7209
|
}
|
|
6690
7210
|
}
|
|
6691
7211
|
return withWorkspaceLifecycleLock(repoPath, async () => {
|
|
6692
7212
|
const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
|
|
6693
|
-
|
|
6694
|
-
runDevpodWorkspaceAction("stop", devpod.id);
|
|
6695
|
-
}
|
|
7213
|
+
const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
|
|
6696
7214
|
const removedRoutes = removeHostRoutesWhere(
|
|
6697
7215
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
6698
7216
|
);
|
|
@@ -6700,8 +7218,9 @@ async function environmentStop(repoPath) {
|
|
|
6700
7218
|
kind: linked ? "linked" : "primary",
|
|
6701
7219
|
repoPath,
|
|
6702
7220
|
...workspace ? { workspace } : {},
|
|
6703
|
-
...devpod ? { devpodId: devpod.id } : {},
|
|
6704
|
-
stopped:
|
|
7221
|
+
...mutation.status === "changed" && devpod ? { devpodId: devpod.id } : {},
|
|
7222
|
+
stopped: !options.delete && mutation.status === "changed",
|
|
7223
|
+
...options.delete ? { deleted: mutation.status === "changed" } : {},
|
|
6705
7224
|
freedRoutes: removedRoutes.length
|
|
6706
7225
|
};
|
|
6707
7226
|
});
|
|
@@ -6709,6 +7228,7 @@ async function environmentStop(repoPath) {
|
|
|
6709
7228
|
var init_environment_stop = __esm({
|
|
6710
7229
|
"src/core/environment-stop.ts"() {
|
|
6711
7230
|
"use strict";
|
|
7231
|
+
init_devpod_mutation();
|
|
6712
7232
|
init_devpod_workspaces();
|
|
6713
7233
|
init_host_routes();
|
|
6714
7234
|
init_workspace();
|
|
@@ -6724,19 +7244,19 @@ __export(stop_exports, {
|
|
|
6724
7244
|
});
|
|
6725
7245
|
async function runStopCommand(options) {
|
|
6726
7246
|
const repoPath = resolveGitCheckoutPath(options.path);
|
|
6727
|
-
const result = await environmentStop(repoPath);
|
|
7247
|
+
const result = await environmentStop(repoPath, { delete: options.delete });
|
|
6728
7248
|
if (options.json) {
|
|
6729
7249
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6730
7250
|
`);
|
|
6731
7251
|
return;
|
|
6732
7252
|
}
|
|
6733
7253
|
const label = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
|
|
6734
|
-
if (!result.stopped && result.freedRoutes === 0) {
|
|
7254
|
+
if (!result.stopped && !result.deleted && result.freedRoutes === 0) {
|
|
6735
7255
|
process.stdout.write(`${label} is already stopped; no routes needed removal.
|
|
6736
7256
|
`);
|
|
6737
7257
|
return;
|
|
6738
7258
|
}
|
|
6739
|
-
const provider = result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
7259
|
+
const provider = result.deleted ? `Deleted DevPod '${result.devpodId}'. ` : result.stopped ? `Stopped DevPod '${result.devpodId}'. ` : "";
|
|
6740
7260
|
process.stdout.write(
|
|
6741
7261
|
`${provider}Freed ${result.freedRoutes} route(s) for ${label.toLowerCase()}.
|
|
6742
7262
|
`
|
|
@@ -6766,7 +7286,7 @@ function ensureGuidance(repoPath) {
|
|
|
6766
7286
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
6767
7287
|
}
|
|
6768
7288
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
6769
|
-
const result = (0,
|
|
7289
|
+
const result = (0, import_node_child_process16.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
6770
7290
|
encoding: "utf-8"
|
|
6771
7291
|
});
|
|
6772
7292
|
if (result.status !== 0) {
|
|
@@ -6804,7 +7324,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6804
7324
|
}
|
|
6805
7325
|
assertRunningDevpod(devpod.id, repoPath);
|
|
6806
7326
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
6807
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
7327
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto5.randomUUID)()}__:`;
|
|
6808
7328
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
6809
7329
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
6810
7330
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -6822,7 +7342,7 @@ async function devpodExec(repoPath, command) {
|
|
|
6822
7342
|
wrappedCommand
|
|
6823
7343
|
];
|
|
6824
7344
|
return new Promise((resolve, reject) => {
|
|
6825
|
-
const child = (0,
|
|
7345
|
+
const child = (0, import_node_child_process16.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
6826
7346
|
let pending = Buffer.alloc(0);
|
|
6827
7347
|
let statusBytes = Buffer.alloc(0);
|
|
6828
7348
|
let readingStatus = false;
|
|
@@ -6881,12 +7401,12 @@ async function devpodExec(repoPath, command) {
|
|
|
6881
7401
|
});
|
|
6882
7402
|
});
|
|
6883
7403
|
}
|
|
6884
|
-
var
|
|
7404
|
+
var import_node_child_process16, import_node_crypto5, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
|
|
6885
7405
|
var init_devpod_exec = __esm({
|
|
6886
7406
|
"src/core/devpod-exec.ts"() {
|
|
6887
7407
|
"use strict";
|
|
6888
|
-
|
|
6889
|
-
|
|
7408
|
+
import_node_child_process16 = require("child_process");
|
|
7409
|
+
import_node_crypto5 = require("crypto");
|
|
6890
7410
|
init_devpod_environment();
|
|
6891
7411
|
init_devpod_workspaces();
|
|
6892
7412
|
init_workspace();
|
|
@@ -7070,7 +7590,7 @@ async function runOpenCommand(name) {
|
|
|
7070
7590
|
);
|
|
7071
7591
|
return;
|
|
7072
7592
|
}
|
|
7073
|
-
const result = (0,
|
|
7593
|
+
const result = (0, import_node_child_process17.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
7074
7594
|
if (result.status !== 0) {
|
|
7075
7595
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
7076
7596
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -7078,11 +7598,11 @@ async function runOpenCommand(name) {
|
|
|
7078
7598
|
process.stdout.write(`Opened ${url}
|
|
7079
7599
|
`);
|
|
7080
7600
|
}
|
|
7081
|
-
var
|
|
7601
|
+
var import_node_child_process17;
|
|
7082
7602
|
var init_open = __esm({
|
|
7083
7603
|
"src/commands/open.ts"() {
|
|
7084
7604
|
"use strict";
|
|
7085
|
-
|
|
7605
|
+
import_node_child_process17 = require("child_process");
|
|
7086
7606
|
init_docker();
|
|
7087
7607
|
init_host_routes();
|
|
7088
7608
|
init_repo_config();
|
|
@@ -7105,7 +7625,7 @@ async function runLogsCommand(options) {
|
|
|
7105
7625
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
7106
7626
|
if (options.follow) {
|
|
7107
7627
|
args.splice(1, 0, "-f");
|
|
7108
|
-
const child = (0,
|
|
7628
|
+
const child = (0, import_node_child_process18.spawn)("docker", args, { stdio: "inherit" });
|
|
7109
7629
|
const onSignal = () => {
|
|
7110
7630
|
child.kill("SIGTERM");
|
|
7111
7631
|
};
|
|
@@ -7119,17 +7639,17 @@ async function runLogsCommand(options) {
|
|
|
7119
7639
|
});
|
|
7120
7640
|
});
|
|
7121
7641
|
} else {
|
|
7122
|
-
const result = (0,
|
|
7642
|
+
const result = (0, import_node_child_process18.spawnSync)("docker", args, { stdio: "inherit" });
|
|
7123
7643
|
if (result.status !== 0) {
|
|
7124
7644
|
throw new Error("Failed to retrieve router logs.");
|
|
7125
7645
|
}
|
|
7126
7646
|
}
|
|
7127
7647
|
}
|
|
7128
|
-
var
|
|
7648
|
+
var import_node_child_process18;
|
|
7129
7649
|
var init_logs = __esm({
|
|
7130
7650
|
"src/commands/logs.ts"() {
|
|
7131
7651
|
"use strict";
|
|
7132
|
-
|
|
7652
|
+
import_node_child_process18 = require("child_process");
|
|
7133
7653
|
init_docker();
|
|
7134
7654
|
init_router();
|
|
7135
7655
|
}
|
|
@@ -7169,17 +7689,17 @@ function asRecord3(value) {
|
|
|
7169
7689
|
return value;
|
|
7170
7690
|
}
|
|
7171
7691
|
function readJson(filePath) {
|
|
7172
|
-
if (!
|
|
7692
|
+
if (!import_node_fs20.default.existsSync(filePath)) {
|
|
7173
7693
|
return void 0;
|
|
7174
7694
|
}
|
|
7175
7695
|
try {
|
|
7176
|
-
return JSON.parse(
|
|
7696
|
+
return JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
|
|
7177
7697
|
} catch {
|
|
7178
7698
|
return void 0;
|
|
7179
7699
|
}
|
|
7180
7700
|
}
|
|
7181
7701
|
function relative(repoPath, filePath) {
|
|
7182
|
-
return
|
|
7702
|
+
return import_node_path20.default.relative(repoPath, filePath) || ".";
|
|
7183
7703
|
}
|
|
7184
7704
|
function redactEnvAssignments(value) {
|
|
7185
7705
|
return value.replace(
|
|
@@ -7220,7 +7740,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
7220
7740
|
["bun.lock", "bun"]
|
|
7221
7741
|
];
|
|
7222
7742
|
for (const [fileName, name] of lockfiles) {
|
|
7223
|
-
if (
|
|
7743
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7224
7744
|
return { name, source: fileName };
|
|
7225
7745
|
}
|
|
7226
7746
|
}
|
|
@@ -7235,9 +7755,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
7235
7755
|
if (typeof engines?.node === "string") {
|
|
7236
7756
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
7237
7757
|
}
|
|
7238
|
-
const nvmrc =
|
|
7239
|
-
if (
|
|
7240
|
-
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();
|
|
7241
7761
|
return { version, source: ".nvmrc" };
|
|
7242
7762
|
}
|
|
7243
7763
|
return void 0;
|
|
@@ -7298,7 +7818,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
7298
7818
|
const files = config.apps.filter(
|
|
7299
7819
|
(app) => app.runtime === "docker"
|
|
7300
7820
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
7301
|
-
(fileName) => !
|
|
7821
|
+
(fileName) => !import_node_path20.default.isAbsolute(fileName) && !import_node_path20.default.normalize(fileName).startsWith("..")
|
|
7302
7822
|
);
|
|
7303
7823
|
return Array.from(new Set(files));
|
|
7304
7824
|
} catch {
|
|
@@ -7316,7 +7836,7 @@ function composeFiles(repoPath) {
|
|
|
7316
7836
|
...configuredComposeFiles(repoPath)
|
|
7317
7837
|
];
|
|
7318
7838
|
return Array.from(new Set(candidates)).filter(
|
|
7319
|
-
(fileName) =>
|
|
7839
|
+
(fileName) => import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))
|
|
7320
7840
|
);
|
|
7321
7841
|
}
|
|
7322
7842
|
function stringArray(value) {
|
|
@@ -7354,7 +7874,7 @@ function inspectServices(repoPath) {
|
|
|
7354
7874
|
const services = [];
|
|
7355
7875
|
for (const fileName of composeFiles(repoPath)) {
|
|
7356
7876
|
try {
|
|
7357
|
-
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"));
|
|
7358
7878
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
7359
7879
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
7360
7880
|
const service = asRecord3(value);
|
|
@@ -7389,8 +7909,8 @@ function inspectServices(repoPath) {
|
|
|
7389
7909
|
return services;
|
|
7390
7910
|
}
|
|
7391
7911
|
function inspectEnvFiles(repoPath) {
|
|
7392
|
-
const files =
|
|
7393
|
-
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");
|
|
7394
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();
|
|
7395
7915
|
return { path: fileName, names };
|
|
7396
7916
|
});
|
|
@@ -7404,18 +7924,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
7404
7924
|
};
|
|
7405
7925
|
}
|
|
7406
7926
|
function inspectDevcontainer(repoPath) {
|
|
7407
|
-
const dir =
|
|
7408
|
-
if (!
|
|
7927
|
+
const dir = import_node_path20.default.join(repoPath, ".devcontainer");
|
|
7928
|
+
if (!import_node_fs20.default.existsSync(dir)) {
|
|
7409
7929
|
return { exists: false, files: [] };
|
|
7410
7930
|
}
|
|
7411
7931
|
return {
|
|
7412
7932
|
exists: true,
|
|
7413
|
-
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}`)
|
|
7414
7934
|
};
|
|
7415
7935
|
}
|
|
7416
7936
|
function inspectDevrouter(repoPath) {
|
|
7417
7937
|
const configPath = getRepoConfigPath(repoPath);
|
|
7418
|
-
if (!
|
|
7938
|
+
if (!import_node_fs20.default.existsSync(configPath)) {
|
|
7419
7939
|
return {
|
|
7420
7940
|
exists: false,
|
|
7421
7941
|
configPath,
|
|
@@ -7459,15 +7979,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
7459
7979
|
["AGENTS.md", "agents"],
|
|
7460
7980
|
["CLAUDE.md", "claude"]
|
|
7461
7981
|
]) {
|
|
7462
|
-
if (
|
|
7982
|
+
if (import_node_fs20.default.existsSync(import_node_path20.default.join(repoPath, fileName))) {
|
|
7463
7983
|
results.push({ path: fileName, kind });
|
|
7464
7984
|
}
|
|
7465
7985
|
}
|
|
7466
|
-
const skillsDir =
|
|
7467
|
-
if (
|
|
7468
|
-
for (const name of
|
|
7469
|
-
const skillPath =
|
|
7470
|
-
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)) {
|
|
7471
7991
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
7472
7992
|
}
|
|
7473
7993
|
}
|
|
@@ -7510,7 +8030,7 @@ function buildIssues(report) {
|
|
|
7510
8030
|
}
|
|
7511
8031
|
function inspectRepo(options = {}) {
|
|
7512
8032
|
const repoPath = resolveRepoPath(options.repo);
|
|
7513
|
-
const pkg = readJson(
|
|
8033
|
+
const pkg = readJson(import_node_path20.default.join(repoPath, "package.json"));
|
|
7514
8034
|
const scripts = inspectScripts(pkg);
|
|
7515
8035
|
const reportWithoutIssues = {
|
|
7516
8036
|
repoPath,
|
|
@@ -7529,12 +8049,12 @@ function inspectRepo(options = {}) {
|
|
|
7529
8049
|
issues: buildIssues(reportWithoutIssues)
|
|
7530
8050
|
};
|
|
7531
8051
|
}
|
|
7532
|
-
var
|
|
8052
|
+
var import_node_fs20, import_node_path20, import_yaml5;
|
|
7533
8053
|
var init_repo_inspect = __esm({
|
|
7534
8054
|
"src/core/repo-inspect.ts"() {
|
|
7535
8055
|
"use strict";
|
|
7536
|
-
|
|
7537
|
-
|
|
8056
|
+
import_node_fs20 = __toESM(require("fs"));
|
|
8057
|
+
import_node_path20 = __toESM(require("path"));
|
|
7538
8058
|
import_yaml5 = __toESM(require("yaml"));
|
|
7539
8059
|
init_repo_config();
|
|
7540
8060
|
}
|
|
@@ -7639,7 +8159,7 @@ function requiredFileChecks(repoPath) {
|
|
|
7639
8159
|
".devcontainer/docker-compose.yml",
|
|
7640
8160
|
".devrouter.yml"
|
|
7641
8161
|
];
|
|
7642
|
-
const missing = required.filter((fileName) => !
|
|
8162
|
+
const missing = required.filter((fileName) => !import_node_fs21.default.existsSync(import_node_path21.default.join(repoPath, fileName)));
|
|
7643
8163
|
return {
|
|
7644
8164
|
id: "repo.devcontainer.verify-files",
|
|
7645
8165
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -7858,12 +8378,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
7858
8378
|
nextSteps: collectNextSteps3(checks)
|
|
7859
8379
|
};
|
|
7860
8380
|
}
|
|
7861
|
-
var
|
|
8381
|
+
var import_node_fs21, import_node_path21;
|
|
7862
8382
|
var init_devcontainer_verify = __esm({
|
|
7863
8383
|
"src/core/devcontainer-verify.ts"() {
|
|
7864
8384
|
"use strict";
|
|
7865
|
-
|
|
7866
|
-
|
|
8385
|
+
import_node_fs21 = __toESM(require("fs"));
|
|
8386
|
+
import_node_path21 = __toESM(require("path"));
|
|
7867
8387
|
init_capabilities();
|
|
7868
8388
|
init_doctor();
|
|
7869
8389
|
init_host_routes();
|
|
@@ -7901,30 +8421,22 @@ function inferDevProcess(repo) {
|
|
|
7901
8421
|
function inferPort2(repo) {
|
|
7902
8422
|
return repo.apps.find((app) => app.port)?.port ?? 3e3;
|
|
7903
8423
|
}
|
|
7904
|
-
function renderDockerfile(nodeMajor, pnpmVersion
|
|
8424
|
+
function renderDockerfile(nodeMajor, pnpmVersion) {
|
|
7905
8425
|
const pnpmPackageSpec = `pnpm@${pnpmVersion}`;
|
|
7906
|
-
|
|
7907
|
-
const devrouterTarball = `devrouter-cli-${version}.tgz`;
|
|
7908
|
-
return `# ${MANAGED_MARKER}
|
|
8426
|
+
return `# ${MANAGED_MARKER2}
|
|
7909
8427
|
FROM node:${nodeMajor}-bookworm-slim
|
|
7910
8428
|
|
|
7911
8429
|
RUN apt-get update \\
|
|
7912
|
-
&& apt-get install -y --no-install-recommends git ca-certificates curl procps openssl
|
|
8430
|
+
&& apt-get install -y --no-install-recommends git ca-certificates curl procps openssl util-linux \\
|
|
7913
8431
|
&& rm -rf /var/lib/apt/lists/*
|
|
7914
8432
|
|
|
7915
8433
|
RUN npm install -g ${shellSingleQuote(pnpmPackageSpec)}
|
|
7916
8434
|
|
|
7917
|
-
RUN npm pack --silent ${shellSingleQuote(devrouterPackageSpec)} \\
|
|
7918
|
-
&& tar -xzf ${shellSingleQuote(devrouterTarball)} --strip-components=2 \\
|
|
7919
|
-
-C /usr/local/bin package/bin/devrouter-process \\
|
|
7920
|
-
&& chmod +x /usr/local/bin/devrouter-process \\
|
|
7921
|
-
&& rm ${shellSingleQuote(devrouterTarball)}
|
|
7922
|
-
|
|
7923
8435
|
WORKDIR /workspaces/app
|
|
7924
8436
|
`;
|
|
7925
8437
|
}
|
|
7926
8438
|
function renderCompose(projectName) {
|
|
7927
|
-
return `# ${
|
|
8439
|
+
return `# ${MANAGED_MARKER2}
|
|
7928
8440
|
services:
|
|
7929
8441
|
postgres:
|
|
7930
8442
|
image: postgres:16-alpine
|
|
@@ -7977,12 +8489,12 @@ volumes:
|
|
|
7977
8489
|
`;
|
|
7978
8490
|
}
|
|
7979
8491
|
function renderDefaultComposeOverlay() {
|
|
7980
|
-
return `# ${
|
|
8492
|
+
return `# ${MANAGED_MARKER2}
|
|
7981
8493
|
services: {}
|
|
7982
8494
|
`;
|
|
7983
8495
|
}
|
|
7984
8496
|
function renderDevrouterComposeOverlay() {
|
|
7985
|
-
return `# ${
|
|
8497
|
+
return `# ${MANAGED_MARKER2}
|
|
7986
8498
|
# A linked worktree's .git file points at the host repository's common Git
|
|
7987
8499
|
# directory. workspace ensure sets this absolute path before DevPod starts.
|
|
7988
8500
|
services:
|
|
@@ -7998,7 +8510,7 @@ services:
|
|
|
7998
8510
|
}
|
|
7999
8511
|
function renderInitDb() {
|
|
8000
8512
|
return `#!/usr/bin/env bash
|
|
8001
|
-
# ${
|
|
8513
|
+
# ${MANAGED_MARKER2}
|
|
8002
8514
|
set -euo pipefail
|
|
8003
8515
|
|
|
8004
8516
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'SQL'
|
|
@@ -8017,10 +8529,9 @@ function renderDevcontainerJson(projectName) {
|
|
|
8017
8529
|
"service": "app",
|
|
8018
8530
|
"workspaceFolder": "/workspaces/${projectName}",
|
|
8019
8531
|
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
|
8020
|
-
"postStartCommand": "bash .devcontainer/post-start.sh",
|
|
8021
8532
|
"customizations": {
|
|
8022
8533
|
"devrouter": {
|
|
8023
|
-
"managed": "${
|
|
8534
|
+
"managed": "${MANAGED_MARKER2}"
|
|
8024
8535
|
},
|
|
8025
8536
|
"vscode": {
|
|
8026
8537
|
"extensions": []
|
|
@@ -8030,7 +8541,7 @@ function renderDevcontainerJson(projectName) {
|
|
|
8030
8541
|
`;
|
|
8031
8542
|
}
|
|
8032
8543
|
function renderEnv(projectName, port) {
|
|
8033
|
-
return `# ${
|
|
8544
|
+
return `# ${MANAGED_MARKER2}
|
|
8034
8545
|
WORKSPACE=${projectName}
|
|
8035
8546
|
HOST=0.0.0.0
|
|
8036
8547
|
HOSTNAME=0.0.0.0
|
|
@@ -8041,7 +8552,7 @@ SHADOW_DATABASE_URL=postgres://prisma:prisma@postgres:5432/shadow
|
|
|
8041
8552
|
}
|
|
8042
8553
|
function renderPostCreate() {
|
|
8043
8554
|
return `#!/usr/bin/env bash
|
|
8044
|
-
# ${
|
|
8555
|
+
# ${MANAGED_MARKER2}
|
|
8045
8556
|
set -euo pipefail
|
|
8046
8557
|
|
|
8047
8558
|
export CI=true
|
|
@@ -8057,7 +8568,7 @@ function shellSingleQuote(value) {
|
|
|
8057
8568
|
}
|
|
8058
8569
|
function renderPostStart(devProcess) {
|
|
8059
8570
|
return `#!/usr/bin/env bash
|
|
8060
|
-
# ${
|
|
8571
|
+
# ${MANAGED_MARKER2}
|
|
8061
8572
|
set -euo pipefail
|
|
8062
8573
|
|
|
8063
8574
|
export CI=true
|
|
@@ -8066,7 +8577,9 @@ set -a
|
|
|
8066
8577
|
. .devcontainer/devcontainer.env
|
|
8067
8578
|
set +a
|
|
8068
8579
|
|
|
8069
|
-
devrouter
|
|
8580
|
+
: "\${DEVROUTER_PROCESS_HELPER:?Run devrouter ensure to start this managed application process.}"
|
|
8581
|
+
|
|
8582
|
+
"$DEVROUTER_PROCESS_HELPER" ensure \\
|
|
8070
8583
|
--name app \\
|
|
8071
8584
|
--match ${shellSingleQuote(devProcess.match)} \\
|
|
8072
8585
|
--log /tmp/devrouter-app.log \\
|
|
@@ -8074,7 +8587,7 @@ devrouter-process ensure \\
|
|
|
8074
8587
|
`;
|
|
8075
8588
|
}
|
|
8076
8589
|
function renderDevrouter(projectName, port, version) {
|
|
8077
|
-
return `# ${
|
|
8590
|
+
return `# ${MANAGED_MARKER2}
|
|
8078
8591
|
version: 1
|
|
8079
8592
|
devrouter:
|
|
8080
8593
|
version: ${version}
|
|
@@ -8096,24 +8609,16 @@ apps:
|
|
|
8096
8609
|
`;
|
|
8097
8610
|
}
|
|
8098
8611
|
function renderReadme(projectName) {
|
|
8099
|
-
return `<!-- ${
|
|
8612
|
+
return `<!-- ${MANAGED_MARKER2} -->
|
|
8100
8613
|
# Devcontainer
|
|
8101
8614
|
|
|
8102
8615
|
Use this repo through the devcontainer, with devrouter providing stable local routes.
|
|
8103
8616
|
|
|
8104
|
-
Primary checkout:
|
|
8617
|
+
Primary or linked checkout:
|
|
8105
8618
|
|
|
8106
8619
|
\`\`\`bash
|
|
8107
8620
|
devrouter setup --yes
|
|
8108
|
-
|
|
8109
|
-
devrouter app run app --repo . --yes
|
|
8110
|
-
devrouter app run db --repo . --yes
|
|
8111
|
-
\`\`\`
|
|
8112
|
-
|
|
8113
|
-
Existing linked worktree:
|
|
8114
|
-
|
|
8115
|
-
\`\`\`bash
|
|
8116
|
-
devrouter workspace ensure .
|
|
8621
|
+
devrouter ensure .
|
|
8117
8622
|
\`\`\`
|
|
8118
8623
|
|
|
8119
8624
|
- App: https://${projectName}.localhost
|
|
@@ -8122,18 +8627,9 @@ devrouter workspace ensure .
|
|
|
8122
8627
|
}
|
|
8123
8628
|
function postWriteNextSteps(repoPath) {
|
|
8124
8629
|
const quotedRepoPath = shellSingleQuote(repoPath);
|
|
8125
|
-
if (isLinkedWorktree(repoPath)) {
|
|
8126
|
-
return [
|
|
8127
|
-
`Run: devrouter setup --repo ${quotedRepoPath} --yes`,
|
|
8128
|
-
`Run: devrouter workspace ensure ${quotedRepoPath}`,
|
|
8129
|
-
`Optional: devrouter repo agents --repo ${quotedRepoPath}`
|
|
8130
|
-
];
|
|
8131
|
-
}
|
|
8132
8630
|
return [
|
|
8133
8631
|
`Run: devrouter setup --repo ${quotedRepoPath} --yes`,
|
|
8134
|
-
`Run:
|
|
8135
|
-
`Run: devrouter app run app --repo ${quotedRepoPath} --yes`,
|
|
8136
|
-
`Run: devrouter app run db --repo ${quotedRepoPath} --yes`,
|
|
8632
|
+
`Run: devrouter ensure ${quotedRepoPath}`,
|
|
8137
8633
|
`Optional: devrouter repo agents --repo ${quotedRepoPath}`
|
|
8138
8634
|
];
|
|
8139
8635
|
}
|
|
@@ -8183,7 +8679,7 @@ function packageManagerIssues(repo) {
|
|
|
8183
8679
|
}
|
|
8184
8680
|
function plannedFiles(repoPath, version) {
|
|
8185
8681
|
const repo = inspectRepo({ repo: repoPath });
|
|
8186
|
-
const projectName = sanitizeProjectName(
|
|
8682
|
+
const projectName = sanitizeProjectName(import_node_path22.default.basename(repo.repoPath));
|
|
8187
8683
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
8188
8684
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
8189
8685
|
const port = inferPort2(repo);
|
|
@@ -8195,7 +8691,7 @@ function plannedFiles(repoPath, version) {
|
|
|
8195
8691
|
files: [
|
|
8196
8692
|
{
|
|
8197
8693
|
relativePath: ".devcontainer/Dockerfile",
|
|
8198
|
-
content: renderDockerfile(nodeMajor, pnpmVersion
|
|
8694
|
+
content: renderDockerfile(nodeMajor, pnpmVersion)
|
|
8199
8695
|
},
|
|
8200
8696
|
{ relativePath: ".devcontainer/docker-compose.yml", content: renderCompose(projectName) },
|
|
8201
8697
|
{
|
|
@@ -8228,8 +8724,8 @@ function plannedFiles(repoPath, version) {
|
|
|
8228
8724
|
};
|
|
8229
8725
|
}
|
|
8230
8726
|
function classifyFile(repoPath, file) {
|
|
8231
|
-
const absolutePath =
|
|
8232
|
-
if (!
|
|
8727
|
+
const absolutePath = import_node_path22.default.join(repoPath, file.relativePath);
|
|
8728
|
+
if (!import_node_fs22.default.existsSync(absolutePath)) {
|
|
8233
8729
|
return {
|
|
8234
8730
|
path: file.relativePath,
|
|
8235
8731
|
action: "create",
|
|
@@ -8237,8 +8733,8 @@ function classifyFile(repoPath, file) {
|
|
|
8237
8733
|
bytes: Buffer.byteLength(file.content)
|
|
8238
8734
|
};
|
|
8239
8735
|
}
|
|
8240
|
-
const current =
|
|
8241
|
-
if (!current.includes(
|
|
8736
|
+
const current = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
|
|
8737
|
+
if (!current.includes(MANAGED_MARKER2)) {
|
|
8242
8738
|
return {
|
|
8243
8739
|
path: file.relativePath,
|
|
8244
8740
|
action: "conflict",
|
|
@@ -8294,11 +8790,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
8294
8790
|
};
|
|
8295
8791
|
}
|
|
8296
8792
|
function writeFile(repoPath, file) {
|
|
8297
|
-
const absolutePath =
|
|
8298
|
-
|
|
8299
|
-
|
|
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");
|
|
8300
8796
|
if (file.executable) {
|
|
8301
|
-
|
|
8797
|
+
import_node_fs22.default.chmodSync(absolutePath, 493);
|
|
8302
8798
|
}
|
|
8303
8799
|
}
|
|
8304
8800
|
function writeDevcontainer(options = {}) {
|
|
@@ -8336,16 +8832,15 @@ function writeDevcontainer(options = {}) {
|
|
|
8336
8832
|
nextSteps: postWriteNextSteps(repoPath)
|
|
8337
8833
|
};
|
|
8338
8834
|
}
|
|
8339
|
-
var
|
|
8835
|
+
var import_node_fs22, import_node_path22, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
8340
8836
|
var init_devcontainer_write = __esm({
|
|
8341
8837
|
"src/core/devcontainer-write.ts"() {
|
|
8342
8838
|
"use strict";
|
|
8343
|
-
|
|
8344
|
-
|
|
8839
|
+
import_node_fs22 = __toESM(require("fs"));
|
|
8840
|
+
import_node_path22 = __toESM(require("path"));
|
|
8345
8841
|
init_repo_config();
|
|
8346
8842
|
init_repo_inspect();
|
|
8347
|
-
|
|
8348
|
-
MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
8843
|
+
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
8349
8844
|
DEFAULT_DEVROUTER_VERSION = "0.0.0";
|
|
8350
8845
|
DEFAULT_PNPM_VERSION = "11.6.0";
|
|
8351
8846
|
VALID_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
@@ -8733,7 +9228,7 @@ function sanitizeRouterId(value) {
|
|
|
8733
9228
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
8734
9229
|
}
|
|
8735
9230
|
function repoHash(repoPath) {
|
|
8736
|
-
return (0,
|
|
9231
|
+
return (0, import_node_crypto6.createHash)("sha1").update(import_node_path23.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
8737
9232
|
}
|
|
8738
9233
|
function asDockerApp(app) {
|
|
8739
9234
|
return app.runtime === "docker";
|
|
@@ -8812,11 +9307,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
8812
9307
|
if (dockerApps.length === 0) {
|
|
8813
9308
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
8814
9309
|
}
|
|
8815
|
-
const cachePath =
|
|
8816
|
-
|
|
8817
|
-
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");
|
|
8818
9313
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
8819
|
-
|
|
9314
|
+
import_node_fs23.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
8820
9315
|
return {
|
|
8821
9316
|
overlayPath,
|
|
8822
9317
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -8830,7 +9325,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8830
9325
|
fileArgs.push("-f", resolved);
|
|
8831
9326
|
}
|
|
8832
9327
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
8833
|
-
const result = (0,
|
|
9328
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8834
9329
|
encoding: "utf-8",
|
|
8835
9330
|
cwd: repoPath
|
|
8836
9331
|
});
|
|
@@ -8858,7 +9353,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
|
|
|
8858
9353
|
"--services",
|
|
8859
9354
|
...services
|
|
8860
9355
|
];
|
|
8861
|
-
const result = (0,
|
|
9356
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8862
9357
|
encoding: "utf-8",
|
|
8863
9358
|
cwd: repoPath
|
|
8864
9359
|
});
|
|
@@ -8884,7 +9379,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
|
8884
9379
|
fileArgs.push("-f", resolved);
|
|
8885
9380
|
}
|
|
8886
9381
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
8887
|
-
const result = (0,
|
|
9382
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8888
9383
|
encoding: "utf-8",
|
|
8889
9384
|
cwd: repoPath
|
|
8890
9385
|
});
|
|
@@ -8910,7 +9405,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
|
|
|
8910
9405
|
String(tail),
|
|
8911
9406
|
...services
|
|
8912
9407
|
];
|
|
8913
|
-
(0,
|
|
9408
|
+
(0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8914
9409
|
stdio: "inherit",
|
|
8915
9410
|
cwd: repoPath
|
|
8916
9411
|
});
|
|
@@ -8922,7 +9417,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
8922
9417
|
fileArgs.push("-f", resolved);
|
|
8923
9418
|
}
|
|
8924
9419
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
8925
|
-
const result = (0,
|
|
9420
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
8926
9421
|
encoding: "utf-8",
|
|
8927
9422
|
cwd: repoPath
|
|
8928
9423
|
});
|
|
@@ -8936,14 +9431,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
8936
9431
|
const port = Number(match[1]);
|
|
8937
9432
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
8938
9433
|
}
|
|
8939
|
-
var
|
|
9434
|
+
var import_node_child_process19, import_node_crypto6, import_node_fs23, import_node_path23, import_yaml6;
|
|
8940
9435
|
var init_docker_run = __esm({
|
|
8941
9436
|
"src/core/docker-run.ts"() {
|
|
8942
9437
|
"use strict";
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
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"));
|
|
8947
9442
|
import_yaml6 = __toESM(require("yaml"));
|
|
8948
9443
|
init_docker_error_guidance();
|
|
8949
9444
|
init_paths();
|
|
@@ -9033,7 +9528,7 @@ function isProcessRunning(pid) {
|
|
|
9033
9528
|
}
|
|
9034
9529
|
}
|
|
9035
9530
|
function readProcessTree(rootPid) {
|
|
9036
|
-
const result = (0,
|
|
9531
|
+
const result = (0, import_node_child_process20.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
9037
9532
|
if (result.status !== 0) {
|
|
9038
9533
|
return [rootPid];
|
|
9039
9534
|
}
|
|
@@ -9144,12 +9639,12 @@ function detectListeningPorts(pids) {
|
|
|
9144
9639
|
if (pids.length === 0) {
|
|
9145
9640
|
return [];
|
|
9146
9641
|
}
|
|
9147
|
-
const result = (0,
|
|
9642
|
+
const result = (0, import_node_child_process20.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
|
|
9148
9643
|
encoding: "utf-8"
|
|
9149
9644
|
});
|
|
9150
9645
|
if (result.error && result.error.code === "ENOENT") {
|
|
9151
9646
|
if (process.platform === "linux") {
|
|
9152
|
-
const ssResult = (0,
|
|
9647
|
+
const ssResult = (0, import_node_child_process20.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
|
|
9153
9648
|
if (ssResult.status === 0 && ssResult.stdout) {
|
|
9154
9649
|
return parseSsListeningPorts(ssResult.stdout, new Set(pids));
|
|
9155
9650
|
}
|
|
@@ -9208,7 +9703,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
9208
9703
|
app.hostRun.command,
|
|
9209
9704
|
true
|
|
9210
9705
|
) : app.hostRun.command;
|
|
9211
|
-
const child = (0,
|
|
9706
|
+
const child = (0, import_node_child_process20.spawn)(spawnCommand, {
|
|
9212
9707
|
cwd: commandCwd,
|
|
9213
9708
|
stdio: "inherit",
|
|
9214
9709
|
shell: true,
|
|
@@ -9576,7 +10071,7 @@ async function execWithAppEnv(options) {
|
|
|
9576
10071
|
options.command[0],
|
|
9577
10072
|
true
|
|
9578
10073
|
);
|
|
9579
|
-
child = (0,
|
|
10074
|
+
child = (0, import_node_child_process20.spawn)(wrapped, {
|
|
9580
10075
|
cwd: deps.repoPath,
|
|
9581
10076
|
stdio: "inherit",
|
|
9582
10077
|
shell: true,
|
|
@@ -9590,7 +10085,7 @@ async function execWithAppEnv(options) {
|
|
|
9590
10085
|
false
|
|
9591
10086
|
);
|
|
9592
10087
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
9593
|
-
child = (0,
|
|
10088
|
+
child = (0, import_node_child_process20.spawn)(cmd, wrappedArgs, {
|
|
9594
10089
|
cwd: deps.repoPath,
|
|
9595
10090
|
stdio: "inherit",
|
|
9596
10091
|
shell: false,
|
|
@@ -9598,7 +10093,7 @@ async function execWithAppEnv(options) {
|
|
|
9598
10093
|
});
|
|
9599
10094
|
}
|
|
9600
10095
|
} else if (options.shell) {
|
|
9601
|
-
child = (0,
|
|
10096
|
+
child = (0, import_node_child_process20.spawn)(options.command[0], {
|
|
9602
10097
|
cwd: deps.repoPath,
|
|
9603
10098
|
stdio: "inherit",
|
|
9604
10099
|
shell: true,
|
|
@@ -9606,7 +10101,7 @@ async function execWithAppEnv(options) {
|
|
|
9606
10101
|
});
|
|
9607
10102
|
} else {
|
|
9608
10103
|
const [command, ...args] = options.command;
|
|
9609
|
-
child = (0,
|
|
10104
|
+
child = (0, import_node_child_process20.spawn)(command, args, {
|
|
9610
10105
|
cwd: deps.repoPath,
|
|
9611
10106
|
stdio: "inherit",
|
|
9612
10107
|
shell: false,
|
|
@@ -9627,11 +10122,11 @@ async function execWithAppEnv(options) {
|
|
|
9627
10122
|
deps.stopDeps();
|
|
9628
10123
|
}
|
|
9629
10124
|
}
|
|
9630
|
-
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;
|
|
9631
10126
|
var init_app_run = __esm({
|
|
9632
10127
|
"src/core/app-run.ts"() {
|
|
9633
10128
|
"use strict";
|
|
9634
|
-
|
|
10129
|
+
import_node_child_process20 = require("child_process");
|
|
9635
10130
|
import_node_net = __toESM(require("net"));
|
|
9636
10131
|
import_node_process = require("process");
|
|
9637
10132
|
import_promises = require("readline/promises");
|
|
@@ -9892,7 +10387,7 @@ var init_version = __esm({
|
|
|
9892
10387
|
|
|
9893
10388
|
// src/cli.ts
|
|
9894
10389
|
var import_commander = require("commander");
|
|
9895
|
-
var CLI_VERSION = true ? "0.0.
|
|
10390
|
+
var CLI_VERSION = true ? "0.0.35" : "0.0.0-dev";
|
|
9896
10391
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
9897
10392
|
function withErrorHandling(action2) {
|
|
9898
10393
|
return async (...args) => {
|
|
@@ -9953,11 +10448,15 @@ program.command("ensure").description("Start and prove a primary or linked check
|
|
|
9953
10448
|
});
|
|
9954
10449
|
})
|
|
9955
10450
|
);
|
|
9956
|
-
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(
|
|
9957
10452
|
withErrorHandling(async (repoPath, _options, command) => {
|
|
9958
10453
|
const options = command.opts();
|
|
9959
10454
|
const { runStopCommand: runStopCommand2 } = await Promise.resolve().then(() => (init_stop(), stop_exports));
|
|
9960
|
-
await runStopCommand2({
|
|
10455
|
+
await runStopCommand2({
|
|
10456
|
+
path: repoPath,
|
|
10457
|
+
delete: Boolean(options.delete),
|
|
10458
|
+
json: Boolean(options.json)
|
|
10459
|
+
});
|
|
9961
10460
|
})
|
|
9962
10461
|
);
|
|
9963
10462
|
program.command("exec").description("Run one literal command inside the exact checkout's running DevPod").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
|