@devrouter/cli 0.0.43 → 0.0.45
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/dist/devrouter.js +401 -113
- package/package.json +1 -1
- package/upgrade-prompts/0.0.44.md +30 -0
- package/upgrade-prompts/0.0.45.md +33 -0
package/dist/devrouter.js
CHANGED
|
@@ -316,9 +316,9 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
|
|
|
316
316
|
|
|
317
317
|
## Workspace isolation (parallel git worktrees / agents)
|
|
318
318
|
|
|
319
|
-
Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the
|
|
319
|
+
Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the workspace-runtime id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
|
|
320
320
|
|
|
321
|
-
- **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
|
|
321
|
+
- **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 workspace-runtime ID. First use reconciles persisted metadata, the exact-path owner record, and both DevPod and Devsy registries. It reuses an established agreement, keeps the readable sanitized branch/path slug when free, or claims a deterministic hash-suffixed fallback on collision before provider or route mutation. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Unreadable or conflicting evidence fails closed. The primary checkout remains non-namespaced.
|
|
322
322
|
- **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.
|
|
323
323
|
- **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.
|
|
324
324
|
- **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>\`.
|
|
@@ -1057,6 +1057,21 @@ function wsFromBranch(branch) {
|
|
|
1057
1057
|
const slug = branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").slice(0, MAX_WORKSPACE_LENGTH).replace(/-+$/, "");
|
|
1058
1058
|
return slug.length > 0 ? slug : void 0;
|
|
1059
1059
|
}
|
|
1060
|
+
function workspaceIdentityFallback(source, attempt) {
|
|
1061
|
+
const readablePrefix = (wsFromBranch(source) ?? "workspace").slice(0, WORKSPACE_IDENTITY_PREFIX_LENGTH).replace(/-+$/, "");
|
|
1062
|
+
const prefix = readablePrefix || "workspace";
|
|
1063
|
+
const hash = (0, import_node_crypto3.createHash)("sha256").update(WORKSPACE_IDENTITY_HASH_DOMAIN, "utf-8").update("\0", "utf-8").update(source, "utf-8").update("\0", "utf-8").update(String(attempt), "utf-8").digest("hex").slice(0, WORKSPACE_IDENTITY_HASH_LENGTH);
|
|
1064
|
+
return `${prefix}-${hash}`;
|
|
1065
|
+
}
|
|
1066
|
+
function workspaceIdentityCandidates(source) {
|
|
1067
|
+
const candidates = [];
|
|
1068
|
+
const legacy = wsFromBranch(source);
|
|
1069
|
+
if (legacy) candidates.push(legacy);
|
|
1070
|
+
for (let attempt = 0; candidates.length < WORKSPACE_IDENTITY_CANDIDATE_LIMIT; attempt += 1) {
|
|
1071
|
+
candidates.push(workspaceIdentityFallback(source, attempt));
|
|
1072
|
+
}
|
|
1073
|
+
return candidates;
|
|
1074
|
+
}
|
|
1060
1075
|
function isLinkedWorktree(repoPath) {
|
|
1061
1076
|
const gitPath = import_node_path4.default.join(repoPath, ".git");
|
|
1062
1077
|
let stat;
|
|
@@ -1138,8 +1153,8 @@ function persistWorkspace(repoPath, value) {
|
|
|
1138
1153
|
if (!gitDir) {
|
|
1139
1154
|
throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
|
|
1140
1155
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1156
|
+
writeFileAtomically(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
|
|
1157
|
+
`);
|
|
1143
1158
|
return workspace;
|
|
1144
1159
|
}
|
|
1145
1160
|
async function withWorkspaceLifecycleLock(repoPath, operation) {
|
|
@@ -1198,15 +1213,21 @@ function resolveWorkspace(repoPath, override) {
|
|
|
1198
1213
|
}
|
|
1199
1214
|
return deriveLinkedWorktreeWorkspace(repoPath);
|
|
1200
1215
|
}
|
|
1201
|
-
var import_node_child_process3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
|
|
1216
|
+
var import_node_child_process3, import_node_crypto3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_IDENTITY_CANDIDATE_LIMIT, WORKSPACE_IDENTITY_HASH_LENGTH, WORKSPACE_IDENTITY_PREFIX_LENGTH, WORKSPACE_IDENTITY_HASH_DOMAIN, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
|
|
1202
1217
|
var init_workspace = __esm({
|
|
1203
1218
|
"src/core/workspace.ts"() {
|
|
1204
1219
|
"use strict";
|
|
1205
1220
|
import_node_child_process3 = require("child_process");
|
|
1221
|
+
import_node_crypto3 = require("crypto");
|
|
1206
1222
|
import_node_fs5 = __toESM(require("fs"));
|
|
1207
1223
|
import_node_path4 = __toESM(require("path"));
|
|
1224
|
+
init_atomic_file();
|
|
1208
1225
|
init_file_lock();
|
|
1209
1226
|
MAX_WORKSPACE_LENGTH = 32;
|
|
1227
|
+
WORKSPACE_IDENTITY_CANDIDATE_LIMIT = 16;
|
|
1228
|
+
WORKSPACE_IDENTITY_HASH_LENGTH = 8;
|
|
1229
|
+
WORKSPACE_IDENTITY_PREFIX_LENGTH = MAX_WORKSPACE_LENGTH - WORKSPACE_IDENTITY_HASH_LENGTH - 1;
|
|
1230
|
+
WORKSPACE_IDENTITY_HASH_DOMAIN = "devrouter-workspace-identity-v1";
|
|
1210
1231
|
WORKSPACE_METADATA_FILE = "devrouter-workspace";
|
|
1211
1232
|
WORKSPACE_LOCK_FILE = "devrouter-workspace.lock";
|
|
1212
1233
|
}
|
|
@@ -2509,7 +2530,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2509
2530
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2510
2531
|
const requiredVersion = config.devrouter?.version;
|
|
2511
2532
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2512
|
-
const cliVersion = true ? "0.0.
|
|
2533
|
+
const cliVersion = true ? "0.0.45" : "0.0.0-dev";
|
|
2513
2534
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2514
2535
|
hasWarnedVersionMismatch = true;
|
|
2515
2536
|
process.stderr.write(
|
|
@@ -3035,8 +3056,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
3035
3056
|
"- Use `devrouter status --repo <REPO_PATH> --json` and `devrouter doctor --repo <REPO_PATH> --json` to inspect managed desired/active resources, fingerprints, transition state, and values-free drift.",
|
|
3036
3057
|
"",
|
|
3037
3058
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
3038
|
-
`- 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
|
|
3039
|
-
"- The owner record survives linked-worktree removal and binds the exact path to its
|
|
3059
|
+
`- 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 workspace-runtime id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
|
|
3060
|
+
"- The owner record survives linked-worktree removal and binds the exact path to its workspace-runtime ID. First use reconciles persisted metadata, the exact-path owner record, and both DevPod and Devsy registries. It preserves an established agreement, uses the readable sanitized identity when free, or claims a deterministic hash-suffixed fallback on collision before provider or route mutation. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Unreadable or conflicting evidence fails closed. The primary checkout stays non-namespaced.",
|
|
3040
3061
|
`- 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.`,
|
|
3041
3062
|
"- 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.",
|
|
3042
3063
|
"- 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/Devsy mutations; they bypass the machine-global ownership lock. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
|
|
@@ -4110,7 +4131,7 @@ function readRegularFile(filePath) {
|
|
|
4110
4131
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
4111
4132
|
}
|
|
4112
4133
|
function adapterFingerprint(adapter) {
|
|
4113
|
-
return (0,
|
|
4134
|
+
return (0, import_node_crypto4.createHash)("sha256").update(adapter).digest("hex");
|
|
4114
4135
|
}
|
|
4115
4136
|
function resolveProcessHelperPath() {
|
|
4116
4137
|
const candidates = [
|
|
@@ -4257,12 +4278,12 @@ function runManagedProcessAction(options) {
|
|
|
4257
4278
|
}
|
|
4258
4279
|
return "drifted";
|
|
4259
4280
|
}
|
|
4260
|
-
var import_node_child_process5,
|
|
4281
|
+
var import_node_child_process5, import_node_crypto4, import_node_fs10, import_node_path9, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
|
|
4261
4282
|
var init_managed_post_start = __esm({
|
|
4262
4283
|
"src/core/managed-post-start.ts"() {
|
|
4263
4284
|
"use strict";
|
|
4264
4285
|
import_node_child_process5 = require("child_process");
|
|
4265
|
-
|
|
4286
|
+
import_node_crypto4 = require("crypto");
|
|
4266
4287
|
import_node_fs10 = __toESM(require("fs"));
|
|
4267
4288
|
import_node_path9 = __toESM(require("path"));
|
|
4268
4289
|
init_devcontainer_config();
|
|
@@ -4764,7 +4785,7 @@ var init_routes = __esm({
|
|
|
4764
4785
|
|
|
4765
4786
|
// src/core/managed-runtime-state.ts
|
|
4766
4787
|
function stateKey(repoPath, workspace) {
|
|
4767
|
-
return (0,
|
|
4788
|
+
return (0, import_node_crypto5.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
|
|
4768
4789
|
}
|
|
4769
4790
|
function managedRuntimeStatePath(repoPath, workspace) {
|
|
4770
4791
|
return import_node_path12.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
|
|
@@ -4855,11 +4876,11 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
|
|
|
4855
4876
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4856
4877
|
});
|
|
4857
4878
|
}
|
|
4858
|
-
var
|
|
4879
|
+
var import_node_crypto5, import_node_fs13, import_node_path12;
|
|
4859
4880
|
var init_managed_runtime_state = __esm({
|
|
4860
4881
|
"src/core/managed-runtime-state.ts"() {
|
|
4861
4882
|
"use strict";
|
|
4862
|
-
|
|
4883
|
+
import_node_crypto5 = require("crypto");
|
|
4863
4884
|
import_node_fs13 = __toESM(require("fs"));
|
|
4864
4885
|
import_node_path12 = __toESM(require("path"));
|
|
4865
4886
|
init_atomic_file();
|
|
@@ -4992,7 +5013,7 @@ function isWildcard(values) {
|
|
|
4992
5013
|
return values?.length === 1 && values[0] === "*";
|
|
4993
5014
|
}
|
|
4994
5015
|
function sha256(contents) {
|
|
4995
|
-
return (0,
|
|
5016
|
+
return (0, import_node_crypto6.createHash)("sha256").update(contents, "utf-8").digest("hex");
|
|
4996
5017
|
}
|
|
4997
5018
|
function safeComposeProject(value) {
|
|
4998
5019
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
|
|
@@ -5177,12 +5198,12 @@ function stopExactManagedService(containerId, service) {
|
|
|
5177
5198
|
throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
|
|
5178
5199
|
}
|
|
5179
5200
|
}
|
|
5180
|
-
var import_node_child_process6,
|
|
5201
|
+
var import_node_child_process6, import_node_crypto6, import_node_fs14, import_node_path13, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
|
|
5181
5202
|
var init_devcontainer_profile = __esm({
|
|
5182
5203
|
"src/core/devcontainer-profile.ts"() {
|
|
5183
5204
|
"use strict";
|
|
5184
5205
|
import_node_child_process6 = require("child_process");
|
|
5185
|
-
|
|
5206
|
+
import_node_crypto6 = require("crypto");
|
|
5186
5207
|
import_node_fs14 = __toESM(require("fs"));
|
|
5187
5208
|
import_node_path13 = __toESM(require("path"));
|
|
5188
5209
|
import_yaml4 = __toESM(require("yaml"));
|
|
@@ -5954,7 +5975,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
5954
5975
|
return normalizeUniqueHosts(names);
|
|
5955
5976
|
}
|
|
5956
5977
|
function parseCertificateDnsHosts(pem) {
|
|
5957
|
-
const certificate = new
|
|
5978
|
+
const certificate = new import_node_crypto7.X509Certificate(pem);
|
|
5958
5979
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
5959
5980
|
if (subjectAltName.length === 0) {
|
|
5960
5981
|
return [];
|
|
@@ -5964,6 +5985,9 @@ function parseCertificateDnsHosts(pem) {
|
|
|
5964
5985
|
function isHostCoveredByCertificateHost(host, certificateHost) {
|
|
5965
5986
|
const normalizedHost = normalizeHost(host);
|
|
5966
5987
|
const normalizedCertificateHost = normalizeHost(certificateHost);
|
|
5988
|
+
if (normalizedHost === normalizedCertificateHost) {
|
|
5989
|
+
return true;
|
|
5990
|
+
}
|
|
5967
5991
|
if (normalizedCertificateHost.startsWith("*.")) {
|
|
5968
5992
|
if (normalizedCertificateHost === "*.localhost") {
|
|
5969
5993
|
return false;
|
|
@@ -5975,7 +5999,7 @@ function isHostCoveredByCertificateHost(host, certificateHost) {
|
|
|
5975
5999
|
const wildcardPart = normalizedHost.slice(0, normalizedHost.length - suffix.length);
|
|
5976
6000
|
return wildcardPart.length > 0 && !wildcardPart.includes(".");
|
|
5977
6001
|
}
|
|
5978
|
-
return
|
|
6002
|
+
return false;
|
|
5979
6003
|
}
|
|
5980
6004
|
function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
5981
6005
|
const normalizedRequired = normalizeUniqueHosts(requiredHosts);
|
|
@@ -5986,22 +6010,54 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
|
5986
6010
|
)
|
|
5987
6011
|
);
|
|
5988
6012
|
}
|
|
5989
|
-
function
|
|
6013
|
+
function compactTLSCertificateHosts(hosts) {
|
|
6014
|
+
const normalizedHosts = normalizeUniqueHosts(hosts);
|
|
6015
|
+
const wildcardHosts = new Set(normalizedHosts.filter((host) => host.startsWith("*.")));
|
|
6016
|
+
const siblingGroups = /* @__PURE__ */ new Map();
|
|
6017
|
+
const compacted = new Set(wildcardHosts);
|
|
6018
|
+
for (const host of normalizedHosts) {
|
|
6019
|
+
if (host.startsWith("*.")) {
|
|
6020
|
+
continue;
|
|
6021
|
+
}
|
|
6022
|
+
const labels = host.split(".");
|
|
6023
|
+
if (labels.length < 3 || labels.at(-1) !== "localhost") {
|
|
6024
|
+
compacted.add(host);
|
|
6025
|
+
continue;
|
|
6026
|
+
}
|
|
6027
|
+
const suffix = labels.slice(1).join(".");
|
|
6028
|
+
const siblings = siblingGroups.get(suffix) ?? [];
|
|
6029
|
+
siblings.push(host);
|
|
6030
|
+
siblingGroups.set(suffix, siblings);
|
|
6031
|
+
}
|
|
6032
|
+
for (const [suffix, siblings] of siblingGroups) {
|
|
6033
|
+
const wildcard = `*.${suffix}`;
|
|
6034
|
+
if (siblings.length > 1 || wildcardHosts.has(wildcard)) {
|
|
6035
|
+
compacted.add(wildcard);
|
|
6036
|
+
continue;
|
|
6037
|
+
}
|
|
6038
|
+
compacted.add(siblings[0]);
|
|
6039
|
+
}
|
|
6040
|
+
const selectedWildcards = Array.from(compacted).filter((host) => host.startsWith("*."));
|
|
6041
|
+
return normalizeUniqueHosts(Array.from(compacted)).filter(
|
|
6042
|
+
(host) => host.startsWith("*.") || !selectedWildcards.some((wildcard) => isHostCoveredByCertificateHost(host, wildcard))
|
|
6043
|
+
);
|
|
6044
|
+
}
|
|
6045
|
+
function readCurrentCertificateHosts(options = {}) {
|
|
5990
6046
|
if (!import_node_fs16.default.existsSync(CERT_FILE)) {
|
|
5991
6047
|
return [];
|
|
5992
6048
|
}
|
|
5993
|
-
const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
|
|
5994
|
-
return parseCertificateDnsHosts(pem);
|
|
5995
|
-
}
|
|
5996
|
-
function currentCertificateHostsOrEmpty() {
|
|
5997
6049
|
try {
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6050
|
+
const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
|
|
6051
|
+
return parseCertificateDnsHosts(pem);
|
|
6052
|
+
} catch (error) {
|
|
6053
|
+
if (options.replaceMalformed) {
|
|
6054
|
+
return [];
|
|
6055
|
+
}
|
|
6056
|
+
throw error;
|
|
6001
6057
|
}
|
|
6002
6058
|
}
|
|
6003
6059
|
function buildDesiredTLSCertificateHosts(requestedHosts, existingCertificateHosts) {
|
|
6004
|
-
return
|
|
6060
|
+
return compactTLSCertificateHosts([
|
|
6005
6061
|
...DEFAULT_TLS_CERT_HOSTS,
|
|
6006
6062
|
...existingCertificateHosts,
|
|
6007
6063
|
...requestedHosts
|
|
@@ -6009,35 +6065,75 @@ function buildDesiredTLSCertificateHosts(requestedHosts, existingCertificateHost
|
|
|
6009
6065
|
}
|
|
6010
6066
|
function getTLSHostCoverage(hosts) {
|
|
6011
6067
|
const requiredHosts = normalizeUniqueHosts([...DEFAULT_TLS_CERT_HOSTS, ...hosts]);
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6068
|
+
return withFileLockSync(
|
|
6069
|
+
TLS_CERTIFICATE_LOCK_FILE,
|
|
6070
|
+
{ activity: "TLS certificate inspection", waitMs: TLS_CERTIFICATE_LOCK_WAIT_MS },
|
|
6071
|
+
() => {
|
|
6072
|
+
const certificateHosts = readCurrentCertificateHosts();
|
|
6073
|
+
const uncoveredHosts = findUncoveredCertificateHosts(requiredHosts, certificateHosts);
|
|
6074
|
+
return {
|
|
6075
|
+
requiredHosts,
|
|
6076
|
+
certificateHosts,
|
|
6077
|
+
uncoveredHosts
|
|
6078
|
+
};
|
|
6079
|
+
}
|
|
6080
|
+
);
|
|
6019
6081
|
}
|
|
6020
6082
|
async function applyTLSCertificate(options, installTrust) {
|
|
6021
6083
|
ensureRouterFiles();
|
|
6022
|
-
const
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6084
|
+
const result = withFileLockSync(
|
|
6085
|
+
TLS_CERTIFICATE_LOCK_FILE,
|
|
6086
|
+
{
|
|
6087
|
+
activity: "TLS certificate refresh",
|
|
6088
|
+
target: options.repoPath,
|
|
6089
|
+
waitMs: TLS_CERTIFICATE_LOCK_WAIT_MS
|
|
6090
|
+
},
|
|
6091
|
+
() => {
|
|
6092
|
+
const alreadyEnabled = isTLSEnabled();
|
|
6093
|
+
if (installTrust) {
|
|
6094
|
+
ensureMkcert();
|
|
6095
|
+
runOrThrow("mkcert", ["-install"]);
|
|
6096
|
+
} else {
|
|
6097
|
+
getMkcertRootCAPath({ repoPath: options.repoPath });
|
|
6098
|
+
}
|
|
6099
|
+
const existingCertificateHosts = readCurrentCertificateHosts({
|
|
6100
|
+
replaceMalformed: installTrust
|
|
6101
|
+
});
|
|
6102
|
+
const desiredHosts = buildDesiredTLSCertificateHosts(
|
|
6103
|
+
options.hosts ?? [],
|
|
6104
|
+
existingCertificateHosts
|
|
6105
|
+
);
|
|
6106
|
+
let certificateHosts = [];
|
|
6107
|
+
let uncoveredHosts = [];
|
|
6108
|
+
for (let attempt = 1; attempt <= TLS_CERTIFICATE_WRITE_ATTEMPTS; attempt += 1) {
|
|
6109
|
+
runOrThrow("mkcert", [
|
|
6110
|
+
"-cert-file",
|
|
6111
|
+
CERT_FILE,
|
|
6112
|
+
"-key-file",
|
|
6113
|
+
CERT_KEY_FILE,
|
|
6114
|
+
...desiredHosts
|
|
6115
|
+
]);
|
|
6116
|
+
certificateHosts = readCurrentCertificateHosts();
|
|
6117
|
+
uncoveredHosts = findUncoveredCertificateHosts(desiredHosts, certificateHosts);
|
|
6118
|
+
if (uncoveredHosts.length === 0) {
|
|
6119
|
+
break;
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
6122
|
+
if (uncoveredHosts.length > 0) {
|
|
6123
|
+
throw new Error(
|
|
6124
|
+
`mkcert did not produce certificate coverage for host(s): ${uncoveredHosts.join(", ")} after ${TLS_CERTIFICATE_WRITE_ATTEMPTS} attempts`
|
|
6125
|
+
);
|
|
6126
|
+
}
|
|
6127
|
+
setTLSEnabled(true);
|
|
6128
|
+
refreshHostRoutesDynamicFile();
|
|
6129
|
+
return { alreadyEnabled, hosts: certificateHosts };
|
|
6130
|
+
}
|
|
6131
|
+
);
|
|
6036
6132
|
const routerContainer = await findContainerByName("devrouter-traefik");
|
|
6037
6133
|
if (routerContainer && await isContainerRunning("devrouter-traefik")) {
|
|
6038
6134
|
startRouterStack();
|
|
6039
6135
|
}
|
|
6040
|
-
return
|
|
6136
|
+
return result;
|
|
6041
6137
|
}
|
|
6042
6138
|
async function installTLS(options = {}) {
|
|
6043
6139
|
return applyTLSCertificate(options, true);
|
|
@@ -6079,18 +6175,22 @@ Run: ${tlsSetupCommand(options.repoPath)}`
|
|
|
6079
6175
|
);
|
|
6080
6176
|
}
|
|
6081
6177
|
}
|
|
6082
|
-
var import_node_child_process8,
|
|
6178
|
+
var import_node_child_process8, import_node_crypto7, import_node_fs16, import_node_path15, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
|
|
6083
6179
|
var init_tls = __esm({
|
|
6084
6180
|
"src/core/tls.ts"() {
|
|
6085
6181
|
"use strict";
|
|
6086
6182
|
import_node_child_process8 = require("child_process");
|
|
6087
|
-
|
|
6183
|
+
import_node_crypto7 = require("crypto");
|
|
6088
6184
|
import_node_fs16 = __toESM(require("fs"));
|
|
6089
6185
|
import_node_path15 = __toESM(require("path"));
|
|
6090
6186
|
init_docker();
|
|
6187
|
+
init_file_lock();
|
|
6091
6188
|
init_host_routes();
|
|
6092
6189
|
init_router();
|
|
6093
6190
|
DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
6191
|
+
TLS_CERTIFICATE_LOCK_FILE = import_node_path15.default.join(DEVROUTER_HOME, "tls-certificate.lock");
|
|
6192
|
+
TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
|
|
6193
|
+
TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
|
|
6094
6194
|
}
|
|
6095
6195
|
});
|
|
6096
6196
|
|
|
@@ -7376,12 +7476,16 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
|
|
|
7376
7476
|
import_node_fs21.default.rmSync(filePath);
|
|
7377
7477
|
return "removed";
|
|
7378
7478
|
}
|
|
7379
|
-
function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
7479
|
+
function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
|
|
7380
7480
|
const directory = ownershipDirectory(repoPath);
|
|
7381
7481
|
import_node_fs21.default.mkdirSync(directory, { recursive: true });
|
|
7382
7482
|
return withFileLockSync(
|
|
7383
7483
|
import_node_path20.default.join(directory, ".lock"),
|
|
7384
|
-
{
|
|
7484
|
+
{
|
|
7485
|
+
activity: "workspace ownership transaction",
|
|
7486
|
+
target: `'${repoPath}'`,
|
|
7487
|
+
waitMs: options.waitMs ?? 5e3
|
|
7488
|
+
},
|
|
7385
7489
|
() => operation({
|
|
7386
7490
|
list: () => listWorkspaceOwnershipInDirectory(directory),
|
|
7387
7491
|
write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
|
|
@@ -7390,8 +7494,169 @@ function withWorkspaceOwnershipTransaction(repoPath, operation) {
|
|
|
7390
7494
|
})
|
|
7391
7495
|
);
|
|
7392
7496
|
}
|
|
7393
|
-
function
|
|
7394
|
-
|
|
7497
|
+
function providerPathOwner(providerWorkspaces, worktreePath) {
|
|
7498
|
+
const owners = providerWorkspaces.filter(
|
|
7499
|
+
(workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
|
|
7500
|
+
);
|
|
7501
|
+
if (owners.length > 1) {
|
|
7502
|
+
throw new Error(
|
|
7503
|
+
`Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
|
|
7504
|
+
);
|
|
7505
|
+
}
|
|
7506
|
+
return owners[0];
|
|
7507
|
+
}
|
|
7508
|
+
function persistedWorkspaceOwners(repoPath, worktreePath) {
|
|
7509
|
+
const owners = /* @__PURE__ */ new Map();
|
|
7510
|
+
for (const worktree of listGitWorktrees(repoPath)) {
|
|
7511
|
+
if (sameWorkspacePath(worktree.path, worktreePath)) continue;
|
|
7512
|
+
let workspace;
|
|
7513
|
+
try {
|
|
7514
|
+
workspace = readPersistedWorkspace(worktree.path);
|
|
7515
|
+
} catch (error) {
|
|
7516
|
+
if (!import_node_fs21.default.existsSync(worktree.path)) continue;
|
|
7517
|
+
throw error;
|
|
7518
|
+
}
|
|
7519
|
+
if (!workspace) continue;
|
|
7520
|
+
const existing = owners.get(workspace);
|
|
7521
|
+
if (existing && !sameWorkspacePath(existing, worktree.path)) {
|
|
7522
|
+
throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
|
|
7523
|
+
}
|
|
7524
|
+
owners.set(workspace, worktree.path);
|
|
7525
|
+
}
|
|
7526
|
+
return owners;
|
|
7527
|
+
}
|
|
7528
|
+
function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
|
|
7529
|
+
const recordOwner = records.find(
|
|
7530
|
+
(record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
|
|
7531
|
+
);
|
|
7532
|
+
if (recordOwner) {
|
|
7533
|
+
return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
|
|
7534
|
+
}
|
|
7535
|
+
const providerOwner = providerWorkspaces.find(
|
|
7536
|
+
(providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
|
|
7537
|
+
);
|
|
7538
|
+
if (providerOwner) {
|
|
7539
|
+
return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
|
|
7540
|
+
}
|
|
7541
|
+
const persistedOwner = persistedOwners.get(workspace);
|
|
7542
|
+
if (persistedOwner) {
|
|
7543
|
+
return `persisted checkout metadata for '${persistedOwner}'`;
|
|
7544
|
+
}
|
|
7545
|
+
return void 0;
|
|
7546
|
+
}
|
|
7547
|
+
function claimWorkspaceIdentity(repoPath, input2) {
|
|
7548
|
+
const worktreePath = comparableWorkspacePath(repoPath);
|
|
7549
|
+
const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
|
|
7550
|
+
return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
|
|
7551
|
+
const records = transaction.list();
|
|
7552
|
+
const exactRecords = records.filter(
|
|
7553
|
+
(record) => sameWorkspacePath(record.worktreePath, worktreePath)
|
|
7554
|
+
);
|
|
7555
|
+
if (exactRecords.length > 1) {
|
|
7556
|
+
throw new Error(
|
|
7557
|
+
`Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
|
|
7558
|
+
);
|
|
7559
|
+
}
|
|
7560
|
+
const exactRecord = exactRecords[0];
|
|
7561
|
+
const persisted = readPersistedWorkspace(worktreePath);
|
|
7562
|
+
const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
|
|
7563
|
+
if (exactRecord) {
|
|
7564
|
+
if (persisted && persisted !== exactRecord.workspace) {
|
|
7565
|
+
throw new Error(
|
|
7566
|
+
`Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
|
|
7567
|
+
);
|
|
7568
|
+
}
|
|
7569
|
+
if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
|
|
7570
|
+
throw new Error(
|
|
7571
|
+
`Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
|
|
7572
|
+
);
|
|
7573
|
+
}
|
|
7574
|
+
const conflict2 = claimConflict(
|
|
7575
|
+
exactRecord.workspace,
|
|
7576
|
+
exactRecord.devpodId,
|
|
7577
|
+
worktreePath,
|
|
7578
|
+
records,
|
|
7579
|
+
input2.providerWorkspaces,
|
|
7580
|
+
persistedOwners,
|
|
7581
|
+
exactRecord
|
|
7582
|
+
);
|
|
7583
|
+
if (conflict2) {
|
|
7584
|
+
throw new Error(
|
|
7585
|
+
`Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
|
|
7586
|
+
);
|
|
7587
|
+
}
|
|
7588
|
+
if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
|
|
7589
|
+
return transaction.write({
|
|
7590
|
+
workspace: exactRecord.workspace,
|
|
7591
|
+
worktreePath,
|
|
7592
|
+
branch: input2.branch ?? null,
|
|
7593
|
+
devpodId: exactRecord.devpodId
|
|
7594
|
+
});
|
|
7595
|
+
}
|
|
7596
|
+
if (persisted && exactProvider && persisted !== exactProvider.id) {
|
|
7597
|
+
throw new Error(
|
|
7598
|
+
`Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
|
|
7599
|
+
);
|
|
7600
|
+
}
|
|
7601
|
+
let workspace = persisted ?? exactProvider?.id;
|
|
7602
|
+
let devpodId = exactProvider?.id ?? persisted;
|
|
7603
|
+
if (!workspace || !devpodId) {
|
|
7604
|
+
if (input2.unavailableRuntimes.length > 0) {
|
|
7605
|
+
throw new Error(
|
|
7606
|
+
`Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
|
|
7607
|
+
);
|
|
7608
|
+
}
|
|
7609
|
+
const candidate = workspaceIdentityCandidates(input2.source).find(
|
|
7610
|
+
(next) => !claimConflict(
|
|
7611
|
+
next,
|
|
7612
|
+
next,
|
|
7613
|
+
worktreePath,
|
|
7614
|
+
records,
|
|
7615
|
+
input2.providerWorkspaces,
|
|
7616
|
+
persistedOwners
|
|
7617
|
+
)
|
|
7618
|
+
);
|
|
7619
|
+
if (!candidate) {
|
|
7620
|
+
throw new Error(
|
|
7621
|
+
`Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
|
|
7622
|
+
);
|
|
7623
|
+
}
|
|
7624
|
+
workspace = candidate;
|
|
7625
|
+
devpodId = candidate;
|
|
7626
|
+
}
|
|
7627
|
+
const conflict = claimConflict(
|
|
7628
|
+
workspace,
|
|
7629
|
+
devpodId,
|
|
7630
|
+
worktreePath,
|
|
7631
|
+
records,
|
|
7632
|
+
input2.providerWorkspaces,
|
|
7633
|
+
persistedOwners
|
|
7634
|
+
);
|
|
7635
|
+
if (conflict) {
|
|
7636
|
+
throw new Error(
|
|
7637
|
+
`Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
|
|
7638
|
+
);
|
|
7639
|
+
}
|
|
7640
|
+
const written = transaction.write({
|
|
7641
|
+
workspace,
|
|
7642
|
+
worktreePath,
|
|
7643
|
+
branch: input2.branch ?? null,
|
|
7644
|
+
devpodId
|
|
7645
|
+
});
|
|
7646
|
+
try {
|
|
7647
|
+
persistWorkspace(worktreePath, workspace);
|
|
7648
|
+
} catch (error) {
|
|
7649
|
+
const cleanup = transaction.removeIfMatches(written);
|
|
7650
|
+
if (cleanup !== "removed") {
|
|
7651
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7652
|
+
throw new Error(
|
|
7653
|
+
`Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
|
|
7654
|
+
);
|
|
7655
|
+
}
|
|
7656
|
+
throw error;
|
|
7657
|
+
}
|
|
7658
|
+
return written;
|
|
7659
|
+
});
|
|
7395
7660
|
}
|
|
7396
7661
|
function removeWorkspaceOwnership(repoPath, workspace) {
|
|
7397
7662
|
return withWorkspaceOwnershipTransaction(
|
|
@@ -8074,7 +8339,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
8074
8339
|
const config = runtimeConfig.config;
|
|
8075
8340
|
loadedConfig = config;
|
|
8076
8341
|
loadedWorkspace = runtimeConfig.workspace;
|
|
8077
|
-
const cliVersion = true ? "0.0.
|
|
8342
|
+
const cliVersion = true ? "0.0.45" : "0.0.0-dev";
|
|
8078
8343
|
const configVersion = config.devrouter?.version;
|
|
8079
8344
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
8080
8345
|
addCheck(checks, {
|
|
@@ -9150,25 +9415,30 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
|
|
|
9150
9415
|
);
|
|
9151
9416
|
}
|
|
9152
9417
|
function resolveLinkedTarget(repoPath) {
|
|
9153
|
-
const
|
|
9154
|
-
const
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9418
|
+
const snapshots = getWorkspaceRegistrySnapshots();
|
|
9419
|
+
const providerWorkspaces = [
|
|
9420
|
+
...snapshots.devpod ?? [],
|
|
9421
|
+
...snapshots.devsy?.map((workspace) => ({
|
|
9422
|
+
id: workspace.id,
|
|
9423
|
+
source: workspace.source,
|
|
9424
|
+
...workspace.lastUsed ? { lastUsed: workspace.lastUsed } : {},
|
|
9425
|
+
...workspace.lastUsedMalformed ? { lastUsedMalformed: true } : {}
|
|
9426
|
+
})) ?? []
|
|
9427
|
+
];
|
|
9428
|
+
const branch = currentBranch(repoPath);
|
|
9429
|
+
const claim = claimWorkspaceIdentity(repoPath, {
|
|
9430
|
+
source: branch ?? repoPath,
|
|
9431
|
+
branch: branch ?? null,
|
|
9432
|
+
providerWorkspaces,
|
|
9433
|
+
unavailableRuntimes: snapshots.unavailable
|
|
9434
|
+
});
|
|
9435
|
+
const existingDevpod = providerWorkspaces.find(
|
|
9436
|
+
(workspace) => workspace.id === claim.devpodId && sameWorkspacePath(workspace.source.localFolder, repoPath)
|
|
9162
9437
|
);
|
|
9163
|
-
if (otherOwner) {
|
|
9164
|
-
throw new Error(
|
|
9165
|
-
`DevPod identity '${candidate}' already belongs to '${otherOwner.source.localFolder}'.`
|
|
9166
|
-
);
|
|
9167
|
-
}
|
|
9168
9438
|
return {
|
|
9169
9439
|
kind: "linked",
|
|
9170
|
-
workspace:
|
|
9171
|
-
devpodId:
|
|
9440
|
+
workspace: claim.workspace,
|
|
9441
|
+
devpodId: claim.devpodId,
|
|
9172
9442
|
hadExactDevpod: Boolean(existingDevpod),
|
|
9173
9443
|
gitCommonDir: resolveGitCommonDir(repoPath)
|
|
9174
9444
|
};
|
|
@@ -9315,15 +9585,6 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
9315
9585
|
managedConfigWritten = true;
|
|
9316
9586
|
}
|
|
9317
9587
|
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
9318
|
-
const ownership = target.kind === "linked" ? {
|
|
9319
|
-
workspace: target.workspace,
|
|
9320
|
-
worktreePath: repoPath,
|
|
9321
|
-
branch: currentBranch(repoPath),
|
|
9322
|
-
devpodId: target.devpodId
|
|
9323
|
-
} : void 0;
|
|
9324
|
-
if (ownership) {
|
|
9325
|
-
writeWorkspaceOwnership(repoPath, ownership);
|
|
9326
|
-
}
|
|
9327
9588
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
9328
9589
|
const startAndProveAttachment = (recreate = false) => {
|
|
9329
9590
|
const requestedTarget = currentTarget();
|
|
@@ -9346,9 +9607,6 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
9346
9607
|
if (error instanceof DevpodStartPostconditionError) environmentStarted = true;
|
|
9347
9608
|
throw error;
|
|
9348
9609
|
}
|
|
9349
|
-
if (ownership) {
|
|
9350
|
-
writeWorkspaceOwnership(repoPath, ownership);
|
|
9351
|
-
}
|
|
9352
9610
|
};
|
|
9353
9611
|
const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
|
|
9354
9612
|
const recreateAndPreflight = async () => {
|
|
@@ -9766,6 +10024,7 @@ var init_workspace_ensure = __esm({
|
|
|
9766
10024
|
init_router();
|
|
9767
10025
|
init_workspace();
|
|
9768
10026
|
init_workspace_ownership();
|
|
10027
|
+
init_workspace_runtime();
|
|
9769
10028
|
DEVCONTAINER_OVERLAY = "docker-compose.devrouter.yml";
|
|
9770
10029
|
DEFAULT_READINESS_TIMEOUT_MS = 12e4;
|
|
9771
10030
|
POLL_INTERVAL_MS = 1e3;
|
|
@@ -9822,6 +10081,11 @@ var init_ensure = __esm({
|
|
|
9822
10081
|
});
|
|
9823
10082
|
|
|
9824
10083
|
// src/core/workspace-lifecycle.ts
|
|
10084
|
+
function withWorkspaceAllocationTransaction(repoPath, operation) {
|
|
10085
|
+
return withWorkspaceOwnershipTransaction(repoPath, operation, {
|
|
10086
|
+
waitMs: WORKSPACE_ALLOCATION_LOCK_WAIT_MS
|
|
10087
|
+
});
|
|
10088
|
+
}
|
|
9825
10089
|
function warnMissingWorkspaceOwnership(repoPath) {
|
|
9826
10090
|
const missing = listMissingWorkspaceOwnership(repoPath);
|
|
9827
10091
|
if (missing.length === 0) return;
|
|
@@ -9983,44 +10247,67 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
9983
10247
|
if (!ws) {
|
|
9984
10248
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
9985
10249
|
}
|
|
9986
|
-
const worktreePath =
|
|
9987
|
-
|
|
9988
|
-
const
|
|
9989
|
-
|
|
10250
|
+
const worktreePath = withWorkspaceAllocationTransaction(mainRepo, () => {
|
|
10251
|
+
const worktrees = listGitWorktrees(mainRepo);
|
|
10252
|
+
const branchWorktrees = worktrees.filter((worktree) => worktree.branch === branch);
|
|
10253
|
+
if (branchWorktrees.length > 1) {
|
|
10254
|
+
throw new Error(
|
|
10255
|
+
`Branch '${branch}' is checked out in multiple worktrees: ${branchWorktrees.map((worktree) => worktree.path).join(", ")}`
|
|
10256
|
+
);
|
|
10257
|
+
}
|
|
10258
|
+
const existingBranch = branchWorktrees[0];
|
|
10259
|
+
const candidatePaths = workspaceIdentityCandidates(branch).map(
|
|
10260
|
+
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
9990
10261
|
);
|
|
9991
|
-
|
|
9992
|
-
|
|
10262
|
+
const generatedPath = candidatePaths.find(
|
|
10263
|
+
(candidate) => !import_node_fs24.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
10264
|
+
);
|
|
10265
|
+
const selectedPath = opts.path ? import_node_path24.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
10266
|
+
if (!selectedPath) {
|
|
10267
|
+
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
9993
10268
|
}
|
|
9994
|
-
if (
|
|
10269
|
+
if (existingBranch && !sameWorkspacePath(existingBranch.path, selectedPath)) {
|
|
9995
10270
|
throw new Error(
|
|
9996
|
-
`
|
|
10271
|
+
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
9997
10272
|
);
|
|
9998
10273
|
}
|
|
9999
|
-
|
|
10274
|
+
if (import_node_fs24.default.existsSync(selectedPath)) {
|
|
10275
|
+
const registered = worktrees.find(
|
|
10276
|
+
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
10277
|
+
);
|
|
10278
|
+
if (!registered || sameWorkspacePath(registered.path, mainRepo)) {
|
|
10279
|
+
throw new Error(
|
|
10280
|
+
`Existing path '${selectedPath}' is not a linked worktree of '${mainRepo}'.`
|
|
10281
|
+
);
|
|
10282
|
+
}
|
|
10283
|
+
if (registered.branch !== branch) {
|
|
10284
|
+
throw new Error(
|
|
10285
|
+
`Existing worktree '${selectedPath}' uses branch '${registered.branch ?? "detached"}', not '${branch}'.`
|
|
10286
|
+
);
|
|
10287
|
+
}
|
|
10288
|
+
process.stdout.write(`Worktree already exists: ${selectedPath}
|
|
10000
10289
|
`);
|
|
10001
|
-
|
|
10002
|
-
if (!opts.path) {
|
|
10003
|
-
assertDefaultWorktreeRootIgnored(mainRepo);
|
|
10290
|
+
return selectedPath;
|
|
10004
10291
|
}
|
|
10005
|
-
|
|
10292
|
+
if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
|
|
10293
|
+
const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
|
|
10006
10294
|
encoding: "utf-8"
|
|
10007
10295
|
});
|
|
10008
10296
|
if (add.status !== 0) {
|
|
10009
10297
|
const addNew = (0, import_node_child_process20.spawnSync)(
|
|
10010
10298
|
"git",
|
|
10011
|
-
["-C", mainRepo, "worktree", "add", "-b", branch,
|
|
10012
|
-
{
|
|
10013
|
-
encoding: "utf-8"
|
|
10014
|
-
}
|
|
10299
|
+
["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
|
|
10300
|
+
{ encoding: "utf-8" }
|
|
10015
10301
|
);
|
|
10016
10302
|
if (addNew.status !== 0) {
|
|
10017
|
-
const detail = [add.stderr, addNew.stderr].map((
|
|
10303
|
+
const detail = [add.stderr, addNew.stderr].map((stderr) => stderr?.trim()).filter(Boolean).join("; ");
|
|
10018
10304
|
throw new Error(`git worktree add failed: ${detail || "unknown error"}`);
|
|
10019
10305
|
}
|
|
10020
10306
|
}
|
|
10021
|
-
process.stdout.write(`Created worktree ${
|
|
10307
|
+
process.stdout.write(`Created worktree ${selectedPath}
|
|
10022
10308
|
`);
|
|
10023
|
-
|
|
10309
|
+
return selectedPath;
|
|
10310
|
+
});
|
|
10024
10311
|
if (opts.noDevpod) {
|
|
10025
10312
|
warnMissingWorkspaceOwnership(mainRepo);
|
|
10026
10313
|
process.stdout.write("Skipped environment startup; no routes were changed.\n");
|
|
@@ -10163,7 +10450,7 @@ async function workspaceStop(target, opts = {}) {
|
|
|
10163
10450
|
async function workspaceDown(target, opts = {}) {
|
|
10164
10451
|
return runWorkspaceLifecycle("down", target, opts);
|
|
10165
10452
|
}
|
|
10166
|
-
var import_node_child_process20, import_node_fs24, import_node_path24;
|
|
10453
|
+
var import_node_child_process20, import_node_fs24, import_node_path24, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
10167
10454
|
var init_workspace_lifecycle = __esm({
|
|
10168
10455
|
"src/core/workspace-lifecycle.ts"() {
|
|
10169
10456
|
"use strict";
|
|
@@ -10177,6 +10464,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
10177
10464
|
init_workspace();
|
|
10178
10465
|
init_workspace_ensure();
|
|
10179
10466
|
init_workspace_ownership();
|
|
10467
|
+
WORKSPACE_ALLOCATION_LOCK_WAIT_MS = 6e4;
|
|
10180
10468
|
}
|
|
10181
10469
|
});
|
|
10182
10470
|
|
|
@@ -10370,7 +10658,7 @@ async function devpodExec(repoPath, command) {
|
|
|
10370
10658
|
}
|
|
10371
10659
|
assertRunningDevpod(devpod.id, repoPath);
|
|
10372
10660
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
10373
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
10661
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto8.randomUUID)()}__:`;
|
|
10374
10662
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
10375
10663
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
10376
10664
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -10447,12 +10735,12 @@ async function devpodExec(repoPath, command) {
|
|
|
10447
10735
|
});
|
|
10448
10736
|
});
|
|
10449
10737
|
}
|
|
10450
|
-
var import_node_child_process22,
|
|
10738
|
+
var import_node_child_process22, import_node_crypto8, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
|
|
10451
10739
|
var init_devpod_exec = __esm({
|
|
10452
10740
|
"src/core/devpod-exec.ts"() {
|
|
10453
10741
|
"use strict";
|
|
10454
10742
|
import_node_child_process22 = require("child_process");
|
|
10455
|
-
|
|
10743
|
+
import_node_crypto8 = require("crypto");
|
|
10456
10744
|
init_devpod_environment();
|
|
10457
10745
|
init_devpod_workspaces();
|
|
10458
10746
|
init_devsy_exec();
|
|
@@ -12277,7 +12565,7 @@ function sanitizeRouterId(value) {
|
|
|
12277
12565
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
12278
12566
|
}
|
|
12279
12567
|
function repoHash(repoPath) {
|
|
12280
|
-
return (0,
|
|
12568
|
+
return (0, import_node_crypto9.createHash)("sha1").update(import_node_path28.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
12281
12569
|
}
|
|
12282
12570
|
function asDockerApp(app) {
|
|
12283
12571
|
return app.runtime === "docker";
|
|
@@ -12480,12 +12768,12 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
12480
12768
|
const port = Number(match[1]);
|
|
12481
12769
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
12482
12770
|
}
|
|
12483
|
-
var import_node_child_process25,
|
|
12771
|
+
var import_node_child_process25, import_node_crypto9, import_node_fs28, import_node_path28, import_yaml7;
|
|
12484
12772
|
var init_docker_run = __esm({
|
|
12485
12773
|
"src/core/docker-run.ts"() {
|
|
12486
12774
|
"use strict";
|
|
12487
12775
|
import_node_child_process25 = require("child_process");
|
|
12488
|
-
|
|
12776
|
+
import_node_crypto9 = require("crypto");
|
|
12489
12777
|
import_node_fs28 = __toESM(require("fs"));
|
|
12490
12778
|
import_node_path28 = __toESM(require("path"));
|
|
12491
12779
|
import_yaml7 = __toESM(require("yaml"));
|
|
@@ -14280,7 +14568,7 @@ var init_version = __esm({
|
|
|
14280
14568
|
|
|
14281
14569
|
// src/cli.ts
|
|
14282
14570
|
var import_commander = require("commander");
|
|
14283
|
-
var CLI_VERSION = true ? "0.0.
|
|
14571
|
+
var CLI_VERSION = true ? "0.0.45" : "0.0.0-dev";
|
|
14284
14572
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
14285
14573
|
function withErrorHandling(action2) {
|
|
14286
14574
|
return async (...args) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.44
|
|
2
|
+
|
|
3
|
+
Shared local TLS certificate refresh now serializes inspection, generation,
|
|
4
|
+
verification, and route-configuration refresh across parallel worktrees.
|
|
5
|
+
|
|
6
|
+
1. Install `@devrouter/cli@0.0.44` on the host and bump `.devrouter.yml` to
|
|
7
|
+
`devrouter.version: 0.0.44`.
|
|
8
|
+
2. Keep using `devrouter ensure <checkout>` for primary and linked checkouts.
|
|
9
|
+
Do not run `mkcert` manually for individual worktree hosts; Devrouter now
|
|
10
|
+
preserves and compacts shared certificate coverage under its own lock.
|
|
11
|
+
3. Do not remove the shared certificate or lock as routine cleanup. Routine
|
|
12
|
+
refresh still fails closed on a malformed certificate; the explicit
|
|
13
|
+
repository-scoped setup command printed by Devrouter replaces that generated
|
|
14
|
+
certificate while holding the same machine-global lock.
|
|
15
|
+
|
|
16
|
+
Verification:
|
|
17
|
+
|
|
18
|
+
- Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
|
|
19
|
+
repository report `0.0.44`.
|
|
20
|
+
- Start two linked checkouts concurrently with `devrouter ensure`; require both
|
|
21
|
+
namespaced HTTPS routes to pass readiness and remain covered afterward.
|
|
22
|
+
- Run `devrouter doctor --repo <checkout>` and confirm no TLS host-coverage
|
|
23
|
+
warning remains.
|
|
24
|
+
|
|
25
|
+
Report template:
|
|
26
|
+
|
|
27
|
+
- CLI/config version: `0.0.44`
|
|
28
|
+
- Parallel worktree routes: `<passed or details>`
|
|
29
|
+
- TLS host coverage: `<passed or details>`
|
|
30
|
+
- Remaining blockers: `<none or details>`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.45
|
|
2
|
+
|
|
3
|
+
Parallel linked worktrees now claim collision-safe workspace identities across
|
|
4
|
+
mixed DevPod and Devsy fleets.
|
|
5
|
+
|
|
6
|
+
1. Install `@devrouter/cli@0.0.45` on the host and bump `.devrouter.yml` to
|
|
7
|
+
`devrouter.version: 0.0.45`.
|
|
8
|
+
2. Keep using `devrouter ensure <checkout>` or `devrouter workspace up
|
|
9
|
+
<branch>`. Do not derive workspace ids or worktree paths in wrapper scripts.
|
|
10
|
+
3. Existing persisted workspace identities stay unchanged. A genuinely new
|
|
11
|
+
checkout keeps its readable branch/path identity when free and receives a
|
|
12
|
+
deterministic hash-suffixed fallback only when another checkout or provider
|
|
13
|
+
already occupies that identity.
|
|
14
|
+
4. Treat an unreadable DevPod or Devsy registry as a startup blocker for a new
|
|
15
|
+
claim. Devrouter fails before provider, process, service, or route mutation.
|
|
16
|
+
|
|
17
|
+
Verification:
|
|
18
|
+
|
|
19
|
+
- Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
|
|
20
|
+
repository report `0.0.45`.
|
|
21
|
+
- Start two new linked checkouts whose branch names share the first 32
|
|
22
|
+
sanitized characters, one with DevPod and one with Devsy.
|
|
23
|
+
- Require distinct persisted workspace ids, exact provider registrations, and
|
|
24
|
+
namespaced routes. Stop each checkout with `devrouter stop <checkout>` and
|
|
25
|
+
require both route sets to disappear.
|
|
26
|
+
|
|
27
|
+
Report template:
|
|
28
|
+
|
|
29
|
+
- CLI/config version: `0.0.45`
|
|
30
|
+
- Colliding-prefix identities: `<passed or details>`
|
|
31
|
+
- DevPod/Devsy ownership: `<passed or details>`
|
|
32
|
+
- Exact stop and route release: `<passed or details>`
|
|
33
|
+
- Remaining blockers: `<none or details>`
|