@devrouter/cli 0.0.79 → 0.1.0
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-lifecycle-worker.js +284 -50
- package/dist/devrouter.js +1396 -224
- package/package.json +4 -1
- package/upgrade-prompts/0.0.80.md +63 -0
- package/upgrade-prompts/0.1.0.md +69 -0
package/dist/devrouter.js
CHANGED
|
@@ -4275,7 +4275,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
|
|
|
4275
4275
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4276
4276
|
const requiredVersion = config.devrouter?.version;
|
|
4277
4277
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4278
|
-
const cliVersion = true ? "0.0
|
|
4278
|
+
const cliVersion = true ? "0.1.0" : "0.0.0-dev";
|
|
4279
4279
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4280
4280
|
hasWarnedVersionMismatch = true;
|
|
4281
4281
|
process.stderr.write(
|
|
@@ -5299,6 +5299,8 @@ var init_capacity_store = __esm({
|
|
|
5299
5299
|
file;
|
|
5300
5300
|
establishedFile;
|
|
5301
5301
|
observedLedger = false;
|
|
5302
|
+
/** Identity of the snapshot file read last, so a replacement cannot reuse its revision. */
|
|
5303
|
+
ledgerGeneration;
|
|
5302
5304
|
readEstablished() {
|
|
5303
5305
|
let descriptor;
|
|
5304
5306
|
try {
|
|
@@ -5325,7 +5327,7 @@ var init_capacity_store = __esm({
|
|
|
5325
5327
|
import_node_fs9.default.closeSync(descriptor);
|
|
5326
5328
|
}
|
|
5327
5329
|
}
|
|
5328
|
-
|
|
5330
|
+
inspect() {
|
|
5329
5331
|
const minimumRevision = this.minimumRevision();
|
|
5330
5332
|
const established = this.readEstablished();
|
|
5331
5333
|
let descriptor;
|
|
@@ -5336,9 +5338,9 @@ var init_capacity_store = __esm({
|
|
|
5336
5338
|
);
|
|
5337
5339
|
} catch (error) {
|
|
5338
5340
|
if (error.code === "ENOENT") {
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
return {
|
|
5341
|
+
this.ledgerGeneration = void 0;
|
|
5342
|
+
if (established || this.observedLedger || minimumRevision > 0) return { kind: "absent" };
|
|
5343
|
+
return { kind: "pristine" };
|
|
5342
5344
|
}
|
|
5343
5345
|
throw error;
|
|
5344
5346
|
}
|
|
@@ -5351,16 +5353,36 @@ var init_capacity_store = __esm({
|
|
|
5351
5353
|
if (count > MAX_BYTES2) throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
5352
5354
|
const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
5353
5355
|
validate(value);
|
|
5354
|
-
|
|
5356
|
+
this.ledgerGeneration = `${stat.dev}:${stat.ino}`;
|
|
5357
|
+
if (value.revision < minimumRevision) return { kind: "stale", revision: value.revision };
|
|
5355
5358
|
this.observedLedger = true;
|
|
5356
|
-
return value;
|
|
5359
|
+
return { kind: "intact", revision: value.revision, snapshot: value };
|
|
5357
5360
|
} finally {
|
|
5358
5361
|
import_node_fs9.default.closeSync(descriptor);
|
|
5359
5362
|
}
|
|
5360
5363
|
}
|
|
5364
|
+
read() {
|
|
5365
|
+
const state = this.inspect();
|
|
5366
|
+
if (state.kind === "intact") return state.snapshot;
|
|
5367
|
+
if (state.kind === "pristine") return { version: 1, revision: 0, reservations: [] };
|
|
5368
|
+
throw new CapacityHistoryError("capacity-ledger-lost");
|
|
5369
|
+
}
|
|
5361
5370
|
establish() {
|
|
5362
5371
|
if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
|
|
5363
|
-
|
|
5372
|
+
this.syncDurability([this.file, this.establishedFile, this.directory]);
|
|
5373
|
+
}
|
|
5374
|
+
/**
|
|
5375
|
+
* Witness for a ledger that is positively absent. The marker records that
|
|
5376
|
+
* history existed here, so no later process can read the store as a fresh
|
|
5377
|
+
* install and start admitting; the snapshot stays absent until reconciliation.
|
|
5378
|
+
* The caller holds the capacity lock and has proven environment cessation.
|
|
5379
|
+
*/
|
|
5380
|
+
establishMarkerOnly() {
|
|
5381
|
+
if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
|
|
5382
|
+
this.syncDurability([this.establishedFile, this.directory]);
|
|
5383
|
+
}
|
|
5384
|
+
syncDurability(files) {
|
|
5385
|
+
for (const file of files) {
|
|
5364
5386
|
const descriptor = import_node_fs9.default.openSync(
|
|
5365
5387
|
file,
|
|
5366
5388
|
import_node_fs9.default.constants.O_RDONLY | import_node_fs9.default.constants.O_NOFOLLOW | import_node_fs9.default.constants.O_NONBLOCK
|
|
@@ -5376,15 +5398,90 @@ var init_capacity_store = __esm({
|
|
|
5376
5398
|
}
|
|
5377
5399
|
}
|
|
5378
5400
|
readForMutation() {
|
|
5401
|
+
const expectedGeneration = this.ledgerGeneration;
|
|
5379
5402
|
const snapshot = this.read();
|
|
5403
|
+
if (expectedGeneration !== void 0 && expectedGeneration !== this.ledgerGeneration)
|
|
5404
|
+
throw new CapacitySnapshotChangedError();
|
|
5380
5405
|
if (this.observedLedger) this.establish();
|
|
5381
5406
|
return snapshot;
|
|
5382
5407
|
}
|
|
5383
5408
|
commit(snapshot) {
|
|
5384
5409
|
this.write(this.file, serializeSnapshot(snapshot));
|
|
5385
5410
|
this.observedLedger = true;
|
|
5411
|
+
this.rememberGeneration();
|
|
5386
5412
|
this.establish();
|
|
5387
5413
|
}
|
|
5414
|
+
/** Remember the published snapshot identity for the next generation fence. */
|
|
5415
|
+
rememberGeneration() {
|
|
5416
|
+
try {
|
|
5417
|
+
const stat = import_node_fs9.default.statSync(this.file);
|
|
5418
|
+
this.ledgerGeneration = stat.isFile() ? `${stat.dev}:${stat.ino}` : void 0;
|
|
5419
|
+
} catch {
|
|
5420
|
+
this.ledgerGeneration = void 0;
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* Durable evidence that recorded history is gone, for a stop whose physical
|
|
5425
|
+
* cessation is already proven. The caller holds that proof; this writes no
|
|
5426
|
+
* row and asserts nothing about any other environment's charge.
|
|
5427
|
+
*/
|
|
5428
|
+
recordLedgerLoss() {
|
|
5429
|
+
assertPrivateDirectory(this.directory);
|
|
5430
|
+
withFileLockSync(
|
|
5431
|
+
`${this.file}.lock`,
|
|
5432
|
+
{ activity: "capacity loss witness", waitMs: 100 },
|
|
5433
|
+
() => {
|
|
5434
|
+
if (this.inspect().kind !== "absent")
|
|
5435
|
+
throw new CapacityHistoryError("capacity-ledger-lost");
|
|
5436
|
+
this.establishMarkerOnly();
|
|
5437
|
+
}
|
|
5438
|
+
);
|
|
5439
|
+
}
|
|
5440
|
+
/**
|
|
5441
|
+
* Replace a provably absent ledger with a fresh baseline under the capacity
|
|
5442
|
+
* lock. `plan` runs inside the lock with the authoritative classification and
|
|
5443
|
+
* re-verifies the caller's evidence, returning the baseline to write or
|
|
5444
|
+
* undefined to refuse without mutation. `confirm` runs in the same lock after
|
|
5445
|
+
* the write; returning false withdraws the fresh snapshot, so the store reads
|
|
5446
|
+
* as lost again instead of publishing a baseline the caller cannot stand behind.
|
|
5447
|
+
*/
|
|
5448
|
+
reconcileLostHistory(plan, confirm) {
|
|
5449
|
+
assertPrivateDirectory(this.directory);
|
|
5450
|
+
return withFileLockSync(
|
|
5451
|
+
`${this.file}.lock`,
|
|
5452
|
+
{ activity: "capacity reconciliation", waitMs: 100 },
|
|
5453
|
+
() => {
|
|
5454
|
+
const baseline = plan(this.inspect());
|
|
5455
|
+
if (!baseline) return { reconciled: false };
|
|
5456
|
+
if (!Number.isSafeInteger(baseline.revision) || baseline.revision < 1)
|
|
5457
|
+
throw new Error("Invalid capacity reconciliation revision.");
|
|
5458
|
+
const pools = structuredClone(baseline.pools ?? []);
|
|
5459
|
+
validatePools(pools);
|
|
5460
|
+
const snapshot = pools.length === 0 ? { version: 1, revision: baseline.revision, reservations: [] } : { version: 1, revision: baseline.revision, reservations: [], pools };
|
|
5461
|
+
validate(snapshot);
|
|
5462
|
+
this.commit(snapshot);
|
|
5463
|
+
if (confirm()) return { reconciled: true, revision: snapshot.revision };
|
|
5464
|
+
this.withdrawSnapshot();
|
|
5465
|
+
return { reconciled: false };
|
|
5466
|
+
}
|
|
5467
|
+
);
|
|
5468
|
+
}
|
|
5469
|
+
/** Remove a snapshot this store just wrote, keeping the loss witness durable. */
|
|
5470
|
+
withdrawSnapshot() {
|
|
5471
|
+
try {
|
|
5472
|
+
import_node_fs9.default.unlinkSync(this.file);
|
|
5473
|
+
} catch (error) {
|
|
5474
|
+
if (error.code !== "ENOENT") throw error;
|
|
5475
|
+
}
|
|
5476
|
+
this.observedLedger = false;
|
|
5477
|
+
this.ledgerGeneration = void 0;
|
|
5478
|
+
const directory = import_node_fs9.default.openSync(this.directory, "r");
|
|
5479
|
+
try {
|
|
5480
|
+
import_node_fs9.default.fsyncSync(directory);
|
|
5481
|
+
} finally {
|
|
5482
|
+
import_node_fs9.default.closeSync(directory);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5388
5485
|
/**
|
|
5389
5486
|
* Caller must revoke journal authority and prove exact physical cessation first.
|
|
5390
5487
|
* An absent reservation still advances the revision to fence that proof.
|
|
@@ -6162,7 +6259,7 @@ var init_reliability_contract = __esm({
|
|
|
6162
6259
|
|
|
6163
6260
|
// src/core/reliability-operation-store.ts
|
|
6164
6261
|
function installedCliVersion() {
|
|
6165
|
-
return true ? "0.0
|
|
6262
|
+
return true ? "0.1.0" : "0.0.0-dev";
|
|
6166
6263
|
}
|
|
6167
6264
|
function compareCliVersions(left, right) {
|
|
6168
6265
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -6480,16 +6577,26 @@ function validate2(record5, identity2) {
|
|
|
6480
6577
|
function createLifecycleCapacityStore(directory = import_node_path13.default.join(DEVROUTER_HOME, "controller")) {
|
|
6481
6578
|
return new CapacityStore(directory, void 0, () => {
|
|
6482
6579
|
try {
|
|
6483
|
-
|
|
6484
|
-
for (const record5 of listReliabilityOperations()) {
|
|
6485
|
-
if (record5.capacity) revision = Math.max(revision, record5.capacity.snapshotRevision ?? 1);
|
|
6486
|
-
}
|
|
6487
|
-
return revision;
|
|
6580
|
+
return listUnsettledCapacityBindings().floor;
|
|
6488
6581
|
} catch {
|
|
6489
6582
|
throw new CapacityHistoryError("capacity-history-unprovable");
|
|
6490
6583
|
}
|
|
6491
6584
|
});
|
|
6492
6585
|
}
|
|
6586
|
+
function listUnsettledCapacityBindings() {
|
|
6587
|
+
let floor = 0;
|
|
6588
|
+
const bindings = [];
|
|
6589
|
+
for (const record5 of listReliabilityOperations()) {
|
|
6590
|
+
if (!record5.capacity) continue;
|
|
6591
|
+
floor = Math.max(floor, record5.capacity.snapshotRevision ?? 1);
|
|
6592
|
+
bindings.push({
|
|
6593
|
+
environmentId: record5.state.environmentId,
|
|
6594
|
+
repoPath: record5.identity.repoPath,
|
|
6595
|
+
workspace: record5.identity.workspace
|
|
6596
|
+
});
|
|
6597
|
+
}
|
|
6598
|
+
return { floor, bindings };
|
|
6599
|
+
}
|
|
6493
6600
|
function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path13.default.join(DEVROUTER_HOME, "controller")) {
|
|
6494
6601
|
if (record5.version === 1) return;
|
|
6495
6602
|
const binding = record5.capacity;
|
|
@@ -9167,19 +9274,165 @@ function parseVersion2(output2) {
|
|
|
9167
9274
|
/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
|
|
9168
9275
|
)?.[1];
|
|
9169
9276
|
}
|
|
9170
|
-
function
|
|
9171
|
-
const
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9277
|
+
function parseSemver(value) {
|
|
9278
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
9279
|
+
value.trim()
|
|
9280
|
+
);
|
|
9281
|
+
if (!match) return void 0;
|
|
9282
|
+
return {
|
|
9283
|
+
major: Number(match[1]),
|
|
9284
|
+
minor: Number(match[2]),
|
|
9285
|
+
patch: Number(match[3]),
|
|
9286
|
+
prerelease: match[4] ? match[4].split(".") : []
|
|
9287
|
+
};
|
|
9288
|
+
}
|
|
9289
|
+
function comparePrerelease(left, right) {
|
|
9290
|
+
if (left.length === 0 && right.length === 0) return 0;
|
|
9291
|
+
if (left.length === 0) return 1;
|
|
9292
|
+
if (right.length === 0) return -1;
|
|
9293
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
9294
|
+
const leftPart = left[index];
|
|
9295
|
+
const rightPart = right[index];
|
|
9296
|
+
if (leftPart === void 0) return -1;
|
|
9297
|
+
if (rightPart === void 0) return 1;
|
|
9298
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
9299
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
9300
|
+
if (leftNumeric && rightNumeric) {
|
|
9301
|
+
const difference = Number(leftPart) - Number(rightPart);
|
|
9302
|
+
if (difference !== 0) return difference < 0 ? -1 : 1;
|
|
9303
|
+
continue;
|
|
9304
|
+
}
|
|
9305
|
+
if (leftNumeric) return -1;
|
|
9306
|
+
if (rightNumeric) return 1;
|
|
9307
|
+
if (leftPart !== rightPart) return leftPart < rightPart ? -1 : 1;
|
|
9177
9308
|
}
|
|
9178
9309
|
return 0;
|
|
9179
9310
|
}
|
|
9311
|
+
function compareSemver2(left, right) {
|
|
9312
|
+
for (const part of ["major", "minor", "patch"]) {
|
|
9313
|
+
if (left[part] !== right[part]) return left[part] < right[part] ? -1 : 1;
|
|
9314
|
+
}
|
|
9315
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
9316
|
+
}
|
|
9317
|
+
function isSupportedDevsyVersion(version) {
|
|
9318
|
+
const parsed = version === void 0 ? void 0 : parseSemver(version);
|
|
9319
|
+
const minimum = parseSemver(SUPPORTED_DEVSY_MIN_VERSION);
|
|
9320
|
+
const maximum = parseSemver(SUPPORTED_DEVSY_MAX_VERSION);
|
|
9321
|
+
if (!parsed || !minimum || !maximum) return false;
|
|
9322
|
+
if (compareSemver2(parsed, minimum) < 0) return false;
|
|
9323
|
+
if (parsed.prerelease.length > 0 && parsed.major === maximum.major && parsed.minor === maximum.minor && parsed.patch === maximum.patch) {
|
|
9324
|
+
return false;
|
|
9325
|
+
}
|
|
9326
|
+
return compareSemver2(parsed, maximum) < 0;
|
|
9327
|
+
}
|
|
9328
|
+
function isDevsyAgentAsset(value) {
|
|
9329
|
+
if (!value || typeof value !== "object") return false;
|
|
9330
|
+
const asset = value;
|
|
9331
|
+
return typeof asset.githubAssetId === "number" && Number.isSafeInteger(asset.githubAssetId) && typeof asset.name === "string" && typeof asset.size === "number" && Number.isSafeInteger(asset.size) && asset.size > 0 && typeof asset.sha256 === "string" && /^[0-9a-f]{64}$/.test(asset.sha256) && typeof asset.url === "string" && asset.url.startsWith("https://");
|
|
9332
|
+
}
|
|
9333
|
+
function devsyAgentManifestPath(cacheRoot, devsyVersion) {
|
|
9334
|
+
return import_node_path22.default.join(cacheRoot, `v${devsyVersion}`, DEVSY_AGENT_MANIFEST_FILE);
|
|
9335
|
+
}
|
|
9336
|
+
function readManifestRecord(cacheRoot, devsyVersion) {
|
|
9337
|
+
try {
|
|
9338
|
+
const parsed = JSON.parse(
|
|
9339
|
+
import_node_fs16.default.readFileSync(devsyAgentManifestPath(cacheRoot, devsyVersion), "utf-8")
|
|
9340
|
+
);
|
|
9341
|
+
if (parsed.version !== 1 || parsed.devsyVersion !== devsyVersion) return void 0;
|
|
9342
|
+
if (parsed.origin !== "release") return void 0;
|
|
9343
|
+
if (!Array.isArray(parsed.assets) || parsed.assets.length === 0) return void 0;
|
|
9344
|
+
if (!parsed.assets.every(isDevsyAgentAsset)) return void 0;
|
|
9345
|
+
return { version: devsyVersion, origin: "release", assets: parsed.assets };
|
|
9346
|
+
} catch {
|
|
9347
|
+
return void 0;
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9350
|
+
function writeManifestRecord(cacheRoot, manifest) {
|
|
9351
|
+
const target = devsyAgentManifestPath(cacheRoot, manifest.version);
|
|
9352
|
+
const directory = import_node_path22.default.dirname(target);
|
|
9353
|
+
import_node_fs16.default.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
9354
|
+
const temporary = import_node_path22.default.join(
|
|
9355
|
+
directory,
|
|
9356
|
+
`.${DEVSY_AGENT_MANIFEST_FILE}.${process.pid}.${(0, import_node_crypto11.randomUUID)()}.tmp`
|
|
9357
|
+
);
|
|
9358
|
+
const record5 = {
|
|
9359
|
+
version: 1,
|
|
9360
|
+
devsyVersion: manifest.version,
|
|
9361
|
+
origin: "release",
|
|
9362
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9363
|
+
assets: [...manifest.assets]
|
|
9364
|
+
};
|
|
9365
|
+
try {
|
|
9366
|
+
import_node_fs16.default.writeFileSync(temporary, `${JSON.stringify(record5, null, 2)}
|
|
9367
|
+
`, { mode: 384 });
|
|
9368
|
+
import_node_fs16.default.renameSync(temporary, target);
|
|
9369
|
+
} finally {
|
|
9370
|
+
import_node_fs16.default.rmSync(temporary, { force: true });
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
function pinnedManifest(version) {
|
|
9374
|
+
return version === PINNED_DEVSY_AGENT_VERSION ? { version, origin: "pinned", assets: DEVSY_AGENT_ASSETS } : void 0;
|
|
9375
|
+
}
|
|
9376
|
+
function manifestForVersion(resolved, version) {
|
|
9377
|
+
if (resolved.assets) return { version, origin: "pinned", assets: resolved.assets };
|
|
9378
|
+
return pinnedManifest(version) ?? readManifestRecord(resolved.cacheRoot, version);
|
|
9379
|
+
}
|
|
9380
|
+
async function resolveReleaseDevsyAgentManifest(version, fetcher) {
|
|
9381
|
+
const url = `${DEVSY_RELEASE_METADATA_BASE_URL}/${DEVSY_RELEASE_TAG_PREFIX}${version}`;
|
|
9382
|
+
let response;
|
|
9383
|
+
try {
|
|
9384
|
+
response = await fetcher(url, { headers: { accept: "application/vnd.github+json" } });
|
|
9385
|
+
} catch (error) {
|
|
9386
|
+
throw new Error(`Devsy ${version} release metadata could not be read: ${errorMessage(error)}`);
|
|
9387
|
+
}
|
|
9388
|
+
if (!response.ok) {
|
|
9389
|
+
throw new Error(`Devsy ${version} release metadata is unavailable (HTTP ${response.status}).`);
|
|
9390
|
+
}
|
|
9391
|
+
let payload;
|
|
9392
|
+
try {
|
|
9393
|
+
payload = await response.json();
|
|
9394
|
+
} catch (error) {
|
|
9395
|
+
throw new Error(`Devsy ${version} release metadata is not valid JSON: ${errorMessage(error)}`);
|
|
9396
|
+
}
|
|
9397
|
+
const release = payload;
|
|
9398
|
+
if (release?.tag_name !== `${DEVSY_RELEASE_TAG_PREFIX}${version}`) {
|
|
9399
|
+
throw new Error(`Devsy ${version} release metadata reported an unexpected tag.`);
|
|
9400
|
+
}
|
|
9401
|
+
if (!Array.isArray(release.assets)) {
|
|
9402
|
+
throw new Error(`Devsy ${version} release metadata reported no assets.`);
|
|
9403
|
+
}
|
|
9404
|
+
const assets = [];
|
|
9405
|
+
for (const entry of release.assets) {
|
|
9406
|
+
const candidate = entry ?? {};
|
|
9407
|
+
const name = candidate.name;
|
|
9408
|
+
if (typeof name !== "string" || !DEVSY_AGENT_ASSET_NAMES.some((assetName) => assetName === name))
|
|
9409
|
+
continue;
|
|
9410
|
+
const id5 = candidate.id;
|
|
9411
|
+
const size = candidate.size;
|
|
9412
|
+
const digest3 = candidate.digest;
|
|
9413
|
+
const downloadUrl = candidate.browser_download_url;
|
|
9414
|
+
if (typeof id5 !== "number" || !Number.isSafeInteger(id5) || typeof size !== "number" || !Number.isSafeInteger(size) || size <= 0) {
|
|
9415
|
+
throw new Error(`Devsy ${version} reported an unusable ${String(name)} asset.`);
|
|
9416
|
+
}
|
|
9417
|
+
const sha2562 = typeof digest3 === "string" && digest3.startsWith("sha256:") ? digest3.slice("sha256:".length).toLowerCase() : "";
|
|
9418
|
+
if (!/^[0-9a-f]{64}$/.test(sha2562)) {
|
|
9419
|
+
throw new Error(
|
|
9420
|
+
`Devsy ${version} does not publish a SHA-256 digest for ${String(name)}; Devrouter cannot verify this release.`
|
|
9421
|
+
);
|
|
9422
|
+
}
|
|
9423
|
+
if (typeof downloadUrl !== "string" || !downloadUrl.startsWith("https://")) {
|
|
9424
|
+
throw new Error(`Devsy ${version} reported an unusable ${String(name)} download URL.`);
|
|
9425
|
+
}
|
|
9426
|
+
assets.push({ githubAssetId: id5, name, size, sha256: sha2562, url: downloadUrl });
|
|
9427
|
+
}
|
|
9428
|
+
if (assets.length === 0) {
|
|
9429
|
+
throw new Error(`Devsy ${version} publishes no official Linux agent asset.`);
|
|
9430
|
+
}
|
|
9431
|
+
return { version, origin: "release", assets };
|
|
9432
|
+
}
|
|
9180
9433
|
function devsyAgentRepairSuggestion(inspection) {
|
|
9181
9434
|
if (inspection.state === "stale") {
|
|
9182
|
-
return `Install Devsy ${
|
|
9435
|
+
return `Install a supported Devsy release (${SUPPORTED_DEVSY_RANGE}) for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
9183
9436
|
}
|
|
9184
9437
|
return inspection.source === "explicit" ? `Fix or unset DEVSY_AGENT_BINARY, then run: ${DEVSY_AGENT_SETUP_COMMAND}` : `Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
9185
9438
|
}
|
|
@@ -9194,8 +9447,8 @@ function nativeAssetName(platform, arch) {
|
|
|
9194
9447
|
if (arch === "x64") return "devsy-linux-amd64";
|
|
9195
9448
|
return void 0;
|
|
9196
9449
|
}
|
|
9197
|
-
function managedBinaryPath(cacheRoot, asset) {
|
|
9198
|
-
return import_node_path22.default.join(cacheRoot, `v${
|
|
9450
|
+
function managedBinaryPath(cacheRoot, devsyVersion, asset) {
|
|
9451
|
+
return import_node_path22.default.join(cacheRoot, `v${devsyVersion}`, asset.name);
|
|
9199
9452
|
}
|
|
9200
9453
|
function hashOpenFile(file) {
|
|
9201
9454
|
const digest3 = (0, import_node_crypto11.createHash)("sha256");
|
|
@@ -9239,7 +9492,7 @@ function resolvedOptions(options) {
|
|
|
9239
9492
|
versionOutput: options.versionOutput ?? installedVersionOutput(),
|
|
9240
9493
|
cacheRoot: options.cacheRoot ?? import_node_path22.default.join(CACHE_DIR, "devsy", "agents"),
|
|
9241
9494
|
lockPath: options.lockPath ?? DEVSY_AGENT_CACHE_LOCK,
|
|
9242
|
-
assets: options.assets
|
|
9495
|
+
assets: options.assets,
|
|
9243
9496
|
nativeAssetName: options.nativeAssetName
|
|
9244
9497
|
};
|
|
9245
9498
|
}
|
|
@@ -9248,48 +9501,67 @@ function inspectDevsyAgent(options = {}) {
|
|
|
9248
9501
|
const installedVersion = parseVersion2(resolved.versionOutput);
|
|
9249
9502
|
const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
|
|
9250
9503
|
const source = explicitPath ? "explicit" : "managed";
|
|
9251
|
-
if (installedVersion
|
|
9504
|
+
if (!installedVersion) {
|
|
9252
9505
|
return {
|
|
9253
9506
|
state: "stale",
|
|
9254
9507
|
source,
|
|
9255
|
-
reason:
|
|
9256
|
-
installedVersion
|
|
9508
|
+
reason: `the installed Devsy version could not be determined; Devrouter supports ${SUPPORTED_DEVSY_RANGE}`
|
|
9257
9509
|
};
|
|
9258
9510
|
}
|
|
9259
|
-
if (
|
|
9511
|
+
if (!isSupportedDevsyVersion(installedVersion)) {
|
|
9260
9512
|
return {
|
|
9261
|
-
|
|
9513
|
+
state: "stale",
|
|
9262
9514
|
source,
|
|
9263
|
-
|
|
9515
|
+
reason: `installed Devsy ${installedVersion} is outside the supported range ${SUPPORTED_DEVSY_RANGE}`,
|
|
9264
9516
|
installedVersion
|
|
9265
9517
|
};
|
|
9266
9518
|
}
|
|
9267
|
-
|
|
9519
|
+
const manifest = manifestForVersion(resolved, installedVersion);
|
|
9520
|
+
if (!manifest) {
|
|
9521
|
+
if (explicitPath) {
|
|
9522
|
+
return {
|
|
9523
|
+
...inspectBinary(explicitPath, resolved.assets ?? DEVSY_AGENT_ASSETS, "invalid"),
|
|
9524
|
+
source,
|
|
9525
|
+
binaryPath: explicitPath,
|
|
9526
|
+
installedVersion
|
|
9527
|
+
};
|
|
9528
|
+
}
|
|
9268
9529
|
return {
|
|
9269
9530
|
state: "ready",
|
|
9270
9531
|
source: "host",
|
|
9271
|
-
reason: `
|
|
9532
|
+
reason: `no verified Devsy ${installedVersion} agent manifest is recorded yet; the host CLI governs its own agent`,
|
|
9272
9533
|
installedVersion,
|
|
9273
|
-
drift: { installed: installedVersion, supported:
|
|
9534
|
+
drift: { installed: installedVersion, supported: PINNED_DEVSY_AGENT_VERSION }
|
|
9274
9535
|
};
|
|
9275
9536
|
}
|
|
9276
9537
|
const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
|
|
9277
|
-
const asset =
|
|
9538
|
+
const asset = manifest.assets.find((candidate) => candidate.name === expectedName);
|
|
9278
9539
|
if (!asset) {
|
|
9279
9540
|
return {
|
|
9280
9541
|
state: "stale",
|
|
9281
9542
|
source,
|
|
9282
|
-
reason: `
|
|
9283
|
-
installedVersion
|
|
9543
|
+
reason: `Devsy ${installedVersion} has no official agent for ${resolved.platform}/${resolved.arch}`,
|
|
9544
|
+
installedVersion,
|
|
9545
|
+
manifestOrigin: manifest.origin
|
|
9546
|
+
};
|
|
9547
|
+
}
|
|
9548
|
+
if (explicitPath) {
|
|
9549
|
+
return {
|
|
9550
|
+
...inspectBinary(explicitPath, [asset], "invalid"),
|
|
9551
|
+
source,
|
|
9552
|
+
binaryPath: explicitPath,
|
|
9553
|
+
installedVersion,
|
|
9554
|
+
manifestOrigin: manifest.origin
|
|
9284
9555
|
};
|
|
9285
9556
|
}
|
|
9286
|
-
const binaryPath = managedBinaryPath(resolved.cacheRoot, asset);
|
|
9557
|
+
const binaryPath = managedBinaryPath(resolved.cacheRoot, installedVersion, asset);
|
|
9287
9558
|
return {
|
|
9288
9559
|
...inspectBinary(binaryPath, [asset], "missing"),
|
|
9289
9560
|
source,
|
|
9290
9561
|
binaryPath,
|
|
9291
9562
|
asset,
|
|
9292
|
-
installedVersion
|
|
9563
|
+
installedVersion,
|
|
9564
|
+
manifestOrigin: manifest.origin
|
|
9293
9565
|
};
|
|
9294
9566
|
}
|
|
9295
9567
|
async function writeAll(handle, chunk, position) {
|
|
@@ -9392,7 +9664,7 @@ async function downloadAndPublish(asset, binaryPath, fetcher, githubCliDownloade
|
|
|
9392
9664
|
const writeChunk = async (chunk) => {
|
|
9393
9665
|
size += chunk.byteLength;
|
|
9394
9666
|
if (size > asset.size) {
|
|
9395
|
-
throw new Error("Devsy agent download exceeded the
|
|
9667
|
+
throw new Error("Devsy agent download exceeded the expected size");
|
|
9396
9668
|
}
|
|
9397
9669
|
digest3.update(chunk);
|
|
9398
9670
|
await writeAll(handle, chunk, size - chunk.byteLength);
|
|
@@ -9442,21 +9714,36 @@ async function downloadAndPublish(asset, binaryPath, fetcher, githubCliDownloade
|
|
|
9442
9714
|
await import_node_fs16.default.promises.rm(temporaryPath, { force: true });
|
|
9443
9715
|
}
|
|
9444
9716
|
}
|
|
9717
|
+
function isReadyInspection(inspection) {
|
|
9718
|
+
return inspection.state === "ready" && inspection.installedVersion !== void 0;
|
|
9719
|
+
}
|
|
9720
|
+
function preparedFromInspection(inspection) {
|
|
9721
|
+
return {
|
|
9722
|
+
version: inspection.installedVersion,
|
|
9723
|
+
binaryPath: inspection.binaryPath,
|
|
9724
|
+
source: inspection.source,
|
|
9725
|
+
asset: inspection.asset,
|
|
9726
|
+
changed: false,
|
|
9727
|
+
transport: "existing"
|
|
9728
|
+
};
|
|
9729
|
+
}
|
|
9730
|
+
function expectedAgentName(resolved) {
|
|
9731
|
+
return resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
|
|
9732
|
+
}
|
|
9733
|
+
async function loadOrResolveManifest(resolved, version, options) {
|
|
9734
|
+
const manifest = manifestForVersion(resolved, version);
|
|
9735
|
+
if (manifest) return manifest;
|
|
9736
|
+
const release = await resolveReleaseDevsyAgentManifest(version, options.fetcher ?? fetch);
|
|
9737
|
+
writeManifestRecord(resolved.cacheRoot, release);
|
|
9738
|
+
return release;
|
|
9739
|
+
}
|
|
9445
9740
|
async function prepareDevsyAgent(options = {}) {
|
|
9446
9741
|
const resolved = resolvedOptions(options);
|
|
9447
9742
|
const inspectOptions = resolved;
|
|
9448
9743
|
const before = inspectDevsyAgent(inspectOptions);
|
|
9449
|
-
if (before.state === "
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
source: before.source,
|
|
9453
|
-
asset: before.asset,
|
|
9454
|
-
changed: false,
|
|
9455
|
-
transport: "existing"
|
|
9456
|
-
};
|
|
9457
|
-
}
|
|
9458
|
-
if (before.source === "explicit" || before.state === "stale" || !before.binaryPath || !before.asset) {
|
|
9459
|
-
throw new DevsyAgentReadinessError(before);
|
|
9744
|
+
if (before.state === "stale") throw new DevsyAgentReadinessError(before);
|
|
9745
|
+
if (isReadyInspection(before) && before.source !== "host") {
|
|
9746
|
+
return preparedFromInspection(before);
|
|
9460
9747
|
}
|
|
9461
9748
|
const lock = options.withLock ?? ((operation) => {
|
|
9462
9749
|
import_node_fs16.default.mkdirSync(import_node_path22.default.dirname(resolved.lockPath), { recursive: true });
|
|
@@ -9473,38 +9760,42 @@ async function prepareDevsyAgent(options = {}) {
|
|
|
9473
9760
|
});
|
|
9474
9761
|
return lock(async () => {
|
|
9475
9762
|
const current = inspectDevsyAgent(inspectOptions);
|
|
9476
|
-
if (current.state === "
|
|
9477
|
-
|
|
9478
|
-
|
|
9763
|
+
if (current.state === "stale") throw new DevsyAgentReadinessError(current);
|
|
9764
|
+
if (isReadyInspection(current) && current.source !== "host") {
|
|
9765
|
+
return preparedFromInspection(current);
|
|
9766
|
+
}
|
|
9767
|
+
const version = current.installedVersion;
|
|
9768
|
+
if (!version) throw new DevsyAgentReadinessError(current);
|
|
9769
|
+
const manifest = await loadOrResolveManifest(resolved, version, options);
|
|
9770
|
+
if (current.source === "explicit") {
|
|
9771
|
+
const verified = inspectDevsyAgent(inspectOptions);
|
|
9772
|
+
if (isReadyInspection(verified)) return preparedFromInspection(verified);
|
|
9773
|
+
throw new DevsyAgentReadinessError(verified);
|
|
9774
|
+
}
|
|
9775
|
+
const asset = manifest.assets.find(
|
|
9776
|
+
(candidate) => candidate.name === expectedAgentName(resolved)
|
|
9777
|
+
);
|
|
9778
|
+
if (!asset) {
|
|
9779
|
+
throw new DevsyAgentReadinessError({
|
|
9780
|
+
state: "stale",
|
|
9479
9781
|
source: current.source,
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
};
|
|
9484
|
-
}
|
|
9485
|
-
if (current.source === "explicit" || current.state === "stale" || !current.binaryPath || !current.asset) {
|
|
9486
|
-
throw new DevsyAgentReadinessError(current);
|
|
9782
|
+
reason: `Devsy ${version} has no official agent for ${resolved.platform}/${resolved.arch}`,
|
|
9783
|
+
installedVersion: version,
|
|
9784
|
+
manifestOrigin: manifest.origin
|
|
9785
|
+
});
|
|
9487
9786
|
}
|
|
9488
9787
|
const transport = await downloadAndPublish(
|
|
9489
|
-
|
|
9490
|
-
|
|
9788
|
+
asset,
|
|
9789
|
+
managedBinaryPath(resolved.cacheRoot, version, asset),
|
|
9491
9790
|
options.fetcher ?? fetch,
|
|
9492
9791
|
options.githubCliDownloader ?? downloadWithGitHubCli
|
|
9493
9792
|
);
|
|
9494
9793
|
const published = inspectDevsyAgent(inspectOptions);
|
|
9495
|
-
if (published
|
|
9496
|
-
|
|
9497
|
-
}
|
|
9498
|
-
return {
|
|
9499
|
-
binaryPath: published.binaryPath,
|
|
9500
|
-
source: published.source,
|
|
9501
|
-
asset: published.asset,
|
|
9502
|
-
changed: true,
|
|
9503
|
-
transport
|
|
9504
|
-
};
|
|
9794
|
+
if (!isReadyInspection(published)) throw new DevsyAgentReadinessError(published);
|
|
9795
|
+
return { ...preparedFromInspection(published), changed: true, transport };
|
|
9505
9796
|
});
|
|
9506
9797
|
}
|
|
9507
|
-
var import_node_child_process7, import_node_crypto11, import_node_fs16, import_node_path22,
|
|
9798
|
+
var import_node_child_process7, import_node_crypto11, import_node_fs16, import_node_path22, PINNED_DEVSY_AGENT_VERSION, SUPPORTED_DEVSY_MIN_VERSION, SUPPORTED_DEVSY_MAX_VERSION, SUPPORTED_DEVSY_RANGE, DEVSY_AGENT_SETUP_COMMAND, DEVSY_RELEASE_TAG_PREFIX, DEVSY_RELEASE_METADATA_BASE_URL, DEVSY_AGENT_ASSETS, DevsyAgentReadinessError, DEVSY_AGENT_CACHE_LOCK, DEVSY_AGENT_LOCK_WAIT_MS, DEVSY_AGENT_MANIFEST_FILE, DEVSY_AGENT_ASSET_NAMES, GITHUB_CLI_STDERR_TAIL_BYTES;
|
|
9508
9799
|
var init_devsy_agent = __esm({
|
|
9509
9800
|
"src/core/devsy-agent.ts"() {
|
|
9510
9801
|
"use strict";
|
|
@@ -9514,8 +9805,13 @@ var init_devsy_agent = __esm({
|
|
|
9514
9805
|
import_node_path22 = __toESM(require("path"));
|
|
9515
9806
|
init_file_lock();
|
|
9516
9807
|
init_router();
|
|
9517
|
-
|
|
9808
|
+
PINNED_DEVSY_AGENT_VERSION = "1.16.2";
|
|
9809
|
+
SUPPORTED_DEVSY_MIN_VERSION = "1.16.2";
|
|
9810
|
+
SUPPORTED_DEVSY_MAX_VERSION = "2.0.0";
|
|
9811
|
+
SUPPORTED_DEVSY_RANGE = `>=${SUPPORTED_DEVSY_MIN_VERSION} <${SUPPORTED_DEVSY_MAX_VERSION}`;
|
|
9518
9812
|
DEVSY_AGENT_SETUP_COMMAND = "devrouter setup --yes --workspace-runtime devsy";
|
|
9813
|
+
DEVSY_RELEASE_TAG_PREFIX = "v";
|
|
9814
|
+
DEVSY_RELEASE_METADATA_BASE_URL = "https://api.github.com/repos/devsy-org/devsy/releases/tags";
|
|
9519
9815
|
DEVSY_AGENT_ASSETS = [
|
|
9520
9816
|
{
|
|
9521
9817
|
githubAssetId: 529830010,
|
|
@@ -9541,6 +9837,8 @@ var init_devsy_agent = __esm({
|
|
|
9541
9837
|
};
|
|
9542
9838
|
DEVSY_AGENT_CACHE_LOCK = import_node_path22.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
|
|
9543
9839
|
DEVSY_AGENT_LOCK_WAIT_MS = 18e5;
|
|
9840
|
+
DEVSY_AGENT_MANIFEST_FILE = "manifest.json";
|
|
9841
|
+
DEVSY_AGENT_ASSET_NAMES = ["devsy-linux-arm64", "devsy-linux-amd64"];
|
|
9544
9842
|
GITHUB_CLI_STDERR_TAIL_BYTES = 4096;
|
|
9545
9843
|
}
|
|
9546
9844
|
});
|
|
@@ -13413,8 +13711,9 @@ function inspectNetworkProviderBinding(input2) {
|
|
|
13413
13711
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(providerContext)) throw new Error();
|
|
13414
13712
|
const contextArgs = ["--context", providerContext];
|
|
13415
13713
|
const version = read(provider, provider === "devsy" ? ["--version"] : ["version"]);
|
|
13416
|
-
const
|
|
13417
|
-
if (
|
|
13714
|
+
const parsedVersion = version.match(/\b(?:v)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/)?.[1];
|
|
13715
|
+
if (provider === "devsy" ? !isSupportedDevsyVersion(parsedVersion) : parsedVersion !== "0.6.15")
|
|
13716
|
+
throw new Error();
|
|
13418
13717
|
const definitions = object3(
|
|
13419
13718
|
JSON.parse(
|
|
13420
13719
|
read(
|
|
@@ -13480,6 +13779,7 @@ var init_network_provider_inspect = __esm({
|
|
|
13480
13779
|
import_node_child_process16 = require("child_process");
|
|
13481
13780
|
import_node_crypto16 = require("crypto");
|
|
13482
13781
|
init_devpod_environment();
|
|
13782
|
+
init_devsy_agent();
|
|
13483
13783
|
}
|
|
13484
13784
|
});
|
|
13485
13785
|
|
|
@@ -15255,6 +15555,9 @@ function holderAttribution(holder, repoPath) {
|
|
|
15255
15555
|
return attribution;
|
|
15256
15556
|
}
|
|
15257
15557
|
function remediationFor(holder, attribution) {
|
|
15558
|
+
if (holder.name === ROUTER_CONTAINER_NAME) {
|
|
15559
|
+
return "The shared devrouter router owns this port as a platform entrypoint and must keep running; change the consumer's fixed host binding, or route the dependency through devrouter's TCP protocol route, then run ensure again.";
|
|
15560
|
+
}
|
|
15258
15561
|
if (attribution.holderWorktreePath) {
|
|
15259
15562
|
return `Stop the holding workspace with 'devrouter stop ${attribution.holderWorktreePath}', or override the consumer's published host binding through its own configuration, then run ensure again.`;
|
|
15260
15563
|
}
|
|
@@ -15296,6 +15599,7 @@ var init_host_port_claims = __esm({
|
|
|
15296
15599
|
import_node_path40 = __toESM(require("path"));
|
|
15297
15600
|
init_devcontainer_profile();
|
|
15298
15601
|
init_network_effect_scope();
|
|
15602
|
+
init_router();
|
|
15299
15603
|
init_workspace();
|
|
15300
15604
|
init_workspace_ownership();
|
|
15301
15605
|
COMPOSE_RENDER_TIMEOUT_MS = 3e4;
|
|
@@ -20253,6 +20557,155 @@ var init_controller = __esm({
|
|
|
20253
20557
|
}
|
|
20254
20558
|
});
|
|
20255
20559
|
|
|
20560
|
+
// src/commands/capacity.ts
|
|
20561
|
+
var capacity_exports = {};
|
|
20562
|
+
__export(capacity_exports, {
|
|
20563
|
+
CapacityReconcileRefusal: () => CapacityReconcileRefusal,
|
|
20564
|
+
runCapacityCommand: () => runCapacityCommand
|
|
20565
|
+
});
|
|
20566
|
+
function shellQuote(value) {
|
|
20567
|
+
return "'" + value.split("'").join("'\\''") + "'";
|
|
20568
|
+
}
|
|
20569
|
+
function enumerateBindings() {
|
|
20570
|
+
try {
|
|
20571
|
+
return listUnsettledCapacityBindings();
|
|
20572
|
+
} catch {
|
|
20573
|
+
throw new CapacityReconcileRefusal("capacity-history-unprovable");
|
|
20574
|
+
}
|
|
20575
|
+
}
|
|
20576
|
+
async function observeDeclaredPools(directory) {
|
|
20577
|
+
const policy = readCapacityPolicy(directory);
|
|
20578
|
+
if (!policy) return [];
|
|
20579
|
+
const observed = [];
|
|
20580
|
+
const unresolved = [];
|
|
20581
|
+
for (const [runtimeDomain, domain] of Object.entries(policy.domains)) {
|
|
20582
|
+
if (domain.kind !== "runtime") continue;
|
|
20583
|
+
const controller = new AbortController();
|
|
20584
|
+
const timer = setTimeout(() => controller.abort(), POOL_PROBE_TIMEOUT_MS);
|
|
20585
|
+
try {
|
|
20586
|
+
const info = await readDockerCapacityInfo(domain.endpoint, controller.signal);
|
|
20587
|
+
if (info.ID !== domain.daemonId) throw new Error("Pool observation identity changed.");
|
|
20588
|
+
observed.push({
|
|
20589
|
+
daemonId: domain.daemonId,
|
|
20590
|
+
runtimeDomain,
|
|
20591
|
+
hostDomain: domain.hostDomain,
|
|
20592
|
+
hostChargeCeilingBytes: domain.hostChargeCeilingBytes
|
|
20593
|
+
});
|
|
20594
|
+
} catch {
|
|
20595
|
+
unresolved.push(runtimeDomain);
|
|
20596
|
+
} finally {
|
|
20597
|
+
clearTimeout(timer);
|
|
20598
|
+
}
|
|
20599
|
+
}
|
|
20600
|
+
if (unresolved.length > 0)
|
|
20601
|
+
throw new CapacityReconcileRefusal("capacity-pools-unresolved", { domains: unresolved });
|
|
20602
|
+
return observed;
|
|
20603
|
+
}
|
|
20604
|
+
function refusalLines(refusal) {
|
|
20605
|
+
const bindings = refusal.detail.bindings ?? [];
|
|
20606
|
+
const domains = refusal.detail.domains ?? [];
|
|
20607
|
+
switch (refusal.reason) {
|
|
20608
|
+
case "capacity-reconcile-confirmation-required":
|
|
20609
|
+
return "Refusing without --yes: reconciliation publishes a fresh empty capacity baseline.";
|
|
20610
|
+
case "capacity-history-intact":
|
|
20611
|
+
return "Capacity ledger history is intact or absent; there is no loss to reconcile.";
|
|
20612
|
+
case "capacity-charges-pending":
|
|
20613
|
+
return [
|
|
20614
|
+
"Capacity reconciliation is blocked by " + bindings.length + " unsettled capacity binding" + (bindings.length === 1 ? "" : "s") + ":",
|
|
20615
|
+
...bindings.map(
|
|
20616
|
+
(binding) => " - " + binding.environmentId + (binding.workspace ? " (workspace " + binding.workspace + ")" : "") + ": run: devrouter stop " + shellQuote(binding.repoPath)
|
|
20617
|
+
)
|
|
20618
|
+
].join("\n");
|
|
20619
|
+
case "capacity-pools-unresolved":
|
|
20620
|
+
return [
|
|
20621
|
+
"Runtime domain" + (domains.length === 1 ? "" : "s") + " could not be observed: " + domains.join(", ") + ".",
|
|
20622
|
+
"Start them so their pool ceiling can be recorded, or remove them from the capacity policy, then retry."
|
|
20623
|
+
].join("\n");
|
|
20624
|
+
case "capacity-ledger-unusable":
|
|
20625
|
+
return "The capacity ledger exists but cannot satisfy the surviving lifecycle evidence; preserve it and investigate before retrying.";
|
|
20626
|
+
case "capacity-history-unprovable":
|
|
20627
|
+
return "Capacity history cannot be proven; inspect the private lifecycle journals before retrying.";
|
|
20628
|
+
}
|
|
20629
|
+
}
|
|
20630
|
+
async function runCapacityCommand(method, options, dependencies = {}) {
|
|
20631
|
+
if (method !== "reconcile") throw new Error(`Unsupported capacity command: ${method}`);
|
|
20632
|
+
const directory = dependencies.directory ?? import_node_path50.default.join(DEVROUTER_HOME, "controller");
|
|
20633
|
+
const json = Boolean(options.json);
|
|
20634
|
+
const emit = (payload, human) => {
|
|
20635
|
+
process.stdout.write(json ? `${JSON.stringify(payload, null, 2)}
|
|
20636
|
+
` : `${human}
|
|
20637
|
+
`);
|
|
20638
|
+
};
|
|
20639
|
+
try {
|
|
20640
|
+
if (!options.yes)
|
|
20641
|
+
throw new CapacityReconcileRefusal("capacity-reconcile-confirmation-required");
|
|
20642
|
+
const store = dependencies.store ?? createLifecycleCapacityStore(directory);
|
|
20643
|
+
const preflight = store.inspect();
|
|
20644
|
+
if (preflight.kind === "intact" || preflight.kind === "pristine")
|
|
20645
|
+
throw new CapacityReconcileRefusal("capacity-history-intact");
|
|
20646
|
+
if (preflight.kind === "stale") throw new CapacityReconcileRefusal("capacity-ledger-unusable");
|
|
20647
|
+
const pending = enumerateBindings().bindings;
|
|
20648
|
+
if (pending.length > 0)
|
|
20649
|
+
throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings: pending });
|
|
20650
|
+
const pools = await (dependencies.observePools ?? observeDeclaredPools)(directory);
|
|
20651
|
+
let appeared = [];
|
|
20652
|
+
const outcome = store.reconcileLostHistory(
|
|
20653
|
+
(state) => {
|
|
20654
|
+
if (state.kind === "intact" || state.kind === "pristine")
|
|
20655
|
+
throw new CapacityReconcileRefusal("capacity-history-intact");
|
|
20656
|
+
if (state.kind === "stale") throw new CapacityReconcileRefusal("capacity-ledger-unusable");
|
|
20657
|
+
const { floor, bindings } = enumerateBindings();
|
|
20658
|
+
if (bindings.length > 0)
|
|
20659
|
+
throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings });
|
|
20660
|
+
return { revision: floor + 1, pools };
|
|
20661
|
+
},
|
|
20662
|
+
() => {
|
|
20663
|
+
appeared = enumerateBindings().bindings;
|
|
20664
|
+
return appeared.length === 0;
|
|
20665
|
+
}
|
|
20666
|
+
);
|
|
20667
|
+
if (!outcome.reconciled)
|
|
20668
|
+
throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings: appeared });
|
|
20669
|
+
emit(
|
|
20670
|
+
{ version: 1, ok: true, reconciled: true, revision: outcome.revision, unresolved: 0 },
|
|
20671
|
+
"Reconciled the lost capacity ledger: baseline revision " + outcome.revision + ", no charges carried. No journal-visible charge remained. Charges that were never journal-bound are not recoverable from the journals."
|
|
20672
|
+
);
|
|
20673
|
+
} catch (error) {
|
|
20674
|
+
if (!(error instanceof CapacityReconcileRefusal)) throw error;
|
|
20675
|
+
process.exitCode = 1;
|
|
20676
|
+
emit(
|
|
20677
|
+
{
|
|
20678
|
+
version: 1,
|
|
20679
|
+
ok: false,
|
|
20680
|
+
reason: error.reason,
|
|
20681
|
+
...error.detail.bindings ? { bindings: error.detail.bindings } : {},
|
|
20682
|
+
...error.detail.domains ? { domains: error.detail.domains } : {}
|
|
20683
|
+
},
|
|
20684
|
+
refusalLines(error)
|
|
20685
|
+
);
|
|
20686
|
+
}
|
|
20687
|
+
}
|
|
20688
|
+
var import_node_path50, POOL_PROBE_TIMEOUT_MS, CapacityReconcileRefusal;
|
|
20689
|
+
var init_capacity = __esm({
|
|
20690
|
+
"src/commands/capacity.ts"() {
|
|
20691
|
+
"use strict";
|
|
20692
|
+
import_node_path50 = __toESM(require("path"));
|
|
20693
|
+
init_capacity_docker_probe();
|
|
20694
|
+
init_capacity_policy();
|
|
20695
|
+
init_reliability_operation_store();
|
|
20696
|
+
init_router();
|
|
20697
|
+
POOL_PROBE_TIMEOUT_MS = 3e3;
|
|
20698
|
+
CapacityReconcileRefusal = class extends Error {
|
|
20699
|
+
constructor(reason, detail = {}) {
|
|
20700
|
+
super(reason);
|
|
20701
|
+
this.reason = reason;
|
|
20702
|
+
this.detail = detail;
|
|
20703
|
+
this.name = "CapacityReconcileRefusal";
|
|
20704
|
+
}
|
|
20705
|
+
};
|
|
20706
|
+
}
|
|
20707
|
+
});
|
|
20708
|
+
|
|
20256
20709
|
// src/core/agents-md.ts
|
|
20257
20710
|
function buildDevrouterSection() {
|
|
20258
20711
|
return [
|
|
@@ -20292,13 +20745,13 @@ function replaceDevrouterSection(content) {
|
|
|
20292
20745
|
return `${content.slice(0, start)}${buildDevrouterSection()}${content.slice(end)}`;
|
|
20293
20746
|
}
|
|
20294
20747
|
function writeRepoFile(repoPath, relPath, content) {
|
|
20295
|
-
const absolutePath = (0,
|
|
20296
|
-
(0, import_node_fs38.mkdirSync)((0,
|
|
20748
|
+
const absolutePath = (0, import_node_path51.join)(repoPath, relPath);
|
|
20749
|
+
(0, import_node_fs38.mkdirSync)((0, import_node_path51.dirname)(absolutePath), { recursive: true });
|
|
20297
20750
|
(0, import_node_fs38.writeFileSync)(absolutePath, content, "utf-8");
|
|
20298
20751
|
return absolutePath;
|
|
20299
20752
|
}
|
|
20300
20753
|
function ensureAgentsMdSection(repoPath) {
|
|
20301
|
-
const filePath = (0,
|
|
20754
|
+
const filePath = (0, import_node_path51.join)(repoPath, AGENTS_MD);
|
|
20302
20755
|
if ((0, import_node_fs38.existsSync)(filePath)) {
|
|
20303
20756
|
const content = (0, import_node_fs38.readFileSync)(filePath, "utf-8");
|
|
20304
20757
|
if (content.includes(DEVROUTER_SENTINEL)) {
|
|
@@ -20323,12 +20776,12 @@ function ensureSkillFile(repoPath) {
|
|
|
20323
20776
|
const filePath = writeRepoFile(repoPath, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT);
|
|
20324
20777
|
return { path: filePath, written: true };
|
|
20325
20778
|
}
|
|
20326
|
-
var import_node_fs38,
|
|
20779
|
+
var import_node_fs38, import_node_path51, DEVROUTER_SENTINEL, DEVROUTER_END_SENTINEL, LEGACY_DEVROUTER_SECTION_END, AGENTS_MD, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT;
|
|
20327
20780
|
var init_agents_md = __esm({
|
|
20328
20781
|
"src/core/agents-md.ts"() {
|
|
20329
20782
|
"use strict";
|
|
20330
20783
|
import_node_fs38 = require("fs");
|
|
20331
|
-
|
|
20784
|
+
import_node_path51 = require("path");
|
|
20332
20785
|
DEVROUTER_SENTINEL = "<!-- devrouter -->";
|
|
20333
20786
|
DEVROUTER_END_SENTINEL = "<!-- /devrouter -->";
|
|
20334
20787
|
LEGACY_DEVROUTER_SECTION_END = "- `devrouter ls`";
|
|
@@ -20570,7 +21023,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
20570
21023
|
- **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.
|
|
20571
21024
|
- **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.
|
|
20572
21025
|
- **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>\`.
|
|
20573
|
-
- **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 workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. For Devsy, run \`devrouter setup --yes --workspace-runtime devsy\` once so Devrouter acquires and verifies the
|
|
21026
|
+
- **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 workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. For Devsy, run \`devrouter setup --yes --workspace-runtime devsy\` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter supports Devsy releases in the range >=1.16.2 <2.0.0 and refuses a release whose published asset has no SHA-256 digest. \`doctor\` reports \`ready\`, \`missing\`, \`stale\`, or \`invalid\` without network access, and \`ensure\` fails before the provider queue when readiness is not \`ready\`. An explicit \`DEVSY_AGENT_BINARY\` remains authoritative and must match the official asset for the installed release.
|
|
20574
21027
|
- **Interrupted lifecycle**: ensure reconciles an interrupted or never-dispatched ensure after positive worker drainage and automatically repairs retained degraded runtimes. Unknown arbitrary exec is never replayed; explicit stop reconciles that uncertainty. Preserve journals and retained configuration when evidence is unavailable. This does not provide OOM prevention or capacity-managed automatic recovery.
|
|
20575
21028
|
- **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.
|
|
20576
21029
|
- **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.
|
|
@@ -20631,6 +21084,8 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
20631
21084
|
- \`devrouter profile plan --repo <path> [--profile <selection>] --contract <repo-relative-yaml> [--output <path>] [--json]\`: validate repository-owned resource policy and emit literal bindings without runtime access
|
|
20632
21085
|
- \`devrouter stop [path] [--delete] [--json]\`: stop the exact workspace runtime and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
|
|
20633
21086
|
- \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running workspace runtime
|
|
21087
|
+
- \`devrouter harness gate [--repo <path>] [--wait-budget-ms <ms>] [--json]\`: defer one harness tool call until this checkout's lifecycle phase settles; prints the harness permission decision
|
|
21088
|
+
- \`devrouter capacity reconcile --yes [--json]\`: replace a provably absent capacity ledger with a fresh empty baseline once no journal-visible charge remains
|
|
20634
21089
|
- \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
|
|
20635
21090
|
- \`devrouter status\`: router/container/network/TLS health
|
|
20636
21091
|
- \`devrouter doctor [--repo .]\`: deep diagnostics (global + repo)
|
|
@@ -20658,6 +21113,61 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
|
|
|
20658
21113
|
- \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
|
|
20659
21114
|
- \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
|
|
20660
21115
|
|
|
21116
|
+
## Agent harness gating
|
|
21117
|
+
|
|
21118
|
+
A managed consumer can gate agent tool calls so a command never starts while the
|
|
21119
|
+
checkout's environment is mid-transition. Wire \`devrouter harness gate\` as a
|
|
21120
|
+
\`PreToolUse\` hook. Claude Code reads it from a settings file:
|
|
21121
|
+
|
|
21122
|
+
{
|
|
21123
|
+
"hooks": {
|
|
21124
|
+
"PreToolUse": [
|
|
21125
|
+
{ "matcher": "Bash", "hooks": [
|
|
21126
|
+
{ "type": "command", "command": "devrouter harness gate", "timeout": 90 }
|
|
21127
|
+
] }
|
|
21128
|
+
]
|
|
21129
|
+
}
|
|
21130
|
+
}
|
|
21131
|
+
|
|
21132
|
+
The Codex CLI reads the same hook from \`$CODEX_HOME/hooks.json\`, where the shell
|
|
21133
|
+
tool is named \`exec_command\`:
|
|
21134
|
+
|
|
21135
|
+
{
|
|
21136
|
+
"hooks": {
|
|
21137
|
+
"PreToolUse": [
|
|
21138
|
+
{ "matcher": "exec_command|Bash|shell", "hooks": [
|
|
21139
|
+
{ "type": "command", "command": "devrouter harness gate", "timeout": 180 }
|
|
21140
|
+
] }
|
|
21141
|
+
]
|
|
21142
|
+
}
|
|
21143
|
+
}
|
|
21144
|
+
|
|
21145
|
+
The gate identifies the requesting harness from its payload and answers in that
|
|
21146
|
+
harness's accepted shape: a refusal is a \`deny\` in both, while an allowed call is
|
|
21147
|
+
\`permissionDecision: allow\` for Claude Code and Codex receives a bare completion
|
|
21148
|
+
carrying the same guidance as \`additionalContext\`, because Codex rejects an
|
|
21149
|
+
\`allow\` decision as unsupported hook output.
|
|
21150
|
+
|
|
21151
|
+
|
|
21152
|
+
the checkout's durable phase is \`queued\`, \`starting\`, \`verifying\`, \`recovering\`
|
|
21153
|
+
or \`stopping\`, and prints the harness permission decision. The deferral consumes
|
|
21154
|
+
no model turns because the harness is blocked on the hook. \`--wait-budget-ms\`
|
|
21155
|
+
bounds the wait (default 30000); a phase that outlasts it returns one \`deny\` that
|
|
21156
|
+
names the phase and asks the agent not to retry automatically. Lifecycle
|
|
21157
|
+
commands (\`devrouter ...\`) always pass through, and unreadable journal evidence
|
|
21158
|
+
is allowed rather than blocking the agent. Set the harness hook \`timeout\` above
|
|
21159
|
+
the wait budget: an overrunning hook is not honored, and the tool proceeds under
|
|
21160
|
+
the harness's normal permission rules. Use \`--json\` for the devrouter decision
|
|
21161
|
+
envelope with wait metrics.
|
|
21162
|
+
|
|
21163
|
+
Gated calls are keyed by the harness \`tool_use_id\` and recorded durably. If the
|
|
21164
|
+
harness re-delivers a call it already granted or cancelled, the gate returns one
|
|
21165
|
+
refusal naming the earlier decision instead of waiting again, because that call
|
|
21166
|
+
may already have run; the agent should verify it and issue a new call. Entries
|
|
21167
|
+
expire after 24 hours, the ledger keeps the newest 64 per checkout, and an
|
|
21168
|
+
unreadable or unwritable ledger never blocks the agent. Payloads without a
|
|
21169
|
+
\`tool_use_id\` keep the wait-only behavior.
|
|
21170
|
+
|
|
20661
21171
|
## Validation workflow
|
|
20662
21172
|
|
|
20663
21173
|
For devcontainer onboarding:
|
|
@@ -20863,12 +21373,13 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
20863
21373
|
`- 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.`,
|
|
20864
21374
|
"- 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.",
|
|
20865
21375
|
"- 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, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
|
|
21376
|
+
"- Managed agent harnesses can defer tool calls through `devrouter harness gate` wired as a `PreToolUse` hook (Claude Code settings, or `$CODEX_HOME/hooks.json` for the Codex CLI with the `exec_command` shell tool): it waits inside the hook process while the checkout's durable phase is queued, starting, verifying, recovering or stopping (no model turns), allows the tool once the phase settles, and refuses once with recovery guidance when `--wait-budget-ms` (default 30000) runs out. It identifies the requesting harness from its payload and answers in that harness's accepted shape, because Codex rejects an `allow` decision. It also records the gated call by harness tool-use id, so a call re-delivered after a granted or cancelled wait returns one refusal instead of repeating a command that may already have run. Lifecycle commands pass through, unreadable journal evidence is allowed rather than blocking the agent, and the hook timeout must exceed the wait budget.",
|
|
20866
21377
|
"- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
|
|
20867
21378
|
"- Busy ensure and exec wait up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
|
|
20868
21379
|
"- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not detect or prevent OOM; bounded automatic recovery runs only when the operator enables the capacity recovery policy.",
|
|
20869
21380
|
"- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged. An optional recovery block adds bounded automatic recovery: one journal-admitted corrective ensure per positively failed required capability, bounded by per-scope restart counts, an aggregate corrective-action cap and a rolling observation window. It is inert unless enabled.",
|
|
20870
21381
|
"- A lock that cannot prove its owner fails closed with the failing inspection stage, the exact lock path, and a portable reproduction command; run the canonical command in a permitted host context. No identity fallback is attempted, and `devrouter controller` prints the underlying cause on stderr while keeping the stable `controller-unavailable` JSON on stdout.",
|
|
20871
|
-
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the
|
|
21382
|
+
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter supports releases in the range >=1.16.2 <2.0.0, verifies a release's published asset against the SHA-256 digest GitHub reports, and never injects a release without one. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access; a supported release with no recorded manifest is `ready` with a drift warning and no injected agent until `setup` records one, while a version outside the supported range or unparseable stays `stale`. `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match the official asset for the installed supported release. `ensure` and `stop` stream bounded phase progress on stderr, so parse command JSON from stdout only.",
|
|
20872
21383
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
20873
21384
|
"- 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.",
|
|
20874
21385
|
"- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
|
|
@@ -20897,6 +21408,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
20897
21408
|
"- `devrouter doctor --repo <REPO_PATH>` warns on risky pre-wrapper DB assignments before `run --` for host apps that depend on postgres.",
|
|
20898
21409
|
"- With TLS enabled, `devrouter doctor --repo <REPO_PATH>` also warns on cert SAN mismatches for configured hosts (`repo.tls-host-coverage`).",
|
|
20899
21410
|
"- `devrouter doctor --repo <REPO_PATH>` errors on configured fixed host-port bindings already held by running containers (`repo.host-port-claims`) and warns when that comparison is unavailable.",
|
|
21411
|
+
"- `devrouter doctor` compares every `devrouter` executable on PATH with the running CLI (`global.cli-path`) and warns when a shell would silently run a different version.",
|
|
20900
21412
|
"- Robust one-shot migrate example (argv-safe, no nested shell quoting): `devrouter app exec <name> --repo <REPO_PATH> --yes -- infisical run --projectId <id> --env=<env> -- pnpm payload migrate`.",
|
|
20901
21413
|
"- Robust one-shot seed example: `devrouter app exec <name> --repo <REPO_PATH> --yes -- infisical run --projectId <id> --env=<env> -- pnpm payload seed`.",
|
|
20902
21414
|
"- Environment probe/debug example (run before migrations): `devrouter app exec <name> --repo <REPO_PATH> --yes -- printenv DB_URL DB_HOST DB_PORT DB_SHADOW_URL`.",
|
|
@@ -21571,7 +22083,7 @@ var init_init = __esm({
|
|
|
21571
22083
|
});
|
|
21572
22084
|
|
|
21573
22085
|
// src/core/upgrade.ts
|
|
21574
|
-
function
|
|
22086
|
+
function parseSemver2(version) {
|
|
21575
22087
|
const normalized = version.trim();
|
|
21576
22088
|
const match = normalized.match(SEMVER_RE);
|
|
21577
22089
|
if (!match) {
|
|
@@ -21584,12 +22096,12 @@ function parseSemver(version) {
|
|
|
21584
22096
|
};
|
|
21585
22097
|
}
|
|
21586
22098
|
function normalizeVersion(version) {
|
|
21587
|
-
const parsed =
|
|
22099
|
+
const parsed = parseSemver2(version);
|
|
21588
22100
|
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
21589
22101
|
}
|
|
21590
22102
|
function compareVersions(a, b) {
|
|
21591
|
-
const left =
|
|
21592
|
-
const right =
|
|
22103
|
+
const left = parseSemver2(a);
|
|
22104
|
+
const right = parseSemver2(b);
|
|
21593
22105
|
if (left.major !== right.major) {
|
|
21594
22106
|
return left.major - right.major;
|
|
21595
22107
|
}
|
|
@@ -21636,7 +22148,7 @@ function readPromptDirectory(promptDirPath) {
|
|
|
21636
22148
|
continue;
|
|
21637
22149
|
}
|
|
21638
22150
|
const version = normalizeVersion(basename);
|
|
21639
|
-
const promptPath =
|
|
22151
|
+
const promptPath = import_node_path52.default.join(promptDirPath, file.name);
|
|
21640
22152
|
const prompt = import_node_fs39.default.readFileSync(promptPath, "utf-8").trim();
|
|
21641
22153
|
releases.push({ version, prompt, promptPath });
|
|
21642
22154
|
}
|
|
@@ -21658,17 +22170,17 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
|
|
|
21658
22170
|
}
|
|
21659
22171
|
function resolvePromptDirectory(explicitPath) {
|
|
21660
22172
|
if (explicitPath) {
|
|
21661
|
-
return
|
|
22173
|
+
return import_node_path52.default.resolve(explicitPath);
|
|
21662
22174
|
}
|
|
21663
|
-
const entryFile = process.argv[1] ?
|
|
21664
|
-
const entryDir =
|
|
21665
|
-
const resolvedEntryDir = import_node_fs39.default.existsSync(entryFile) ?
|
|
22175
|
+
const entryFile = process.argv[1] ? import_node_path52.default.resolve(process.argv[1]) : __filename;
|
|
22176
|
+
const entryDir = import_node_path52.default.dirname(entryFile);
|
|
22177
|
+
const resolvedEntryDir = import_node_fs39.default.existsSync(entryFile) ? import_node_path52.default.dirname(import_node_fs39.default.realpathSync(entryFile)) : entryDir;
|
|
21666
22178
|
const candidates = [
|
|
21667
|
-
|
|
21668
|
-
|
|
21669
|
-
|
|
21670
|
-
|
|
21671
|
-
|
|
22179
|
+
import_node_path52.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
22180
|
+
import_node_path52.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
|
|
22181
|
+
import_node_path52.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
|
|
22182
|
+
import_node_path52.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
|
|
22183
|
+
import_node_path52.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
|
|
21672
22184
|
];
|
|
21673
22185
|
const existing = candidates.find((candidate) => import_node_fs39.default.existsSync(candidate));
|
|
21674
22186
|
return existing ?? candidates[0];
|
|
@@ -21686,12 +22198,12 @@ function loadUpgradeCatalog(options = {}) {
|
|
|
21686
22198
|
releases
|
|
21687
22199
|
};
|
|
21688
22200
|
}
|
|
21689
|
-
var import_node_fs39,
|
|
22201
|
+
var import_node_fs39, import_node_path52, SEMVER_RE, UPGRADE_PROMPTS_DIR;
|
|
21690
22202
|
var init_upgrade = __esm({
|
|
21691
22203
|
"src/core/upgrade.ts"() {
|
|
21692
22204
|
"use strict";
|
|
21693
22205
|
import_node_fs39 = __toESM(require("fs"));
|
|
21694
|
-
|
|
22206
|
+
import_node_path52 = __toESM(require("path"));
|
|
21695
22207
|
init_repo_config();
|
|
21696
22208
|
SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
21697
22209
|
UPGRADE_PROMPTS_DIR = "upgrade-prompts";
|
|
@@ -21926,7 +22438,7 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
|
|
|
21926
22438
|
details: conflicts.map(
|
|
21927
22439
|
(conflict) => `${conflict.service}: ${conflict.hostIp ?? "*"}:${conflict.hostPort}/${conflict.protocol} held by '${conflict.holderContainer}'` + (conflict.holderWorkspace ? ` (workspace '${conflict.holderWorkspace}')` : "")
|
|
21928
22440
|
).join("; "),
|
|
21929
|
-
suggestion: "Stop the holding workspace or change the consumer's own published binding, then run ensure again. Devrouter never rewrites consumer-declared host bindings."
|
|
22441
|
+
suggestion: conflicts.some((conflict) => conflict.holderContainer === ROUTER_CONTAINER_NAME) ? "The shared devrouter router owns its entrypoint ports and must keep running; change the consumer's own published binding or route that dependency through devrouter's TCP protocol route, then run ensure again. Devrouter never rewrites consumer-declared host bindings." : "Stop the holding workspace or change the consumer's own published binding, then run ensure again. Devrouter never rewrites consumer-declared host bindings."
|
|
21930
22442
|
};
|
|
21931
22443
|
} catch (error) {
|
|
21932
22444
|
return {
|
|
@@ -21939,11 +22451,11 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
|
|
|
21939
22451
|
}
|
|
21940
22452
|
}
|
|
21941
22453
|
function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
21942
|
-
const devcontainerDir =
|
|
22454
|
+
const devcontainerDir = import_node_path53.default.join(repoPath, ".devcontainer");
|
|
21943
22455
|
if (!import_node_fs40.default.existsSync(devcontainerDir)) {
|
|
21944
22456
|
return [];
|
|
21945
22457
|
}
|
|
21946
|
-
const compose = inspectCompose(
|
|
22458
|
+
const compose = inspectCompose(import_node_path53.default.join(devcontainerDir, "docker-compose.yml"), workspace);
|
|
21947
22459
|
const checks = [];
|
|
21948
22460
|
if (compose.parseError) {
|
|
21949
22461
|
checks.push({
|
|
@@ -21970,7 +22482,7 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
21970
22482
|
details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
|
|
21971
22483
|
suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
|
|
21972
22484
|
});
|
|
21973
|
-
const dockerfilePath =
|
|
22485
|
+
const dockerfilePath = import_node_path53.default.join(devcontainerDir, "Dockerfile");
|
|
21974
22486
|
const dockerfileExists = import_node_fs40.default.existsSync(dockerfilePath);
|
|
21975
22487
|
const dockerfile = dockerfileExists ? import_node_fs40.default.readFileSync(dockerfilePath, "utf-8") : "";
|
|
21976
22488
|
const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
|
|
@@ -22030,17 +22542,18 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
|
22030
22542
|
});
|
|
22031
22543
|
return checks;
|
|
22032
22544
|
}
|
|
22033
|
-
var import_node_fs40,
|
|
22545
|
+
var import_node_fs40, import_node_path53, import_yaml5;
|
|
22034
22546
|
var init_devcontainer_diagnostics = __esm({
|
|
22035
22547
|
"src/core/devcontainer-diagnostics.ts"() {
|
|
22036
22548
|
"use strict";
|
|
22037
22549
|
import_node_fs40 = __toESM(require("fs"));
|
|
22038
|
-
|
|
22550
|
+
import_node_path53 = __toESM(require("path"));
|
|
22039
22551
|
import_yaml5 = __toESM(require("yaml"));
|
|
22040
22552
|
init_devcontainer_profile();
|
|
22041
22553
|
init_host_port_claims();
|
|
22042
22554
|
init_managed_post_start();
|
|
22043
22555
|
init_repo_config();
|
|
22556
|
+
init_router();
|
|
22044
22557
|
init_workspace();
|
|
22045
22558
|
init_workspace_ownership();
|
|
22046
22559
|
}
|
|
@@ -22535,7 +23048,7 @@ function parseMinimumNodeMajor(value) {
|
|
|
22535
23048
|
return Number(match[1]);
|
|
22536
23049
|
}
|
|
22537
23050
|
function readPackageJson(repoPath) {
|
|
22538
|
-
const packagePath =
|
|
23051
|
+
const packagePath = import_node_path54.default.join(repoPath, "package.json");
|
|
22539
23052
|
if (!import_node_fs43.default.existsSync(packagePath)) {
|
|
22540
23053
|
return void 0;
|
|
22541
23054
|
}
|
|
@@ -22610,16 +23123,18 @@ function devsyAgentCheck(inspection) {
|
|
|
22610
23123
|
`state=${inspection.state}`,
|
|
22611
23124
|
`source=${inspection.source}`,
|
|
22612
23125
|
...inspection.installedVersion ? [`version=${inspection.installedVersion}`] : [],
|
|
22613
|
-
...inspection.asset ? [`asset=${inspection.asset.name}`] : []
|
|
23126
|
+
...inspection.asset ? [`asset=${inspection.asset.name}`] : [],
|
|
23127
|
+
...inspection.manifestOrigin ? [`manifest=${inspection.manifestOrigin}`] : [],
|
|
23128
|
+
`supported=${SUPPORTED_DEVSY_RANGE}`
|
|
22614
23129
|
].join(", ");
|
|
22615
23130
|
if (inspection.state === "ready") {
|
|
22616
23131
|
if (inspection.drift) {
|
|
22617
23132
|
return {
|
|
22618
23133
|
id: "global.devsy-agent",
|
|
22619
23134
|
level: "warn",
|
|
22620
|
-
summary: `Devsy ${inspection.drift.installed}
|
|
23135
|
+
summary: `Devsy ${inspection.drift.installed} governs its own agent; Devrouter injects no agent for it.`,
|
|
22621
23136
|
details,
|
|
22622
|
-
suggestion: `
|
|
23137
|
+
suggestion: `Run: devrouter setup --yes --workspace-runtime devsy to verify and inject the official Devsy ${inspection.drift.installed} agent for managed starts`
|
|
22623
23138
|
};
|
|
22624
23139
|
}
|
|
22625
23140
|
return {
|
|
@@ -22629,7 +23144,7 @@ function devsyAgentCheck(inspection) {
|
|
|
22629
23144
|
details
|
|
22630
23145
|
};
|
|
22631
23146
|
}
|
|
22632
|
-
const summary = inspection.state === "missing" ? "
|
|
23147
|
+
const summary = inspection.state === "missing" ? "No verified Devsy agent is cached for the installed Devsy version." : inspection.state === "stale" ? "The installed Devsy version is outside the supported range." : "Devsy agent source is invalid.";
|
|
22633
23148
|
return {
|
|
22634
23149
|
id: "global.devsy-agent",
|
|
22635
23150
|
level: "error",
|
|
@@ -22638,6 +23153,125 @@ function devsyAgentCheck(inspection) {
|
|
|
22638
23153
|
suggestion: devsyAgentRepairSuggestion(inspection)
|
|
22639
23154
|
};
|
|
22640
23155
|
}
|
|
23156
|
+
function realpathOrUndefined(value) {
|
|
23157
|
+
if (!value) {
|
|
23158
|
+
return void 0;
|
|
23159
|
+
}
|
|
23160
|
+
try {
|
|
23161
|
+
return import_node_fs43.default.realpathSync(value);
|
|
23162
|
+
} catch {
|
|
23163
|
+
return void 0;
|
|
23164
|
+
}
|
|
23165
|
+
}
|
|
23166
|
+
function isExecutableFile(filePath) {
|
|
23167
|
+
try {
|
|
23168
|
+
if (!import_node_fs43.default.statSync(filePath).isFile()) {
|
|
23169
|
+
return false;
|
|
23170
|
+
}
|
|
23171
|
+
import_node_fs43.default.accessSync(filePath, import_node_fs43.default.constants.X_OK);
|
|
23172
|
+
return true;
|
|
23173
|
+
} catch {
|
|
23174
|
+
return false;
|
|
23175
|
+
}
|
|
23176
|
+
}
|
|
23177
|
+
function displayPath(filePath) {
|
|
23178
|
+
const home = process.env.HOME;
|
|
23179
|
+
if (!home || home.length < 2 || !filePath.startsWith(`${home}${import_node_path54.default.sep}`)) {
|
|
23180
|
+
return filePath;
|
|
23181
|
+
}
|
|
23182
|
+
return `~${filePath.slice(home.length)}`;
|
|
23183
|
+
}
|
|
23184
|
+
function probeInstalledCliVersion(executable) {
|
|
23185
|
+
const result = (0, import_node_child_process29.spawnSync)(executable, ["-V"], {
|
|
23186
|
+
encoding: "utf-8",
|
|
23187
|
+
timeout: CLI_PATH_PROBE_TIMEOUT_MS
|
|
23188
|
+
});
|
|
23189
|
+
if (result.error) {
|
|
23190
|
+
return void 0;
|
|
23191
|
+
}
|
|
23192
|
+
return INSTALLED_CLI_VERSION_RE.exec(outputFromResult(result) ?? "")?.[1];
|
|
23193
|
+
}
|
|
23194
|
+
function buildCliPathCheck(options = {}) {
|
|
23195
|
+
const id5 = "global.cli-path";
|
|
23196
|
+
const runningVersion = options.runningVersion ?? (true ? "0.1.0" : "0.0.0-dev");
|
|
23197
|
+
if (runningVersion === "0.0.0-dev") {
|
|
23198
|
+
return {
|
|
23199
|
+
id: id5,
|
|
23200
|
+
level: "ok",
|
|
23201
|
+
summary: "Devrouter runs from an unstamped build, so PATH installs are not compared.",
|
|
23202
|
+
details: "installedVersion=unknown; comparison=skipped"
|
|
23203
|
+
};
|
|
23204
|
+
}
|
|
23205
|
+
const runningEntry = realpathOrUndefined(options.runningEntry ?? process.argv[1]);
|
|
23206
|
+
const probeVersion = options.probeVersion ?? probeInstalledCliVersion;
|
|
23207
|
+
const entries = [];
|
|
23208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23209
|
+
for (const dir of (options.pathValue ?? process.env.PATH ?? "").split(import_node_path54.default.delimiter)) {
|
|
23210
|
+
if (dir.length === 0) {
|
|
23211
|
+
continue;
|
|
23212
|
+
}
|
|
23213
|
+
const executable = import_node_path54.default.join(dir, "devrouter");
|
|
23214
|
+
if (!isExecutableFile(executable)) {
|
|
23215
|
+
continue;
|
|
23216
|
+
}
|
|
23217
|
+
const resolved = realpathOrUndefined(executable) ?? executable;
|
|
23218
|
+
if (seen.has(resolved)) {
|
|
23219
|
+
continue;
|
|
23220
|
+
}
|
|
23221
|
+
seen.add(resolved);
|
|
23222
|
+
const running = runningEntry !== void 0 && resolved === runningEntry;
|
|
23223
|
+
entries.push({
|
|
23224
|
+
executable,
|
|
23225
|
+
running,
|
|
23226
|
+
version: running ? runningVersion : probeVersion(executable)
|
|
23227
|
+
});
|
|
23228
|
+
}
|
|
23229
|
+
const shellInstall = entries[0];
|
|
23230
|
+
if (!shellInstall) {
|
|
23231
|
+
return {
|
|
23232
|
+
id: id5,
|
|
23233
|
+
level: "ok",
|
|
23234
|
+
summary: "No devrouter executable is on PATH.",
|
|
23235
|
+
details: `runningVersion=${runningVersion}`
|
|
23236
|
+
};
|
|
23237
|
+
}
|
|
23238
|
+
const details = entries.slice(0, 4).map(
|
|
23239
|
+
(entry) => `${displayPath(entry.executable)}=${entry.version ?? "unknown"}${entry.running ? " (running)" : ""}`
|
|
23240
|
+
).join(", ");
|
|
23241
|
+
let newestVersion;
|
|
23242
|
+
for (const entry of entries) {
|
|
23243
|
+
if (entry.version === void 0) {
|
|
23244
|
+
continue;
|
|
23245
|
+
}
|
|
23246
|
+
if (newestVersion === void 0 || compareSemver(entry.version, newestVersion) > 0) {
|
|
23247
|
+
newestVersion = entry.version;
|
|
23248
|
+
}
|
|
23249
|
+
}
|
|
23250
|
+
if (newestVersion !== void 0 && compareSemver(newestVersion, runningVersion) > 0) {
|
|
23251
|
+
return {
|
|
23252
|
+
id: id5,
|
|
23253
|
+
level: "warn",
|
|
23254
|
+
summary: `Another Devrouter install on PATH (${newestVersion}) is newer than the running CLI (${runningVersion}).`,
|
|
23255
|
+
details,
|
|
23256
|
+
suggestion: `Update the installs so one version serves every shell: npm install -g @devrouter/cli@${newestVersion}`
|
|
23257
|
+
};
|
|
23258
|
+
}
|
|
23259
|
+
if (!shellInstall.running && shellInstall.version !== runningVersion) {
|
|
23260
|
+
return {
|
|
23261
|
+
id: id5,
|
|
23262
|
+
level: "warn",
|
|
23263
|
+
summary: `devrouter on PATH resolves to a different install (${shellInstall.version ?? "unknown version"}) than the running CLI (${runningVersion}).`,
|
|
23264
|
+
details,
|
|
23265
|
+
suggestion: `Align the installs so shell commands run the same CLI: npm install -g @devrouter/cli@${runningVersion}`
|
|
23266
|
+
};
|
|
23267
|
+
}
|
|
23268
|
+
return {
|
|
23269
|
+
id: id5,
|
|
23270
|
+
level: "ok",
|
|
23271
|
+
summary: "Devrouter installs on PATH match the running CLI.",
|
|
23272
|
+
details: `${details}; runningVersion=${runningVersion}`
|
|
23273
|
+
};
|
|
23274
|
+
}
|
|
22641
23275
|
function buildGlobalToolChecks(repoPath) {
|
|
22642
23276
|
const previous = process.env[DEVSY_DISABLE_TELEMETRY];
|
|
22643
23277
|
process.env[DEVSY_DISABLE_TELEMETRY] = "true";
|
|
@@ -22730,19 +23364,23 @@ function buildGlobalToolChecksWithoutTelemetry(repoPath) {
|
|
|
22730
23364
|
details: configProblems.length > 0 ? [configDetails, ...configProblems].join(" ") : configDetails,
|
|
22731
23365
|
suggestion: configProblems.length > 0 ? "Run: devrouter setup --yes --workspace-runtime <devpod|devsy>" : void 0
|
|
22732
23366
|
});
|
|
23367
|
+
checks.push(buildCliPathCheck());
|
|
22733
23368
|
checks.push(nodeToolchainCheck(repoPath));
|
|
22734
23369
|
return checks;
|
|
22735
23370
|
}
|
|
22736
|
-
var import_node_child_process29, import_node_fs43,
|
|
23371
|
+
var import_node_child_process29, import_node_fs43, import_node_path54, DEVSY_DISABLE_TELEMETRY, CLI_PATH_PROBE_TIMEOUT_MS, INSTALLED_CLI_VERSION_RE;
|
|
22737
23372
|
var init_tool_diagnostics = __esm({
|
|
22738
23373
|
"src/core/tool-diagnostics.ts"() {
|
|
22739
23374
|
"use strict";
|
|
22740
23375
|
import_node_child_process29 = require("child_process");
|
|
22741
23376
|
import_node_fs43 = __toESM(require("fs"));
|
|
22742
|
-
|
|
23377
|
+
import_node_path54 = __toESM(require("path"));
|
|
22743
23378
|
init_devsy_agent();
|
|
23379
|
+
init_repo_config();
|
|
22744
23380
|
init_workspace_runtime();
|
|
22745
23381
|
DEVSY_DISABLE_TELEMETRY = "DEVSY_DISABLE_TELEMETRY";
|
|
23382
|
+
CLI_PATH_PROBE_TIMEOUT_MS = 5e3;
|
|
23383
|
+
INSTALLED_CLI_VERSION_RE = /Installed CLI version:\s*(\S+)/;
|
|
22746
23384
|
}
|
|
22747
23385
|
});
|
|
22748
23386
|
|
|
@@ -22756,10 +23394,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
|
|
|
22756
23394
|
if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
|
|
22757
23395
|
const comparableRepo = comparableWorkspacePath(repoPath);
|
|
22758
23396
|
const comparableWorktree = comparableWorkspacePath(worktreePath);
|
|
22759
|
-
const localRoot =
|
|
23397
|
+
const localRoot = import_node_path55.default.join(comparableRepo, "trees") + import_node_path55.default.sep;
|
|
22760
23398
|
if (comparableWorktree.startsWith(localRoot)) return true;
|
|
22761
|
-
const legacyPrefix = `${
|
|
22762
|
-
return
|
|
23399
|
+
const legacyPrefix = `${import_node_path55.default.basename(comparableRepo)}-`;
|
|
23400
|
+
return import_node_path55.default.dirname(comparableWorktree) === import_node_path55.default.dirname(comparableRepo) && import_node_path55.default.basename(comparableWorktree).startsWith(legacyPrefix);
|
|
22763
23401
|
}
|
|
22764
23402
|
function previewActions(devpodStatus, routeCount, includeRecord) {
|
|
22765
23403
|
const actions = [
|
|
@@ -23011,11 +23649,11 @@ function applyWorkspaceGc(plan) {
|
|
|
23011
23649
|
candidates
|
|
23012
23650
|
};
|
|
23013
23651
|
}
|
|
23014
|
-
var
|
|
23652
|
+
var import_node_path55;
|
|
23015
23653
|
var init_workspace_gc = __esm({
|
|
23016
23654
|
"src/core/workspace-gc.ts"() {
|
|
23017
23655
|
"use strict";
|
|
23018
|
-
|
|
23656
|
+
import_node_path55 = __toESM(require("path"));
|
|
23019
23657
|
init_devpod_mutation();
|
|
23020
23658
|
init_devpod_workspaces();
|
|
23021
23659
|
init_host_routes();
|
|
@@ -23255,12 +23893,19 @@ async function buildDoctorReport(options = {}) {
|
|
|
23255
23893
|
summary: "Capacity ledger is consistent with available lifecycle history."
|
|
23256
23894
|
});
|
|
23257
23895
|
} catch (error) {
|
|
23896
|
+
const absent = (() => {
|
|
23897
|
+
try {
|
|
23898
|
+
return createLifecycleCapacityStore().inspect().kind === "absent";
|
|
23899
|
+
} catch {
|
|
23900
|
+
return false;
|
|
23901
|
+
}
|
|
23902
|
+
})();
|
|
23258
23903
|
addCheck(checks, {
|
|
23259
23904
|
id: "global.capacity-ledger",
|
|
23260
23905
|
level: "error",
|
|
23261
23906
|
summary: "Capacity ledger history is unavailable for safe admission.",
|
|
23262
23907
|
details: error instanceof CapacityHistoryError ? error.code : "capacity-history-unprovable",
|
|
23263
|
-
suggestion: "Preserve ledger and lifecycle records; restore verified history before retrying admission."
|
|
23908
|
+
suggestion: absent ? "Preserve ledger and lifecycle records; with every enrolled environment stopped, run: devrouter capacity reconcile --yes" : "Preserve ledger and lifecycle records; restore verified history before retrying admission."
|
|
23264
23909
|
});
|
|
23265
23910
|
}
|
|
23266
23911
|
let statusNextSteps = [];
|
|
@@ -23396,7 +24041,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
23396
24041
|
suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
|
|
23397
24042
|
});
|
|
23398
24043
|
}
|
|
23399
|
-
const cliVersion = true ? "0.0
|
|
24044
|
+
const cliVersion = true ? "0.1.0" : "0.0.0-dev";
|
|
23400
24045
|
const configVersion = config.devrouter?.version;
|
|
23401
24046
|
const installedKnown = cliVersion !== "0.0.0-dev";
|
|
23402
24047
|
const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
|
|
@@ -23432,7 +24077,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
23432
24077
|
(app) => app.docker.composeFiles.map((filePath) => ({
|
|
23433
24078
|
app: app.name,
|
|
23434
24079
|
filePath,
|
|
23435
|
-
absolutePath:
|
|
24080
|
+
absolutePath: import_node_path56.default.resolve(repo.path, filePath)
|
|
23436
24081
|
}))
|
|
23437
24082
|
).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
|
|
23438
24083
|
addCheck(checks, {
|
|
@@ -23477,7 +24122,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
23477
24122
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
23478
24123
|
app: app.name,
|
|
23479
24124
|
cwd: app.hostRun.cwd,
|
|
23480
|
-
absolutePath:
|
|
24125
|
+
absolutePath: import_node_path56.default.resolve(repo.path, app.hostRun.cwd)
|
|
23481
24126
|
})).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
|
|
23482
24127
|
addCheck(checks, {
|
|
23483
24128
|
id: "repo.host-cwd",
|
|
@@ -23622,12 +24267,12 @@ async function buildDoctorReport(options = {}) {
|
|
|
23622
24267
|
nextSteps
|
|
23623
24268
|
};
|
|
23624
24269
|
}
|
|
23625
|
-
var import_node_fs44,
|
|
24270
|
+
var import_node_fs44, import_node_path56, import_yaml6, POSTGRES_DEFAULTS;
|
|
23626
24271
|
var init_doctor = __esm({
|
|
23627
24272
|
"src/core/doctor.ts"() {
|
|
23628
24273
|
"use strict";
|
|
23629
24274
|
import_node_fs44 = __toESM(require("fs"));
|
|
23630
|
-
|
|
24275
|
+
import_node_path56 = __toESM(require("path"));
|
|
23631
24276
|
import_yaml6 = __toESM(require("yaml"));
|
|
23632
24277
|
init_capacity_store();
|
|
23633
24278
|
init_docker();
|
|
@@ -23775,7 +24420,7 @@ async function runSetup(options = {}) {
|
|
|
23775
24420
|
id: "global.devsy-agent",
|
|
23776
24421
|
summary: prepared.changed && asset ? `Downloaded and verified ${asset.name} for Devsy.` : prepared.source === "host" ? "The host Devsy CLI manages its own agent; no Devrouter-managed agent is required." : `Verified the ${prepared.source} Devsy agent source.`,
|
|
23777
24422
|
details: [
|
|
23778
|
-
`version=${
|
|
24423
|
+
`version=${prepared.version}`,
|
|
23779
24424
|
...asset ? [`asset=${asset.name}`] : [],
|
|
23780
24425
|
`source=${prepared.source}`,
|
|
23781
24426
|
`transport=${prepared.transport}`
|
|
@@ -24257,6 +24902,519 @@ var init_status2 = __esm({
|
|
|
24257
24902
|
}
|
|
24258
24903
|
});
|
|
24259
24904
|
|
|
24905
|
+
// src/core/harness-continuation.ts
|
|
24906
|
+
function harnessContinuationPath(repoRoot) {
|
|
24907
|
+
const key = (0, import_node_crypto26.createHash)("sha256").update(comparableWorkspacePath(repoRoot)).digest("hex");
|
|
24908
|
+
return import_node_path57.default.join(DEVROUTER_HOME, "harness", `${key}.json`);
|
|
24909
|
+
}
|
|
24910
|
+
function counter4(value) {
|
|
24911
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
24912
|
+
}
|
|
24913
|
+
function validEntry(value) {
|
|
24914
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
24915
|
+
const entry = value;
|
|
24916
|
+
return typeof entry.toolUseId === "string" && entry.toolUseId.length > 0 && entry.toolUseId.length <= MAX_TOOL_USE_ID_LENGTH && typeof entry.payloadSha256 === "string" && /^[0-9a-f]{64}$/.test(entry.payloadSha256) && (entry.state === "waiting" || entry.state === "granted" || entry.state === "refused" || entry.state === "interrupted") && typeof entry.phase === "string" && entry.phase.length <= MAX_PHASE_LENGTH && counter4(entry.budgetMs) && counter4(entry.claimedAtMs) && (entry.settledAtMs === void 0 || counter4(entry.settledAtMs)) && (entry.waitedMs === void 0 || counter4(entry.waitedMs));
|
|
24917
|
+
}
|
|
24918
|
+
function readLedgerSync(file, nowMs) {
|
|
24919
|
+
let stat;
|
|
24920
|
+
try {
|
|
24921
|
+
stat = import_node_fs45.default.lstatSync(file);
|
|
24922
|
+
} catch {
|
|
24923
|
+
return [];
|
|
24924
|
+
}
|
|
24925
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_LEDGER_BYTES) return [];
|
|
24926
|
+
if (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0) return [];
|
|
24927
|
+
let parsed;
|
|
24928
|
+
try {
|
|
24929
|
+
parsed = JSON.parse(import_node_fs45.default.readFileSync(file, "utf-8"));
|
|
24930
|
+
} catch {
|
|
24931
|
+
return [];
|
|
24932
|
+
}
|
|
24933
|
+
if (!Array.isArray(parsed)) return [];
|
|
24934
|
+
return parsed.filter(validEntry).filter(
|
|
24935
|
+
(entry) => nowMs - (entry.settledAtMs ?? entry.claimedAtMs) < HARNESS_CONTINUATION_TTL_MS
|
|
24936
|
+
);
|
|
24937
|
+
}
|
|
24938
|
+
function readHarnessContinuations(repoRoot, nowMs = Date.now()) {
|
|
24939
|
+
try {
|
|
24940
|
+
return readLedgerSync(harnessContinuationPath(repoRoot), nowMs);
|
|
24941
|
+
} catch {
|
|
24942
|
+
return [];
|
|
24943
|
+
}
|
|
24944
|
+
}
|
|
24945
|
+
function claimHarnessContinuation(repoRoot, claim2) {
|
|
24946
|
+
const nowMs = claim2.nowMs ?? Date.now();
|
|
24947
|
+
try {
|
|
24948
|
+
const file = harnessContinuationPath(repoRoot);
|
|
24949
|
+
import_node_fs45.default.mkdirSync(import_node_path57.default.dirname(file), { recursive: true, mode: 448 });
|
|
24950
|
+
return withFileLockSync(
|
|
24951
|
+
`${file}.lock`,
|
|
24952
|
+
{ activity: "harness continuation claim", waitMs: HARNESS_CONTINUATION_LOCK_WAIT_MS },
|
|
24953
|
+
() => {
|
|
24954
|
+
const entries = readLedgerSync(file, nowMs);
|
|
24955
|
+
const existing = entries.find((entry) => entry.toolUseId === claim2.toolUseId);
|
|
24956
|
+
if (existing) return { kind: "replay", entry: existing };
|
|
24957
|
+
entries.push({
|
|
24958
|
+
toolUseId: claim2.toolUseId,
|
|
24959
|
+
payloadSha256: claim2.payloadSha256,
|
|
24960
|
+
state: "waiting",
|
|
24961
|
+
phase: claim2.phase,
|
|
24962
|
+
budgetMs: claim2.budgetMs,
|
|
24963
|
+
claimedAtMs: nowMs
|
|
24964
|
+
});
|
|
24965
|
+
writeFileAtomically(
|
|
24966
|
+
file,
|
|
24967
|
+
`${JSON.stringify(entries.slice(-HARNESS_CONTINUATION_MAX_ENTRIES))}
|
|
24968
|
+
`
|
|
24969
|
+
);
|
|
24970
|
+
return { kind: "claimed", recorded: true };
|
|
24971
|
+
}
|
|
24972
|
+
);
|
|
24973
|
+
} catch {
|
|
24974
|
+
return { kind: "claimed", recorded: false };
|
|
24975
|
+
}
|
|
24976
|
+
}
|
|
24977
|
+
function settleHarnessContinuation(repoRoot, toolUseId, settlement) {
|
|
24978
|
+
const nowMs = settlement.nowMs ?? Date.now();
|
|
24979
|
+
try {
|
|
24980
|
+
const file = harnessContinuationPath(repoRoot);
|
|
24981
|
+
import_node_fs45.default.mkdirSync(import_node_path57.default.dirname(file), { recursive: true, mode: 448 });
|
|
24982
|
+
withFileLockSync(
|
|
24983
|
+
`${file}.lock`,
|
|
24984
|
+
{ activity: "harness continuation settlement", waitMs: HARNESS_CONTINUATION_LOCK_WAIT_MS },
|
|
24985
|
+
() => {
|
|
24986
|
+
const entries = readLedgerSync(file, nowMs);
|
|
24987
|
+
const index = entries.findIndex((entry) => entry.toolUseId === toolUseId);
|
|
24988
|
+
if (index < 0 || entries[index]?.state !== "waiting") return;
|
|
24989
|
+
entries[index] = {
|
|
24990
|
+
...entries[index],
|
|
24991
|
+
state: settlement.state,
|
|
24992
|
+
settledAtMs: nowMs,
|
|
24993
|
+
...settlement.waitedMs === void 0 ? {} : { waitedMs: settlement.waitedMs }
|
|
24994
|
+
};
|
|
24995
|
+
writeFileAtomically(
|
|
24996
|
+
file,
|
|
24997
|
+
`${JSON.stringify(entries.slice(-HARNESS_CONTINUATION_MAX_ENTRIES))}
|
|
24998
|
+
`
|
|
24999
|
+
);
|
|
25000
|
+
}
|
|
25001
|
+
);
|
|
25002
|
+
} catch {
|
|
25003
|
+
}
|
|
25004
|
+
}
|
|
25005
|
+
var import_node_crypto26, import_node_fs45, import_node_path57, HARNESS_CONTINUATION_MAX_ENTRIES, HARNESS_CONTINUATION_TTL_MS, HARNESS_CONTINUATION_LOCK_WAIT_MS, MAX_LEDGER_BYTES, MAX_TOOL_USE_ID_LENGTH, MAX_PHASE_LENGTH;
|
|
25006
|
+
var init_harness_continuation = __esm({
|
|
25007
|
+
"src/core/harness-continuation.ts"() {
|
|
25008
|
+
"use strict";
|
|
25009
|
+
import_node_crypto26 = require("crypto");
|
|
25010
|
+
import_node_fs45 = __toESM(require("fs"));
|
|
25011
|
+
import_node_path57 = __toESM(require("path"));
|
|
25012
|
+
init_atomic_file();
|
|
25013
|
+
init_file_lock();
|
|
25014
|
+
init_router();
|
|
25015
|
+
init_workspace();
|
|
25016
|
+
HARNESS_CONTINUATION_MAX_ENTRIES = 64;
|
|
25017
|
+
HARNESS_CONTINUATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
25018
|
+
HARNESS_CONTINUATION_LOCK_WAIT_MS = 200;
|
|
25019
|
+
MAX_LEDGER_BYTES = 256 * 1024;
|
|
25020
|
+
MAX_TOOL_USE_ID_LENGTH = 256;
|
|
25021
|
+
MAX_PHASE_LENGTH = 32;
|
|
25022
|
+
}
|
|
25023
|
+
});
|
|
25024
|
+
|
|
25025
|
+
// src/core/harness-gate.ts
|
|
25026
|
+
function isTransitionalHarnessPhase(phase) {
|
|
25027
|
+
return TRANSITIONAL_PHASES.includes(phase);
|
|
25028
|
+
}
|
|
25029
|
+
function findHarnessGateRepoRoot(cwd) {
|
|
25030
|
+
let current = import_node_path58.default.resolve(cwd);
|
|
25031
|
+
for (let depth = 0; depth < REPO_ROOT_MAX_DEPTH; depth += 1) {
|
|
25032
|
+
if (import_node_fs46.default.existsSync(import_node_path58.default.join(current, ".devrouter.yml"))) {
|
|
25033
|
+
return current;
|
|
25034
|
+
}
|
|
25035
|
+
const parent = import_node_path58.default.dirname(current);
|
|
25036
|
+
if (parent === current) {
|
|
25037
|
+
return void 0;
|
|
25038
|
+
}
|
|
25039
|
+
current = parent;
|
|
25040
|
+
}
|
|
25041
|
+
return void 0;
|
|
25042
|
+
}
|
|
25043
|
+
function readHarnessGateObservation(repoRoot) {
|
|
25044
|
+
try {
|
|
25045
|
+
const identity2 = {
|
|
25046
|
+
repoPath: comparableWorkspacePath(repoRoot),
|
|
25047
|
+
workspace: resolveWorktreeWorkspace(repoRoot) ?? null,
|
|
25048
|
+
provider: resolveWorkspaceRuntimeOrDefault(repoRoot)
|
|
25049
|
+
};
|
|
25050
|
+
const record5 = readReliabilityOperation(identity2);
|
|
25051
|
+
if (!record5) {
|
|
25052
|
+
return { phase: "unknown", detail: "no-lifecycle-record" };
|
|
25053
|
+
}
|
|
25054
|
+
return { phase: record5.state.phase, detail: "lifecycle-record" };
|
|
25055
|
+
} catch {
|
|
25056
|
+
return { phase: "unknown", detail: "evidence-unavailable" };
|
|
25057
|
+
}
|
|
25058
|
+
}
|
|
25059
|
+
async function waitForHarnessGate(options) {
|
|
25060
|
+
const now = options.now ?? (() => Date.now());
|
|
25061
|
+
const sleep3 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
25062
|
+
const interval = Math.max(1, options.pollIntervalMs ?? HARNESS_GATE_POLL_INTERVAL_MS);
|
|
25063
|
+
const budget = Math.max(0, Math.min(options.budgetMs, HARNESS_GATE_MAX_BUDGET_MS));
|
|
25064
|
+
let started;
|
|
25065
|
+
let observations = 0;
|
|
25066
|
+
let deferred = false;
|
|
25067
|
+
for (; ; ) {
|
|
25068
|
+
const observation = options.observe();
|
|
25069
|
+
if (started === void 0) {
|
|
25070
|
+
started = now();
|
|
25071
|
+
}
|
|
25072
|
+
observations += 1;
|
|
25073
|
+
options.onObservation?.(observation, now() - started);
|
|
25074
|
+
if (!isTransitionalHarnessPhase(observation.phase)) {
|
|
25075
|
+
const waitedMs2 = now() - started;
|
|
25076
|
+
return {
|
|
25077
|
+
version: 1,
|
|
25078
|
+
decision: deferred ? "deferred-allow" : "allow",
|
|
25079
|
+
reason: deferred ? "settled-after-wait" : "settled",
|
|
25080
|
+
waitedMs: waitedMs2,
|
|
25081
|
+
observations,
|
|
25082
|
+
observedPhase: observation.phase
|
|
25083
|
+
};
|
|
25084
|
+
}
|
|
25085
|
+
const waitedMs = now() - started;
|
|
25086
|
+
if (waitedMs + interval > budget) {
|
|
25087
|
+
return {
|
|
25088
|
+
version: 1,
|
|
25089
|
+
decision: "refuse",
|
|
25090
|
+
reason: "budget-exhausted",
|
|
25091
|
+
waitedMs,
|
|
25092
|
+
observations,
|
|
25093
|
+
observedPhase: observation.phase
|
|
25094
|
+
};
|
|
25095
|
+
}
|
|
25096
|
+
if (!deferred && options.claim) {
|
|
25097
|
+
const claim2 = options.claim(observation);
|
|
25098
|
+
if (claim2.kind === "replay") {
|
|
25099
|
+
return {
|
|
25100
|
+
version: 1,
|
|
25101
|
+
decision: "refuse",
|
|
25102
|
+
reason: "continuation-replay",
|
|
25103
|
+
waitedMs,
|
|
25104
|
+
observations,
|
|
25105
|
+
observedPhase: observation.phase,
|
|
25106
|
+
continuation: { state: claim2.state, settledAtMs: claim2.settledAtMs }
|
|
25107
|
+
};
|
|
25108
|
+
}
|
|
25109
|
+
}
|
|
25110
|
+
deferred = true;
|
|
25111
|
+
await sleep3(Math.min(interval, budget - waitedMs));
|
|
25112
|
+
}
|
|
25113
|
+
}
|
|
25114
|
+
function parseHarnessGateBudget(value) {
|
|
25115
|
+
if (value === void 0) {
|
|
25116
|
+
return HARNESS_GATE_DEFAULT_BUDGET_MS;
|
|
25117
|
+
}
|
|
25118
|
+
const parsed = typeof value === "number" ? value : Number(String(value));
|
|
25119
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
25120
|
+
return HARNESS_GATE_DEFAULT_BUDGET_MS;
|
|
25121
|
+
}
|
|
25122
|
+
return Math.min(Math.trunc(parsed), HARNESS_GATE_MAX_BUDGET_MS);
|
|
25123
|
+
}
|
|
25124
|
+
var import_node_fs46, import_node_path58, HARNESS_GATE_DEFAULT_BUDGET_MS, HARNESS_GATE_MAX_BUDGET_MS, HARNESS_GATE_POLL_INTERVAL_MS, TRANSITIONAL_PHASES, REPO_ROOT_MAX_DEPTH;
|
|
25125
|
+
var init_harness_gate = __esm({
|
|
25126
|
+
"src/core/harness-gate.ts"() {
|
|
25127
|
+
"use strict";
|
|
25128
|
+
import_node_fs46 = __toESM(require("fs"));
|
|
25129
|
+
import_node_path58 = __toESM(require("path"));
|
|
25130
|
+
init_reliability_operation_store();
|
|
25131
|
+
init_workspace();
|
|
25132
|
+
init_workspace_runtime();
|
|
25133
|
+
HARNESS_GATE_DEFAULT_BUDGET_MS = 3e4;
|
|
25134
|
+
HARNESS_GATE_MAX_BUDGET_MS = 6e5;
|
|
25135
|
+
HARNESS_GATE_POLL_INTERVAL_MS = 2e3;
|
|
25136
|
+
TRANSITIONAL_PHASES = [
|
|
25137
|
+
"queued",
|
|
25138
|
+
"starting",
|
|
25139
|
+
"verifying",
|
|
25140
|
+
"recovering",
|
|
25141
|
+
"stopping"
|
|
25142
|
+
];
|
|
25143
|
+
REPO_ROOT_MAX_DEPTH = 64;
|
|
25144
|
+
}
|
|
25145
|
+
});
|
|
25146
|
+
|
|
25147
|
+
// src/commands/harness.ts
|
|
25148
|
+
var harness_exports = {};
|
|
25149
|
+
__export(harness_exports, {
|
|
25150
|
+
detectHarnessHook: () => detectHarnessHook,
|
|
25151
|
+
runHarnessCommand: () => runHarnessCommand
|
|
25152
|
+
});
|
|
25153
|
+
function detectHarnessHook(payload) {
|
|
25154
|
+
const turnId = payload.turn_id;
|
|
25155
|
+
return typeof turnId === "string" && turnId.length > 0 ? "codex" : "claude";
|
|
25156
|
+
}
|
|
25157
|
+
function readStdin() {
|
|
25158
|
+
if (process.stdin.isTTY) {
|
|
25159
|
+
return Promise.resolve("");
|
|
25160
|
+
}
|
|
25161
|
+
return new Promise((resolve) => {
|
|
25162
|
+
let input2 = "";
|
|
25163
|
+
let settled = false;
|
|
25164
|
+
const finish = () => {
|
|
25165
|
+
if (settled) return;
|
|
25166
|
+
settled = true;
|
|
25167
|
+
clearTimeout(timer);
|
|
25168
|
+
resolve(input2);
|
|
25169
|
+
};
|
|
25170
|
+
const timer = setTimeout(finish, STDIN_TIMEOUT_MS);
|
|
25171
|
+
process.stdin.setEncoding("utf-8");
|
|
25172
|
+
process.stdin.on("data", (chunk) => {
|
|
25173
|
+
input2 += chunk;
|
|
25174
|
+
if (input2.length > MAX_HOOK_PAYLOAD_BYTES) {
|
|
25175
|
+
finish();
|
|
25176
|
+
process.stdin.destroy();
|
|
25177
|
+
}
|
|
25178
|
+
});
|
|
25179
|
+
process.stdin.on("end", finish);
|
|
25180
|
+
process.stdin.on("error", finish);
|
|
25181
|
+
});
|
|
25182
|
+
}
|
|
25183
|
+
function isRecord7(value) {
|
|
25184
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25185
|
+
}
|
|
25186
|
+
function permitReason(decision) {
|
|
25187
|
+
const seconds = (decision.waitedMs / 1e3).toFixed(1);
|
|
25188
|
+
if (decision.reason === "continuation-replay") {
|
|
25189
|
+
const settled = decision.continuation?.settledAtMs;
|
|
25190
|
+
const age = settled === void 0 ? "" : ` ${Math.max(0, Math.round((Date.now() - settled) / 1e3))}s ago`;
|
|
25191
|
+
if (decision.continuation?.state === "granted") {
|
|
25192
|
+
return `devrouter: this tool call was already permitted after a wait${age}; it is not re-evaluated. Verify whether it ran before issuing a new call.`;
|
|
25193
|
+
}
|
|
25194
|
+
if (decision.continuation?.state === "refused") {
|
|
25195
|
+
return `devrouter: this tool call was already refused${age}; do not repeat it. Run 'devrouter status .' and issue a new call once it settles.`;
|
|
25196
|
+
}
|
|
25197
|
+
return `devrouter: this tool call's wait was cancelled${age}; the harness may have run it. Verify whether it ran before issuing a new call.`;
|
|
25198
|
+
}
|
|
25199
|
+
switch (decision.decision) {
|
|
25200
|
+
case "refuse":
|
|
25201
|
+
return `devrouter: the managed environment is still ${decision.observedPhase} after waiting ${seconds}s. Do not retry automatically; run 'devrouter status .' and continue once it settles.`;
|
|
25202
|
+
case "deferred-allow":
|
|
25203
|
+
return `devrouter: environment settled after ${seconds}s; the tool may run now.`;
|
|
25204
|
+
default:
|
|
25205
|
+
return "devrouter: environment settled.";
|
|
25206
|
+
}
|
|
25207
|
+
}
|
|
25208
|
+
function hookOutput(decision, kind) {
|
|
25209
|
+
if (decision.decision === "refuse") {
|
|
25210
|
+
return JSON.stringify({
|
|
25211
|
+
hookSpecificOutput: {
|
|
25212
|
+
hookEventName: "PreToolUse",
|
|
25213
|
+
permissionDecision: "deny",
|
|
25214
|
+
permissionDecisionReason: permitReason(decision)
|
|
25215
|
+
}
|
|
25216
|
+
});
|
|
25217
|
+
}
|
|
25218
|
+
if (kind === "codex") {
|
|
25219
|
+
return JSON.stringify({
|
|
25220
|
+
hookSpecificOutput: {
|
|
25221
|
+
hookEventName: "PreToolUse",
|
|
25222
|
+
additionalContext: permitReason(decision)
|
|
25223
|
+
}
|
|
25224
|
+
});
|
|
25225
|
+
}
|
|
25226
|
+
return JSON.stringify({
|
|
25227
|
+
hookSpecificOutput: {
|
|
25228
|
+
hookEventName: "PreToolUse",
|
|
25229
|
+
permissionDecision: "allow",
|
|
25230
|
+
permissionDecisionReason: permitReason(decision)
|
|
25231
|
+
}
|
|
25232
|
+
});
|
|
25233
|
+
}
|
|
25234
|
+
function gateDecision(decision, reason, observedPhase = "unknown") {
|
|
25235
|
+
return {
|
|
25236
|
+
version: 1,
|
|
25237
|
+
decision,
|
|
25238
|
+
reason,
|
|
25239
|
+
waitedMs: 0,
|
|
25240
|
+
observations: 0,
|
|
25241
|
+
observedPhase
|
|
25242
|
+
};
|
|
25243
|
+
}
|
|
25244
|
+
async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
|
|
25245
|
+
if (method !== "gate") {
|
|
25246
|
+
throw new Error(`Unsupported harness method: ${method}`);
|
|
25247
|
+
}
|
|
25248
|
+
const json = options.json === true;
|
|
25249
|
+
const stderr = dependencies.stderr ?? ((line) => process.stderr.write(`${line}
|
|
25250
|
+
`));
|
|
25251
|
+
const observe = dependencies.observe ?? readHarnessGateObservation;
|
|
25252
|
+
const raw = await (dependencies.stdin ?? readStdin)();
|
|
25253
|
+
let kind = "claude";
|
|
25254
|
+
let payload;
|
|
25255
|
+
try {
|
|
25256
|
+
const parsed = JSON.parse(raw);
|
|
25257
|
+
if (isRecord7(parsed)) payload = parsed;
|
|
25258
|
+
} catch {
|
|
25259
|
+
payload = void 0;
|
|
25260
|
+
}
|
|
25261
|
+
if (!payload) {
|
|
25262
|
+
const decision2 = gateDecision("allow", "hook-payload-invalid");
|
|
25263
|
+
process.stdout.write(
|
|
25264
|
+
json ? `${JSON.stringify(decision2)}
|
|
25265
|
+
` : `${hookOutput(decision2, kind)}
|
|
25266
|
+
`
|
|
25267
|
+
);
|
|
25268
|
+
return;
|
|
25269
|
+
}
|
|
25270
|
+
kind = detectHarnessHook(payload);
|
|
25271
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
25272
|
+
const toolInput = isRecord7(payload.tool_input) ? payload.tool_input : {};
|
|
25273
|
+
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
25274
|
+
if (toolName === "Bash" && DEVROUTER_COMMAND_RE.test(command)) {
|
|
25275
|
+
const decision2 = gateDecision("allow", "devrouter-command");
|
|
25276
|
+
process.stdout.write(
|
|
25277
|
+
json ? `${JSON.stringify(decision2)}
|
|
25278
|
+
` : `${hookOutput(decision2, kind)}
|
|
25279
|
+
`
|
|
25280
|
+
);
|
|
25281
|
+
return;
|
|
25282
|
+
}
|
|
25283
|
+
const sessionCwd = repoPath ?? (typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : void 0);
|
|
25284
|
+
const repoRoot = sessionCwd ? findHarnessGateRepoRoot(sessionCwd) : void 0;
|
|
25285
|
+
if (!repoRoot) {
|
|
25286
|
+
const decision2 = gateDecision("allow", "unmanaged-checkout");
|
|
25287
|
+
process.stdout.write(
|
|
25288
|
+
json ? `${JSON.stringify(decision2)}
|
|
25289
|
+
` : `${hookOutput(decision2, kind)}
|
|
25290
|
+
`
|
|
25291
|
+
);
|
|
25292
|
+
return;
|
|
25293
|
+
}
|
|
25294
|
+
const budgetMs = parseHarnessGateBudget(options.waitBudgetMs);
|
|
25295
|
+
const toolUseId = typeof payload.tool_use_id === "string" && payload.tool_use_id.length > 0 && payload.tool_use_id.length <= MAX_TOOL_USE_ID_LENGTH2 ? payload.tool_use_id : void 0;
|
|
25296
|
+
const payloadSha256 = (0, import_node_crypto27.createHash)("sha256").update(JSON.stringify(toolInput)).digest("hex");
|
|
25297
|
+
const claim2 = dependencies.claim ?? ((root, input2) => {
|
|
25298
|
+
const outcome = claimHarnessContinuation(root, input2);
|
|
25299
|
+
if (outcome.kind === "claimed") {
|
|
25300
|
+
if (!outcome.recorded) {
|
|
25301
|
+
stderr(
|
|
25302
|
+
"devrouter harness gate: continuation ledger is unavailable; repeat protection is off for this checkout"
|
|
25303
|
+
);
|
|
25304
|
+
}
|
|
25305
|
+
return { kind: "claimed" };
|
|
25306
|
+
}
|
|
25307
|
+
const entry = outcome.entry;
|
|
25308
|
+
return {
|
|
25309
|
+
kind: "replay",
|
|
25310
|
+
state: entry.state === "granted" ? "granted" : entry.state === "refused" ? "refused" : "interrupted",
|
|
25311
|
+
settledAtMs: entry.settledAtMs
|
|
25312
|
+
};
|
|
25313
|
+
});
|
|
25314
|
+
const settle = dependencies.settle ?? ((root, id5, settlement) => {
|
|
25315
|
+
settleHarnessContinuation(root, id5, settlement);
|
|
25316
|
+
});
|
|
25317
|
+
const lookup = dependencies.lookup ?? ((root, id5) => {
|
|
25318
|
+
const entry = readHarnessContinuations(root).find((candidate) => candidate.toolUseId === id5);
|
|
25319
|
+
if (!entry) return void 0;
|
|
25320
|
+
return {
|
|
25321
|
+
state: entry.state === "granted" ? "granted" : entry.state === "refused" ? "refused" : (
|
|
25322
|
+
// A claim still waiting belongs to a hook that never returned.
|
|
25323
|
+
"interrupted"
|
|
25324
|
+
),
|
|
25325
|
+
settledAtMs: entry.settledAtMs
|
|
25326
|
+
};
|
|
25327
|
+
});
|
|
25328
|
+
if (toolUseId) {
|
|
25329
|
+
const replay = lookup(repoRoot, toolUseId);
|
|
25330
|
+
if (replay) {
|
|
25331
|
+
const decision2 = {
|
|
25332
|
+
version: 1,
|
|
25333
|
+
decision: "refuse",
|
|
25334
|
+
reason: "continuation-replay",
|
|
25335
|
+
waitedMs: 0,
|
|
25336
|
+
observations: 0,
|
|
25337
|
+
observedPhase: "unknown",
|
|
25338
|
+
continuation: { state: replay.state, settledAtMs: replay.settledAtMs }
|
|
25339
|
+
};
|
|
25340
|
+
process.stdout.write(
|
|
25341
|
+
json ? `${JSON.stringify({ ...decision2, checkout: repoRoot })}
|
|
25342
|
+
` : `${hookOutput(decision2, kind)}
|
|
25343
|
+
`
|
|
25344
|
+
);
|
|
25345
|
+
return;
|
|
25346
|
+
}
|
|
25347
|
+
}
|
|
25348
|
+
const interruption = (signal) => {
|
|
25349
|
+
if (toolUseId) {
|
|
25350
|
+
settle(repoRoot, toolUseId, { state: "interrupted" });
|
|
25351
|
+
}
|
|
25352
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
25353
|
+
};
|
|
25354
|
+
if (toolUseId) {
|
|
25355
|
+
process.once("SIGINT", interruption);
|
|
25356
|
+
process.once("SIGTERM", interruption);
|
|
25357
|
+
}
|
|
25358
|
+
let announced = false;
|
|
25359
|
+
let decision;
|
|
25360
|
+
try {
|
|
25361
|
+
decision = await waitForHarnessGate({
|
|
25362
|
+
observe: () => observe(repoRoot),
|
|
25363
|
+
budgetMs,
|
|
25364
|
+
now: dependencies.now,
|
|
25365
|
+
sleep: dependencies.sleep,
|
|
25366
|
+
claim: toolUseId ? (observation) => claim2(repoRoot, {
|
|
25367
|
+
toolUseId,
|
|
25368
|
+
payloadSha256,
|
|
25369
|
+
phase: observation.phase,
|
|
25370
|
+
budgetMs
|
|
25371
|
+
}) : void 0,
|
|
25372
|
+
onObservation: (observation) => {
|
|
25373
|
+
if (!announced && isTransitionalHarnessPhase(observation.phase)) {
|
|
25374
|
+
announced = true;
|
|
25375
|
+
stderr(
|
|
25376
|
+
`devrouter harness gate: ${observation.phase} in progress; deferring this tool call for up to ${Math.round(budgetMs / 1e3)}s without model turns`
|
|
25377
|
+
);
|
|
25378
|
+
}
|
|
25379
|
+
}
|
|
25380
|
+
});
|
|
25381
|
+
} finally {
|
|
25382
|
+
if (toolUseId) {
|
|
25383
|
+
process.removeListener("SIGINT", interruption);
|
|
25384
|
+
process.removeListener("SIGTERM", interruption);
|
|
25385
|
+
}
|
|
25386
|
+
}
|
|
25387
|
+
if (toolUseId && decision.reason !== "continuation-replay" && decision.decision !== "allow") {
|
|
25388
|
+
settle(repoRoot, toolUseId, {
|
|
25389
|
+
state: decision.decision === "refuse" ? "refused" : "granted",
|
|
25390
|
+
waitedMs: decision.waitedMs
|
|
25391
|
+
});
|
|
25392
|
+
}
|
|
25393
|
+
if (decision.decision === "deferred-allow") {
|
|
25394
|
+
stderr(
|
|
25395
|
+
`devrouter harness gate: settled after ${(decision.waitedMs / 1e3).toFixed(1)}s (${decision.observations} checks)`
|
|
25396
|
+
);
|
|
25397
|
+
}
|
|
25398
|
+
process.stdout.write(
|
|
25399
|
+
json ? `${JSON.stringify({ ...decision, checkout: repoRoot })}
|
|
25400
|
+
` : `${hookOutput(decision, kind)}
|
|
25401
|
+
`
|
|
25402
|
+
);
|
|
25403
|
+
}
|
|
25404
|
+
var import_node_crypto27, MAX_HOOK_PAYLOAD_BYTES, STDIN_TIMEOUT_MS, MAX_TOOL_USE_ID_LENGTH2, DEVROUTER_COMMAND_RE;
|
|
25405
|
+
var init_harness = __esm({
|
|
25406
|
+
"src/commands/harness.ts"() {
|
|
25407
|
+
"use strict";
|
|
25408
|
+
import_node_crypto27 = require("crypto");
|
|
25409
|
+
init_harness_continuation();
|
|
25410
|
+
init_harness_gate();
|
|
25411
|
+
MAX_HOOK_PAYLOAD_BYTES = 256 * 1024;
|
|
25412
|
+
STDIN_TIMEOUT_MS = 5e3;
|
|
25413
|
+
MAX_TOOL_USE_ID_LENGTH2 = 256;
|
|
25414
|
+
DEVROUTER_COMMAND_RE = /(?:^|[\s;&|()])(?:npx\s+|pnpm\s+(?:exec\s+)?|npm\s+(?:exec\s+)?|yarn\s+)?devrouter(?:-process)?(?:\s|$)/;
|
|
25415
|
+
}
|
|
25416
|
+
});
|
|
25417
|
+
|
|
24260
25418
|
// src/commands/doctor.ts
|
|
24261
25419
|
var doctor_exports = {};
|
|
24262
25420
|
__export(doctor_exports, {
|
|
@@ -24459,17 +25617,17 @@ function asRecord3(value) {
|
|
|
24459
25617
|
return value;
|
|
24460
25618
|
}
|
|
24461
25619
|
function readJson(filePath) {
|
|
24462
|
-
if (!
|
|
25620
|
+
if (!import_node_fs47.default.existsSync(filePath)) {
|
|
24463
25621
|
return void 0;
|
|
24464
25622
|
}
|
|
24465
25623
|
try {
|
|
24466
|
-
return JSON.parse(
|
|
25624
|
+
return JSON.parse(import_node_fs47.default.readFileSync(filePath, "utf-8"));
|
|
24467
25625
|
} catch {
|
|
24468
25626
|
return void 0;
|
|
24469
25627
|
}
|
|
24470
25628
|
}
|
|
24471
25629
|
function relative(repoPath, filePath) {
|
|
24472
|
-
return
|
|
25630
|
+
return import_node_path59.default.relative(repoPath, filePath) || ".";
|
|
24473
25631
|
}
|
|
24474
25632
|
function redactEnvAssignments(value) {
|
|
24475
25633
|
return value.replace(
|
|
@@ -24510,7 +25668,7 @@ function inspectPackageManager(repoPath, pkg) {
|
|
|
24510
25668
|
["bun.lock", "bun"]
|
|
24511
25669
|
];
|
|
24512
25670
|
for (const [fileName, name] of lockfiles) {
|
|
24513
|
-
if (
|
|
25671
|
+
if (import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))) {
|
|
24514
25672
|
return { name, source: fileName };
|
|
24515
25673
|
}
|
|
24516
25674
|
}
|
|
@@ -24525,9 +25683,9 @@ function inspectNode(repoPath, pkg) {
|
|
|
24525
25683
|
if (typeof engines?.node === "string") {
|
|
24526
25684
|
return { version: engines.node, source: "package.json:engines.node" };
|
|
24527
25685
|
}
|
|
24528
|
-
const nvmrc =
|
|
24529
|
-
if (
|
|
24530
|
-
const version =
|
|
25686
|
+
const nvmrc = import_node_path59.default.join(repoPath, ".nvmrc");
|
|
25687
|
+
if (import_node_fs47.default.existsSync(nvmrc)) {
|
|
25688
|
+
const version = import_node_fs47.default.readFileSync(nvmrc, "utf-8").trim();
|
|
24531
25689
|
return { version, source: ".nvmrc" };
|
|
24532
25690
|
}
|
|
24533
25691
|
return void 0;
|
|
@@ -24588,7 +25746,7 @@ function configuredComposeFiles(repoPath) {
|
|
|
24588
25746
|
const files = config.apps.filter(
|
|
24589
25747
|
(app) => app.runtime === "docker"
|
|
24590
25748
|
).flatMap((app) => app.docker.composeFiles).filter(
|
|
24591
|
-
(fileName) => !
|
|
25749
|
+
(fileName) => !import_node_path59.default.isAbsolute(fileName) && !import_node_path59.default.normalize(fileName).startsWith("..")
|
|
24592
25750
|
);
|
|
24593
25751
|
return Array.from(new Set(files));
|
|
24594
25752
|
} catch {
|
|
@@ -24606,7 +25764,7 @@ function composeFiles(repoPath) {
|
|
|
24606
25764
|
...configuredComposeFiles(repoPath)
|
|
24607
25765
|
];
|
|
24608
25766
|
return Array.from(new Set(candidates)).filter(
|
|
24609
|
-
(fileName) =>
|
|
25767
|
+
(fileName) => import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))
|
|
24610
25768
|
);
|
|
24611
25769
|
}
|
|
24612
25770
|
function stringArray2(value) {
|
|
@@ -24644,7 +25802,7 @@ function inspectServices(repoPath) {
|
|
|
24644
25802
|
const services = [];
|
|
24645
25803
|
for (const fileName of composeFiles(repoPath)) {
|
|
24646
25804
|
try {
|
|
24647
|
-
const parsed = import_yaml7.default.parse(
|
|
25805
|
+
const parsed = import_yaml7.default.parse(import_node_fs47.default.readFileSync(import_node_path59.default.join(repoPath, fileName), "utf-8"));
|
|
24648
25806
|
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
24649
25807
|
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
24650
25808
|
const service = asRecord3(value);
|
|
@@ -24679,8 +25837,8 @@ function inspectServices(repoPath) {
|
|
|
24679
25837
|
return services;
|
|
24680
25838
|
}
|
|
24681
25839
|
function inspectEnvFiles(repoPath) {
|
|
24682
|
-
const files =
|
|
24683
|
-
const content =
|
|
25840
|
+
const files = import_node_fs47.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
25841
|
+
const content = import_node_fs47.default.readFileSync(import_node_path59.default.join(repoPath, fileName), "utf-8");
|
|
24684
25842
|
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();
|
|
24685
25843
|
return { path: fileName, names };
|
|
24686
25844
|
});
|
|
@@ -24694,18 +25852,18 @@ function inspectEnvFiles(repoPath) {
|
|
|
24694
25852
|
};
|
|
24695
25853
|
}
|
|
24696
25854
|
function inspectDevcontainer(repoPath) {
|
|
24697
|
-
const dir =
|
|
24698
|
-
if (!
|
|
25855
|
+
const dir = import_node_path59.default.join(repoPath, ".devcontainer");
|
|
25856
|
+
if (!import_node_fs47.default.existsSync(dir)) {
|
|
24699
25857
|
return { exists: false, files: [] };
|
|
24700
25858
|
}
|
|
24701
25859
|
return {
|
|
24702
25860
|
exists: true,
|
|
24703
|
-
files:
|
|
25861
|
+
files: import_node_fs47.default.readdirSync(dir).filter((fileName) => import_node_fs47.default.statSync(import_node_path59.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
24704
25862
|
};
|
|
24705
25863
|
}
|
|
24706
25864
|
function inspectDevrouter(repoPath) {
|
|
24707
25865
|
const configPath = getRepoConfigPath(repoPath);
|
|
24708
|
-
if (!
|
|
25866
|
+
if (!import_node_fs47.default.existsSync(configPath)) {
|
|
24709
25867
|
return {
|
|
24710
25868
|
exists: false,
|
|
24711
25869
|
configPath,
|
|
@@ -24749,15 +25907,15 @@ function inspectAgentGuidance(repoPath) {
|
|
|
24749
25907
|
["AGENTS.md", "agents"],
|
|
24750
25908
|
["CLAUDE.md", "claude"]
|
|
24751
25909
|
]) {
|
|
24752
|
-
if (
|
|
25910
|
+
if (import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))) {
|
|
24753
25911
|
results.push({ path: fileName, kind });
|
|
24754
25912
|
}
|
|
24755
25913
|
}
|
|
24756
|
-
const skillsDir =
|
|
24757
|
-
if (
|
|
24758
|
-
for (const name of
|
|
24759
|
-
const skillPath =
|
|
24760
|
-
if (
|
|
25914
|
+
const skillsDir = import_node_path59.default.join(repoPath, ".agents", "skills");
|
|
25915
|
+
if (import_node_fs47.default.existsSync(skillsDir)) {
|
|
25916
|
+
for (const name of import_node_fs47.default.readdirSync(skillsDir).sort()) {
|
|
25917
|
+
const skillPath = import_node_path59.default.join(skillsDir, name, "SKILL.md");
|
|
25918
|
+
if (import_node_fs47.default.existsSync(skillPath)) {
|
|
24761
25919
|
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
24762
25920
|
}
|
|
24763
25921
|
}
|
|
@@ -24800,7 +25958,7 @@ function buildIssues(report) {
|
|
|
24800
25958
|
}
|
|
24801
25959
|
function inspectRepo(options = {}) {
|
|
24802
25960
|
const repoPath = resolveRepoPath(options.repo);
|
|
24803
|
-
const pkg = readJson(
|
|
25961
|
+
const pkg = readJson(import_node_path59.default.join(repoPath, "package.json"));
|
|
24804
25962
|
const scripts = inspectScripts(pkg);
|
|
24805
25963
|
const reportWithoutIssues = {
|
|
24806
25964
|
repoPath,
|
|
@@ -24819,12 +25977,12 @@ function inspectRepo(options = {}) {
|
|
|
24819
25977
|
issues: buildIssues(reportWithoutIssues)
|
|
24820
25978
|
};
|
|
24821
25979
|
}
|
|
24822
|
-
var
|
|
25980
|
+
var import_node_fs47, import_node_path59, import_yaml7;
|
|
24823
25981
|
var init_repo_inspect = __esm({
|
|
24824
25982
|
"src/core/repo-inspect.ts"() {
|
|
24825
25983
|
"use strict";
|
|
24826
|
-
|
|
24827
|
-
|
|
25984
|
+
import_node_fs47 = __toESM(require("fs"));
|
|
25985
|
+
import_node_path59 = __toESM(require("path"));
|
|
24828
25986
|
import_yaml7 = __toESM(require("yaml"));
|
|
24829
25987
|
init_repo_config();
|
|
24830
25988
|
}
|
|
@@ -24929,7 +26087,7 @@ function requiredFileChecks(repoPath) {
|
|
|
24929
26087
|
".devcontainer/docker-compose.yml",
|
|
24930
26088
|
".devrouter.yml"
|
|
24931
26089
|
];
|
|
24932
|
-
const missing = required.filter((fileName) => !
|
|
26090
|
+
const missing = required.filter((fileName) => !import_node_fs48.default.existsSync(import_node_path60.default.join(repoPath, fileName)));
|
|
24933
26091
|
return {
|
|
24934
26092
|
id: "repo.devcontainer.verify-files",
|
|
24935
26093
|
level: missing.length === 0 ? "ok" : "error",
|
|
@@ -25154,12 +26312,12 @@ async function verifyDevcontainer(options = {}) {
|
|
|
25154
26312
|
nextSteps: collectNextSteps3(checks)
|
|
25155
26313
|
};
|
|
25156
26314
|
}
|
|
25157
|
-
var
|
|
26315
|
+
var import_node_fs48, import_node_path60;
|
|
25158
26316
|
var init_devcontainer_verify = __esm({
|
|
25159
26317
|
"src/core/devcontainer-verify.ts"() {
|
|
25160
26318
|
"use strict";
|
|
25161
|
-
|
|
25162
|
-
|
|
26319
|
+
import_node_fs48 = __toESM(require("fs"));
|
|
26320
|
+
import_node_path60 = __toESM(require("path"));
|
|
25163
26321
|
init_capabilities();
|
|
25164
26322
|
init_doctor();
|
|
25165
26323
|
init_host_routes();
|
|
@@ -25456,7 +26614,7 @@ function packageManagerIssues(repo) {
|
|
|
25456
26614
|
}
|
|
25457
26615
|
function plannedFiles(repoPath, version) {
|
|
25458
26616
|
const repo = inspectRepo({ repo: repoPath });
|
|
25459
|
-
const projectName = sanitizeProjectName(
|
|
26617
|
+
const projectName = sanitizeProjectName(import_node_path61.default.basename(repo.repoPath));
|
|
25460
26618
|
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
25461
26619
|
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
25462
26620
|
const port = inferPort2(repo);
|
|
@@ -25501,8 +26659,8 @@ function plannedFiles(repoPath, version) {
|
|
|
25501
26659
|
};
|
|
25502
26660
|
}
|
|
25503
26661
|
function classifyFile(repoPath, file) {
|
|
25504
|
-
const absolutePath =
|
|
25505
|
-
if (!
|
|
26662
|
+
const absolutePath = import_node_path61.default.join(repoPath, file.relativePath);
|
|
26663
|
+
if (!import_node_fs49.default.existsSync(absolutePath)) {
|
|
25506
26664
|
return {
|
|
25507
26665
|
path: file.relativePath,
|
|
25508
26666
|
action: "create",
|
|
@@ -25510,7 +26668,7 @@ function classifyFile(repoPath, file) {
|
|
|
25510
26668
|
bytes: Buffer.byteLength(file.content)
|
|
25511
26669
|
};
|
|
25512
26670
|
}
|
|
25513
|
-
const current =
|
|
26671
|
+
const current = import_node_fs49.default.readFileSync(absolutePath, "utf-8");
|
|
25514
26672
|
if (!current.includes(MANAGED_MARKER2)) {
|
|
25515
26673
|
return {
|
|
25516
26674
|
path: file.relativePath,
|
|
@@ -25567,11 +26725,11 @@ function buildPlan(repoPath, dryRun, version) {
|
|
|
25567
26725
|
};
|
|
25568
26726
|
}
|
|
25569
26727
|
function writeFile(repoPath, file) {
|
|
25570
|
-
const absolutePath =
|
|
25571
|
-
|
|
25572
|
-
|
|
26728
|
+
const absolutePath = import_node_path61.default.join(repoPath, file.relativePath);
|
|
26729
|
+
import_node_fs49.default.mkdirSync(import_node_path61.default.dirname(absolutePath), { recursive: true });
|
|
26730
|
+
import_node_fs49.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
25573
26731
|
if (file.executable) {
|
|
25574
|
-
|
|
26732
|
+
import_node_fs49.default.chmodSync(absolutePath, 493);
|
|
25575
26733
|
}
|
|
25576
26734
|
}
|
|
25577
26735
|
function writeDevcontainer(options = {}) {
|
|
@@ -25609,12 +26767,12 @@ function writeDevcontainer(options = {}) {
|
|
|
25609
26767
|
nextSteps: postWriteNextSteps(repoPath)
|
|
25610
26768
|
};
|
|
25611
26769
|
}
|
|
25612
|
-
var
|
|
26770
|
+
var import_node_fs49, import_node_path61, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
25613
26771
|
var init_devcontainer_write = __esm({
|
|
25614
26772
|
"src/core/devcontainer-write.ts"() {
|
|
25615
26773
|
"use strict";
|
|
25616
|
-
|
|
25617
|
-
|
|
26774
|
+
import_node_fs49 = __toESM(require("fs"));
|
|
26775
|
+
import_node_path61 = __toESM(require("path"));
|
|
25618
26776
|
init_repo_config();
|
|
25619
26777
|
init_repo_inspect();
|
|
25620
26778
|
MANAGED_MARKER2 = "devrouter:managed devcontainer";
|
|
@@ -25727,11 +26885,11 @@ var init_repo_devcontainer = __esm({
|
|
|
25727
26885
|
function fail3(message) {
|
|
25728
26886
|
throw new Error(message);
|
|
25729
26887
|
}
|
|
25730
|
-
function
|
|
26888
|
+
function isRecord8(value) {
|
|
25731
26889
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25732
26890
|
}
|
|
25733
26891
|
function requireRecord(value, label2) {
|
|
25734
|
-
if (!
|
|
26892
|
+
if (!isRecord8(value)) fail3(`${label2} must be a mapping.`);
|
|
25735
26893
|
return value;
|
|
25736
26894
|
}
|
|
25737
26895
|
function requireExactKeys(value, allowed, label2) {
|
|
@@ -25948,13 +27106,13 @@ function buildProfilePlanReport(options) {
|
|
|
25948
27106
|
return { ...report, contractPath, bindings };
|
|
25949
27107
|
}
|
|
25950
27108
|
function loadContract(repoPath, contractPath) {
|
|
25951
|
-
if (
|
|
27109
|
+
if (import_node_path62.default.isAbsolute(contractPath)) {
|
|
25952
27110
|
fail3("profile plan contract path must be relative to the repository.");
|
|
25953
27111
|
}
|
|
25954
27112
|
const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
|
|
25955
27113
|
let metadata;
|
|
25956
27114
|
try {
|
|
25957
|
-
metadata =
|
|
27115
|
+
metadata = import_node_fs50.default.lstatSync(absolutePath);
|
|
25958
27116
|
} catch (error) {
|
|
25959
27117
|
fail3(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
|
|
25960
27118
|
}
|
|
@@ -25964,16 +27122,16 @@ function loadContract(repoPath, contractPath) {
|
|
|
25964
27122
|
if (metadata.size > MAX_CONTRACT_BYTES) {
|
|
25965
27123
|
fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
25966
27124
|
}
|
|
25967
|
-
const realRepo =
|
|
25968
|
-
const realContract =
|
|
27125
|
+
const realRepo = import_node_fs50.default.realpathSync(repoPath);
|
|
27126
|
+
const realContract = import_node_fs50.default.realpathSync(absolutePath);
|
|
25969
27127
|
assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
|
|
25970
|
-
const source =
|
|
27128
|
+
const source = import_node_fs50.default.readFileSync(absolutePath, "utf-8");
|
|
25971
27129
|
if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
|
|
25972
27130
|
fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
|
|
25973
27131
|
}
|
|
25974
27132
|
return {
|
|
25975
27133
|
contract: parseProfilePlanContract(source),
|
|
25976
|
-
contractPath:
|
|
27134
|
+
contractPath: import_node_path62.default.relative(repoPath, absolutePath).split(import_node_path62.default.sep).join("/")
|
|
25977
27135
|
};
|
|
25978
27136
|
}
|
|
25979
27137
|
function resolveProfilePlan(options) {
|
|
@@ -25987,12 +27145,12 @@ function resolveProfilePlan(options) {
|
|
|
25987
27145
|
contractPath: loaded.contractPath
|
|
25988
27146
|
});
|
|
25989
27147
|
}
|
|
25990
|
-
var
|
|
27148
|
+
var import_node_fs50, import_node_path62, import_yaml8, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
|
|
25991
27149
|
var init_profile_plan = __esm({
|
|
25992
27150
|
"src/core/profile-plan.ts"() {
|
|
25993
27151
|
"use strict";
|
|
25994
|
-
|
|
25995
|
-
|
|
27152
|
+
import_node_fs50 = __toESM(require("fs"));
|
|
27153
|
+
import_node_path62 = __toESM(require("path"));
|
|
25996
27154
|
import_yaml8 = require("yaml");
|
|
25997
27155
|
init_paths();
|
|
25998
27156
|
init_profile_resolution();
|
|
@@ -26024,7 +27182,7 @@ async function runProfileResolveCommand(options) {
|
|
|
26024
27182
|
async function runProfilePlanCommand(options) {
|
|
26025
27183
|
const report = resolveProfilePlan(options);
|
|
26026
27184
|
if (options.output) {
|
|
26027
|
-
writeFileAtomically(
|
|
27185
|
+
writeFileAtomically(import_node_path63.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
|
|
26028
27186
|
`);
|
|
26029
27187
|
}
|
|
26030
27188
|
if (options.json) {
|
|
@@ -26071,14 +27229,14 @@ function printProfilePlanSummary(report, output2) {
|
|
|
26071
27229
|
process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
|
|
26072
27230
|
`);
|
|
26073
27231
|
}
|
|
26074
|
-
if (output2) process.stdout.write(`Output: ${
|
|
27232
|
+
if (output2) process.stdout.write(`Output: ${import_node_path63.default.resolve(output2)}
|
|
26075
27233
|
`);
|
|
26076
27234
|
}
|
|
26077
|
-
var
|
|
27235
|
+
var import_node_path63;
|
|
26078
27236
|
var init_profile = __esm({
|
|
26079
27237
|
"src/commands/profile.ts"() {
|
|
26080
27238
|
"use strict";
|
|
26081
|
-
|
|
27239
|
+
import_node_path63 = __toESM(require("path"));
|
|
26082
27240
|
init_atomic_file();
|
|
26083
27241
|
init_output();
|
|
26084
27242
|
init_profile_plan();
|
|
@@ -26368,7 +27526,7 @@ function sanitizeRouterId(value) {
|
|
|
26368
27526
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
26369
27527
|
}
|
|
26370
27528
|
function repoHash(repoPath) {
|
|
26371
|
-
return (0,
|
|
27529
|
+
return (0, import_node_crypto28.createHash)("sha1").update(import_node_path64.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
26372
27530
|
}
|
|
26373
27531
|
function asDockerApp(app) {
|
|
26374
27532
|
return app.runtime === "docker";
|
|
@@ -26447,11 +27605,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
26447
27605
|
if (dockerApps.length === 0) {
|
|
26448
27606
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
26449
27607
|
}
|
|
26450
|
-
const cachePath =
|
|
26451
|
-
|
|
26452
|
-
const overlayPath =
|
|
27608
|
+
const cachePath = import_node_path64.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
27609
|
+
import_node_fs51.default.mkdirSync(cachePath, { recursive: true });
|
|
27610
|
+
const overlayPath = import_node_path64.default.join(cachePath, "compose.devrouter.yml");
|
|
26453
27611
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
26454
|
-
|
|
27612
|
+
import_node_fs51.default.writeFileSync(overlayPath, import_yaml9.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
26455
27613
|
return {
|
|
26456
27614
|
overlayPath,
|
|
26457
27615
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
@@ -26571,14 +27729,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
|
|
|
26571
27729
|
const port = Number(match[1]);
|
|
26572
27730
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
26573
27731
|
}
|
|
26574
|
-
var import_node_child_process33,
|
|
27732
|
+
var import_node_child_process33, import_node_crypto28, import_node_fs51, import_node_path64, import_yaml9;
|
|
26575
27733
|
var init_docker_run = __esm({
|
|
26576
27734
|
"src/core/docker-run.ts"() {
|
|
26577
27735
|
"use strict";
|
|
26578
27736
|
import_node_child_process33 = require("child_process");
|
|
26579
|
-
|
|
26580
|
-
|
|
26581
|
-
|
|
27737
|
+
import_node_crypto28 = require("crypto");
|
|
27738
|
+
import_node_fs51 = __toESM(require("fs"));
|
|
27739
|
+
import_node_path64 = __toESM(require("path"));
|
|
26582
27740
|
import_yaml9 = __toESM(require("yaml"));
|
|
26583
27741
|
init_docker_error_guidance();
|
|
26584
27742
|
init_paths();
|
|
@@ -27400,13 +28558,13 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
27400
28558
|
const startedAt = Date.now();
|
|
27401
28559
|
let rootStat;
|
|
27402
28560
|
try {
|
|
27403
|
-
rootStat =
|
|
28561
|
+
rootStat = import_node_fs52.default.lstatSync(worktreePath);
|
|
27404
28562
|
} catch (error) {
|
|
27405
28563
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
27406
28564
|
}
|
|
27407
28565
|
let rootEntries;
|
|
27408
28566
|
try {
|
|
27409
|
-
rootEntries =
|
|
28567
|
+
rootEntries = import_node_fs52.default.readdirSync(worktreePath, { withFileTypes: true });
|
|
27410
28568
|
} catch (error) {
|
|
27411
28569
|
return { status: "unknown", reason: describeError(error, worktreePath) };
|
|
27412
28570
|
}
|
|
@@ -27432,10 +28590,10 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
27432
28590
|
timedOut = true;
|
|
27433
28591
|
break;
|
|
27434
28592
|
}
|
|
27435
|
-
const entryPath =
|
|
28593
|
+
const entryPath = import_node_path65.default.join(dirPath, entry.name);
|
|
27436
28594
|
let entryStat;
|
|
27437
28595
|
try {
|
|
27438
|
-
entryStat =
|
|
28596
|
+
entryStat = import_node_fs52.default.lstatSync(entryPath);
|
|
27439
28597
|
} catch (error) {
|
|
27440
28598
|
if (error?.code === "ENOENT") continue;
|
|
27441
28599
|
unreadableReason = describeIncompleteWalk(error);
|
|
@@ -27445,7 +28603,7 @@ function measureWorktreeConsumption(worktreePath, options) {
|
|
|
27445
28603
|
if (!entryStat.isDirectory()) continue;
|
|
27446
28604
|
let childEntries;
|
|
27447
28605
|
try {
|
|
27448
|
-
childEntries =
|
|
28606
|
+
childEntries = import_node_fs52.default.readdirSync(entryPath, { withFileTypes: true });
|
|
27449
28607
|
} catch (error) {
|
|
27450
28608
|
unreadableReason = describeIncompleteWalk(error);
|
|
27451
28609
|
break;
|
|
@@ -27509,12 +28667,12 @@ function describeError(error, worktreePath) {
|
|
|
27509
28667
|
}
|
|
27510
28668
|
return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
|
|
27511
28669
|
}
|
|
27512
|
-
var
|
|
28670
|
+
var import_node_fs52, import_node_path65, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
|
|
27513
28671
|
var init_workspace_consumption = __esm({
|
|
27514
28672
|
"src/core/workspace-consumption.ts"() {
|
|
27515
28673
|
"use strict";
|
|
27516
|
-
|
|
27517
|
-
|
|
28674
|
+
import_node_fs52 = __toESM(require("fs"));
|
|
28675
|
+
import_node_path65 = __toESM(require("path"));
|
|
27518
28676
|
init_devpod_environment();
|
|
27519
28677
|
DEFAULT_DEADLINE_MS = 1e4;
|
|
27520
28678
|
BLOCK_SIZE_BYTES = 512;
|
|
@@ -27522,7 +28680,7 @@ var init_workspace_consumption = __esm({
|
|
|
27522
28680
|
});
|
|
27523
28681
|
|
|
27524
28682
|
// src/core/workspace-cleanup.ts
|
|
27525
|
-
function
|
|
28683
|
+
function isRecord9(value) {
|
|
27526
28684
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27527
28685
|
}
|
|
27528
28686
|
function isSha(value) {
|
|
@@ -27615,7 +28773,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
|
|
|
27615
28773
|
}
|
|
27616
28774
|
function readGitSnapshot(worktree, commandRunner) {
|
|
27617
28775
|
const comparablePath = comparableWorkspacePath(worktree.path);
|
|
27618
|
-
if (worktree.prunable || !
|
|
28776
|
+
if (worktree.prunable || !import_node_fs53.default.existsSync(comparablePath)) {
|
|
27619
28777
|
return { worktree, checkout: "missing", head: null, committerDate: null };
|
|
27620
28778
|
}
|
|
27621
28779
|
const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
|
|
@@ -27666,14 +28824,14 @@ function parseGitHubChanges(value, project, branch) {
|
|
|
27666
28824
|
if (!Array.isArray(value)) return void 0;
|
|
27667
28825
|
const changes = [];
|
|
27668
28826
|
for (const item of value) {
|
|
27669
|
-
if (!
|
|
27670
|
-
const repository =
|
|
27671
|
-
const headRepository =
|
|
28827
|
+
if (!isRecord9(item)) return void 0;
|
|
28828
|
+
const repository = isRecord9(item.repository) ? item.repository.nameWithOwner : void 0;
|
|
28829
|
+
const headRepository = isRecord9(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
|
|
27672
28830
|
if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
|
|
27673
28831
|
continue;
|
|
27674
28832
|
}
|
|
27675
28833
|
const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
|
|
27676
|
-
const mergeCommit =
|
|
28834
|
+
const mergeCommit = isRecord9(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
|
|
27677
28835
|
const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
|
|
27678
28836
|
changes.push({
|
|
27679
28837
|
sourceBranch: branch,
|
|
@@ -27690,14 +28848,14 @@ function parseGitLabChanges(value, _project, branch) {
|
|
|
27690
28848
|
if (!Array.isArray(value)) return void 0;
|
|
27691
28849
|
const changes = [];
|
|
27692
28850
|
for (const item of value) {
|
|
27693
|
-
if (!
|
|
28851
|
+
if (!isRecord9(item)) return void 0;
|
|
27694
28852
|
const sourceProjectId = item.source_project_id;
|
|
27695
28853
|
const targetProjectId = item.target_project_id;
|
|
27696
28854
|
const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
|
|
27697
28855
|
if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
|
|
27698
28856
|
continue;
|
|
27699
28857
|
}
|
|
27700
|
-
const diffRefs =
|
|
28858
|
+
const diffRefs = isRecord9(item.diff_refs) ? item.diff_refs : void 0;
|
|
27701
28859
|
const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
|
|
27702
28860
|
const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
|
|
27703
28861
|
changes.push({
|
|
@@ -28140,7 +29298,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
28140
29298
|
containers2 = measureContainersFn(worktreePaths);
|
|
28141
29299
|
} catch (error) {
|
|
28142
29300
|
const reason = `container measurement failed: ${describeCause(error)}`;
|
|
28143
|
-
containers2 = new Map(worktreePaths.map((
|
|
29301
|
+
containers2 = new Map(worktreePaths.map((path66) => [path66, unknownContainers(reason)]));
|
|
28144
29302
|
}
|
|
28145
29303
|
}
|
|
28146
29304
|
const rows2 = records.map((record5) => {
|
|
@@ -28190,12 +29348,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
|
|
|
28190
29348
|
networkCapacity: (dependencies.inspectNetworkCapacity ?? inspectNetworkCapacity)()
|
|
28191
29349
|
};
|
|
28192
29350
|
}
|
|
28193
|
-
var import_node_child_process35,
|
|
29351
|
+
var import_node_child_process35, import_node_fs53, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
|
|
28194
29352
|
var init_workspace_cleanup = __esm({
|
|
28195
29353
|
"src/core/workspace-cleanup.ts"() {
|
|
28196
29354
|
"use strict";
|
|
28197
29355
|
import_node_child_process35 = require("child_process");
|
|
28198
|
-
|
|
29356
|
+
import_node_fs53 = __toESM(require("fs"));
|
|
28199
29357
|
init_devpod_workspaces();
|
|
28200
29358
|
init_host_routes();
|
|
28201
29359
|
init_network_diagnostics();
|
|
@@ -28307,7 +29465,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
|
|
|
28307
29465
|
);
|
|
28308
29466
|
}
|
|
28309
29467
|
function defaultWorktreePath(mainRepo, ws) {
|
|
28310
|
-
return
|
|
29468
|
+
return import_node_path66.default.join(mainRepo, "trees", ws);
|
|
28311
29469
|
}
|
|
28312
29470
|
function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
28313
29471
|
const ignored = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
|
|
@@ -28315,12 +29473,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
|
|
|
28315
29473
|
});
|
|
28316
29474
|
if (ignored.status !== 0) {
|
|
28317
29475
|
throw new Error(
|
|
28318
|
-
`Default worktree root '${
|
|
29476
|
+
`Default worktree root '${import_node_path66.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path66.default.join(mainRepo, ".gitignore")}' or use --path.`
|
|
28319
29477
|
);
|
|
28320
29478
|
}
|
|
28321
29479
|
}
|
|
28322
29480
|
function legacyDefaultWorktreePath(mainRepo, ws) {
|
|
28323
|
-
return
|
|
29481
|
+
return import_node_path66.default.join(import_node_path66.default.dirname(mainRepo), `${import_node_path66.default.basename(mainRepo)}-${ws}`);
|
|
28324
29482
|
}
|
|
28325
29483
|
function teardownFallbackPath(mainRepo, workspace) {
|
|
28326
29484
|
const candidates = [
|
|
@@ -28432,7 +29590,7 @@ function assertFullDownPreflight(mainRepo, target) {
|
|
|
28432
29590
|
if (sameWorkspacePath(target.worktreePath, mainRepo)) {
|
|
28433
29591
|
throw new Error("Refusing to remove the primary Git checkout.");
|
|
28434
29592
|
}
|
|
28435
|
-
if (!target.worktree || target.worktree.prunable || !
|
|
29593
|
+
if (!target.worktree || target.worktree.prunable || !import_node_fs54.default.existsSync(target.worktreePath)) return;
|
|
28436
29594
|
if (target.worktree.locked) {
|
|
28437
29595
|
throw new Error(
|
|
28438
29596
|
`Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
|
|
@@ -28472,9 +29630,9 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
28472
29630
|
(candidate) => defaultWorktreePath(mainRepo, candidate)
|
|
28473
29631
|
);
|
|
28474
29632
|
const generatedPath = candidatePaths.find(
|
|
28475
|
-
(candidate) => !
|
|
29633
|
+
(candidate) => !import_node_fs54.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
|
|
28476
29634
|
);
|
|
28477
|
-
const selectedPath = opts.path ?
|
|
29635
|
+
const selectedPath = opts.path ? import_node_path66.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
|
|
28478
29636
|
if (!selectedPath) {
|
|
28479
29637
|
throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
|
|
28480
29638
|
}
|
|
@@ -28483,7 +29641,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
28483
29641
|
`Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
|
|
28484
29642
|
);
|
|
28485
29643
|
}
|
|
28486
|
-
if (
|
|
29644
|
+
if (import_node_fs54.default.existsSync(selectedPath)) {
|
|
28487
29645
|
const registered = worktrees.find(
|
|
28488
29646
|
(worktree) => sameWorkspacePath(worktree.path, selectedPath)
|
|
28489
29647
|
);
|
|
@@ -28646,7 +29804,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
28646
29804
|
}
|
|
28647
29805
|
const result = await mutateWorkspaceRuntime("delete", resolved, worktrees, opts.quiet);
|
|
28648
29806
|
if (removeWorktree) {
|
|
28649
|
-
if (resolved.worktree && !resolved.worktree.prunable &&
|
|
29807
|
+
if (resolved.worktree && !resolved.worktree.prunable && import_node_fs54.default.existsSync(resolved.worktreePath)) {
|
|
28650
29808
|
const rm = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
|
|
28651
29809
|
encoding: "utf-8"
|
|
28652
29810
|
});
|
|
@@ -28665,7 +29823,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
28665
29823
|
}
|
|
28666
29824
|
return result;
|
|
28667
29825
|
};
|
|
28668
|
-
return resolved.worktree && !resolved.worktree.prunable &&
|
|
29826
|
+
return resolved.worktree && !resolved.worktree.prunable && import_node_fs54.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
|
|
28669
29827
|
}
|
|
28670
29828
|
async function workspaceStop(target, opts = {}) {
|
|
28671
29829
|
return runWorkspaceLifecycle("stop", target, opts);
|
|
@@ -28673,13 +29831,13 @@ async function workspaceStop(target, opts = {}) {
|
|
|
28673
29831
|
async function workspaceDown(target, opts = {}) {
|
|
28674
29832
|
return runWorkspaceLifecycle("down", target, opts);
|
|
28675
29833
|
}
|
|
28676
|
-
var import_node_child_process36,
|
|
29834
|
+
var import_node_child_process36, import_node_fs54, import_node_path66, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
|
|
28677
29835
|
var init_workspace_lifecycle = __esm({
|
|
28678
29836
|
"src/core/workspace-lifecycle.ts"() {
|
|
28679
29837
|
"use strict";
|
|
28680
29838
|
import_node_child_process36 = require("child_process");
|
|
28681
|
-
|
|
28682
|
-
|
|
29839
|
+
import_node_fs54 = __toESM(require("fs"));
|
|
29840
|
+
import_node_path66 = __toESM(require("path"));
|
|
28683
29841
|
init_devpod_mutation();
|
|
28684
29842
|
init_devpod_workspaces();
|
|
28685
29843
|
init_devsy_mutation();
|
|
@@ -28863,7 +30021,7 @@ var init_version = __esm({
|
|
|
28863
30021
|
|
|
28864
30022
|
// src/cli.ts
|
|
28865
30023
|
var import_commander = require("commander");
|
|
28866
|
-
var CLI_VERSION = true ? "0.0
|
|
30024
|
+
var CLI_VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
28867
30025
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
28868
30026
|
function withErrorHandling(action2) {
|
|
28869
30027
|
return async (...args) => {
|
|
@@ -28911,6 +30069,13 @@ controllerCommand.command("status").option("--session <id>").option("--cursor <c
|
|
|
28911
30069
|
await runControllerCommand2("status", options);
|
|
28912
30070
|
})
|
|
28913
30071
|
);
|
|
30072
|
+
var capacityCommand = program.command("capacity").description("Inspect machines-local capacity ledger state");
|
|
30073
|
+
capacityCommand.command("reconcile").description("Replace a provably lost capacity ledger with a fresh empty baseline").option("--yes").option("--json").action(
|
|
30074
|
+
withErrorHandling(async (options) => {
|
|
30075
|
+
const { runCapacityCommand: runCapacityCommand2 } = await Promise.resolve().then(() => (init_capacity(), capacity_exports));
|
|
30076
|
+
await runCapacityCommand2("reconcile", options);
|
|
30077
|
+
})
|
|
30078
|
+
);
|
|
28914
30079
|
program.command("init").description("Print an AI onboarding prompt template for adapting a repository to devrouter").option("--repo <path>", "Repository path to embed in the prompt (defaults to current directory)").option("--entries-json <json>", "Optional JSON array of app entries to embed in the prompt").option("--json", "Output prompt and command intents as JSON").option("--write-agents", "Write/update devrouter section in AGENTS.md").option("--write-skill", "Write .agents/skills/devrouter/SKILL.md").action(
|
|
28915
30080
|
withErrorHandling(
|
|
28916
30081
|
async (options) => {
|
|
@@ -28994,6 +30159,13 @@ program.command("status").description("Show router/container/network/TLS status
|
|
|
28994
30159
|
await runStatusCommand2(options);
|
|
28995
30160
|
})
|
|
28996
30161
|
);
|
|
30162
|
+
var harnessCommand = program.command("harness").description("Agent harness integration for managed environments");
|
|
30163
|
+
harnessCommand.command("gate").description("Defer one harness tool call until this checkout's lifecycle phase settles").option("--repo <path>", "Repository path (defaults to the harness payload cwd)").option("--wait-budget-ms <ms>", "Bounded defer budget in milliseconds before one refusal").option("--json", "Print the devrouter decision envelope instead of the hook decision").action(
|
|
30164
|
+
withErrorHandling(async (options) => {
|
|
30165
|
+
const { runHarnessCommand: runHarnessCommand2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
30166
|
+
await runHarnessCommand2("gate", options, options.repo);
|
|
30167
|
+
})
|
|
30168
|
+
);
|
|
28997
30169
|
program.command("doctor").alias("verify").description("Run global + repo diagnostics with actionable fixes for humans and AI agents").option("--repo <path>", "Repository path to validate (defaults to current directory)").option("--json", "Output JSON").action(
|
|
28998
30170
|
withErrorHandling(async (options) => {
|
|
28999
30171
|
const { runDoctorCommand: runDoctorCommand2 } = await Promise.resolve().then(() => (init_doctor2(), doctor_exports));
|
|
@@ -29238,10 +30410,10 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
|
|
|
29238
30410
|
workspaceCommand2.command("journal").description("Inspect and recover workspace lifecycle journals").command("settle").description(
|
|
29239
30411
|
"Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
|
|
29240
30412
|
).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
|
|
29241
|
-
withErrorHandling(async (
|
|
30413
|
+
withErrorHandling(async (path66, _options, command) => {
|
|
29242
30414
|
const options = command.opts();
|
|
29243
30415
|
const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
29244
|
-
await runWorkspaceJournalSettleCommand2({ ...
|
|
30416
|
+
await runWorkspaceJournalSettleCommand2({ ...path66 ? { path: path66 } : {}, ...options });
|
|
29245
30417
|
})
|
|
29246
30418
|
);
|
|
29247
30419
|
function parseVersionRequest(argv) {
|