@orkestrel/scaffold 0.0.48 → 0.0.49
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/bin/main.js +271 -53
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +7 -0
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -0
- package/dist/host/claude/rules/quality.md +4 -1
- package/dist/host/guides/scaffold.md +150 -65
- package/dist/host/manifest.json +217 -109
- package/dist/host/tests/config.test.ts +103 -1
- package/dist/src/core/index.cjs +32 -4
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +65 -8
- package/dist/src/core/index.d.ts +65 -8
- package/dist/src/core/index.js +31 -5
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +769 -137
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +681 -376
- package/dist/src/server/index.d.ts +681 -376
- package/dist/src/server/index.js +766 -141
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -2
|
@@ -3,6 +3,7 @@ let _orkestrel_contract = require("@orkestrel/contract");
|
|
|
3
3
|
let _src_core = require("../core/index.cjs");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
|
+
let node_os = require("node:os");
|
|
6
7
|
let node_path = require("node:path");
|
|
7
8
|
let node_url = require("node:url");
|
|
8
9
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
@@ -44,13 +45,13 @@ var RESERVED_SEGMENT_PATTERN = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|conin\$|co
|
|
|
44
45
|
*/
|
|
45
46
|
var DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
46
47
|
/**
|
|
47
|
-
* The Git branch syntax the
|
|
48
|
+
* The Git branch syntax the repository endpoint accepts.
|
|
48
49
|
*
|
|
49
50
|
* @remarks
|
|
50
51
|
* A branch is caller-supplied and reaches a URL path, so it is closed to
|
|
51
52
|
* alphanumerics, dot, underscore, hyphen, and the separator, must open with an
|
|
52
53
|
* alphanumeric, and may carry no `..` anywhere. That last refusal is what stops
|
|
53
|
-
* a branch from walking out of the
|
|
54
|
+
* a branch from walking out of the repository path it addresses.
|
|
54
55
|
*/
|
|
55
56
|
var BRANCH_PATTERN = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
56
57
|
/**
|
|
@@ -84,7 +85,7 @@ var MAX_PATH_DEPTH = 64;
|
|
|
84
85
|
var MAX_INVENTORY_PATHS = 1e5;
|
|
85
86
|
/** Maximum characters one caller-supplied upstream endpoint may carry. */
|
|
86
87
|
var MAX_ENDPOINT_LENGTH = 2048;
|
|
87
|
-
/** Maximum characters one
|
|
88
|
+
/** Maximum characters one repository branch may carry. */
|
|
88
89
|
var MAX_BRANCH_LENGTH = 255;
|
|
89
90
|
/**
|
|
90
91
|
* Maximum simultaneous upstream requests.
|
|
@@ -226,10 +227,10 @@ var isEndpoint = (0, _orkestrel_contract.stringOf)({
|
|
|
226
227
|
max: MAX_ENDPOINT_LENGTH
|
|
227
228
|
});
|
|
228
229
|
/**
|
|
229
|
-
* Narrow a value to a Git branch the
|
|
230
|
+
* Narrow a value to a Git branch the repository endpoint accepts.
|
|
230
231
|
*
|
|
231
232
|
* @remarks
|
|
232
|
-
* A branch reaches the
|
|
233
|
+
* A branch reaches the repository URL's path, so the syntax is closed rather than
|
|
233
234
|
* merely bounded and no `..` is admitted anywhere in it.
|
|
234
235
|
*
|
|
235
236
|
* @example
|
|
@@ -271,6 +272,24 @@ var isTimeout = (0, _orkestrel_contract.andOf)(_orkestrel_contract.isInteger, (0
|
|
|
271
272
|
* ```
|
|
272
273
|
*/
|
|
273
274
|
var isDependencyNames = (0, _orkestrel_contract.andOf)(_src_core.isCollection, (0, _orkestrel_contract.arrayOf)(_src_core.isDependencyName));
|
|
275
|
+
/**
|
|
276
|
+
* Narrow a value to a bounded list of target-relative paths.
|
|
277
|
+
*
|
|
278
|
+
* @remarks
|
|
279
|
+
* Composed from the core collection and path guards rather than restated, so
|
|
280
|
+
* the containment law that keeps a caller-supplied path inside its target has
|
|
281
|
+
* exactly one home. It bounds what a caller may hand a public method, which is
|
|
282
|
+
* why it is not {@link isInventory}: that one bounds what a checkout may hold.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* import { isPaths } from '@orkestrel/scaffold/server'
|
|
287
|
+
*
|
|
288
|
+
* isPaths(['AGENTS.md']) // true
|
|
289
|
+
* isPaths(['../secrets']) // false
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
var isPaths = (0, _orkestrel_contract.andOf)(_src_core.isCollection, (0, _orkestrel_contract.arrayOf)(_src_core.isPath));
|
|
274
293
|
/** Narrow a value to a bounded list of declared runtime dependencies. */
|
|
275
294
|
var isDependencies = (0, _orkestrel_contract.andOf)(_src_core.isCollection, (0, _orkestrel_contract.arrayOf)(_src_core.isDependency));
|
|
276
295
|
/** Narrow a value to a bounded list of fetched guide mirrors. */
|
|
@@ -290,13 +309,19 @@ var isCatalogEntries = (0, _orkestrel_contract.andOf)(_src_core.isCollection, (0
|
|
|
290
309
|
* ```ts
|
|
291
310
|
* import { isManifestEntry } from '@orkestrel/scaffold/server'
|
|
292
311
|
*
|
|
293
|
-
* isManifestEntry({
|
|
312
|
+
* isManifestEntry({
|
|
313
|
+
* storage: 'AGENTS.md',
|
|
314
|
+
* destination: 'AGENTS.md',
|
|
315
|
+
* executable: false,
|
|
316
|
+
* digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
317
|
+
* }) // true
|
|
294
318
|
* ```
|
|
295
319
|
*/
|
|
296
320
|
var isManifestEntry = (0, _orkestrel_contract.recordOf)({
|
|
297
321
|
storage: _src_core.isPath,
|
|
298
322
|
destination: _src_core.isPath,
|
|
299
|
-
executable: _orkestrel_contract.isBoolean
|
|
323
|
+
executable: _orkestrel_contract.isBoolean,
|
|
324
|
+
digest: isDigest
|
|
300
325
|
});
|
|
301
326
|
/**
|
|
302
327
|
* Narrow a value to one {@link HostManifest}.
|
|
@@ -312,7 +337,33 @@ var isHostManifest = (0, _orkestrel_contract.recordOf)({
|
|
|
312
337
|
digest: isDigest
|
|
313
338
|
});
|
|
314
339
|
/**
|
|
315
|
-
* Narrow a value to
|
|
340
|
+
* Narrow a value to one {@link Host}.
|
|
341
|
+
*
|
|
342
|
+
* @remarks
|
|
343
|
+
* A whole vendored host handed in as a value is as untrusted as one read from a
|
|
344
|
+
* directory a caller named, so both halves are guarded: the manifest by the same
|
|
345
|
+
* membership law a read root is held to, and the bytes by the core snapshot law,
|
|
346
|
+
* which bounds the fill and reads every key as a path and every value as exact
|
|
347
|
+
* lowercase hexadecimal. Whether those halves agree with each other is the
|
|
348
|
+
* reader's question rather than this one's, because a guard has only `false` to
|
|
349
|
+
* say and a mismatch has a path to name.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* import { isHost } from '@orkestrel/scaffold/server'
|
|
354
|
+
*
|
|
355
|
+
* const digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
|
356
|
+
*
|
|
357
|
+
* isHost({ manifest: { entries: [], roots: [], digest }, bytes: {} }) // true
|
|
358
|
+
* isHost({ manifest: { entries: [], roots: [], digest } }) // false
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
var isHost = (0, _orkestrel_contract.recordOf)({
|
|
362
|
+
manifest: isHostManifest,
|
|
363
|
+
bytes: _src_core.isSnapshot
|
|
364
|
+
});
|
|
365
|
+
/**
|
|
366
|
+
* Narrow a value to a {@link Worktree}.
|
|
316
367
|
*
|
|
317
368
|
* @remarks
|
|
318
369
|
* Both path lists are target-relative, so both are measured by the core path
|
|
@@ -322,13 +373,13 @@ var isHostManifest = (0, _orkestrel_contract.recordOf)({
|
|
|
322
373
|
*
|
|
323
374
|
* @example
|
|
324
375
|
* ```ts
|
|
325
|
-
* import {
|
|
376
|
+
* import { isWorktree } from '@orkestrel/scaffold/server'
|
|
326
377
|
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
378
|
+
* isWorktree({ tracked: ['AGENTS.md'], dirty: [] }) // true
|
|
379
|
+
* isWorktree({ tracked: ['../secrets'], dirty: [] }) // false
|
|
329
380
|
* ```
|
|
330
381
|
*/
|
|
331
|
-
var
|
|
382
|
+
var isWorktree = (0, _orkestrel_contract.recordOf)({
|
|
332
383
|
tracked: (0, _orkestrel_contract.andOf)(isInventory, (0, _orkestrel_contract.arrayOf)(_src_core.isPath)),
|
|
333
384
|
dirty: (0, _orkestrel_contract.andOf)(isInventory, (0, _orkestrel_contract.arrayOf)(_src_core.isPath))
|
|
334
385
|
});
|
|
@@ -350,6 +401,12 @@ var isMaterializerHooks = (0, _orkestrel_contract.recordOf)({
|
|
|
350
401
|
/**
|
|
351
402
|
* Narrow a value to {@link MaterializerOptions}.
|
|
352
403
|
*
|
|
404
|
+
* @remarks
|
|
405
|
+
* `host` admits both representations of one vendored root: a directory path and
|
|
406
|
+
* a whole {@link Host} value. They share a key because they are one setting
|
|
407
|
+
* stated two ways rather than two settings, so nothing downstream has to
|
|
408
|
+
* reconcile a pair that could disagree.
|
|
409
|
+
*
|
|
353
410
|
* @example
|
|
354
411
|
* ```ts
|
|
355
412
|
* import { isMaterializerOptions } from '@orkestrel/scaffold/server'
|
|
@@ -359,7 +416,7 @@ var isMaterializerHooks = (0, _orkestrel_contract.recordOf)({
|
|
|
359
416
|
* ```
|
|
360
417
|
*/
|
|
361
418
|
var isMaterializerOptions = (0, _orkestrel_contract.recordOf)({
|
|
362
|
-
host: isFilesystemPath,
|
|
419
|
+
host: (0, _orkestrel_contract.unionOf)(isFilesystemPath, isHost),
|
|
363
420
|
on: isMaterializerHooks,
|
|
364
421
|
error: _orkestrel_contract.isFunction
|
|
365
422
|
}, true);
|
|
@@ -373,6 +430,7 @@ var isMaterializerOptions = (0, _orkestrel_contract.recordOf)({
|
|
|
373
430
|
var isUpstreamHooks = (0, _orkestrel_contract.recordOf)({
|
|
374
431
|
release: _orkestrel_contract.isFunction,
|
|
375
432
|
mirror: _orkestrel_contract.isFunction,
|
|
433
|
+
file: _orkestrel_contract.isFunction,
|
|
376
434
|
error: _orkestrel_contract.isFunction,
|
|
377
435
|
destroy: _orkestrel_contract.isFunction
|
|
378
436
|
}, true);
|
|
@@ -392,12 +450,12 @@ var isUpstreamHooks = (0, _orkestrel_contract.recordOf)({
|
|
|
392
450
|
* ```ts
|
|
393
451
|
* import { isUpstreamOptions } from '@orkestrel/scaffold/server'
|
|
394
452
|
*
|
|
395
|
-
* isUpstreamOptions({
|
|
453
|
+
* isUpstreamOptions({ repository: { branch: 'main' }, concurrency: 4 }) // true
|
|
396
454
|
* isUpstreamOptions({ concurrency: 0 }) // false
|
|
397
455
|
* ```
|
|
398
456
|
*/
|
|
399
457
|
var isUpstreamOptions = (0, _orkestrel_contract.recordOf)({
|
|
400
|
-
|
|
458
|
+
repository: (0, _orkestrel_contract.recordOf)({
|
|
401
459
|
base: isEndpoint,
|
|
402
460
|
branch: isBranch,
|
|
403
461
|
timeout: isTimeout
|
|
@@ -474,7 +532,7 @@ function matchesGitPath(path) {
|
|
|
474
532
|
* directories. It is the inversion the contract asks for: the candidate set
|
|
475
533
|
* is re-derived from the plan and narrowed by what git tracks, and the audit
|
|
476
534
|
* must agree with that derivation rather than supply the set itself.
|
|
477
|
-
*
|
|
535
|
+
* Git metadata is protected because losing history is not a repair,
|
|
478
536
|
* and a target's own `src` and `app` trees are protected because a
|
|
479
537
|
* workspace's source is the one thing scaffold never plans and never owns. A
|
|
480
538
|
* plan the compiler emits never maps a protected root, so this guard exists
|
|
@@ -505,7 +563,7 @@ function matchesProtectedPath(path) {
|
|
|
505
563
|
* The vendoring deny-list. A host root is staged from a real checkout, so the
|
|
506
564
|
* refusal is stated over the path rather than over the file's content: a
|
|
507
565
|
* credential is recognizable by where it sits and what it is called long before
|
|
508
|
-
* anything reads it.
|
|
566
|
+
* anything reads it. Git metadata is included through
|
|
509
567
|
* {@link matchesGitPath}, so one call answers the whole question and no caller
|
|
510
568
|
* has to remember to ask twice.
|
|
511
569
|
*
|
|
@@ -602,6 +660,25 @@ function computeDigest(content) {
|
|
|
602
660
|
return (0, node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
603
661
|
}
|
|
604
662
|
/**
|
|
663
|
+
* Projects exact bytes stated in hexadecimal to their SHA-256 digest.
|
|
664
|
+
*
|
|
665
|
+
* @param hex - The exact lowercase hexadecimal bytes to digest.
|
|
666
|
+
* @returns Sixty-four lowercase hexadecimal digits.
|
|
667
|
+
* @throws `ScaffoldError('INVALID', …)` when `hex` is not exact bounded
|
|
668
|
+
* lowercase hexadecimal text.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```ts
|
|
672
|
+
* import { hexToDigest } from '@orkestrel/scaffold/server'
|
|
673
|
+
*
|
|
674
|
+
* hexToDigest('68690a') // '98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4'
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
function hexToDigest(hex) {
|
|
678
|
+
if (!(0, _src_core.isHex)(hex)) throw new _src_core.ScaffoldError("INVALID", "Digest input is not exact hexadecimal bytes", { hex });
|
|
679
|
+
return (0, node_crypto.createHash)("sha256").update(Buffer.from(hex, "hex")).digest("hex");
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
605
682
|
* Compute the digest of a vendored host's declared membership.
|
|
606
683
|
*
|
|
607
684
|
* @param entries - The ordered file membership declarations.
|
|
@@ -628,7 +705,8 @@ function computeManifestDigest(entries, roots) {
|
|
|
628
705
|
entries: entries.map((entry) => ({
|
|
629
706
|
storage: entry.storage,
|
|
630
707
|
destination: entry.destination,
|
|
631
|
-
executable: entry.executable
|
|
708
|
+
executable: entry.executable,
|
|
709
|
+
digest: entry.digest
|
|
632
710
|
})),
|
|
633
711
|
roots: [...roots]
|
|
634
712
|
}));
|
|
@@ -1270,6 +1348,7 @@ function readSnapshot(target, paths) {
|
|
|
1270
1348
|
* Read a vendored host's manifest, when it carries one.
|
|
1271
1349
|
*
|
|
1272
1350
|
* @param host - The vendored host root to read.
|
|
1351
|
+
* @param name - The root-relative manifest path. Default: `manifest.json`.
|
|
1273
1352
|
* @returns The manifest, or `undefined` when the host carries none.
|
|
1274
1353
|
* @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
|
|
1275
1354
|
* @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
|
|
@@ -1295,9 +1374,8 @@ function readSnapshot(target, paths) {
|
|
|
1295
1374
|
* readHostManifest('./dist/host') // the manifest, or undefined for a raw root
|
|
1296
1375
|
* ```
|
|
1297
1376
|
*/
|
|
1298
|
-
function readHostManifest(host) {
|
|
1377
|
+
function readHostManifest(host, name = MANIFEST_NAME) {
|
|
1299
1378
|
if (!isFilesystemPath(host)) throw new _src_core.ScaffoldError("INVALID", "Host root is not a host path", { host });
|
|
1300
|
-
const name = MANIFEST_NAME;
|
|
1301
1379
|
const full = resolveContainedPath(host, name);
|
|
1302
1380
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Host manifest leaves its root at ${host}`, { host });
|
|
1303
1381
|
const status = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(full));
|
|
@@ -1316,6 +1394,55 @@ function readHostManifest(host) {
|
|
|
1316
1394
|
return manifest;
|
|
1317
1395
|
}
|
|
1318
1396
|
/**
|
|
1397
|
+
* Reads the installed vendored host floor as a value.
|
|
1398
|
+
*
|
|
1399
|
+
* @param root - The vendored host root. Default: the installed package's
|
|
1400
|
+
* vendored root, resolved from this module's location.
|
|
1401
|
+
* @returns The verified manifest and the exact bytes of every declared entry.
|
|
1402
|
+
* @throws `ScaffoldError('TARGET', …)` when the root is not a readable physical
|
|
1403
|
+
* directory, its manifest is absent or unreadable, the manifest does not verify,
|
|
1404
|
+
* or a declared file is unreadable or misses its digest.
|
|
1405
|
+
*
|
|
1406
|
+
* @remarks
|
|
1407
|
+
* Reads the same default floor the {@link Materializer} uses. Each declared
|
|
1408
|
+
* file is addressed through the manifest's storage name and retained under its
|
|
1409
|
+
* destination, so the returned value has the same shape as the installed root.
|
|
1410
|
+
* When this module executes from TypeScript source, the committed inventory is
|
|
1411
|
+
* the manifest and each checkout destination supplies its bytes. The emitted
|
|
1412
|
+
* module reads the staged `manifest.json` file and each storage path instead.
|
|
1413
|
+
*
|
|
1414
|
+
* @example
|
|
1415
|
+
* ```ts
|
|
1416
|
+
* import { readHostFloor } from '@orkestrel/scaffold/server'
|
|
1417
|
+
*
|
|
1418
|
+
* readHostFloor().manifest // the installed floor's verified membership
|
|
1419
|
+
* ```
|
|
1420
|
+
*/
|
|
1421
|
+
function readHostFloor(root) {
|
|
1422
|
+
const location = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
1423
|
+
const module = (0, node_path.dirname)(location);
|
|
1424
|
+
const source = root === void 0 && (0, node_path.extname)(location) === ".ts";
|
|
1425
|
+
const host = root ?? (0, node_path.resolve)(module, source ? "../.." : "../../host");
|
|
1426
|
+
if (!isPhysicalDirectory(host)) throw new _src_core.ScaffoldError("TARGET", `The vendored host root is not readable at ${host}`, { host });
|
|
1427
|
+
const manifest = readHostManifest(host, source ? _src_core.HOST_INVENTORY_PATH : MANIFEST_NAME);
|
|
1428
|
+
if (manifest === void 0) throw new _src_core.ScaffoldError("TARGET", `The vendored host carries no manifest at ${host}`, { host });
|
|
1429
|
+
const bytes = {};
|
|
1430
|
+
for (const entry of manifest.entries) {
|
|
1431
|
+
const path = source ? entry.destination : entry.storage;
|
|
1432
|
+
const hex = readFileHex(host, path);
|
|
1433
|
+
if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new _src_core.ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
|
|
1434
|
+
host,
|
|
1435
|
+
path,
|
|
1436
|
+
destination: entry.destination
|
|
1437
|
+
});
|
|
1438
|
+
bytes[entry.destination] = hex;
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
manifest,
|
|
1442
|
+
bytes
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1319
1446
|
* Derive one vendored-host manifest entry from a file in a checkout.
|
|
1320
1447
|
*
|
|
1321
1448
|
* @param destination - The target-relative path the file is written to.
|
|
@@ -1339,20 +1466,155 @@ function readHostManifest(host) {
|
|
|
1339
1466
|
* import { readManifestEntry } from '@orkestrel/scaffold/server'
|
|
1340
1467
|
*
|
|
1341
1468
|
* readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
|
|
1342
|
-
* // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
|
|
1469
|
+
* // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false, digest: '...' }
|
|
1343
1470
|
* ```
|
|
1344
1471
|
*/
|
|
1345
1472
|
function readManifestEntry(destination, source) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (!status.success || status.value.size > _src_core.MAX_ARTIFACT_BYTES) return void 0;
|
|
1473
|
+
const digest = computeFileDigest(source);
|
|
1474
|
+
if (digest === void 0) return void 0;
|
|
1349
1475
|
return {
|
|
1350
1476
|
storage: pathToStorage(destination),
|
|
1351
1477
|
destination,
|
|
1352
|
-
executable: matchesExecutablePath(destination)
|
|
1478
|
+
executable: matchesExecutablePath(destination),
|
|
1479
|
+
digest
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Assemble a whole vendored host from live files and the installed floor.
|
|
1484
|
+
*
|
|
1485
|
+
* @param files - The host-owned vendored files read from the repository, one
|
|
1486
|
+
* row per path.
|
|
1487
|
+
* @param floor - The installed host floor, which fixes the membership a fill
|
|
1488
|
+
* may draw from and supplies the bytes owned by another surface.
|
|
1489
|
+
* @returns The assembled host, or `undefined` when any row produced no answer or
|
|
1490
|
+
* names a path the floor does not declare, or when a host-owned path is absent.
|
|
1491
|
+
*
|
|
1492
|
+
* @remarks
|
|
1493
|
+
* The one place the host-owned all-or-nothing rule is decided, so no verb
|
|
1494
|
+
* restates it. The host surface contributes one baseline: a fill carries live
|
|
1495
|
+
* bytes for every path that surface writes, or it is nothing. A row that failed,
|
|
1496
|
+
* went missing, names an undeclared path, or leaves a host-owned path absent
|
|
1497
|
+
* answers `undefined`. Deferred paths are presence-only and retain the installed
|
|
1498
|
+
* floor bytes that their catalog or mirror surface owns; repair never writes
|
|
1499
|
+
* those floor bytes. One `Host` can therefore carry live host bytes beside floor
|
|
1500
|
+
* bytes without mixing baselines within a surface.
|
|
1501
|
+
*
|
|
1502
|
+
* The emitted entries keep the release's own order and its storage and
|
|
1503
|
+
* executable declarations, and carry digests recomputed over the bytes the fill
|
|
1504
|
+
* actually holds. That is what lets a reader verify the value against itself,
|
|
1505
|
+
* and it is why an undeclared path is refused rather than added: membership
|
|
1506
|
+
* moves with a release, never with a fetch.
|
|
1507
|
+
*
|
|
1508
|
+
* @example
|
|
1509
|
+
* ```ts
|
|
1510
|
+
* import { filesToHost } from '@orkestrel/scaffold/server'
|
|
1511
|
+
*
|
|
1512
|
+
* filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
|
|
1513
|
+
* // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
|
|
1514
|
+
* ```
|
|
1515
|
+
*/
|
|
1516
|
+
function filesToHost(files, floor) {
|
|
1517
|
+
const declared = new Set(floor.manifest.entries.map((entry) => entry.destination));
|
|
1518
|
+
const held = /* @__PURE__ */ new Map();
|
|
1519
|
+
for (const file of files) {
|
|
1520
|
+
if (file.lookup !== "found" || !declared.has(file.path)) return void 0;
|
|
1521
|
+
if (!(0, _src_core.isDeferredPath)(file.path)) held.set(file.path, file.hex);
|
|
1522
|
+
}
|
|
1523
|
+
const entries = [];
|
|
1524
|
+
const bytes = {};
|
|
1525
|
+
for (const entry of floor.manifest.entries) {
|
|
1526
|
+
const hex = (0, _src_core.isDeferredPath)(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
|
|
1527
|
+
if (hex === void 0) return void 0;
|
|
1528
|
+
entries.push({
|
|
1529
|
+
storage: entry.storage,
|
|
1530
|
+
destination: entry.destination,
|
|
1531
|
+
executable: entry.executable,
|
|
1532
|
+
digest: hexToDigest(hex)
|
|
1533
|
+
});
|
|
1534
|
+
bytes[entry.destination] = hex;
|
|
1535
|
+
}
|
|
1536
|
+
return {
|
|
1537
|
+
manifest: {
|
|
1538
|
+
entries,
|
|
1539
|
+
roots: floor.manifest.roots,
|
|
1540
|
+
digest: computeManifestDigest(entries, floor.manifest.roots)
|
|
1541
|
+
},
|
|
1542
|
+
bytes
|
|
1353
1543
|
};
|
|
1354
1544
|
}
|
|
1355
1545
|
/**
|
|
1546
|
+
* Stage the named destinations of a value host into a private root.
|
|
1547
|
+
*
|
|
1548
|
+
* @param host - The host whose bytes are written, keyed by destination.
|
|
1549
|
+
* @param root - The private directory to fill; it must already be a directory
|
|
1550
|
+
* this process may write into.
|
|
1551
|
+
* @param destinations - The destinations to stage, each declared by `host`.
|
|
1552
|
+
* @returns The entry staged for each destination, in the order requested.
|
|
1553
|
+
* @throws `ScaffoldError('INVALID', …)` when `root` is not a host path or a
|
|
1554
|
+
* storage name leaves it.
|
|
1555
|
+
* @throws `ScaffoldError('TARGET', …)` when a destination is one the host does
|
|
1556
|
+
* not declare, carries no bytes, or carries bytes that miss its declared digest.
|
|
1557
|
+
* @throws `ScaffoldError('WRITE', …)` when a file cannot be written or does not
|
|
1558
|
+
* read back as the bytes it was given.
|
|
1559
|
+
*
|
|
1560
|
+
* @remarks
|
|
1561
|
+
* Each file lands under the storage name the manifest declares and takes the
|
|
1562
|
+
* executable bit that manifest records, so a root filled from a value is the
|
|
1563
|
+
* same shape as one staged from a checkout and a reader cannot tell them apart.
|
|
1564
|
+
* That is what lets a mutation copy real files with real modes from bytes a
|
|
1565
|
+
* caller supplied, instead of degrading them to plain text writes.
|
|
1566
|
+
*
|
|
1567
|
+
* The bytes are digested before the write and the staged file after it, so a
|
|
1568
|
+
* value that disagrees with its own manifest is told apart from a write that
|
|
1569
|
+
* did not land.
|
|
1570
|
+
*
|
|
1571
|
+
* @example
|
|
1572
|
+
* ```ts
|
|
1573
|
+
* import { stageBytes } from '@orkestrel/scaffold/server'
|
|
1574
|
+
*
|
|
1575
|
+
* stageBytes(host, '/tmp/orkestrel-host-a1b2', ['scripts/codex.sh'])
|
|
1576
|
+
* // [{ storage: 'scripts/codex.sh', destination: 'scripts/codex.sh', executable: true, digest: '…' }]
|
|
1577
|
+
* ```
|
|
1578
|
+
*/
|
|
1579
|
+
function stageBytes(host, root, destinations) {
|
|
1580
|
+
if (!isFilesystemPath(root)) throw new _src_core.ScaffoldError("INVALID", "Staging host root is not a host path", { host: root });
|
|
1581
|
+
const declared = new Map(host.manifest.entries.map((entry) => [entry.destination, entry]));
|
|
1582
|
+
const staged = [];
|
|
1583
|
+
for (const destination of destinations) {
|
|
1584
|
+
const entry = declared.get(destination);
|
|
1585
|
+
const hex = host.bytes[destination];
|
|
1586
|
+
if (entry === void 0 || hex === void 0) throw new _src_core.ScaffoldError("TARGET", `The host carries no bytes for ${destination}`, {
|
|
1587
|
+
host: root,
|
|
1588
|
+
destination
|
|
1589
|
+
});
|
|
1590
|
+
if (hexToDigest(hex) !== entry.digest) throw new _src_core.ScaffoldError("TARGET", `The host bytes for ${destination} miss its digest`, {
|
|
1591
|
+
host: root,
|
|
1592
|
+
destination
|
|
1593
|
+
});
|
|
1594
|
+
const full = resolveContainedPath(root, entry.storage);
|
|
1595
|
+
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Host storage leaves its root at ${entry.storage}`, {
|
|
1596
|
+
host: root,
|
|
1597
|
+
storage: entry.storage
|
|
1598
|
+
});
|
|
1599
|
+
const written = (0, _orkestrel_contract.attempt)(() => {
|
|
1600
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(full), { recursive: true });
|
|
1601
|
+
(0, node_fs.writeFileSync)(full, Buffer.from(hex, "hex"), { flag: "wx" });
|
|
1602
|
+
if (entry.executable) (0, node_fs.chmodSync)(full, 493);
|
|
1603
|
+
});
|
|
1604
|
+
if (!written.success) throw new _src_core.ScaffoldError("WRITE", `Host bytes could not be staged at ${entry.storage}`, {
|
|
1605
|
+
host: root,
|
|
1606
|
+
storage: entry.storage,
|
|
1607
|
+
error: written.error
|
|
1608
|
+
});
|
|
1609
|
+
if (computeFileDigest(full) !== entry.digest) throw new _src_core.ScaffoldError("WRITE", `Staged host bytes at ${entry.storage} did not read back`, {
|
|
1610
|
+
host: root,
|
|
1611
|
+
storage: entry.storage
|
|
1612
|
+
});
|
|
1613
|
+
staged.push(entry);
|
|
1614
|
+
}
|
|
1615
|
+
return staged;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1356
1618
|
* Stage a vendored host root from a real checkout.
|
|
1357
1619
|
*
|
|
1358
1620
|
* @param checkout - The checkout the vendored paths are read from.
|
|
@@ -1442,7 +1704,7 @@ function stageHost(checkout, host) {
|
|
|
1442
1704
|
missing
|
|
1443
1705
|
});
|
|
1444
1706
|
const stored = /* @__PURE__ */ new Set([MANIFEST_NAME]);
|
|
1445
|
-
const
|
|
1707
|
+
const candidates = [];
|
|
1446
1708
|
for (const destination of vendored) {
|
|
1447
1709
|
const full = resolveContainedPath(source, destination);
|
|
1448
1710
|
if (full === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its checkout at ${destination}`, {
|
|
@@ -1461,9 +1723,9 @@ function stageHost(checkout, host) {
|
|
|
1461
1723
|
storage: entry.storage
|
|
1462
1724
|
});
|
|
1463
1725
|
stored.add(entry.storage);
|
|
1464
|
-
|
|
1726
|
+
candidates.push(entry);
|
|
1465
1727
|
}
|
|
1466
|
-
|
|
1728
|
+
candidates.sort((first, second) => first.storage < second.storage ? -1 : 1);
|
|
1467
1729
|
roots.sort();
|
|
1468
1730
|
const root = (0, node_path.resolve)(host);
|
|
1469
1731
|
const established = (0, _orkestrel_contract.attempt)(() => (0, node_fs.mkdirSync)(root, { recursive: true }));
|
|
@@ -1471,7 +1733,8 @@ function stageHost(checkout, host) {
|
|
|
1471
1733
|
host: root,
|
|
1472
1734
|
...established.success ? {} : { error: established.error }
|
|
1473
1735
|
});
|
|
1474
|
-
|
|
1736
|
+
const entries = [];
|
|
1737
|
+
for (const entry of candidates) {
|
|
1475
1738
|
const origin = resolveContainedPath(source, entry.destination);
|
|
1476
1739
|
const destination = resolveContainedPath(root, entry.storage);
|
|
1477
1740
|
if (origin === void 0 || destination === void 0) throw new _src_core.ScaffoldError("INVALID", `Vendored path leaves its root at ${entry.destination}`, {
|
|
@@ -1490,6 +1753,15 @@ function stageHost(checkout, host) {
|
|
|
1490
1753
|
storage: entry.storage,
|
|
1491
1754
|
error: copied.error
|
|
1492
1755
|
});
|
|
1756
|
+
const digest = computeFileDigest(destination);
|
|
1757
|
+
if (digest === void 0) throw new _src_core.ScaffoldError("WRITE", `Vendored file could not be verified at ${entry.storage}`, {
|
|
1758
|
+
host: root,
|
|
1759
|
+
storage: entry.storage
|
|
1760
|
+
});
|
|
1761
|
+
entries.push({
|
|
1762
|
+
...entry,
|
|
1763
|
+
digest
|
|
1764
|
+
});
|
|
1493
1765
|
}
|
|
1494
1766
|
const manifest = {
|
|
1495
1767
|
entries,
|
|
@@ -1511,6 +1783,67 @@ function stageHost(checkout, host) {
|
|
|
1511
1783
|
return entries;
|
|
1512
1784
|
}
|
|
1513
1785
|
/**
|
|
1786
|
+
* Stages the committed inventory of the files a vendored host carries.
|
|
1787
|
+
*
|
|
1788
|
+
* @param checkout - The checkout whose vendored paths are inventoried.
|
|
1789
|
+
* @param path - The host path where the JSON inventory is written.
|
|
1790
|
+
* @returns The validated manifest written to `path`.
|
|
1791
|
+
* @throws `ScaffoldError('INVALID', …)` when `path` is not a host path.
|
|
1792
|
+
* @throws `ScaffoldError('WRITE', …)` when a temporary host or the inventory
|
|
1793
|
+
* cannot be written or removed.
|
|
1794
|
+
* @throws `ScaffoldError('TARGET', …)` when the staged inventory does not read
|
|
1795
|
+
* back through the manifest validator.
|
|
1796
|
+
*
|
|
1797
|
+
* @remarks
|
|
1798
|
+
* Uses {@link stageHost} as the single vendored-path expansion. The temporary
|
|
1799
|
+
* host supplies the same entries, roots, per-file digests, and membership
|
|
1800
|
+
* digest as the published host while the requested output remains one JSON
|
|
1801
|
+
* file.
|
|
1802
|
+
*
|
|
1803
|
+
* @example
|
|
1804
|
+
* ```ts
|
|
1805
|
+
* import { stageInventory } from '@orkestrel/scaffold/server'
|
|
1806
|
+
*
|
|
1807
|
+
* stageInventory(process.cwd(), 'host.json') // the committed host inventory
|
|
1808
|
+
* ```
|
|
1809
|
+
*/
|
|
1810
|
+
function stageInventory(checkout, path) {
|
|
1811
|
+
if (!isFilesystemPath(path)) throw new _src_core.ScaffoldError("INVALID", "Inventory destination is not a host path", { path });
|
|
1812
|
+
const temporary = (0, _orkestrel_contract.attempt)(() => (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "orkestrel-scaffold-host-")));
|
|
1813
|
+
if (!temporary.success) throw new _src_core.ScaffoldError("WRITE", "Inventory staging root could not be established", {
|
|
1814
|
+
path,
|
|
1815
|
+
error: temporary.error
|
|
1816
|
+
});
|
|
1817
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
1818
|
+
stageHost(checkout, temporary.value);
|
|
1819
|
+
const manifest = readHostManifest(temporary.value);
|
|
1820
|
+
if (manifest === void 0) throw new _src_core.ScaffoldError("TARGET", "The staged inventory carries no manifest", { path });
|
|
1821
|
+
const target = (0, node_path.resolve)(path);
|
|
1822
|
+
const published = (0, _orkestrel_contract.attempt)(() => {
|
|
1823
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(target), { recursive: true });
|
|
1824
|
+
(0, node_fs.writeFileSync)(target, `${JSON.stringify(manifest, null, " ")}\n`, "utf8");
|
|
1825
|
+
});
|
|
1826
|
+
if (!published.success) throw new _src_core.ScaffoldError("WRITE", `Inventory could not be written at ${target}`, {
|
|
1827
|
+
path: target,
|
|
1828
|
+
error: published.error
|
|
1829
|
+
});
|
|
1830
|
+
const text = readFileText((0, node_path.dirname)(target), (0, node_path.basename)(target), _src_core.MAX_MANIFEST_BYTES);
|
|
1831
|
+
const verified = text === void 0 ? void 0 : (0, _orkestrel_contract.parseJSONAs)(text, isHostManifest);
|
|
1832
|
+
if (verified === void 0 || verified.digest !== computeManifestDigest(verified.entries, verified.roots)) throw new _src_core.ScaffoldError("TARGET", `Inventory does not read back at ${target}`, { path: target });
|
|
1833
|
+
return verified;
|
|
1834
|
+
});
|
|
1835
|
+
const removed = (0, _orkestrel_contract.attempt)(() => (0, node_fs.rmSync)(temporary.value, {
|
|
1836
|
+
recursive: true,
|
|
1837
|
+
force: true
|
|
1838
|
+
}));
|
|
1839
|
+
if (!removed.success) throw new _src_core.ScaffoldError("WRITE", `Inventory staging root could not be removed`, {
|
|
1840
|
+
path: temporary.value,
|
|
1841
|
+
error: removed.error
|
|
1842
|
+
});
|
|
1843
|
+
if (!staged.success) throw staged.error;
|
|
1844
|
+
return staged.value;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1514
1847
|
* Capture one directory's physical identity.
|
|
1515
1848
|
*
|
|
1516
1849
|
* @param path - The resolved directory path to capture.
|
|
@@ -2196,6 +2529,12 @@ var WriteTransaction = class {
|
|
|
2196
2529
|
* case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
|
|
2197
2530
|
* a case-insensitive filesystem rather than silently resolved.
|
|
2198
2531
|
*
|
|
2532
|
+
* That host arrives as a directory path or as a whole {@link Host} value, and
|
|
2533
|
+
* every verb reads one immutable host either way. A value is owned, verified
|
|
2534
|
+
* against its own membership and digests, and read in memory; a write fills it
|
|
2535
|
+
* into a private root and copies from there, so the executable declarations the
|
|
2536
|
+
* release fixed reach the target from either representation.
|
|
2537
|
+
*
|
|
2199
2538
|
* What a mutation guarantees is exactly what {@link WriteTransaction}
|
|
2200
2539
|
* guarantees, and no more: a caught failure part way through a commit rolls the
|
|
2201
2540
|
* whole commit back, no destination ever receives half-written bytes, and a
|
|
@@ -2221,25 +2560,34 @@ var Materializer = class Materializer {
|
|
|
2221
2560
|
static #opening = "<!-- orkestrel:catalog -->";
|
|
2222
2561
|
static #closing = "<!-- /orkestrel:catalog -->";
|
|
2223
2562
|
#emitter;
|
|
2224
|
-
#
|
|
2563
|
+
#root;
|
|
2564
|
+
#value;
|
|
2225
2565
|
#manifest;
|
|
2226
2566
|
#entries;
|
|
2227
2567
|
#destroyed = false;
|
|
2228
2568
|
/**
|
|
2229
2569
|
* Construct a materializer over one vendored host root.
|
|
2230
2570
|
*
|
|
2231
|
-
* @param options - The vendored host
|
|
2232
|
-
* listener-error handler.
|
|
2571
|
+
* @param options - The vendored host, in either representation, the initial
|
|
2572
|
+
* listeners, and the listener-error handler.
|
|
2233
2573
|
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
|
|
2234
2574
|
* is not an option bag this materializer accepts, and `TARGET` when the host
|
|
2235
|
-
* carries a manifest that cannot be read
|
|
2575
|
+
* carries a manifest that cannot be read, does not match what it stores, or
|
|
2576
|
+
* is a value that does not agree with the bytes beside it.
|
|
2236
2577
|
*
|
|
2237
2578
|
* @remarks
|
|
2238
|
-
* `host` defaults to this package's own vendored root, resolved from
|
|
2239
|
-
* module's own location so it never depends on the caller's working
|
|
2240
|
-
* directory. A
|
|
2579
|
+
* A `host` path defaults to this package's own vendored root, resolved from
|
|
2580
|
+
* this module's own location so it never depends on the caller's working
|
|
2581
|
+
* directory. A root carrying no manifest is read as a raw checkout and every
|
|
2241
2582
|
* artifact maps onto it one to one.
|
|
2242
2583
|
*
|
|
2584
|
+
* A `host` value is owned before it is read and then held immutable, so the
|
|
2585
|
+
* bytes this check measured are the bytes every later read returns. It is
|
|
2586
|
+
* verified the way a root is, against the same membership law: the manifest
|
|
2587
|
+
* digest must cover the membership beside it, no two entries may claim one
|
|
2588
|
+
* destination, and the fill must carry exactly one hashing byte string per
|
|
2589
|
+
* declared entry.
|
|
2590
|
+
*
|
|
2243
2591
|
* The host is read here rather than on first use, so a broken vendored root
|
|
2244
2592
|
* fails at construction where the caller can still act on it, and so nothing
|
|
2245
2593
|
* has to carry a second flag recording whether the read has happened yet.
|
|
@@ -2250,15 +2598,27 @@ var Materializer = class Materializer {
|
|
|
2250
2598
|
...options?.on === void 0 ? {} : { on: options.on },
|
|
2251
2599
|
...options?.error === void 0 ? {} : { error: options.error }
|
|
2252
2600
|
});
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2601
|
+
const supplied = options?.host ?? readHostFloor();
|
|
2602
|
+
if (supplied !== void 0 && !isFilesystemPath(supplied)) {
|
|
2603
|
+
const value = this.#own(supplied);
|
|
2604
|
+
this.#value = value;
|
|
2605
|
+
this.#root = void 0;
|
|
2606
|
+
this.#manifest = value.manifest;
|
|
2607
|
+
this.#entries = new Map(value.manifest.entries.map((entry) => [entry.destination, entry]));
|
|
2608
|
+
this.#verify(value);
|
|
2609
|
+
} else {
|
|
2610
|
+
const root = supplied;
|
|
2611
|
+
this.#value = void 0;
|
|
2612
|
+
this.#root = root;
|
|
2613
|
+
const read = (0, _orkestrel_contract.attempt)(() => readHostManifest(root));
|
|
2614
|
+
if (!read.success) throw this.#error("TARGET", "The vendored host carries a manifest that cannot be read.", {
|
|
2615
|
+
host: root,
|
|
2616
|
+
error: read.error
|
|
2617
|
+
});
|
|
2618
|
+
this.#manifest = read.value;
|
|
2619
|
+
this.#entries = new Map((read.value?.entries ?? []).map((entry) => [entry.destination, entry]));
|
|
2620
|
+
if (read.value !== void 0) this.#reconcile(read.value, root);
|
|
2621
|
+
}
|
|
2262
2622
|
}
|
|
2263
2623
|
/** The materializer's observation channel. */
|
|
2264
2624
|
get emitter() {
|
|
@@ -2454,7 +2814,7 @@ var Materializer = class Materializer {
|
|
|
2454
2814
|
*
|
|
2455
2815
|
* @param plan - The compiled plan that decides which paths are foreign.
|
|
2456
2816
|
* @param audit - The preview returned by this materializer's `audit` method; it must agree with the candidate set this call re-derives.
|
|
2457
|
-
* @param
|
|
2817
|
+
* @param worktree - The target's git state; only a tracked path is ever deleted.
|
|
2458
2818
|
* @param target - The directory to delete from.
|
|
2459
2819
|
* @returns The paths removed.
|
|
2460
2820
|
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
@@ -2475,11 +2835,11 @@ var Materializer = class Materializer {
|
|
|
2475
2835
|
* any foreign finding, including one the deletion itself would skip, because a
|
|
2476
2836
|
* preview stale anywhere is stale evidence.
|
|
2477
2837
|
*/
|
|
2478
|
-
remove(plan, audit,
|
|
2838
|
+
remove(plan, audit, worktree, target) {
|
|
2479
2839
|
this.#assertAlive();
|
|
2480
2840
|
const accepted = this.#accept(plan, _src_core.isPlan, "plan");
|
|
2481
2841
|
const preview = this.#accept(audit, _src_core.isAudit, "audit");
|
|
2482
|
-
const state = this.#accept(
|
|
2842
|
+
const state = this.#accept(worktree, isWorktree, "worktree");
|
|
2483
2843
|
const directory = this.#accept(target, isFilesystemPath, "target");
|
|
2484
2844
|
if (state.dirty.length > 0) throw this.#error("TARGET", `The target at ${directory} carries uncommitted changes.`, {
|
|
2485
2845
|
target: directory,
|
|
@@ -2522,20 +2882,41 @@ var Materializer = class Materializer {
|
|
|
2522
2882
|
this.#emitter.emit("destroy");
|
|
2523
2883
|
this.#emitter.destroy();
|
|
2524
2884
|
}
|
|
2525
|
-
#
|
|
2526
|
-
const
|
|
2885
|
+
#own(host) {
|
|
2886
|
+
const owned = (0, _src_core.cloneValue)(host);
|
|
2887
|
+
if (isHost(owned)) return owned;
|
|
2888
|
+
throw this.#error("INVALID", "The host argument is not the exact shape this materializer accepts.", { field: "host" });
|
|
2889
|
+
}
|
|
2890
|
+
#verify(host) {
|
|
2891
|
+
const { entries, roots } = host.manifest;
|
|
2892
|
+
if (host.manifest.digest !== computeManifestDigest(entries, roots)) throw this.#error("TARGET", "The vendored host manifest does not cover the membership beside it.");
|
|
2893
|
+
if (this.#entries.size !== entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.");
|
|
2894
|
+
if (new Set(entries.map((entry) => entry.storage)).size !== entries.length) throw this.#error("TARGET", "The vendored host manifest maps two destinations to one file.");
|
|
2895
|
+
const held = Object.keys(host.bytes);
|
|
2896
|
+
if (held.length !== entries.length) throw this.#error("TARGET", "The vendored host does not carry what its manifest declares.", {
|
|
2897
|
+
held: held.length,
|
|
2898
|
+
declared: entries.length
|
|
2899
|
+
});
|
|
2900
|
+
for (const entry of entries) {
|
|
2901
|
+
const hex = host.bytes[entry.destination];
|
|
2902
|
+
if (hex === void 0) throw this.#error("TARGET", `The vendored host carries no bytes for ${entry.destination}.`, { destination: entry.destination });
|
|
2903
|
+
if (hexToDigest(hex) !== entry.digest) throw this.#error("TARGET", `The vendored host carries bytes for ${entry.destination} that miss its digest.`, { destination: entry.destination });
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
#reconcile(manifest, root) {
|
|
2907
|
+
const walked = (0, _orkestrel_contract.attempt)(() => listFiles(root));
|
|
2527
2908
|
if (!walked.success) throw this.#error("TARGET", "The vendored host cannot be inventoried.", {
|
|
2528
|
-
host:
|
|
2909
|
+
host: root,
|
|
2529
2910
|
error: walked.error
|
|
2530
2911
|
});
|
|
2531
2912
|
const declared = [...manifest.entries.map((entry) => entry.storage), "manifest.json"].sort();
|
|
2532
2913
|
const stored = walked.value;
|
|
2533
2914
|
if (stored.length !== declared.length || stored.some((name, index) => name !== declared[index])) throw this.#error("TARGET", "The vendored host does not store what its manifest declares.", {
|
|
2534
|
-
host:
|
|
2915
|
+
host: root,
|
|
2535
2916
|
stored: stored.length,
|
|
2536
2917
|
declared: declared.length
|
|
2537
2918
|
});
|
|
2538
|
-
if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host:
|
|
2919
|
+
if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host: root });
|
|
2539
2920
|
}
|
|
2540
2921
|
#hydrate(plan) {
|
|
2541
2922
|
const artifacts = [];
|
|
@@ -2551,7 +2932,7 @@ var Materializer = class Materializer {
|
|
|
2551
2932
|
artifacts.push(...expanded);
|
|
2552
2933
|
}
|
|
2553
2934
|
if (remaining < 0) throw this.#error("TARGET", "The hydrated plan retains more bytes than one plan may.", {
|
|
2554
|
-
host: this.#
|
|
2935
|
+
host: this.#root,
|
|
2555
2936
|
limit: _src_core.MAX_TOTAL_ARTIFACT_BYTES
|
|
2556
2937
|
});
|
|
2557
2938
|
return {
|
|
@@ -2585,7 +2966,8 @@ var Materializer = class Materializer {
|
|
|
2585
2966
|
continue;
|
|
2586
2967
|
}
|
|
2587
2968
|
if (this.#manifest !== void 0) continue;
|
|
2588
|
-
const
|
|
2969
|
+
const root = this.#root;
|
|
2970
|
+
const directory = root === void 0 ? void 0 : resolveContainedPath(root, source);
|
|
2589
2971
|
if (directory !== void 0 && isPhysicalDirectory(directory)) roots.add(artifact.path);
|
|
2590
2972
|
}
|
|
2591
2973
|
return [...roots];
|
|
@@ -2597,7 +2979,7 @@ var Materializer = class Materializer {
|
|
|
2597
2979
|
const matched = manifest.entries.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`));
|
|
2598
2980
|
const rooted = manifest.roots.some((root) => root === source || root.startsWith(`${source}/`));
|
|
2599
2981
|
if (matched.length === 0 && !rooted) throw this.#error("TARGET", `The vendored host does not carry ${source}.`, {
|
|
2600
|
-
host: this.#
|
|
2982
|
+
host: this.#root,
|
|
2601
2983
|
source
|
|
2602
2984
|
});
|
|
2603
2985
|
const expanded = [];
|
|
@@ -2608,29 +2990,30 @@ var Materializer = class Materializer {
|
|
|
2608
2990
|
expanded.push(this.#presence(artifact, path, entry.destination));
|
|
2609
2991
|
continue;
|
|
2610
2992
|
}
|
|
2611
|
-
if (
|
|
2993
|
+
if ((0, _src_core.isDeferredPath)(path)) {
|
|
2612
2994
|
expanded.push(this.#presence(artifact, path, entry.destination));
|
|
2613
2995
|
continue;
|
|
2614
2996
|
}
|
|
2615
|
-
const hex = this.#read(entry
|
|
2997
|
+
const hex = this.#read(entry, budget);
|
|
2616
2998
|
budget -= hex.length / 2;
|
|
2617
2999
|
expanded.push(this.#hydrated(artifact, path, entry.destination, hex));
|
|
2618
3000
|
}
|
|
2619
3001
|
return expanded;
|
|
2620
3002
|
}
|
|
2621
3003
|
#expandRaw(artifact, source, remaining) {
|
|
2622
|
-
const
|
|
3004
|
+
const root = this.#root;
|
|
3005
|
+
const full = root === void 0 ? void 0 : resolveContainedPath(root, source);
|
|
2623
3006
|
if (full === void 0) throw this.#error("TARGET", `The host source at ${source} leaves its root.`, {
|
|
2624
|
-
host: this.#
|
|
3007
|
+
host: this.#root,
|
|
2625
3008
|
source
|
|
2626
3009
|
});
|
|
2627
3010
|
if (isPhysicalFile(full)) {
|
|
2628
3011
|
if (_src_core.WORKSPACE_OWNED_PATHS.includes(artifact.path)) return [this.#presence(artifact, artifact.path, source)];
|
|
2629
|
-
if (
|
|
2630
|
-
return [this.#hydrated(artifact, artifact.path, source, this.#
|
|
3012
|
+
if ((0, _src_core.isDeferredPath)(artifact.path)) return [this.#presence(artifact, artifact.path, source)];
|
|
3013
|
+
return [this.#hydrated(artifact, artifact.path, source, this.#readRoot(source, remaining))];
|
|
2631
3014
|
}
|
|
2632
3015
|
if (!isPhysicalDirectory(full)) throw this.#error("TARGET", `The host source at ${source} is not a readable file.`, {
|
|
2633
|
-
host: this.#
|
|
3016
|
+
host: this.#root,
|
|
2634
3017
|
source
|
|
2635
3018
|
});
|
|
2636
3019
|
const expanded = [];
|
|
@@ -2642,11 +3025,11 @@ var Materializer = class Materializer {
|
|
|
2642
3025
|
expanded.push(this.#presence(artifact, path, destination));
|
|
2643
3026
|
continue;
|
|
2644
3027
|
}
|
|
2645
|
-
if (
|
|
3028
|
+
if ((0, _src_core.isDeferredPath)(path)) {
|
|
2646
3029
|
expanded.push(this.#presence(artifact, path, destination));
|
|
2647
3030
|
continue;
|
|
2648
3031
|
}
|
|
2649
|
-
const hex = this.#
|
|
3032
|
+
const hex = this.#readRoot(destination, budget);
|
|
2650
3033
|
budget -= hex.length / 2;
|
|
2651
3034
|
expanded.push(this.#hydrated(artifact, path, destination, hex));
|
|
2652
3035
|
}
|
|
@@ -2670,15 +3053,12 @@ var Materializer = class Materializer {
|
|
|
2670
3053
|
const source = artifact.source ?? artifact.path;
|
|
2671
3054
|
if (destination === source) return artifact.path;
|
|
2672
3055
|
if (!destination.startsWith(`${source}/`)) throw this.#error("TARGET", `The vendored destination ${destination} is outside ${source}.`, {
|
|
2673
|
-
host: this.#
|
|
3056
|
+
host: this.#root,
|
|
2674
3057
|
source,
|
|
2675
3058
|
destination
|
|
2676
3059
|
});
|
|
2677
3060
|
return `${artifact.path}/${destination.slice(source.length + 1)}`;
|
|
2678
3061
|
}
|
|
2679
|
-
#deferred(path) {
|
|
2680
|
-
return path === _src_core.CATALOG_AGENT_PATH || path.startsWith("guides/") && path.endsWith(".md");
|
|
2681
|
-
}
|
|
2682
3062
|
#presence(artifact, path, destination) {
|
|
2683
3063
|
return {
|
|
2684
3064
|
path,
|
|
@@ -2700,10 +3080,20 @@ var Materializer = class Materializer {
|
|
|
2700
3080
|
hex
|
|
2701
3081
|
};
|
|
2702
3082
|
}
|
|
2703
|
-
#read(
|
|
2704
|
-
const
|
|
3083
|
+
#read(entry, budget) {
|
|
3084
|
+
const held = this.#value?.bytes[entry.destination];
|
|
3085
|
+
if (held === void 0) return this.#readRoot(entry.storage, budget);
|
|
3086
|
+
if (held.length / 2 > Math.max(0, Math.min(_src_core.MAX_ARTIFACT_BYTES, budget))) throw this.#error("TARGET", `The vendored host cannot be read at ${entry.destination}.`, {
|
|
3087
|
+
destination: entry.destination,
|
|
3088
|
+
limit: _src_core.MAX_ARTIFACT_BYTES
|
|
3089
|
+
});
|
|
3090
|
+
return held;
|
|
3091
|
+
}
|
|
3092
|
+
#readRoot(storage, budget) {
|
|
3093
|
+
const root = this.#root;
|
|
3094
|
+
const hex = root === void 0 ? void 0 : readFileHex(root, storage, Math.max(0, Math.min(_src_core.MAX_ARTIFACT_BYTES, budget)));
|
|
2705
3095
|
if (hex === void 0) throw this.#error("TARGET", `The vendored host cannot be read at ${storage}.`, {
|
|
2706
|
-
host: this.#
|
|
3096
|
+
host: this.#root,
|
|
2707
3097
|
storage
|
|
2708
3098
|
});
|
|
2709
3099
|
return hex;
|
|
@@ -2809,20 +3199,49 @@ var Materializer = class Materializer {
|
|
|
2809
3199
|
skipped,
|
|
2810
3200
|
removed: []
|
|
2811
3201
|
});
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3202
|
+
const filled = this.#fill(writes);
|
|
3203
|
+
try {
|
|
3204
|
+
const transaction = this.#open(target, paths, preconditions);
|
|
3205
|
+
const staged = (0, _orkestrel_contract.attempt)(() => {
|
|
3206
|
+
for (const artifact of writes) if (artifact.origin === "host") this.#copy(transaction, artifact, filled ?? this.#root);
|
|
3207
|
+
else transaction.write(artifact.path, artifact.content);
|
|
3208
|
+
for (const path of directories) transaction.establish(path);
|
|
3209
|
+
});
|
|
3210
|
+
const written = this.#close(transaction, staged, target);
|
|
3211
|
+
for (const path of written) this.#emitter.emit("write", path);
|
|
3212
|
+
return this.#finish({
|
|
3213
|
+
target,
|
|
3214
|
+
written,
|
|
3215
|
+
skipped,
|
|
3216
|
+
removed: []
|
|
3217
|
+
});
|
|
3218
|
+
} finally {
|
|
3219
|
+
if (filled !== void 0) (0, node_fs.rmSync)(filled, {
|
|
3220
|
+
recursive: true,
|
|
3221
|
+
force: true
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
#fill(writes) {
|
|
3226
|
+
const value = this.#value;
|
|
3227
|
+
if (value === void 0) return void 0;
|
|
3228
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
3229
|
+
for (const artifact of writes) {
|
|
3230
|
+
if (artifact.origin !== "host") continue;
|
|
3231
|
+
destinations.add(artifact.source ?? artifact.path);
|
|
3232
|
+
}
|
|
3233
|
+
if (destinations.size === 0) return void 0;
|
|
3234
|
+
const opened = (0, _orkestrel_contract.attempt)(() => (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "orkestrel-scaffold-fill-")));
|
|
3235
|
+
if (!opened.success) throw this.#error("WRITE", "The supplied host could not be filled into a private root.", { error: opened.error });
|
|
3236
|
+
const root = opened.value;
|
|
3237
|
+
const stored = (0, _orkestrel_contract.attempt)(() => stageBytes(value, root, [...destinations]));
|
|
3238
|
+
if (stored.success) return root;
|
|
3239
|
+
(0, node_fs.rmSync)(root, {
|
|
3240
|
+
recursive: true,
|
|
3241
|
+
force: true
|
|
2825
3242
|
});
|
|
3243
|
+
this.#emitter.emit("error", stored.error);
|
|
3244
|
+
throw stored.error;
|
|
2826
3245
|
}
|
|
2827
3246
|
#purge(target, removals, skipped, preconditions) {
|
|
2828
3247
|
if (removals.length === 0) return this.#finish({
|
|
@@ -2866,13 +3285,13 @@ var Materializer = class Materializer {
|
|
|
2866
3285
|
this.#emitter.emit("error", committed.error);
|
|
2867
3286
|
throw committed.error;
|
|
2868
3287
|
}
|
|
2869
|
-
#copy(transaction, artifact) {
|
|
3288
|
+
#copy(transaction, artifact, root) {
|
|
2870
3289
|
const destination = artifact.source ?? artifact.path;
|
|
2871
3290
|
const entry = this.#entries.get(destination);
|
|
2872
3291
|
const storage = entry === void 0 ? destination : entry.storage;
|
|
2873
|
-
const source = resolveContainedPath(
|
|
3292
|
+
const source = root === void 0 ? void 0 : resolveContainedPath(root, storage);
|
|
2874
3293
|
if (source === void 0) throw this.#error("TARGET", `The vendored source at ${storage} leaves its root.`, {
|
|
2875
|
-
host:
|
|
3294
|
+
host: root,
|
|
2876
3295
|
storage
|
|
2877
3296
|
});
|
|
2878
3297
|
transaction.copy(artifact.path, source, entry?.executable === true);
|
|
@@ -2969,6 +3388,11 @@ var Materializer = class Materializer {
|
|
|
2969
3388
|
* there is no fleet to report, so an unreachable or malformed list is a coded
|
|
2970
3389
|
* `FETCH` failure.
|
|
2971
3390
|
*
|
|
3391
|
+
* The vendored-file inventory is the other read a whole call rests on, and it
|
|
3392
|
+
* fails the other way: it fails every row of its call rather than throwing, so
|
|
3393
|
+
* the caller receives one whole dead answer it can replace with one whole
|
|
3394
|
+
* baseline instead of a mixture it would have to reconcile.
|
|
3395
|
+
*
|
|
2972
3396
|
* Requests are unauthenticated because every fleet repository is public, and
|
|
2973
3397
|
* they follow no redirect, so a misconfigured or hostile endpoint cannot move a
|
|
2974
3398
|
* read to another host. Each one is bounded by its endpoint's timeout and by the
|
|
@@ -2989,19 +3413,20 @@ var Materializer = class Materializer {
|
|
|
2989
3413
|
* ```
|
|
2990
3414
|
*/
|
|
2991
3415
|
var Upstream = class Upstream {
|
|
2992
|
-
static #
|
|
3416
|
+
static #defaultRepository = "https://raw.githubusercontent.com";
|
|
2993
3417
|
static #defaultRegistry = "https://registry.npmjs.org";
|
|
2994
3418
|
static #defaultBranch = "main";
|
|
2995
3419
|
static #defaultTimeout = 1e4;
|
|
2996
3420
|
static #defaultConcurrency = 6;
|
|
2997
3421
|
static #defaultRetries = 0;
|
|
2998
3422
|
static #scope = "orkestrel";
|
|
3423
|
+
static #vendor = "scaffold";
|
|
2999
3424
|
static #unreadable = "the answer carries no readable latest version";
|
|
3000
3425
|
static #packument = "application/vnd.npm.install-v1+json";
|
|
3001
3426
|
#emitter;
|
|
3002
|
-
#
|
|
3003
|
-
#
|
|
3004
|
-
#
|
|
3427
|
+
#repositoryBase;
|
|
3428
|
+
#repositoryBranch;
|
|
3429
|
+
#repositoryTimeout;
|
|
3005
3430
|
#registryBase;
|
|
3006
3431
|
#registryTimeout;
|
|
3007
3432
|
#concurrency;
|
|
@@ -3011,7 +3436,7 @@ var Upstream = class Upstream {
|
|
|
3011
3436
|
#controller = new AbortController();
|
|
3012
3437
|
#destroyed = false;
|
|
3013
3438
|
/**
|
|
3014
|
-
* Construct a reader over one
|
|
3439
|
+
* Construct a reader over one raw content host and one registry.
|
|
3015
3440
|
*
|
|
3016
3441
|
* @param options - The endpoints, the request bounds, the initial
|
|
3017
3442
|
* listeners, and the listener-error handler.
|
|
@@ -3035,9 +3460,9 @@ var Upstream = class Upstream {
|
|
|
3035
3460
|
...options?.on === void 0 ? {} : { on: options.on },
|
|
3036
3461
|
...options?.error === void 0 ? {} : { error: options.error }
|
|
3037
3462
|
});
|
|
3038
|
-
this.#
|
|
3039
|
-
this.#
|
|
3040
|
-
this.#
|
|
3463
|
+
this.#repositoryBase = this.#endpoint(options?.repository?.base ?? Upstream.#defaultRepository, "repository");
|
|
3464
|
+
this.#repositoryBranch = options?.repository?.branch ?? Upstream.#defaultBranch;
|
|
3465
|
+
this.#repositoryTimeout = options?.repository?.timeout ?? Upstream.#defaultTimeout;
|
|
3041
3466
|
this.#registryBase = this.#endpoint(options?.registry?.base ?? Upstream.#defaultRegistry, "registry");
|
|
3042
3467
|
this.#registryTimeout = options?.registry?.timeout ?? Upstream.#defaultTimeout;
|
|
3043
3468
|
this.#concurrency = options?.concurrency ?? Upstream.#defaultConcurrency;
|
|
@@ -3115,6 +3540,54 @@ var Upstream = class Upstream {
|
|
|
3115
3540
|
return this.#gather(accepted, (name) => this.#mirror(name, observed, allowance));
|
|
3116
3541
|
}
|
|
3117
3542
|
/**
|
|
3543
|
+
* Read each named vendored file from the repository, beside the target bytes it answers for.
|
|
3544
|
+
*
|
|
3545
|
+
* @param paths - The target-relative vendored paths to read.
|
|
3546
|
+
* @param current - The target files as exact bytes, keyed by the same paths.
|
|
3547
|
+
* @returns One file verdict per path, in input order.
|
|
3548
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `paths` is not a bounded
|
|
3549
|
+
* list of target-relative paths or `current` is not a snapshot, and
|
|
3550
|
+
* `DESTROYED` when the reader is torn down before or during the call.
|
|
3551
|
+
*
|
|
3552
|
+
* @remarks
|
|
3553
|
+
* The committed inventory is read once per call and decides every row, so a
|
|
3554
|
+
* path whose declared digest already matches the target's own bytes is `found`
|
|
3555
|
+
* without a request and the call spends nothing on it. An inventory that
|
|
3556
|
+
* produces no answer fails every row of the call rather than leaving some rows
|
|
3557
|
+
* live and some dead, which is what leaves the caller one whole baseline to
|
|
3558
|
+
* fall back to. A path the inventory does not name is `missing`.
|
|
3559
|
+
*
|
|
3560
|
+
* A fetched response's decoded content is verified against the digest the
|
|
3561
|
+
* inventory declares for that path, before any character decoding. Transport
|
|
3562
|
+
* encoding is transparent and does not enter the comparison, so content that
|
|
3563
|
+
* does not hash to the inventory's claim fails its row rather than reaching a
|
|
3564
|
+
* write. That is integrity against a single committed baseline, not
|
|
3565
|
+
* authenticity: it detects truncated, substituted, or stale content, and it
|
|
3566
|
+
* says nothing about who published the inventory.
|
|
3567
|
+
*
|
|
3568
|
+
* A guide mirror is never answered here whatever the caller asks for and
|
|
3569
|
+
* whatever the target holds, because those bytes belong to `fetch` and to the
|
|
3570
|
+
* mirror verb that writes them.
|
|
3571
|
+
*
|
|
3572
|
+
* @example
|
|
3573
|
+
* ```ts
|
|
3574
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
3575
|
+
*
|
|
3576
|
+
* const upstream = new Upstream()
|
|
3577
|
+
* await upstream.read(['AGENTS.md'], { 'AGENTS.md': '2320416745' })
|
|
3578
|
+
* upstream.destroy()
|
|
3579
|
+
* ```
|
|
3580
|
+
*/
|
|
3581
|
+
async read(paths, current) {
|
|
3582
|
+
this.#assertAlive();
|
|
3583
|
+
const accepted = this.#accept(paths, isPaths, "paths");
|
|
3584
|
+
const observed = this.#accept(current, _src_core.isSnapshot, "current");
|
|
3585
|
+
if (accepted.length === 0) return [];
|
|
3586
|
+
const allowance = { remaining: this.#budget };
|
|
3587
|
+
const inventory = await this.#inventory(allowance);
|
|
3588
|
+
return this.#gather(accepted, (path) => this.#file(path, inventory, observed, allowance));
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3118
3591
|
* Catalog the published fleet from the registry's organization package list.
|
|
3119
3592
|
*
|
|
3120
3593
|
* @returns One row per published package, sorted by name.
|
|
@@ -3191,14 +3664,14 @@ var Upstream = class Upstream {
|
|
|
3191
3664
|
return parsed.href.replace(/\/+$/u, "");
|
|
3192
3665
|
}
|
|
3193
3666
|
async #release(dependency, allowance) {
|
|
3194
|
-
const outcome = await this.#
|
|
3667
|
+
const outcome = await this.#readWithRetries(this.#registryURL(dependency.name), this.#registryTimeout, allowance, Upstream.#packument);
|
|
3195
3668
|
const latest = outcome.lookup === "found" ? this.#releaseVersion(outcome.content, dependency.range) : void 0;
|
|
3196
3669
|
const tagged = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
|
|
3197
3670
|
const major = tagged === void 0 ? void 0 : (0, _src_core.extractVersion)(tagged)?.[0];
|
|
3198
3671
|
const release = latest === void 0 ? {
|
|
3199
3672
|
name: dependency.name,
|
|
3200
3673
|
range: dependency.range,
|
|
3201
|
-
lookup: outcome.lookup === "
|
|
3674
|
+
lookup: outcome.lookup === "found" ? "unmatched" : outcome.lookup,
|
|
3202
3675
|
note: outcome.lookup === "found" ? Upstream.#unreadable : outcome.note,
|
|
3203
3676
|
...major === void 0 ? {} : { major }
|
|
3204
3677
|
} : {
|
|
@@ -3213,7 +3686,7 @@ var Upstream = class Upstream {
|
|
|
3213
3686
|
}
|
|
3214
3687
|
async #mirror(name, current, allowance) {
|
|
3215
3688
|
const path = (0, _src_core.nameToGuide)(name);
|
|
3216
|
-
const outcome = await this.#
|
|
3689
|
+
const outcome = await this.#readWithRetries(this.#guideURL(name), this.#repositoryTimeout, allowance);
|
|
3217
3690
|
const observed = current[path];
|
|
3218
3691
|
const mirror = outcome.lookup === "found" ? {
|
|
3219
3692
|
name,
|
|
@@ -3231,8 +3704,102 @@ var Upstream = class Upstream {
|
|
|
3231
3704
|
this.#emitter.emit("mirror", mirror);
|
|
3232
3705
|
return mirror;
|
|
3233
3706
|
}
|
|
3707
|
+
async #file(path, inventory, current, allowance) {
|
|
3708
|
+
const file = await this.#answer(path, inventory, current[path], allowance);
|
|
3709
|
+
this.#emitter.emit("file", file);
|
|
3710
|
+
return file;
|
|
3711
|
+
}
|
|
3712
|
+
async #answer(path, inventory, observed, allowance) {
|
|
3713
|
+
const carried = observed === void 0 ? {} : { observed };
|
|
3714
|
+
if (inventory.lookup !== "found") return {
|
|
3715
|
+
path,
|
|
3716
|
+
lookup: inventory.lookup,
|
|
3717
|
+
note: inventory.note,
|
|
3718
|
+
...carried
|
|
3719
|
+
};
|
|
3720
|
+
if ((0, _src_core.inferGroup)(path) === "guides") return {
|
|
3721
|
+
path,
|
|
3722
|
+
lookup: "missing",
|
|
3723
|
+
note: `${path} is a guide mirror the fleet serves`,
|
|
3724
|
+
...carried
|
|
3725
|
+
};
|
|
3726
|
+
if (inventory.duplicates.has(path)) return {
|
|
3727
|
+
path,
|
|
3728
|
+
lookup: "failed",
|
|
3729
|
+
note: `the inventory names ${path} more than once`,
|
|
3730
|
+
...carried
|
|
3731
|
+
};
|
|
3732
|
+
const digest = inventory.digests.get(path);
|
|
3733
|
+
if (digest === void 0) return {
|
|
3734
|
+
path,
|
|
3735
|
+
lookup: "missing",
|
|
3736
|
+
note: `the inventory does not name ${path}`,
|
|
3737
|
+
...carried
|
|
3738
|
+
};
|
|
3739
|
+
if (observed !== void 0 && hexToDigest(observed) === digest) return {
|
|
3740
|
+
path,
|
|
3741
|
+
lookup: "found",
|
|
3742
|
+
hex: observed,
|
|
3743
|
+
...carried
|
|
3744
|
+
};
|
|
3745
|
+
const outcome = await this.#readWithRetries(this.#vendorURL(path), this.#repositoryTimeout, allowance, void 0, true);
|
|
3746
|
+
if (outcome.lookup !== "found") return {
|
|
3747
|
+
path,
|
|
3748
|
+
lookup: outcome.lookup,
|
|
3749
|
+
note: outcome.note,
|
|
3750
|
+
...carried
|
|
3751
|
+
};
|
|
3752
|
+
if (hexToDigest(outcome.hex) !== digest) return {
|
|
3753
|
+
path,
|
|
3754
|
+
lookup: "failed",
|
|
3755
|
+
note: `the bytes served for ${path} do not match the digest the inventory declares`,
|
|
3756
|
+
...carried
|
|
3757
|
+
};
|
|
3758
|
+
return {
|
|
3759
|
+
path,
|
|
3760
|
+
lookup: "found",
|
|
3761
|
+
hex: outcome.hex,
|
|
3762
|
+
...carried
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
async #inventory(allowance) {
|
|
3766
|
+
const url = this.#vendorURL(_src_core.HOST_INVENTORY_PATH);
|
|
3767
|
+
const empty = {
|
|
3768
|
+
digests: /* @__PURE__ */ new Map(),
|
|
3769
|
+
duplicates: /* @__PURE__ */ new Set()
|
|
3770
|
+
};
|
|
3771
|
+
const outcome = await this.#readWithRetries(url, this.#repositoryTimeout, allowance);
|
|
3772
|
+
if (outcome.lookup !== "found") return {
|
|
3773
|
+
...empty,
|
|
3774
|
+
lookup: outcome.lookup,
|
|
3775
|
+
note: outcome.lookup === "missing" ? `the vendored inventory at ${url} is not published there` : `the vendored inventory at ${url} produced no answer: ${outcome.note}`
|
|
3776
|
+
};
|
|
3777
|
+
const manifest = (0, _orkestrel_contract.parseJSONAs)(outcome.content, isHostManifest);
|
|
3778
|
+
if (manifest === void 0) return {
|
|
3779
|
+
...empty,
|
|
3780
|
+
lookup: "failed",
|
|
3781
|
+
note: `the vendored inventory at ${url} is not a readable manifest`
|
|
3782
|
+
};
|
|
3783
|
+
if (manifest.digest !== computeManifestDigest(manifest.entries, manifest.roots)) return {
|
|
3784
|
+
...empty,
|
|
3785
|
+
lookup: "failed",
|
|
3786
|
+
note: `the vendored inventory at ${url} does not match its own membership digest`
|
|
3787
|
+
};
|
|
3788
|
+
const digests = /* @__PURE__ */ new Map();
|
|
3789
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
3790
|
+
for (const entry of manifest.entries) {
|
|
3791
|
+
if (digests.has(entry.destination)) duplicates.add(entry.destination);
|
|
3792
|
+
digests.set(entry.destination, entry.digest);
|
|
3793
|
+
}
|
|
3794
|
+
return {
|
|
3795
|
+
lookup: "found",
|
|
3796
|
+
digests,
|
|
3797
|
+
duplicates,
|
|
3798
|
+
note: ""
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3234
3801
|
async #entry(name, allowance) {
|
|
3235
|
-
const outcome = await this.#
|
|
3802
|
+
const outcome = await this.#readWithRetries(this.#registryURL(name), this.#registryTimeout, allowance, Upstream.#packument);
|
|
3236
3803
|
const version = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
|
|
3237
3804
|
if (version !== void 0) return {
|
|
3238
3805
|
name,
|
|
@@ -3242,7 +3809,7 @@ var Upstream = class Upstream {
|
|
|
3242
3809
|
};
|
|
3243
3810
|
return {
|
|
3244
3811
|
name,
|
|
3245
|
-
lookup: outcome.lookup === "
|
|
3812
|
+
lookup: outcome.lookup === "found" ? "unmatched" : outcome.lookup,
|
|
3246
3813
|
note: outcome.lookup === "found" ? Upstream.#unreadable : outcome.note
|
|
3247
3814
|
};
|
|
3248
3815
|
}
|
|
@@ -3270,7 +3837,7 @@ var Upstream = class Upstream {
|
|
|
3270
3837
|
}
|
|
3271
3838
|
async #packages(allowance) {
|
|
3272
3839
|
const url = `${this.#registryBase}/-/org/${Upstream.#scope}/package`;
|
|
3273
|
-
const outcome = await this.#
|
|
3840
|
+
const outcome = await this.#readWithRetries(url, this.#registryTimeout, allowance);
|
|
3274
3841
|
if (outcome.lookup !== "found") throw this.#error("FETCH", `The organization package list at ${url} produced no answer.`, {
|
|
3275
3842
|
url,
|
|
3276
3843
|
note: outcome.note
|
|
@@ -3316,15 +3883,22 @@ var Upstream = class Upstream {
|
|
|
3316
3883
|
return `${this.#registryBase}/${encodeURIComponent(name).replaceAll("%40", "@")}`;
|
|
3317
3884
|
}
|
|
3318
3885
|
#guideURL(name) {
|
|
3319
|
-
const branch = this.#
|
|
3886
|
+
const branch = this.#encode(this.#repositoryBranch);
|
|
3320
3887
|
const repository = encodeURIComponent(name.slice(name.lastIndexOf("/") + 1));
|
|
3321
|
-
return `${this.#
|
|
3888
|
+
return `${this.#repositoryBase}/${Upstream.#scope}/${repository}/refs/heads/${branch}/${(0, _src_core.nameToGuide)(name)}`;
|
|
3889
|
+
}
|
|
3890
|
+
#vendorURL(path) {
|
|
3891
|
+
const branch = this.#encode(this.#repositoryBranch);
|
|
3892
|
+
return `${this.#repositoryBase}/${Upstream.#scope}/${Upstream.#vendor}/refs/heads/${branch}/${this.#encode(path)}`;
|
|
3322
3893
|
}
|
|
3323
|
-
|
|
3894
|
+
#encode(path) {
|
|
3895
|
+
return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
3896
|
+
}
|
|
3897
|
+
async #readWithRetries(url, timeout, allowance, accept, binary = false) {
|
|
3324
3898
|
let note = "";
|
|
3325
|
-
for (let
|
|
3899
|
+
for (let round = 0; round <= this.#retries; round += 1) {
|
|
3326
3900
|
this.#assertAlive();
|
|
3327
|
-
const outcome = await this.#request(url, timeout, allowance, accept);
|
|
3901
|
+
const outcome = binary ? await this.#request(url, timeout, allowance, accept, true) : await this.#request(url, timeout, allowance, accept);
|
|
3328
3902
|
if (outcome.lookup !== "failed") return outcome;
|
|
3329
3903
|
note = outcome.note;
|
|
3330
3904
|
}
|
|
@@ -3332,18 +3906,29 @@ var Upstream = class Upstream {
|
|
|
3332
3906
|
url,
|
|
3333
3907
|
note
|
|
3334
3908
|
});
|
|
3335
|
-
return {
|
|
3909
|
+
return binary ? {
|
|
3336
3910
|
lookup: "failed",
|
|
3337
|
-
|
|
3911
|
+
hex: "",
|
|
3338
3912
|
note
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
async #request(url, timeout, allowance, accept) {
|
|
3342
|
-
if (allowance.remaining <= 0) return {
|
|
3913
|
+
} : {
|
|
3343
3914
|
lookup: "failed",
|
|
3344
3915
|
content: "",
|
|
3345
|
-
note
|
|
3916
|
+
note
|
|
3346
3917
|
};
|
|
3918
|
+
}
|
|
3919
|
+
async #request(url, timeout, allowance, accept, binary = false) {
|
|
3920
|
+
if (allowance.remaining <= 0) {
|
|
3921
|
+
const note = `the call spent its ${String(this.#budget)}-byte allowance`;
|
|
3922
|
+
return binary ? {
|
|
3923
|
+
lookup: "failed",
|
|
3924
|
+
hex: "",
|
|
3925
|
+
note
|
|
3926
|
+
} : {
|
|
3927
|
+
lookup: "failed",
|
|
3928
|
+
content: "",
|
|
3929
|
+
note
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3347
3932
|
try {
|
|
3348
3933
|
const response = await fetch(url, {
|
|
3349
3934
|
signal: AbortSignal.any([this.#controller.signal, AbortSignal.timeout(timeout)]),
|
|
@@ -3352,7 +3937,11 @@ var Upstream = class Upstream {
|
|
|
3352
3937
|
});
|
|
3353
3938
|
if (response.status === 404) {
|
|
3354
3939
|
await response.body?.cancel();
|
|
3355
|
-
return {
|
|
3940
|
+
return binary ? {
|
|
3941
|
+
lookup: "missing",
|
|
3942
|
+
hex: "",
|
|
3943
|
+
note: "HTTP 404"
|
|
3944
|
+
} : {
|
|
3356
3945
|
lookup: "missing",
|
|
3357
3946
|
content: "",
|
|
3358
3947
|
note: "HTTP 404"
|
|
@@ -3360,39 +3949,61 @@ var Upstream = class Upstream {
|
|
|
3360
3949
|
}
|
|
3361
3950
|
if (response.status >= 300 && response.status < 400) {
|
|
3362
3951
|
await response.body?.cancel();
|
|
3363
|
-
|
|
3952
|
+
const note = `HTTP ${String(response.status)}, and a redirect is never followed`;
|
|
3953
|
+
return binary ? {
|
|
3954
|
+
lookup: "failed",
|
|
3955
|
+
hex: "",
|
|
3956
|
+
note
|
|
3957
|
+
} : {
|
|
3364
3958
|
lookup: "failed",
|
|
3365
3959
|
content: "",
|
|
3366
|
-
note
|
|
3960
|
+
note
|
|
3367
3961
|
};
|
|
3368
3962
|
}
|
|
3369
3963
|
if (!response.ok) {
|
|
3370
3964
|
await response.body?.cancel();
|
|
3371
|
-
|
|
3965
|
+
const note = `HTTP ${String(response.status)}`;
|
|
3966
|
+
return binary ? {
|
|
3967
|
+
lookup: "failed",
|
|
3968
|
+
hex: "",
|
|
3969
|
+
note
|
|
3970
|
+
} : {
|
|
3372
3971
|
lookup: "failed",
|
|
3373
3972
|
content: "",
|
|
3374
|
-
note
|
|
3973
|
+
note
|
|
3375
3974
|
};
|
|
3376
3975
|
}
|
|
3377
|
-
return await this.#body(response, allowance);
|
|
3976
|
+
return binary ? await this.#body(response, allowance, true) : await this.#body(response, allowance);
|
|
3378
3977
|
} catch (error) {
|
|
3379
3978
|
this.#assertAlive();
|
|
3380
|
-
|
|
3979
|
+
const note = this.#note(error);
|
|
3980
|
+
return binary ? {
|
|
3981
|
+
lookup: "failed",
|
|
3982
|
+
hex: "",
|
|
3983
|
+
note
|
|
3984
|
+
} : {
|
|
3381
3985
|
lookup: "failed",
|
|
3382
3986
|
content: "",
|
|
3383
|
-
note
|
|
3987
|
+
note
|
|
3384
3988
|
};
|
|
3385
3989
|
}
|
|
3386
3990
|
}
|
|
3387
|
-
async #body(response, allowance) {
|
|
3991
|
+
async #body(response, allowance, binary = false) {
|
|
3388
3992
|
const body = response.body;
|
|
3389
|
-
if (body === null)
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3993
|
+
if (body === null) {
|
|
3994
|
+
const note = `HTTP ${String(response.status)}, and the answer carries no body`;
|
|
3995
|
+
return binary ? {
|
|
3996
|
+
lookup: "failed",
|
|
3997
|
+
hex: "",
|
|
3998
|
+
note
|
|
3999
|
+
} : {
|
|
4000
|
+
lookup: "failed",
|
|
4001
|
+
content: "",
|
|
4002
|
+
note
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
3394
4005
|
const reader = body.getReader();
|
|
3395
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
4006
|
+
const decoder = binary ? void 0 : new TextDecoder("utf-8", { fatal: true });
|
|
3396
4007
|
const chunks = [];
|
|
3397
4008
|
let total = 0;
|
|
3398
4009
|
try {
|
|
@@ -3403,30 +4014,44 @@ var Upstream = class Upstream {
|
|
|
3403
4014
|
allowance.remaining -= chunk.value.byteLength;
|
|
3404
4015
|
if (total > this.#limit) {
|
|
3405
4016
|
await reader.cancel();
|
|
3406
|
-
|
|
4017
|
+
const note = `the response passed the ${String(this.#limit)}-byte response limit`;
|
|
4018
|
+
return binary ? {
|
|
4019
|
+
lookup: "failed",
|
|
4020
|
+
hex: "",
|
|
4021
|
+
note
|
|
4022
|
+
} : {
|
|
3407
4023
|
lookup: "failed",
|
|
3408
4024
|
content: "",
|
|
3409
|
-
note
|
|
4025
|
+
note
|
|
3410
4026
|
};
|
|
3411
4027
|
}
|
|
3412
4028
|
if (allowance.remaining < 0) {
|
|
3413
4029
|
await reader.cancel();
|
|
3414
|
-
|
|
4030
|
+
const note = `the call spent its ${String(this.#budget)}-byte allowance`;
|
|
4031
|
+
return binary ? {
|
|
4032
|
+
lookup: "failed",
|
|
4033
|
+
hex: "",
|
|
4034
|
+
note
|
|
4035
|
+
} : {
|
|
3415
4036
|
lookup: "failed",
|
|
3416
4037
|
content: "",
|
|
3417
|
-
note
|
|
4038
|
+
note
|
|
3418
4039
|
};
|
|
3419
4040
|
}
|
|
3420
|
-
chunks.push(decoder.decode(chunk.value, { stream: true }));
|
|
4041
|
+
chunks.push(decoder === void 0 ? Buffer.from(chunk.value).toString("hex") : decoder.decode(chunk.value, { stream: true }));
|
|
3421
4042
|
}
|
|
3422
|
-
chunks.push(decoder.decode());
|
|
4043
|
+
if (decoder !== void 0) chunks.push(decoder.decode());
|
|
3423
4044
|
} catch (error) {
|
|
3424
4045
|
await reader.cancel().catch(() => void 0);
|
|
3425
4046
|
throw error;
|
|
3426
4047
|
} finally {
|
|
3427
4048
|
reader.releaseLock();
|
|
3428
4049
|
}
|
|
3429
|
-
return {
|
|
4050
|
+
return binary ? {
|
|
4051
|
+
lookup: "found",
|
|
4052
|
+
hex: chunks.join(""),
|
|
4053
|
+
note: ""
|
|
4054
|
+
} : {
|
|
3430
4055
|
lookup: "found",
|
|
3431
4056
|
content: chunks.join(""),
|
|
3432
4057
|
note: ""
|
|
@@ -3501,6 +4126,8 @@ exports.WriteTransaction = WriteTransaction;
|
|
|
3501
4126
|
exports.computeDigest = computeDigest;
|
|
3502
4127
|
exports.computeFileDigest = computeFileDigest;
|
|
3503
4128
|
exports.computeManifestDigest = computeManifestDigest;
|
|
4129
|
+
exports.filesToHost = filesToHost;
|
|
4130
|
+
exports.hexToDigest = hexToDigest;
|
|
3504
4131
|
exports.isBranch = isBranch;
|
|
3505
4132
|
exports.isCatalogEntries = isCatalogEntries;
|
|
3506
4133
|
exports.isDependencies = isDependencies;
|
|
@@ -3509,19 +4136,21 @@ exports.isDigest = isDigest;
|
|
|
3509
4136
|
exports.isEndpoint = isEndpoint;
|
|
3510
4137
|
exports.isExactCaseFile = isExactCaseFile;
|
|
3511
4138
|
exports.isFilesystemPath = isFilesystemPath;
|
|
4139
|
+
exports.isHost = isHost;
|
|
3512
4140
|
exports.isHostManifest = isHostManifest;
|
|
3513
4141
|
exports.isInventory = isInventory;
|
|
3514
4142
|
exports.isManifestEntry = isManifestEntry;
|
|
3515
4143
|
exports.isMaterializerHooks = isMaterializerHooks;
|
|
3516
4144
|
exports.isMaterializerOptions = isMaterializerOptions;
|
|
3517
4145
|
exports.isMirrors = isMirrors;
|
|
4146
|
+
exports.isPaths = isPaths;
|
|
3518
4147
|
exports.isPhysicalDirectory = isPhysicalDirectory;
|
|
3519
4148
|
exports.isPhysicalFile = isPhysicalFile;
|
|
3520
|
-
exports.isRepository = isRepository;
|
|
3521
4149
|
exports.isTimeout = isTimeout;
|
|
3522
4150
|
exports.isUpstreamHooks = isUpstreamHooks;
|
|
3523
4151
|
exports.isUpstreamOptions = isUpstreamOptions;
|
|
3524
4152
|
exports.isVacant = isVacant;
|
|
4153
|
+
exports.isWorktree = isWorktree;
|
|
3525
4154
|
exports.listDirectories = listDirectories;
|
|
3526
4155
|
exports.listFiles = listFiles;
|
|
3527
4156
|
exports.matchesAnchor = matchesAnchor;
|
|
@@ -3537,11 +4166,14 @@ exports.readAnchor = readAnchor;
|
|
|
3537
4166
|
exports.readExpectation = readExpectation;
|
|
3538
4167
|
exports.readFileHex = readFileHex;
|
|
3539
4168
|
exports.readFileText = readFileText;
|
|
4169
|
+
exports.readHostFloor = readHostFloor;
|
|
3540
4170
|
exports.readHostManifest = readHostManifest;
|
|
3541
4171
|
exports.readManifestEntry = readManifestEntry;
|
|
3542
4172
|
exports.readSnapshot = readSnapshot;
|
|
3543
4173
|
exports.resolveContainedPath = resolveContainedPath;
|
|
3544
4174
|
exports.resolveRealPath = resolveRealPath;
|
|
4175
|
+
exports.stageBytes = stageBytes;
|
|
3545
4176
|
exports.stageHost = stageHost;
|
|
4177
|
+
exports.stageInventory = stageInventory;
|
|
3546
4178
|
|
|
3547
4179
|
//# sourceMappingURL=index.cjs.map
|