@orkestrel/scaffold 0.0.101 → 0.0.103
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/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +33 -18
- package/dist/host/configs/helpers.ts +209 -148
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/oxlintrc.json +1 -11
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +36 -30
- package/dist/host/tests/config.test.ts +1263 -687
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1699 -1026
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/src/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { andOf, arrayOf, attempt, boundsOf, compareValues, compileGuard, holds, isArray, isBoolean, isError, isFunction, isInteger, isNumber, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, recordShape, stringOf, stringShape, unionOf, whereOf } from "@orkestrel/contract";
|
|
3
|
-
import { CANON_PATHS, CATALOG_AGENT_PATH, CATALOG_CLOSING_MARKER, CATALOG_OPENING_MARKER, CONTROL_CHARACTER_PATTERN, DEPENDENCY_NAME_PATTERN, EXECUTABLE_PATHS, HOST_INVENTORY_PATH, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, REFERENCE_PATHS, ScaffoldError, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, contentToHex, extractRangeMajor, extractVersion, inferGroup, insertManifestDependencies, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isFloorPath, isHex, isManifestScript, isMirror, isPath, isPlan, isRetainedPath, isSnapshot, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, renderSkillPointer, replaceManifestOverrides, replaceManifestRanges, replaceManifestScripts } from "../core/index.js";
|
|
2
|
+
import { andOf, arrayOf, attempt, boundsOf, compareValues, compileGuard, holds, isArray, isBoolean, isDefined, isError, isFunction, isInteger, isNonNegativeInteger, isNumber, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, recordShape, stringOf, stringShape, unionOf, whereOf } from "@orkestrel/contract";
|
|
3
|
+
import { CANON_PATHS, CATALOG_AGENT_PATH, CATALOG_CLOSING_MARKER, CATALOG_OPENING_MARKER, CONTROL_CHARACTER_PATTERN, DEPENDENCY_NAME_PATTERN, EXECUTABLE_PATHS, HOST_INVENTORY_PATH, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, REFERENCE_PATHS, ScaffoldError, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, computeTotal, contentToHex, extractRangeMajor, extractVersion, inferGroup, insertManifestDependencies, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isFloorPath, isHex, isManifestScript, isMirror, isPath, isPlan, isRetainedPath, isSnapshot, iterateAnswers, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, renderSkillPointer, replaceManifestOverrides, replaceManifestRanges, replaceManifestScripts } from "../core/index.js";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -481,18 +481,61 @@ function isExactCaseFile(path) {
|
|
|
481
481
|
if (!isPhysicalFile(full)) return false;
|
|
482
482
|
const root = parse(full).root;
|
|
483
483
|
const segments = relative(root, full).split(sep);
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
return segments.every((segment, index) => {
|
|
485
|
+
const parent = join(root, ...segments.slice(0, index));
|
|
486
486
|
const entries = attempt(() => readdirSync(parent));
|
|
487
|
-
if (entries.success)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
487
|
+
if (entries.success) return entries.value.includes(segment);
|
|
488
|
+
const actual = attempt(() => realpathSync.native(join(parent, segment)));
|
|
489
|
+
return actual.success && basename(actual.value) === segment;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Places each result at the index its entry names, in a fresh array.
|
|
494
|
+
*
|
|
495
|
+
* @param entries - The index and result pairs, in any order; each index a non-negative integer.
|
|
496
|
+
* @returns An array holding each result at its index, whose length is the highest index plus one,
|
|
497
|
+
* and where an index no entry names stays a hole.
|
|
498
|
+
*
|
|
499
|
+
* @remarks
|
|
500
|
+
* A worker pool finishes its items out of order and can skip one, so its answer is merged by
|
|
501
|
+
* position rather than by arrival: each index is written once into a fresh array, which needs no
|
|
502
|
+
* sort and keeps a skipped index absent instead of closing the gap. A repeated index keeps its last
|
|
503
|
+
* result.
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```ts
|
|
507
|
+
* import { collectIndexed } from '@orkestrel/scaffold/server'
|
|
508
|
+
*
|
|
509
|
+
* collectIndexed([[0, 'first'], [2, 'third']]).length // 3
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
function collectIndexed(entries) {
|
|
513
|
+
return Object.assign([], Object.fromEntries(entries));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Lists a path followed by each directory above it, nearest first.
|
|
517
|
+
*
|
|
518
|
+
* @param path - The host path to climb from.
|
|
519
|
+
* @param limit - The most paths to list; every ancestor by default.
|
|
520
|
+
* @returns The path, then `dirname` of each result in turn, ending at the first path that is its
|
|
521
|
+
* own parent (`.` for a relative path, the filesystem root for an absolute one) or at `limit`.
|
|
522
|
+
*
|
|
523
|
+
* @remarks
|
|
524
|
+
* The climb is derived from the path text alone and reads no filesystem, so a caller bounds a walk
|
|
525
|
+
* by the length it asks for rather than by a counter it reassigns. The final path of an unbounded
|
|
526
|
+
* list is the one `dirname` leaves unchanged.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* import { listAncestors } from '@orkestrel/scaffold/server'
|
|
531
|
+
*
|
|
532
|
+
* listAncestors('notes/entry/file.md') // ['notes/entry/file.md', 'notes/entry', 'notes', '.']
|
|
533
|
+
* listAncestors('notes/entry/file.md', 2) // ['notes/entry/file.md', 'notes/entry']
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
function listAncestors(path, limit = Number.POSITIVE_INFINITY) {
|
|
537
|
+
const parent = dirname(path);
|
|
538
|
+
return parent === path || limit <= 1 ? [path] : [path, ...listAncestors(parent, limit - 1)];
|
|
496
539
|
}
|
|
497
540
|
/**
|
|
498
541
|
* Tests whether a path is a physical directory this package will read or write into.
|
|
@@ -548,14 +591,15 @@ function computeFileDigest(path) {
|
|
|
548
591
|
if (!before.isFile() || before.nlink !== 1 || before.size > MAX_ARTIFACT_BYTES) return void 0;
|
|
549
592
|
const hash = createHash("sha256");
|
|
550
593
|
const buffer = Buffer.alloc(65536);
|
|
551
|
-
|
|
594
|
+
const lengths = [];
|
|
552
595
|
for (;;) {
|
|
553
596
|
const length = readSync(handle, buffer, 0, buffer.byteLength, null);
|
|
554
597
|
if (length === 0) break;
|
|
555
|
-
|
|
556
|
-
if (
|
|
598
|
+
lengths.push(length);
|
|
599
|
+
if (computeTotal(lengths) > MAX_ARTIFACT_BYTES) return void 0;
|
|
557
600
|
hash.update(buffer.subarray(0, length));
|
|
558
601
|
}
|
|
602
|
+
const size = computeTotal(lengths);
|
|
559
603
|
const after = fstatSync(handle);
|
|
560
604
|
if (size !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) return;
|
|
561
605
|
return hash.digest("hex");
|
|
@@ -577,8 +621,8 @@ function computeFileDigest(path) {
|
|
|
577
621
|
* yet, and a lexical answer is not enough: a link anywhere in the existing
|
|
578
622
|
* prefix moves the destination somewhere the text never named. So the deepest
|
|
579
623
|
* existing ancestor is resolved and the remaining segments are re-joined onto
|
|
580
|
-
* it.
|
|
581
|
-
* cannot make it walk indefinitely.
|
|
624
|
+
* it. {@link climbRealPath} performs the climb and bounds it by the path-depth
|
|
625
|
+
* ceiling, so an adversarial path cannot make it walk indefinitely.
|
|
582
626
|
*
|
|
583
627
|
* The caller's own text is collapsed first, which is what `resolve` does with a
|
|
584
628
|
* `..` the caller wrote: it cancels the segment before it as text, before any
|
|
@@ -612,32 +656,50 @@ function computeFileDigest(path) {
|
|
|
612
656
|
* ```
|
|
613
657
|
*/
|
|
614
658
|
function resolveRealPath(path) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
659
|
+
return isFilesystemPath(path) ? climbRealPath(resolve(path), [], 0) : void 0;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Climbs from an absolute path to its deepest existing ancestor and resolves it through every link.
|
|
663
|
+
*
|
|
664
|
+
* @param current - The absolute host path the climb reads at this depth.
|
|
665
|
+
* @param pending - The absent segments already climbed past, outermost first.
|
|
666
|
+
* @param depth - The steps already taken, from 0 through `MAX_PATH_DEPTH`.
|
|
667
|
+
* @returns The real path of the first existing ancestor joined with `pending` and every segment the
|
|
668
|
+
* climb passes, or `undefined` when `depth` is not a safe integer from 0 through `MAX_PATH_DEPTH`,
|
|
669
|
+
* the climb reaches the root, a link target cannot be read or carries a `..` segment, an existing
|
|
670
|
+
* entry is not a link, or an entry cannot be read.
|
|
671
|
+
*
|
|
672
|
+
* @remarks
|
|
673
|
+
* Each step reads one path and moves the climb to one other: a dangling link redirects it to the
|
|
674
|
+
* link's target with the same suffix, and an absent name moves it to the parent with that name added
|
|
675
|
+
* to the suffix. One successor per step makes the climb a bounded recursion rather than a worklist,
|
|
676
|
+
* and the depth argument is its bound, so an adversarial path or a link cycle answers `undefined`
|
|
677
|
+
* after `MAX_PATH_DEPTH` steps instead of walking indefinitely. {@link resolveRealPath} starts the
|
|
678
|
+
* climb at depth 0 with nothing pending.
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```ts
|
|
682
|
+
* import { resolve } from 'node:path'
|
|
683
|
+
* import { climbRealPath } from '@orkestrel/scaffold/server'
|
|
684
|
+
*
|
|
685
|
+
* climbRealPath(resolve('./packages/new/src'), [], 0) // the real path of packages, plus new/src
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
function climbRealPath(current, pending, depth) {
|
|
689
|
+
if (!Number.isSafeInteger(depth) || depth < 0 || depth > 64) return void 0;
|
|
690
|
+
const real = attempt(() => realpathSync(current));
|
|
691
|
+
if (real.success) return pending.length === 0 ? real.value : join(real.value, ...pending);
|
|
692
|
+
if (!matchesMissingPath(real.error)) return void 0;
|
|
693
|
+
const status = attempt(() => lstatSync(current));
|
|
694
|
+
if (status.success) {
|
|
695
|
+
if (!status.value.isSymbolicLink()) return void 0;
|
|
696
|
+
const target = attempt(() => readlinkSync(current));
|
|
697
|
+
if (!target.success || target.value.split(/[\\/]/u).includes("..")) return void 0;
|
|
698
|
+
return climbRealPath(resolve(dirname(current), target.value), pending, depth + 1);
|
|
640
699
|
}
|
|
700
|
+
if (!matchesMissingPath(status.error)) return void 0;
|
|
701
|
+
const parent = dirname(current);
|
|
702
|
+
return parent === current ? void 0 : climbRealPath(parent, [relative(parent, current), ...pending], depth + 1);
|
|
641
703
|
}
|
|
642
704
|
/**
|
|
643
705
|
* Resolves a root-relative path and refuses one that leaves its root.
|
|
@@ -764,13 +826,12 @@ function listFiles(root) {
|
|
|
764
826
|
});
|
|
765
827
|
}
|
|
766
828
|
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Listing root is not a physical directory at ${root}`, { root });
|
|
767
|
-
const files = [];
|
|
768
829
|
const pending = [{
|
|
769
830
|
full: root,
|
|
770
831
|
path: "",
|
|
771
832
|
depth: 0
|
|
772
833
|
}];
|
|
773
|
-
|
|
834
|
+
const entries = [];
|
|
774
835
|
while (pending.length > 0) {
|
|
775
836
|
const current = pending.pop();
|
|
776
837
|
if (current === void 0) break;
|
|
@@ -785,20 +846,21 @@ function listFiles(root) {
|
|
|
785
846
|
for (;;) {
|
|
786
847
|
const entry = handle.readSync();
|
|
787
848
|
if (entry === null) break;
|
|
788
|
-
|
|
789
|
-
|
|
849
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
850
|
+
const file = !entry.isDirectory() || entry.isSymbolicLink();
|
|
851
|
+
entries.push({
|
|
852
|
+
path,
|
|
853
|
+
file
|
|
854
|
+
});
|
|
855
|
+
if (entries.length > 1e5) throw new ScaffoldError("TARGET", "Listing exceeds the paths one inventory may report", {
|
|
790
856
|
root,
|
|
791
857
|
limit: MAX_INVENTORY_PATHS
|
|
792
858
|
});
|
|
793
|
-
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
794
859
|
if (!isPath(path)) throw new ScaffoldError("TARGET", `Listing found a path this package cannot plan at ${path}`, {
|
|
795
860
|
root,
|
|
796
861
|
path
|
|
797
862
|
});
|
|
798
|
-
if (
|
|
799
|
-
files.push(path);
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
863
|
+
if (file) continue;
|
|
802
864
|
const depth = current.depth + 1;
|
|
803
865
|
if (depth >= 64) throw new ScaffoldError("TARGET", `Listing exceeds the depth one path may carry at ${path}`, {
|
|
804
866
|
root,
|
|
@@ -815,7 +877,7 @@ function listFiles(root) {
|
|
|
815
877
|
attempt(() => handle.closeSync());
|
|
816
878
|
if (!walked.success) throw walked.error;
|
|
817
879
|
}
|
|
818
|
-
return
|
|
880
|
+
return entries.filter(({ file }) => file).map(({ path }) => path).sort();
|
|
819
881
|
}
|
|
820
882
|
/**
|
|
821
883
|
* Lists a directory's descendant directories as sorted root-relative paths.
|
|
@@ -857,13 +919,12 @@ function listDirectories(root) {
|
|
|
857
919
|
});
|
|
858
920
|
}
|
|
859
921
|
if (!status.value.isDirectory() || status.value.isSymbolicLink()) throw new ScaffoldError("TARGET", `Listing root is not a physical directory at ${root}`, { root });
|
|
860
|
-
const directories = [];
|
|
861
922
|
const pending = [{
|
|
862
923
|
full: root,
|
|
863
924
|
path: "",
|
|
864
925
|
depth: 0
|
|
865
926
|
}];
|
|
866
|
-
|
|
927
|
+
const entries = [];
|
|
867
928
|
while (pending.length > 0) {
|
|
868
929
|
const current = pending.pop();
|
|
869
930
|
if (current === void 0) break;
|
|
@@ -878,24 +939,27 @@ function listDirectories(root) {
|
|
|
878
939
|
for (;;) {
|
|
879
940
|
const entry = handle.readSync();
|
|
880
941
|
if (entry === null) break;
|
|
881
|
-
|
|
882
|
-
|
|
942
|
+
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
943
|
+
const directory = entry.isDirectory() && !entry.isSymbolicLink();
|
|
944
|
+
entries.push({
|
|
945
|
+
path,
|
|
946
|
+
directory
|
|
947
|
+
});
|
|
948
|
+
if (entries.length > 1e5) throw new ScaffoldError("TARGET", "Listing exceeds the paths one inventory may report", {
|
|
883
949
|
root,
|
|
884
950
|
limit: MAX_INVENTORY_PATHS
|
|
885
951
|
});
|
|
886
|
-
const path = current.path === "" ? entry.name : `${current.path}/${entry.name}`;
|
|
887
952
|
if (!isPath(path)) throw new ScaffoldError("TARGET", `Listing found a path this package cannot plan at ${path}`, {
|
|
888
953
|
root,
|
|
889
954
|
path
|
|
890
955
|
});
|
|
891
|
-
if (!
|
|
956
|
+
if (!directory) continue;
|
|
892
957
|
const depth = current.depth + 1;
|
|
893
958
|
if (depth >= 64) throw new ScaffoldError("TARGET", `Listing exceeds the depth one path may carry at ${path}`, {
|
|
894
959
|
root,
|
|
895
960
|
path,
|
|
896
961
|
limit: 64
|
|
897
962
|
});
|
|
898
|
-
directories.push(path);
|
|
899
963
|
pending.push({
|
|
900
964
|
full: join(current.full, entry.name),
|
|
901
965
|
path,
|
|
@@ -906,7 +970,7 @@ function listDirectories(root) {
|
|
|
906
970
|
attempt(() => handle.closeSync());
|
|
907
971
|
if (!walked.success) throw walked.error;
|
|
908
972
|
}
|
|
909
|
-
return
|
|
973
|
+
return entries.filter(({ directory }) => directory).map(({ path }) => path).sort();
|
|
910
974
|
}
|
|
911
975
|
/**
|
|
912
976
|
* Lists the canon paths a target holds, filtered to a plan's groups.
|
|
@@ -940,14 +1004,12 @@ function listDirectories(root) {
|
|
|
940
1004
|
* ```
|
|
941
1005
|
*/
|
|
942
1006
|
function listCanonPaths(target, groups) {
|
|
943
|
-
|
|
944
|
-
for (const member of CANON_PATHS) {
|
|
1007
|
+
return CANON_PATHS.flatMap((member) => {
|
|
945
1008
|
const full = resolveContainedPath(target, member);
|
|
946
|
-
if (full === void 0)
|
|
947
|
-
if (isPhysicalFile(full))
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
return held.filter((path) => groups.includes(inferGroup(path)));
|
|
1009
|
+
if (full === void 0) return [];
|
|
1010
|
+
if (isPhysicalFile(full)) return [member];
|
|
1011
|
+
return isPhysicalDirectory(full) ? listFiles(full).map((name) => `${member}/${name}`) : [];
|
|
1012
|
+
}).filter((path) => groups.includes(inferGroup(path)));
|
|
951
1013
|
}
|
|
952
1014
|
/**
|
|
953
1015
|
* Removes every directory one set of deletions emptied.
|
|
@@ -980,24 +1042,13 @@ function listCanonPaths(target, groups) {
|
|
|
980
1042
|
* ```
|
|
981
1043
|
*/
|
|
982
1044
|
function pruneEmptiedDirectories(target, removed) {
|
|
983
|
-
|
|
984
|
-
for (const path of removed) {
|
|
985
|
-
let parent = dirname(path);
|
|
986
|
-
while (parent !== "." && parent !== dirname(parent) && !candidates.has(parent)) {
|
|
987
|
-
candidates.add(parent);
|
|
988
|
-
parent = dirname(parent);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
const ordered = [...candidates].sort((left, right) => compareValues(right.split("/").length, left.split("/").length) || compareValues(left, right));
|
|
992
|
-
const pruned = [];
|
|
993
|
-
for (const candidate of ordered) {
|
|
1045
|
+
return [...new Set(Iterator.from(removed).flatMap((path) => listAncestors(path).slice(1, -1)).toArray())].sort((left, right) => compareValues(right.split("/").length, left.split("/").length) || compareValues(left, right)).flatMap((candidate) => {
|
|
994
1046
|
const directory = resolveContainedPath(target, candidate);
|
|
995
|
-
if (directory === void 0 || !isPhysicalDirectory(directory))
|
|
1047
|
+
if (directory === void 0 || !isPhysicalDirectory(directory)) return [];
|
|
996
1048
|
const entries = attempt(() => readdirSync(directory));
|
|
997
|
-
if (!entries.success || entries.value.length > 0)
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
return pruned;
|
|
1049
|
+
if (!entries.success || entries.value.length > 0) return [];
|
|
1050
|
+
return attempt(() => rmdirSync(directory)).success ? [candidate] : [];
|
|
1051
|
+
});
|
|
1001
1052
|
}
|
|
1002
1053
|
/**
|
|
1003
1054
|
* Reads one contained file as its exact bytes in lowercase hexadecimal.
|
|
@@ -1045,12 +1096,15 @@ function readFileHex(root, path, limit = MAX_ARTIFACT_BYTES) {
|
|
|
1045
1096
|
const before = fstatSync(handle);
|
|
1046
1097
|
if (!before.isFile() || before.nlink !== 1 || before.size > limit) return void 0;
|
|
1047
1098
|
const bytes = Buffer.alloc(before.size);
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1099
|
+
const reads = [];
|
|
1100
|
+
for (;;) {
|
|
1101
|
+
const position = computeTotal(reads);
|
|
1102
|
+
if (position >= bytes.byteLength) break;
|
|
1103
|
+
const length = readSync(handle, bytes, position, bytes.byteLength - position, position);
|
|
1051
1104
|
if (length === 0) break;
|
|
1052
|
-
|
|
1105
|
+
reads.push(length);
|
|
1053
1106
|
}
|
|
1107
|
+
const offset = computeTotal(reads);
|
|
1054
1108
|
const overflow = readSync(handle, Buffer.alloc(1), 0, 1, offset);
|
|
1055
1109
|
const after = fstatSync(handle);
|
|
1056
1110
|
if (offset !== bytes.byteLength || overflow !== 0 || after.size !== before.size || after.mtimeMs !== before.mtimeMs) return;
|
|
@@ -1122,9 +1176,7 @@ function readSnapshot(target, paths) {
|
|
|
1122
1176
|
target,
|
|
1123
1177
|
limit: MAX_COLLECTION_ITEMS
|
|
1124
1178
|
});
|
|
1125
|
-
|
|
1126
|
-
const snapshot = {};
|
|
1127
|
-
for (const path of paths) {
|
|
1179
|
+
const read = Iterator.from(paths).reduce((state, path) => {
|
|
1128
1180
|
const full = resolveContainedPath(target, path);
|
|
1129
1181
|
if (full === void 0) throw new ScaffoldError("INVALID", `Snapshot path leaves its target at ${path}`, {
|
|
1130
1182
|
target,
|
|
@@ -1132,27 +1184,32 @@ function readSnapshot(target, paths) {
|
|
|
1132
1184
|
});
|
|
1133
1185
|
const status = attempt(() => lstatSync(full));
|
|
1134
1186
|
if (!status.success) {
|
|
1135
|
-
if (matchesMissingPath(status.error))
|
|
1187
|
+
if (matchesMissingPath(status.error)) return state;
|
|
1136
1188
|
throw new ScaffoldError("TARGET", `Snapshot path cannot be inspected at ${path}`, {
|
|
1137
1189
|
target,
|
|
1138
1190
|
path,
|
|
1139
1191
|
error: status.error
|
|
1140
1192
|
});
|
|
1141
1193
|
}
|
|
1142
|
-
if (isPhysicalDirectory(full)) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
const hex = readFileHex(target, path, Math.min(MAX_ARTIFACT_BYTES,
|
|
1194
|
+
if (isPhysicalDirectory(full)) return {
|
|
1195
|
+
...state,
|
|
1196
|
+
entries: [...state.entries, [path, ""]]
|
|
1197
|
+
};
|
|
1198
|
+
const hex = readFileHex(target, path, Math.min(MAX_ARTIFACT_BYTES, MAX_TOTAL_ARTIFACT_BYTES - computeTotal(state.spent)));
|
|
1147
1199
|
if (hex === void 0) throw new ScaffoldError("TARGET", `Snapshot path is not a readable file at ${path}`, {
|
|
1148
1200
|
target,
|
|
1149
1201
|
path,
|
|
1150
1202
|
limit: MAX_TOTAL_ARTIFACT_BYTES
|
|
1151
1203
|
});
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1204
|
+
return {
|
|
1205
|
+
entries: [...state.entries, [path, hex]],
|
|
1206
|
+
spent: [...state.spent, hex.length / 2]
|
|
1207
|
+
};
|
|
1208
|
+
}, {
|
|
1209
|
+
entries: [],
|
|
1210
|
+
spent: []
|
|
1211
|
+
});
|
|
1212
|
+
return Object.fromEntries(read.entries);
|
|
1156
1213
|
}
|
|
1157
1214
|
/**
|
|
1158
1215
|
* Reads a vendored host's manifest, when it carries one.
|
|
@@ -1236,20 +1293,18 @@ function readHostFloor(root) {
|
|
|
1236
1293
|
if (!isPhysicalDirectory(host)) throw new ScaffoldError("TARGET", `The vendored host root is not readable at ${host}`, { host });
|
|
1237
1294
|
const manifest = readHostManifest(host, source ? HOST_INVENTORY_PATH : MANIFEST_NAME);
|
|
1238
1295
|
if (manifest === void 0) throw new ScaffoldError("TARGET", `The vendored host carries no manifest at ${host}`, { host });
|
|
1239
|
-
const bytes = {};
|
|
1240
|
-
for (const entry of manifest.entries) {
|
|
1241
|
-
const path = source ? entry.destination : entry.storage;
|
|
1242
|
-
const hex = readFileHex(host, path);
|
|
1243
|
-
if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
|
|
1244
|
-
host,
|
|
1245
|
-
path,
|
|
1246
|
-
destination: entry.destination
|
|
1247
|
-
});
|
|
1248
|
-
bytes[entry.destination] = hex;
|
|
1249
|
-
}
|
|
1250
1296
|
return {
|
|
1251
1297
|
manifest,
|
|
1252
|
-
bytes
|
|
1298
|
+
bytes: Object.fromEntries(manifest.entries.map((entry) => {
|
|
1299
|
+
const path = source ? entry.destination : entry.storage;
|
|
1300
|
+
const hex = readFileHex(host, path);
|
|
1301
|
+
if (hex === void 0 || hexToDigest(hex) !== entry.digest) throw new ScaffoldError("TARGET", `The vendored host cannot read the declared file at ${path}`, {
|
|
1302
|
+
host,
|
|
1303
|
+
path,
|
|
1304
|
+
destination: entry.destination
|
|
1305
|
+
});
|
|
1306
|
+
return [entry.destination, hex];
|
|
1307
|
+
}))
|
|
1253
1308
|
};
|
|
1254
1309
|
}
|
|
1255
1310
|
/**
|
|
@@ -1329,24 +1384,21 @@ function readManifestEntry(destination, source) {
|
|
|
1329
1384
|
*/
|
|
1330
1385
|
function filesToHost(files, floor) {
|
|
1331
1386
|
const declared = new Set(floor.manifest.entries.map((entry) => entry.destination));
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
const entries = [];
|
|
1338
|
-
const bytes = {};
|
|
1339
|
-
for (const entry of floor.manifest.entries) {
|
|
1387
|
+
const rows = [...iterateAnswers(files, (file) => file.lookup !== "found" || !declared.has(file.path) ? void 0 : isFloorPath(file.path) ? [] : [[file.path, file.hex]])];
|
|
1388
|
+
if (!rows.every(isDefined)) return void 0;
|
|
1389
|
+
const held = new Map(rows.flat());
|
|
1390
|
+
const filled = [...iterateAnswers(floor.manifest.entries, (entry) => {
|
|
1340
1391
|
const hex = isFloorPath(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
|
|
1341
|
-
|
|
1342
|
-
entries.push({
|
|
1392
|
+
return hex === void 0 ? void 0 : [{
|
|
1343
1393
|
storage: entry.storage,
|
|
1344
1394
|
destination: entry.destination,
|
|
1345
1395
|
executable: entry.executable,
|
|
1346
1396
|
digest: hexToDigest(hex)
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1397
|
+
}, hex];
|
|
1398
|
+
})];
|
|
1399
|
+
if (!filled.every(isDefined)) return void 0;
|
|
1400
|
+
const entries = filled.map(([entry]) => entry);
|
|
1401
|
+
const bytes = Object.fromEntries(filled.map(([entry, hex]) => [entry.destination, hex]));
|
|
1350
1402
|
return {
|
|
1351
1403
|
manifest: {
|
|
1352
1404
|
entries,
|
|
@@ -1394,8 +1446,7 @@ function filesToHost(files, floor) {
|
|
|
1394
1446
|
function stageBytes(host, root, destinations) {
|
|
1395
1447
|
if (!isFilesystemPath(root)) throw new ScaffoldError("INVALID", "Staging host root is not a host path", { host: root });
|
|
1396
1448
|
const declared = new Map(host.manifest.entries.map((entry) => [entry.destination, entry]));
|
|
1397
|
-
|
|
1398
|
-
for (const destination of destinations) {
|
|
1449
|
+
return Iterator.from(destinations).map((destination) => {
|
|
1399
1450
|
const entry = declared.get(destination);
|
|
1400
1451
|
const hex = host.bytes[destination];
|
|
1401
1452
|
if (entry === void 0 || hex === void 0) throw new ScaffoldError("TARGET", `The host carries no bytes for ${destination}`, {
|
|
@@ -1425,9 +1476,8 @@ function stageBytes(host, root, destinations) {
|
|
|
1425
1476
|
host: root,
|
|
1426
1477
|
storage: entry.storage
|
|
1427
1478
|
});
|
|
1428
|
-
|
|
1429
|
-
}
|
|
1430
|
-
return staged;
|
|
1479
|
+
return entry;
|
|
1480
|
+
}).toArray();
|
|
1431
1481
|
}
|
|
1432
1482
|
/**
|
|
1433
1483
|
* Stages a vendored host root from a real checkout.
|
|
@@ -1504,37 +1554,36 @@ function stageHost(checkout, host, options) {
|
|
|
1504
1554
|
const source = resolve(checkout);
|
|
1505
1555
|
if (!isPhysicalDirectory(source)) throw new ScaffoldError("TARGET", `Staging checkout is not a physical directory at ${source}`, { checkout: source });
|
|
1506
1556
|
if (!isVacant(host)) throw new ScaffoldError("TARGET", `Staging host root is not vacant at ${host}`, { host });
|
|
1507
|
-
const
|
|
1508
|
-
const roots = [];
|
|
1509
|
-
const missing = [];
|
|
1510
|
-
for (const path of [
|
|
1557
|
+
const reads = [
|
|
1511
1558
|
...HOST_PATHS,
|
|
1512
1559
|
...CANON_PATHS,
|
|
1513
1560
|
...REFERENCE_PATHS
|
|
1514
|
-
]) {
|
|
1561
|
+
].map((path) => {
|
|
1515
1562
|
const full = resolveContainedPath(source, path);
|
|
1516
1563
|
if (full === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its checkout at ${path}`, {
|
|
1517
1564
|
checkout: source,
|
|
1518
1565
|
path
|
|
1519
1566
|
});
|
|
1520
|
-
if (isPhysicalFile(full)) {
|
|
1521
|
-
vendored
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1567
|
+
if (isPhysicalFile(full)) return {
|
|
1568
|
+
vendored: [path],
|
|
1569
|
+
roots: [],
|
|
1570
|
+
missing: []
|
|
1571
|
+
};
|
|
1572
|
+
if (!isPhysicalDirectory(full)) return {
|
|
1573
|
+
vendored: [],
|
|
1574
|
+
roots: [],
|
|
1575
|
+
missing: CANON_PATHS.includes(path) ? [] : [path]
|
|
1576
|
+
};
|
|
1577
|
+
const nested = listDirectories(full);
|
|
1578
|
+
return {
|
|
1579
|
+
vendored: listFiles(full).map((name) => `${path}/${name}`).filter((destination) => !matchesSensitivePath(destination)),
|
|
1580
|
+
roots: [path, ...nested.map((name) => `${path}/${name}`).filter((rooted) => !matchesSensitivePath(rooted))],
|
|
1581
|
+
missing: []
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
const vendored = reads.flatMap((read) => read.vendored);
|
|
1585
|
+
const roots = reads.flatMap((read) => read.roots).sort();
|
|
1586
|
+
const missing = reads.flatMap((read) => read.missing);
|
|
1538
1587
|
if (missing.length > 0) throw new ScaffoldError("TARGET", "The checkout does not carry every vendored path", {
|
|
1539
1588
|
checkout: source,
|
|
1540
1589
|
missing
|
|
@@ -1554,9 +1603,8 @@ function stageHost(checkout, host, options) {
|
|
|
1554
1603
|
path
|
|
1555
1604
|
});
|
|
1556
1605
|
}
|
|
1557
|
-
const
|
|
1558
|
-
const candidates =
|
|
1559
|
-
for (const destination of vendored) {
|
|
1606
|
+
const storages = vendored.map((destination) => pathToStorage(destination));
|
|
1607
|
+
const candidates = vendored.map((destination, index) => {
|
|
1560
1608
|
const full = resolveContainedPath(source, destination);
|
|
1561
1609
|
if (full === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its checkout at ${destination}`, {
|
|
1562
1610
|
checkout: source,
|
|
@@ -1568,16 +1616,13 @@ function stageHost(checkout, host, options) {
|
|
|
1568
1616
|
path: destination,
|
|
1569
1617
|
limit: MAX_ARTIFACT_BYTES
|
|
1570
1618
|
});
|
|
1571
|
-
if (
|
|
1619
|
+
if (entry.storage === "manifest.json" || storages.indexOf(entry.storage) < index) throw new ScaffoldError("TARGET", `Two vendored paths claim the storage name ${entry.storage}`, {
|
|
1572
1620
|
checkout: source,
|
|
1573
1621
|
path: destination,
|
|
1574
1622
|
storage: entry.storage
|
|
1575
1623
|
});
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
candidates.sort((first, second) => first.storage < second.storage ? -1 : 1);
|
|
1580
|
-
roots.sort();
|
|
1624
|
+
return entry;
|
|
1625
|
+
}).sort((first, second) => first.storage < second.storage ? -1 : 1);
|
|
1581
1626
|
const baseline = resolveContainedPath(source, options?.inventory ?? HOST_INVENTORY_PATH);
|
|
1582
1627
|
if (baseline === void 0) throw new ScaffoldError("INVALID", "Surface inventory leaves its checkout", {
|
|
1583
1628
|
checkout: source,
|
|
@@ -1597,8 +1642,7 @@ function stageHost(checkout, host, options) {
|
|
|
1597
1642
|
host: root,
|
|
1598
1643
|
...established.success ? {} : { error: established.error }
|
|
1599
1644
|
});
|
|
1600
|
-
const entries =
|
|
1601
|
-
for (const entry of candidates) {
|
|
1645
|
+
const entries = candidates.map((entry) => {
|
|
1602
1646
|
const origin = resolveContainedPath(source, entry.destination);
|
|
1603
1647
|
const destination = resolveContainedPath(root, entry.storage);
|
|
1604
1648
|
if (origin === void 0 || destination === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its root at ${entry.destination}`, {
|
|
@@ -1622,17 +1666,16 @@ function stageHost(checkout, host, options) {
|
|
|
1622
1666
|
host: root,
|
|
1623
1667
|
storage: entry.storage
|
|
1624
1668
|
});
|
|
1625
|
-
|
|
1669
|
+
return {
|
|
1626
1670
|
...entry,
|
|
1627
1671
|
digest
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1630
1674
|
const collisions = readSurfaceCollisions(join(root, "guides"));
|
|
1631
|
-
const growth = []
|
|
1632
|
-
if (published !== void 0) for (const [name, owners] of collisions) {
|
|
1675
|
+
const growth = published === void 0 ? [] : [...collisions].flatMap(([name, owners]) => {
|
|
1633
1676
|
const record = published.get(name);
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1677
|
+
return record !== void 0 && owners.every((owner) => record.includes(owner)) ? [] : [`${name} staged (${owners.join(", ")}), recorded (${record?.join(", ") ?? "absent"})`];
|
|
1678
|
+
});
|
|
1636
1679
|
if (growth.length > 0) throw new ScaffoldError("TARGET", `Staged Surface collisions differ from the inventory: ${growth.join("; ")}`, {
|
|
1637
1680
|
checkout: source,
|
|
1638
1681
|
host: root,
|
|
@@ -1692,26 +1735,19 @@ function readSurfaceCollisions(root) {
|
|
|
1692
1735
|
error: loaded.error
|
|
1693
1736
|
});
|
|
1694
1737
|
const guide = loaded.value;
|
|
1695
|
-
const
|
|
1696
|
-
for (const path of listFiles(root)) {
|
|
1697
|
-
if (path.includes("/") || extname(path) !== ".md" || path === "README.md") continue;
|
|
1738
|
+
const claims = listFiles(root).filter((path) => !path.includes("/") && extname(path) === ".md" && path !== "README.md").flatMap((path) => {
|
|
1698
1739
|
const text = readFileText(root, path);
|
|
1699
1740
|
if (text === void 0) throw new ScaffoldError("TARGET", `Surface guide cannot be read at ${path}`, {
|
|
1700
1741
|
root,
|
|
1701
1742
|
path
|
|
1702
1743
|
});
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
for (const name of [...owners.keys()].sort()) {
|
|
1711
|
-
const claimed = owners.get(name);
|
|
1712
|
-
if (claimed !== void 0 && claimed.size > 1) collisions.set(name, [...claimed].sort());
|
|
1713
|
-
}
|
|
1714
|
-
return collisions;
|
|
1744
|
+
return guide.createGuide(text).surface().map((symbol) => [symbol.name, basename(path, ".md")]);
|
|
1745
|
+
});
|
|
1746
|
+
const grouped = Map.groupBy(claims, ([name]) => name);
|
|
1747
|
+
return new Map([...grouped.keys()].sort().flatMap((name) => {
|
|
1748
|
+
const owners = [...new Set((grouped.get(name) ?? []).map(([, owner]) => owner))];
|
|
1749
|
+
return owners.length > 1 ? [[name, owners.sort()]] : [];
|
|
1750
|
+
}));
|
|
1715
1751
|
}
|
|
1716
1752
|
/**
|
|
1717
1753
|
* Reads the Surface collision baseline from a committed inventory.
|
|
@@ -2461,17 +2497,15 @@ var WriteTransaction = class {
|
|
|
2461
2497
|
paths
|
|
2462
2498
|
});
|
|
2463
2499
|
this.#target = resolve(target);
|
|
2464
|
-
|
|
2465
|
-
for (const path of paths) {
|
|
2500
|
+
this.#expectations = new Map(Array.from(paths, (path) => {
|
|
2466
2501
|
const expectation = readExpectation(this.#resolve(this.#target, path));
|
|
2467
2502
|
if (expectation === void 0) throw new ScaffoldError("TARGET", `The destination at ${path} is a shape this package will not write over.`, {
|
|
2468
2503
|
target: this.#target,
|
|
2469
2504
|
path
|
|
2470
2505
|
});
|
|
2471
|
-
|
|
2472
|
-
}
|
|
2473
|
-
this.#expectations
|
|
2474
|
-
const destinations = new Set([...expectations.values()].map((expectation) => expectation.path));
|
|
2506
|
+
return [path, expectation];
|
|
2507
|
+
}));
|
|
2508
|
+
const destinations = new Set([...this.#expectations.values()].map((expectation) => expectation.path));
|
|
2475
2509
|
for (const precondition of preconditions ?? []) {
|
|
2476
2510
|
if (!destinations.has(precondition.path)) throw new ScaffoldError("INVALID", `The precondition at ${precondition.path} names no destination of this transaction.`, {
|
|
2477
2511
|
target: this.#target,
|
|
@@ -2660,8 +2694,14 @@ var WriteTransaction = class {
|
|
|
2660
2694
|
const taken = [];
|
|
2661
2695
|
const applied = attempt(() => {
|
|
2662
2696
|
this.#preflight();
|
|
2663
|
-
for (const path of this.#staged)
|
|
2664
|
-
|
|
2697
|
+
for (const path of this.#staged) {
|
|
2698
|
+
this.#promote(path);
|
|
2699
|
+
promoted.push(path);
|
|
2700
|
+
}
|
|
2701
|
+
for (const path of this.#taken) {
|
|
2702
|
+
this.#take(path);
|
|
2703
|
+
taken.push(path);
|
|
2704
|
+
}
|
|
2665
2705
|
});
|
|
2666
2706
|
this.#open = false;
|
|
2667
2707
|
if (!applied.success) {
|
|
@@ -2718,8 +2758,7 @@ var WriteTransaction = class {
|
|
|
2718
2758
|
path
|
|
2719
2759
|
});
|
|
2720
2760
|
const base = resolve(root);
|
|
2721
|
-
|
|
2722
|
-
for (let depth = 0; depth <= 64; depth += 1) {
|
|
2761
|
+
for (const ancestor of listAncestors(destination, 66).slice(1)) {
|
|
2723
2762
|
const state = readExpectation(ancestor);
|
|
2724
2763
|
if (state === void 0 || state.shape === "file") throw new ScaffoldError("WRITE", `The path ${path} passes through a link or a file.`, {
|
|
2725
2764
|
root,
|
|
@@ -2727,9 +2766,6 @@ var WriteTransaction = class {
|
|
|
2727
2766
|
ancestor
|
|
2728
2767
|
});
|
|
2729
2768
|
if (ancestor === base) return destination;
|
|
2730
|
-
const parent = dirname(ancestor);
|
|
2731
|
-
if (parent === ancestor) break;
|
|
2732
|
-
ancestor = parent;
|
|
2733
2769
|
}
|
|
2734
2770
|
throw new ScaffoldError("INVALID", `The path ${path} does not resolve beneath its root.`, {
|
|
2735
2771
|
root,
|
|
@@ -2738,41 +2774,44 @@ var WriteTransaction = class {
|
|
|
2738
2774
|
}
|
|
2739
2775
|
#establish(path) {
|
|
2740
2776
|
const destination = resolve(path);
|
|
2741
|
-
const missing =
|
|
2742
|
-
let current = destination;
|
|
2743
|
-
let anchor = readAnchor(current);
|
|
2744
|
-
for (let depth = 0; anchor === void 0 && depth <= 64; depth += 1) {
|
|
2745
|
-
const state = readExpectation(current);
|
|
2746
|
-
if (state === void 0 || state.shape !== "absent") throw new ScaffoldError("WRITE", `The directory at ${current} is not a physical directory.`, { path: destination });
|
|
2747
|
-
missing.push(current);
|
|
2748
|
-
const parent = dirname(current);
|
|
2749
|
-
if (parent === current) throw new ScaffoldError("WRITE", `The directory at ${destination} has no existing ancestor.`, { path: destination });
|
|
2750
|
-
current = parent;
|
|
2751
|
-
anchor = readAnchor(current);
|
|
2752
|
-
}
|
|
2753
|
-
if (anchor === void 0) throw new ScaffoldError("WRITE", `The directory at ${destination} is nested past the ceiling.`, {
|
|
2754
|
-
path: destination,
|
|
2755
|
-
limit: 64
|
|
2756
|
-
});
|
|
2777
|
+
const { anchor: located, missing } = this.#locate(destination);
|
|
2757
2778
|
const created = [];
|
|
2758
2779
|
for (const segment of [...missing].reverse()) {
|
|
2780
|
+
const anchor = created.at(-1) ?? located;
|
|
2759
2781
|
if (!matchesAnchor(anchor)) throw new ScaffoldError("WRITE", `The directory at ${anchor.path} changed while writing.`, {
|
|
2760
2782
|
path: destination,
|
|
2761
2783
|
ancestor: anchor.path
|
|
2762
2784
|
});
|
|
2763
2785
|
mkdirSync(segment);
|
|
2764
|
-
this.#created.push(segment);
|
|
2765
2786
|
const established = readAnchor(segment);
|
|
2787
|
+
this.#created.push(established ?? segment);
|
|
2766
2788
|
if (established === void 0) throw new ScaffoldError("WRITE", `The directory at ${segment} changed while writing.`, { path: destination });
|
|
2767
|
-
anchor = established;
|
|
2768
|
-
this.#created[this.#created.length - 1] = established;
|
|
2769
2789
|
created.push(established);
|
|
2770
2790
|
}
|
|
2771
2791
|
return {
|
|
2772
|
-
anchor,
|
|
2792
|
+
anchor: created.at(-1) ?? located,
|
|
2773
2793
|
created
|
|
2774
2794
|
};
|
|
2775
2795
|
}
|
|
2796
|
+
#locate(destination) {
|
|
2797
|
+
const missing = [];
|
|
2798
|
+
for (const [depth, current] of listAncestors(destination, 66).entries()) {
|
|
2799
|
+
const anchor = readAnchor(current);
|
|
2800
|
+
if (anchor !== void 0) return {
|
|
2801
|
+
anchor,
|
|
2802
|
+
missing
|
|
2803
|
+
};
|
|
2804
|
+
if (depth > 64) break;
|
|
2805
|
+
const state = readExpectation(current);
|
|
2806
|
+
if (state === void 0 || state.shape !== "absent") throw new ScaffoldError("WRITE", `The directory at ${current} is not a physical directory.`, { path: destination });
|
|
2807
|
+
missing.push(current);
|
|
2808
|
+
if (dirname(current) === current) throw new ScaffoldError("WRITE", `The directory at ${destination} has no existing ancestor.`, { path: destination });
|
|
2809
|
+
}
|
|
2810
|
+
throw new ScaffoldError("WRITE", `The directory at ${destination} is nested past the ceiling.`, {
|
|
2811
|
+
path: destination,
|
|
2812
|
+
limit: 64
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2776
2815
|
#preflight() {
|
|
2777
2816
|
for (const anchor of this.#created) {
|
|
2778
2817
|
if (isString(anchor)) throw new ScaffoldError("WRITE", `The directory at ${anchor} changed while writing.`, {
|
|
@@ -2793,7 +2832,7 @@ var WriteTransaction = class {
|
|
|
2793
2832
|
path
|
|
2794
2833
|
});
|
|
2795
2834
|
}
|
|
2796
|
-
#promote(path
|
|
2835
|
+
#promote(path) {
|
|
2797
2836
|
const expectation = this.#expectation(path);
|
|
2798
2837
|
if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2799
2838
|
const staged = this.#resolve(this.#stage, path);
|
|
@@ -2804,23 +2843,19 @@ var WriteTransaction = class {
|
|
|
2804
2843
|
linkSync(expectation.path, backup);
|
|
2805
2844
|
}
|
|
2806
2845
|
renameSync(staged, expectation.path);
|
|
2807
|
-
promoted.push(path);
|
|
2808
2846
|
}
|
|
2809
|
-
#take(path
|
|
2847
|
+
#take(path) {
|
|
2810
2848
|
const expectation = this.#expectation(path);
|
|
2811
2849
|
if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2812
2850
|
const backup = this.#resolve(this.#backup, path);
|
|
2813
2851
|
mkdirSync(dirname(backup), { recursive: true });
|
|
2814
2852
|
renameSync(expectation.path, backup);
|
|
2815
|
-
taken.push(path);
|
|
2816
2853
|
}
|
|
2817
2854
|
#recover(promoted, taken) {
|
|
2818
|
-
const
|
|
2819
|
-
for (const path of [...taken].reverse()) {
|
|
2855
|
+
const restoring = [...[...taken].reverse().flatMap((path) => {
|
|
2820
2856
|
const restored = attempt(() => renameSync(this.#resolve(this.#backup, path), this.#expectation(path).path));
|
|
2821
|
-
|
|
2822
|
-
}
|
|
2823
|
-
for (const path of [...promoted].reverse()) {
|
|
2857
|
+
return restored.success ? [] : [restored.error];
|
|
2858
|
+
}), ...[...promoted].reverse().flatMap((path) => {
|
|
2824
2859
|
const expectation = this.#expectation(path);
|
|
2825
2860
|
const restored = attempt(() => {
|
|
2826
2861
|
if (expectation.shape === "absent") {
|
|
@@ -2829,21 +2864,23 @@ var WriteTransaction = class {
|
|
|
2829
2864
|
}
|
|
2830
2865
|
renameSync(this.#resolve(this.#backup, path), expectation.path);
|
|
2831
2866
|
});
|
|
2832
|
-
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
}
|
|
2841
|
-
for (const created of [...this.#created].reverse()) {
|
|
2867
|
+
return restored.success ? [] : [restored.error];
|
|
2868
|
+
})];
|
|
2869
|
+
const cleared = restoring.length === 0 ? attempt(() => rmSync(this.#root, {
|
|
2870
|
+
recursive: true,
|
|
2871
|
+
force: true
|
|
2872
|
+
})) : void 0;
|
|
2873
|
+
const clearing = cleared === void 0 || cleared.success ? [] : [cleared.error];
|
|
2874
|
+
const removing = [...this.#created].reverse().flatMap((created) => {
|
|
2842
2875
|
const path = isString(created) ? created : created.path;
|
|
2843
2876
|
const removed = attempt(() => rmdirSync(path));
|
|
2844
|
-
|
|
2845
|
-
}
|
|
2846
|
-
return
|
|
2877
|
+
return !removed.success && !matchesMissingPath(removed.error) ? [removed.error] : [];
|
|
2878
|
+
});
|
|
2879
|
+
return [
|
|
2880
|
+
...restoring,
|
|
2881
|
+
...clearing,
|
|
2882
|
+
...removing
|
|
2883
|
+
];
|
|
2847
2884
|
}
|
|
2848
2885
|
#stagePath(path) {
|
|
2849
2886
|
const staged = this.#resolve(this.#stage, path);
|
|
@@ -3070,18 +3107,17 @@ var Materializer = class {
|
|
|
3070
3107
|
const derived = this.#derive(accepted, directory, hydrated);
|
|
3071
3108
|
this.#reconfirm(derived.findings, preview.findings, directory);
|
|
3072
3109
|
const drifted = new Map(derived.findings.map((finding) => [finding.path, finding.drift]));
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
}
|
|
3110
|
+
const judged = hydrated.artifacts.map((artifact) => ({
|
|
3111
|
+
artifact,
|
|
3112
|
+
drift: drifted.get(artifact.path)
|
|
3113
|
+
}));
|
|
3114
|
+
const skipped = judged.flatMap(({ artifact, drift }) => drift === "missing" || drift === "stale" ? [] : [artifact.path]);
|
|
3115
|
+
const selected = judged.flatMap(({ artifact, drift }) => drift === "missing" || drift === "stale" ? [{
|
|
3116
|
+
artifact,
|
|
3117
|
+
missing: drift === "missing"
|
|
3118
|
+
}] : []);
|
|
3119
|
+
const preconditions = selected.map(({ artifact, missing }) => this.#bind(directory, artifact.path, missing));
|
|
3120
|
+
const writes = selected.map(({ artifact }) => artifact);
|
|
3085
3121
|
return this.#apply(directory, writes, [], skipped, preconditions);
|
|
3086
3122
|
}
|
|
3087
3123
|
/**
|
|
@@ -3106,15 +3142,15 @@ var Materializer = class {
|
|
|
3106
3142
|
const writes = [];
|
|
3107
3143
|
const skipped = [];
|
|
3108
3144
|
const preconditions = [];
|
|
3109
|
-
|
|
3145
|
+
const spent = [];
|
|
3110
3146
|
for (const fetched of accepted) {
|
|
3111
|
-
const hex = fetched.lookup === "found" ? contentToHex(fetched.content) : fetched.observed === void 0 ? this.#reference(fetched.path,
|
|
3147
|
+
const hex = fetched.lookup === "found" ? contentToHex(fetched.content) : fetched.observed === void 0 ? this.#reference(fetched.path, MAX_TOTAL_ARTIFACT_BYTES - computeTotal(spent)) : void 0;
|
|
3112
3148
|
if (hex === void 0 || fetched.observed === hex) {
|
|
3113
3149
|
skipped.push(fetched.path);
|
|
3114
3150
|
continue;
|
|
3115
3151
|
}
|
|
3116
|
-
|
|
3117
|
-
if (
|
|
3152
|
+
spent.push(hex.length / 2);
|
|
3153
|
+
if (computeTotal(spent) > MAX_TOTAL_ARTIFACT_BYTES) throw this.#error("TARGET", "The guide mirrors exceed the total artifact byte limit.");
|
|
3118
3154
|
const current = readFileHex(directory, fetched.path);
|
|
3119
3155
|
if (current !== fetched.observed) throw this.#error("TARGET", `The mirror at ${fetched.path} moved since it was fetched.`, {
|
|
3120
3156
|
target: directory,
|
|
@@ -3247,18 +3283,10 @@ var Materializer = class {
|
|
|
3247
3283
|
const derived = this.#derive(accepted, directory);
|
|
3248
3284
|
this.#reconfirmCandidates(derived.findings, preview.findings, directory);
|
|
3249
3285
|
const tracked = new Set(state.tracked);
|
|
3250
|
-
const
|
|
3251
|
-
const skipped =
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
if (!tracked.has(finding.path) || matchesProtectedPath(finding.path)) {
|
|
3255
|
-
skipped.push(finding.path);
|
|
3256
|
-
continue;
|
|
3257
|
-
}
|
|
3258
|
-
removals.push(finding.path);
|
|
3259
|
-
}
|
|
3260
|
-
const preconditions = [];
|
|
3261
|
-
for (const path of removals) preconditions.push(this.#bind(directory, path, false));
|
|
3286
|
+
const foreign = derived.findings.filter((finding) => finding.drift === "foreign").map((finding) => finding.path);
|
|
3287
|
+
const skipped = foreign.filter((path) => !tracked.has(path) || matchesProtectedPath(path));
|
|
3288
|
+
const removals = foreign.filter((path) => tracked.has(path) && !matchesProtectedPath(path));
|
|
3289
|
+
const preconditions = removals.map((path) => this.#bind(directory, path, false));
|
|
3262
3290
|
return this.#purge(directory, removals, skipped, preconditions);
|
|
3263
3291
|
}
|
|
3264
3292
|
/**
|
|
@@ -3318,49 +3346,51 @@ var Materializer = class {
|
|
|
3318
3346
|
if (this.#entries.size !== manifest.entries.length) throw this.#error("TARGET", "The vendored host manifest maps two files to one destination.", { host: root });
|
|
3319
3347
|
}
|
|
3320
3348
|
#hydrate(plan) {
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3349
|
+
const groups = [];
|
|
3350
|
+
const spent = [];
|
|
3323
3351
|
for (const artifact of plan.artifacts) {
|
|
3324
3352
|
if (artifact.origin !== "host") {
|
|
3325
|
-
|
|
3326
|
-
|
|
3353
|
+
spent.push(computeBytes(artifact.content));
|
|
3354
|
+
groups.push([artifact]);
|
|
3327
3355
|
continue;
|
|
3328
3356
|
}
|
|
3329
|
-
const expanded = this.#expand(artifact,
|
|
3330
|
-
|
|
3331
|
-
|
|
3357
|
+
const expanded = this.#expand(artifact, MAX_TOTAL_ARTIFACT_BYTES - computeTotal(spent));
|
|
3358
|
+
spent.push(computeTotal(expanded.map((one) => one.origin === "host" ? (one.hex?.length ?? 0) / 2 : computeBytes(one.content))));
|
|
3359
|
+
groups.push(expanded);
|
|
3332
3360
|
}
|
|
3333
|
-
if (
|
|
3361
|
+
if (computeTotal(spent) > MAX_TOTAL_ARTIFACT_BYTES) throw this.#error("TARGET", "The hydrated plan retains more bytes than one plan may.", {
|
|
3334
3362
|
host: this.#root,
|
|
3335
3363
|
limit: MAX_TOTAL_ARTIFACT_BYTES
|
|
3336
3364
|
});
|
|
3337
3365
|
return {
|
|
3338
3366
|
blueprint: plan.blueprint,
|
|
3339
3367
|
groups: plan.groups,
|
|
3340
|
-
artifacts
|
|
3368
|
+
artifacts: groups.flat()
|
|
3341
3369
|
};
|
|
3342
3370
|
}
|
|
3343
3371
|
#derive(plan, target, hydrated = this.#hydrate(plan)) {
|
|
3344
|
-
const paths = new Set(
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
target
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3372
|
+
const paths = /* @__PURE__ */ new Set([
|
|
3373
|
+
...hydrated.artifacts.map((artifact) => artifact.path),
|
|
3374
|
+
...this.#roots(plan).flatMap((root) => {
|
|
3375
|
+
const directory = resolveContainedPath(target, root);
|
|
3376
|
+
if (directory === void 0) throw this.#error("TARGET", `The owned root at ${root} leaves its target.`, {
|
|
3377
|
+
target,
|
|
3378
|
+
path: root
|
|
3379
|
+
});
|
|
3380
|
+
return listFiles(directory).map((name) => `${root}/${name}`);
|
|
3381
|
+
}),
|
|
3382
|
+
...listCanonPaths(target, plan.groups)
|
|
3383
|
+
]);
|
|
3354
3384
|
const questions = [];
|
|
3355
3385
|
if (plan.groups.includes("guides")) {
|
|
3356
3386
|
const directory = resolveContainedPath(target, "guides");
|
|
3357
|
-
|
|
3387
|
+
const spent = [];
|
|
3358
3388
|
for (const file of directory === void 0 ? [] : listFiles(directory)) {
|
|
3359
3389
|
const path = `guides/${file}`;
|
|
3360
3390
|
if (file.includes("/") || file === "README.md" || !file.endsWith(".md") || path === nameToGuide(plan.blueprint.name)) continue;
|
|
3361
|
-
const hosted = this.#reference(path,
|
|
3391
|
+
const hosted = this.#reference(path, MAX_TOTAL_ARTIFACT_BYTES - computeTotal(spent));
|
|
3362
3392
|
if (hosted === void 0) continue;
|
|
3363
|
-
|
|
3393
|
+
spent.push(hosted.length / 2);
|
|
3364
3394
|
const present = readFileHex(target, path);
|
|
3365
3395
|
if (present !== void 0 && present !== hosted) questions.push({
|
|
3366
3396
|
field: "guides",
|
|
@@ -3385,20 +3415,15 @@ var Materializer = class {
|
|
|
3385
3415
|
return readFileHex(this.#root, path, Math.max(0, Math.min(MAX_ARTIFACT_BYTES, budget)));
|
|
3386
3416
|
}
|
|
3387
3417
|
#roots(plan) {
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
if (artifact.origin !== "host") continue;
|
|
3418
|
+
return [...new Set(plan.artifacts.flatMap((artifact) => {
|
|
3419
|
+
if (artifact.origin !== "host") return [];
|
|
3391
3420
|
const source = artifact.source ?? artifact.path;
|
|
3392
|
-
if (this.#manifest?.roots.includes(source) === true)
|
|
3393
|
-
|
|
3394
|
-
continue;
|
|
3395
|
-
}
|
|
3396
|
-
if (this.#manifest !== void 0) continue;
|
|
3421
|
+
if (this.#manifest?.roots.includes(source) === true) return [artifact.path];
|
|
3422
|
+
if (this.#manifest !== void 0) return [];
|
|
3397
3423
|
const root = this.#root;
|
|
3398
3424
|
const directory = root === void 0 ? void 0 : resolveContainedPath(root, source);
|
|
3399
|
-
|
|
3400
|
-
}
|
|
3401
|
-
return [...roots];
|
|
3425
|
+
return directory !== void 0 && isPhysicalDirectory(directory) ? [artifact.path] : [];
|
|
3426
|
+
}))];
|
|
3402
3427
|
}
|
|
3403
3428
|
#expand(artifact, remaining) {
|
|
3404
3429
|
const manifest = this.#manifest;
|
|
@@ -3411,15 +3436,15 @@ var Materializer = class {
|
|
|
3411
3436
|
source
|
|
3412
3437
|
});
|
|
3413
3438
|
const expanded = [];
|
|
3414
|
-
|
|
3439
|
+
const spent = [];
|
|
3415
3440
|
for (const entry of matched) {
|
|
3416
3441
|
const path = this.#remap(artifact, entry.destination);
|
|
3417
3442
|
if (isRetainedPath(path)) {
|
|
3418
3443
|
expanded.push(this.#presence(artifact, path, entry.destination));
|
|
3419
3444
|
continue;
|
|
3420
3445
|
}
|
|
3421
|
-
const hex = this.#read(entry,
|
|
3422
|
-
|
|
3446
|
+
const hex = this.#read(entry, remaining - computeTotal(spent));
|
|
3447
|
+
spent.push(hex.length / 2);
|
|
3423
3448
|
expanded.push(this.#hydrated(artifact, path, entry.destination, hex));
|
|
3424
3449
|
}
|
|
3425
3450
|
return expanded;
|
|
@@ -3440,7 +3465,7 @@ var Materializer = class {
|
|
|
3440
3465
|
source
|
|
3441
3466
|
});
|
|
3442
3467
|
const expanded = [];
|
|
3443
|
-
|
|
3468
|
+
const spent = [];
|
|
3444
3469
|
for (const name of listFiles(full)) {
|
|
3445
3470
|
const path = `${artifact.path}/${name}`;
|
|
3446
3471
|
const destination = `${source}/${name}`;
|
|
@@ -3448,8 +3473,8 @@ var Materializer = class {
|
|
|
3448
3473
|
expanded.push(this.#presence(artifact, path, destination));
|
|
3449
3474
|
continue;
|
|
3450
3475
|
}
|
|
3451
|
-
const hex = this.#readRoot(destination,
|
|
3452
|
-
|
|
3476
|
+
const hex = this.#readRoot(destination, remaining - computeTotal(spent));
|
|
3477
|
+
spent.push(hex.length / 2);
|
|
3453
3478
|
expanded.push(this.#hydrated(artifact, path, destination, hex));
|
|
3454
3479
|
}
|
|
3455
3480
|
return expanded;
|
|
@@ -3457,16 +3482,15 @@ var Materializer = class {
|
|
|
3457
3482
|
#empty(plan) {
|
|
3458
3483
|
const manifest = this.#manifest;
|
|
3459
3484
|
if (manifest === void 0) return [];
|
|
3460
|
-
const directories =
|
|
3461
|
-
|
|
3462
|
-
if (artifact.origin !== "host") continue;
|
|
3485
|
+
const directories = plan.artifacts.flatMap((artifact) => {
|
|
3486
|
+
if (artifact.origin !== "host") return [];
|
|
3463
3487
|
const source = artifact.source ?? artifact.path;
|
|
3464
|
-
|
|
3465
|
-
if (root !== source && !root.startsWith(`${source}/`))
|
|
3466
|
-
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
return [...directories].sort();
|
|
3488
|
+
return manifest.roots.flatMap((root) => {
|
|
3489
|
+
if (root !== source && !root.startsWith(`${source}/`)) return [];
|
|
3490
|
+
return manifest.entries.some((entry) => entry.destination === root || entry.destination.startsWith(`${root}/`)) ? [] : [this.#remap(artifact, root)];
|
|
3491
|
+
});
|
|
3492
|
+
});
|
|
3493
|
+
return [...new Set(directories)].sort();
|
|
3470
3494
|
}
|
|
3471
3495
|
#remap(artifact, destination) {
|
|
3472
3496
|
const source = artifact.source ?? artifact.path;
|
|
@@ -3663,11 +3687,7 @@ var Materializer = class {
|
|
|
3663
3687
|
#fill(writes) {
|
|
3664
3688
|
const value = this.#value;
|
|
3665
3689
|
if (value === void 0) return void 0;
|
|
3666
|
-
const destinations =
|
|
3667
|
-
for (const artifact of writes) {
|
|
3668
|
-
if (artifact.origin !== "host") continue;
|
|
3669
|
-
destinations.add(artifact.source ?? artifact.path);
|
|
3670
|
-
}
|
|
3690
|
+
const destinations = new Set(writes.flatMap((artifact) => artifact.origin === "host" ? [artifact.source ?? artifact.path] : []));
|
|
3671
3691
|
if (destinations.size === 0) return void 0;
|
|
3672
3692
|
const opened = attempt(() => mkdtempSync(join(tmpdir(), "orkestrel-scaffold-fill-")));
|
|
3673
3693
|
if (!opened.success) throw this.#error("WRITE", "The supplied host could not be filled into a private root.", { error: opened.error });
|
|
@@ -3737,8 +3757,7 @@ var Materializer = class {
|
|
|
3737
3757
|
}
|
|
3738
3758
|
#recatalog(entries) {
|
|
3739
3759
|
const layers = catalogToLayers(entries);
|
|
3740
|
-
const placed =
|
|
3741
|
-
for (const [index, layer] of layers.entries()) for (const name of layer) placed.set(name, index);
|
|
3760
|
+
const placed = new Map(layers.flatMap((layer, index) => layer.map((name) => [name, index])));
|
|
3742
3761
|
const rows = entries.map((entry) => {
|
|
3743
3762
|
if (entry.lookup !== "found") return [
|
|
3744
3763
|
`\`${entry.name}\``,
|
|
@@ -3765,11 +3784,7 @@ var Materializer = class {
|
|
|
3765
3784
|
"Runtime dependencies",
|
|
3766
3785
|
"Peer dependencies"
|
|
3767
3786
|
];
|
|
3768
|
-
const widths = headers.map((header, column) =>
|
|
3769
|
-
let width = Math.max(3, header.length);
|
|
3770
|
-
for (const row of rows) width = Math.max(width, row[column]?.length ?? 0);
|
|
3771
|
-
return width;
|
|
3772
|
-
});
|
|
3787
|
+
const widths = headers.map((header, column) => Math.max(3, header.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
3773
3788
|
const table = [
|
|
3774
3789
|
headers,
|
|
3775
3790
|
widths.map((width) => "-".repeat(width)),
|
|
@@ -3825,6 +3840,66 @@ var Materializer = class {
|
|
|
3825
3840
|
}
|
|
3826
3841
|
};
|
|
3827
3842
|
//#endregion
|
|
3843
|
+
//#region src/server/ReadAllowance.ts
|
|
3844
|
+
/**
|
|
3845
|
+
* Represents the byte allowance one upstream call spends across every read it makes.
|
|
3846
|
+
*
|
|
3847
|
+
* @remarks
|
|
3848
|
+
* Each call constructs its own allowance from the reader's budget, so concurrent
|
|
3849
|
+
* calls never spend each other's bytes. A read that brings the call exactly to
|
|
3850
|
+
* its budget leaves `remaining` at zero and is accepted; the next request is
|
|
3851
|
+
* refused before it is sent, and a body that passes the budget is refused at the
|
|
3852
|
+
* chunk that overshoots.
|
|
3853
|
+
*
|
|
3854
|
+
* @example
|
|
3855
|
+
* ```ts
|
|
3856
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
3857
|
+
*
|
|
3858
|
+
* const allowance = new ReadAllowance(1_024)
|
|
3859
|
+
* allowance.spend(1_000)
|
|
3860
|
+
* allowance.remaining // 24
|
|
3861
|
+
* ```
|
|
3862
|
+
*/
|
|
3863
|
+
var ReadAllowance = class {
|
|
3864
|
+
#remaining;
|
|
3865
|
+
/**
|
|
3866
|
+
* Constructs an allowance holding the whole budget.
|
|
3867
|
+
*
|
|
3868
|
+
* @param budget - The bytes the call may read in total.
|
|
3869
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `budget` is not a
|
|
3870
|
+
* non-negative safe integer.
|
|
3871
|
+
*/
|
|
3872
|
+
constructor(budget) {
|
|
3873
|
+
if (!isNonNegativeInteger(budget) || !Number.isSafeInteger(budget)) throw new ScaffoldError("INVALID", "The read budget is not a non-negative safe integer.", { field: "budget" });
|
|
3874
|
+
this.#remaining = budget;
|
|
3875
|
+
}
|
|
3876
|
+
/** The bytes the call has left; negative once a read overshoots the budget. */
|
|
3877
|
+
get remaining() {
|
|
3878
|
+
return this.#remaining;
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* Spends bytes a read consumed from the allowance.
|
|
3882
|
+
*
|
|
3883
|
+
* @param bytes - The decoded bytes the read consumed.
|
|
3884
|
+
* @returns Nothing.
|
|
3885
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `bytes` is not a
|
|
3886
|
+
* non-negative safe integer.
|
|
3887
|
+
*
|
|
3888
|
+
* @example
|
|
3889
|
+
* ```ts
|
|
3890
|
+
* import { ReadAllowance } from '@orkestrel/scaffold/server'
|
|
3891
|
+
*
|
|
3892
|
+
* const allowance = new ReadAllowance(10)
|
|
3893
|
+
* allowance.spend(12)
|
|
3894
|
+
* allowance.remaining // -2
|
|
3895
|
+
* ```
|
|
3896
|
+
*/
|
|
3897
|
+
spend(bytes) {
|
|
3898
|
+
if (!isNonNegativeInteger(bytes) || !Number.isSafeInteger(bytes)) throw new ScaffoldError("INVALID", "The spent bytes are not a non-negative safe integer.", { field: "bytes" });
|
|
3899
|
+
this.#remaining -= bytes;
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
//#endregion
|
|
3828
3903
|
//#region src/server/Upstream.ts
|
|
3829
3904
|
/**
|
|
3830
3905
|
* Represents the reading spine: one bounded, unauthenticated, redirect-free request per answer.
|
|
@@ -3855,9 +3930,9 @@ var Materializer = class {
|
|
|
3855
3930
|
* reader's own abort signal, so {@link Upstream.destroy} cancels what is in
|
|
3856
3931
|
* flight instead of waiting for it.
|
|
3857
3932
|
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3933
|
+
* Each call constructs its own {@link ReadAllowance} from the reader's budget and passes it
|
|
3934
|
+
* through the private reads rather than holding it on the instance, because concurrent calls
|
|
3935
|
+
* each own a budget and must not spend each other's.
|
|
3861
3936
|
*
|
|
3862
3937
|
* @example
|
|
3863
3938
|
* ```ts
|
|
@@ -3950,7 +4025,7 @@ var Upstream = class {
|
|
|
3950
4025
|
async lookup(dependencies) {
|
|
3951
4026
|
this.#assertAlive();
|
|
3952
4027
|
const accepted = this.#accept(dependencies, isDependencies, "dependencies");
|
|
3953
|
-
const allowance =
|
|
4028
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
3954
4029
|
return this.#gather(accepted, (dependency) => this.#release(dependency, allowance));
|
|
3955
4030
|
}
|
|
3956
4031
|
/**
|
|
@@ -3984,7 +4059,7 @@ var Upstream = class {
|
|
|
3984
4059
|
this.#assertAlive();
|
|
3985
4060
|
const accepted = this.#accept(names, isDependencyNames, "names");
|
|
3986
4061
|
const observed = this.#accept(current, isSnapshot, "current");
|
|
3987
|
-
const allowance =
|
|
4062
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
3988
4063
|
return this.#gather(accepted, (name) => this.#mirror(name, observed, allowance));
|
|
3989
4064
|
}
|
|
3990
4065
|
/**
|
|
@@ -4031,7 +4106,7 @@ var Upstream = class {
|
|
|
4031
4106
|
const accepted = this.#accept(paths, isPaths, "paths");
|
|
4032
4107
|
const observed = this.#accept(current, isSnapshot, "current");
|
|
4033
4108
|
if (accepted.length === 0) return [];
|
|
4034
|
-
const allowance =
|
|
4109
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
4035
4110
|
const inventory = await this.#inventory(allowance);
|
|
4036
4111
|
return this.#gather(accepted, (path) => this.#file(path, inventory, observed, allowance));
|
|
4037
4112
|
}
|
|
@@ -4064,7 +4139,7 @@ var Upstream = class {
|
|
|
4064
4139
|
*/
|
|
4065
4140
|
async catalog() {
|
|
4066
4141
|
this.#assertAlive();
|
|
4067
|
-
const allowance =
|
|
4142
|
+
const allowance = new ReadAllowance(this.#budget);
|
|
4068
4143
|
const names = await this.#packages(allowance);
|
|
4069
4144
|
return this.#gather(names, (name) => this.#entry(name, allowance));
|
|
4070
4145
|
}
|
|
@@ -4233,16 +4308,10 @@ var Upstream = class {
|
|
|
4233
4308
|
lookup: "failed",
|
|
4234
4309
|
note: `the vendored inventory at ${url} does not match its own membership digest`
|
|
4235
4310
|
};
|
|
4236
|
-
const digests = /* @__PURE__ */ new Map();
|
|
4237
|
-
const duplicates = /* @__PURE__ */ new Set();
|
|
4238
|
-
for (const entry of manifest.entries) {
|
|
4239
|
-
if (digests.has(entry.destination)) duplicates.add(entry.destination);
|
|
4240
|
-
digests.set(entry.destination, entry.digest);
|
|
4241
|
-
}
|
|
4242
4311
|
return {
|
|
4243
4312
|
lookup: "found",
|
|
4244
|
-
digests,
|
|
4245
|
-
duplicates,
|
|
4313
|
+
digests: new Map(manifest.entries.map((entry) => [entry.destination, entry.digest])),
|
|
4314
|
+
duplicates: new Set(manifest.entries.map((entry) => entry.destination).filter((destination, index, destinations) => destinations.indexOf(destination) !== index)),
|
|
4246
4315
|
note: ""
|
|
4247
4316
|
};
|
|
4248
4317
|
}
|
|
@@ -4271,18 +4340,10 @@ var Upstream = class {
|
|
|
4271
4340
|
if (!isRecord(manifest)) return [];
|
|
4272
4341
|
const declared = manifest[section];
|
|
4273
4342
|
if (!isRecord(declared)) return [];
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
if (CONTROL_CHARACTER_PATTERN.test(name) || CONTROL_CHARACTER_PATTERN.test(range)) continue;
|
|
4279
|
-
if (name.length === 0 || name.length > MAX_DEPENDENCY_NAME_LENGTH) continue;
|
|
4280
|
-
edges.push({
|
|
4281
|
-
name,
|
|
4282
|
-
range
|
|
4283
|
-
});
|
|
4284
|
-
}
|
|
4285
|
-
return edges;
|
|
4343
|
+
return Object.entries(declared).flatMap(([name, range]) => !isString(range) || range.length === 0 || range.length > MAX_RANGE_LENGTH || CONTROL_CHARACTER_PATTERN.test(name) || CONTROL_CHARACTER_PATTERN.test(range) || name.length === 0 || name.length > MAX_DEPENDENCY_NAME_LENGTH ? [] : [{
|
|
4344
|
+
name,
|
|
4345
|
+
range
|
|
4346
|
+
}]).slice(0, MAX_COLLECTION_ITEMS);
|
|
4286
4347
|
}
|
|
4287
4348
|
async #packages(allowance) {
|
|
4288
4349
|
const url = `${this.#registryBase}/-/org/${ORKESTREL_SCOPE}/package`;
|
|
@@ -4311,11 +4372,7 @@ var Upstream = class {
|
|
|
4311
4372
|
const parsed = parseJSON(content);
|
|
4312
4373
|
if (!isRecord(parsed)) return void 0;
|
|
4313
4374
|
const versions = parsed.versions;
|
|
4314
|
-
|
|
4315
|
-
if (isRecord(versions)) for (const version of Object.keys(versions)) {
|
|
4316
|
-
if (!this.#admits(range, version)) continue;
|
|
4317
|
-
if (selected === void 0 || compareVersions(version, selected) > 0) selected = version;
|
|
4318
|
-
}
|
|
4375
|
+
const selected = isRecord(versions) ? Object.keys(versions).filter((version) => this.#admits(range, version)).reduce((highest, version) => highest === void 0 || compareVersions(version, highest) > 0 ? version : highest, void 0) : void 0;
|
|
4319
4376
|
if (selected !== void 0) return selected;
|
|
4320
4377
|
const latest = this.#latest(content);
|
|
4321
4378
|
return latest !== void 0 && this.#admits(range, latest) ? latest : void 0;
|
|
@@ -4343,14 +4400,12 @@ var Upstream = class {
|
|
|
4343
4400
|
#encode(path) {
|
|
4344
4401
|
return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
4345
4402
|
}
|
|
4346
|
-
async #readWithRetries(url, timeout, allowance, accept, binary = false) {
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
note = outcome.note;
|
|
4353
|
-
}
|
|
4403
|
+
async #readWithRetries(url, timeout, allowance, accept, binary = false, attempt = 0) {
|
|
4404
|
+
this.#assertAlive();
|
|
4405
|
+
const outcome = binary ? await this.#request(url, timeout, allowance, accept, true) : await this.#request(url, timeout, allowance, accept);
|
|
4406
|
+
if (outcome.lookup !== "failed") return outcome;
|
|
4407
|
+
if (attempt < this.#retries) return binary ? this.#readWithRetries(url, timeout, allowance, accept, true, attempt + 1) : this.#readWithRetries(url, timeout, allowance, accept, false, attempt + 1);
|
|
4408
|
+
const note = outcome.note;
|
|
4354
4409
|
this.#error("FETCH", `The upstream read at ${url} produced no answer.`, {
|
|
4355
4410
|
url,
|
|
4356
4411
|
note
|
|
@@ -4454,14 +4509,14 @@ var Upstream = class {
|
|
|
4454
4509
|
const reader = body.getReader();
|
|
4455
4510
|
const decoder = binary ? void 0 : new TextDecoder("utf-8", { fatal: true });
|
|
4456
4511
|
const chunks = [];
|
|
4457
|
-
|
|
4512
|
+
const sizes = [];
|
|
4458
4513
|
try {
|
|
4459
4514
|
for (;;) {
|
|
4460
4515
|
const chunk = await reader.read();
|
|
4461
4516
|
if (chunk.done) break;
|
|
4462
|
-
|
|
4463
|
-
allowance.
|
|
4464
|
-
if (
|
|
4517
|
+
sizes.push(chunk.value.byteLength);
|
|
4518
|
+
allowance.spend(chunk.value.byteLength);
|
|
4519
|
+
if (computeTotal(sizes) > this.#limit) {
|
|
4465
4520
|
await reader.cancel();
|
|
4466
4521
|
const note = `the response passed the ${String(this.#limit)}-byte response limit`;
|
|
4467
4522
|
return binary ? {
|
|
@@ -4514,31 +4569,28 @@ var Upstream = class {
|
|
|
4514
4569
|
return code === void 0 ? message : `${message} (${code})`;
|
|
4515
4570
|
}
|
|
4516
4571
|
async #gather(items, run) {
|
|
4517
|
-
const
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
return
|
|
4572
|
+
const entries = items.entries();
|
|
4573
|
+
const failed = new AbortController();
|
|
4574
|
+
const workers = Array.from({ length: Math.min(this.#concurrency, items.length) }, () => this.#drain(entries, run, failed));
|
|
4575
|
+
const settled = await Promise.allSettled(workers);
|
|
4576
|
+
if (failed.signal.aborted) {
|
|
4577
|
+
const reason = failed.signal.reason;
|
|
4578
|
+
throw isRecord(reason) ? reason.error : reason;
|
|
4579
|
+
}
|
|
4580
|
+
return collectIndexed(settled.flatMap((worker) => worker.status === "fulfilled" ? worker.value : []));
|
|
4526
4581
|
}
|
|
4527
|
-
async #drain(
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
state.cursor += 1;
|
|
4532
|
-
if (index >= items.length) return;
|
|
4533
|
-
const item = items[index];
|
|
4534
|
-
if (item === void 0) return;
|
|
4582
|
+
async #drain(entries, run, failed) {
|
|
4583
|
+
const produced = [];
|
|
4584
|
+
for (const [index, item] of entries) {
|
|
4585
|
+
if (failed.signal.aborted || item === void 0) break;
|
|
4535
4586
|
try {
|
|
4536
|
-
|
|
4587
|
+
produced.push([index, await run(item)]);
|
|
4537
4588
|
} catch (error) {
|
|
4538
|
-
|
|
4539
|
-
|
|
4589
|
+
failed.abort(Object.freeze({ error }));
|
|
4590
|
+
break;
|
|
4540
4591
|
}
|
|
4541
4592
|
}
|
|
4593
|
+
return produced;
|
|
4542
4594
|
}
|
|
4543
4595
|
#accept(value, guard, field) {
|
|
4544
4596
|
const snapshot = cloneValue(value);
|
|
@@ -4555,6 +4607,6 @@ var Upstream = class {
|
|
|
4555
4607
|
}
|
|
4556
4608
|
};
|
|
4557
4609
|
//#endregion
|
|
4558
|
-
export { BRANCH_PATTERN, DEFAULT_BRANCH, DEFAULT_REGISTRY_BASE, DEFAULT_REPOSITORY_BASE, DEFAULT_UPSTREAM_CONCURRENCY, DEFAULT_UPSTREAM_RETRIES, DEFAULT_UPSTREAM_TIMEOUT, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, ORKESTREL_SCOPE, PACKUMENT_MEDIA_TYPE, RESERVED_SEGMENT_PATTERN, SCAFFOLD_REPOSITORY, UNREADABLE_VERSION_NOTE, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, decodeHexText, filesToHost, hexToDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHost, isHostManifest, isInventory, isManifestEntry, isManifestRegionSet, isMaterializerHooks, isMaterializerOptions, isMirrors, isPaths, isPhysicalDirectory, isPhysicalFile, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, isWorktree, listCanonPaths, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, pruneEmptiedDirectories, readAnchor, readExpectation, readFileHex, readFileText, readHostFloor, readHostManifest, readManifestEntry, readSnapshot, readSurfaceBaseline, readSurfaceCollisions, resolveContainedPath, resolveRealPath, stageBytes, stageHost, stageInventory };
|
|
4610
|
+
export { BRANCH_PATTERN, DEFAULT_BRANCH, DEFAULT_REGISTRY_BASE, DEFAULT_REPOSITORY_BASE, DEFAULT_UPSTREAM_CONCURRENCY, DEFAULT_UPSTREAM_RETRIES, DEFAULT_UPSTREAM_TIMEOUT, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, ORKESTREL_SCOPE, PACKUMENT_MEDIA_TYPE, RESERVED_SEGMENT_PATTERN, ReadAllowance, SCAFFOLD_REPOSITORY, UNREADABLE_VERSION_NOTE, Upstream, WriteTransaction, climbRealPath, collectIndexed, computeDigest, computeFileDigest, computeManifestDigest, decodeHexText, filesToHost, hexToDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHost, isHostManifest, isInventory, isManifestEntry, isManifestRegionSet, isMaterializerHooks, isMaterializerOptions, isMirrors, isPaths, isPhysicalDirectory, isPhysicalFile, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, isWorktree, listAncestors, listCanonPaths, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, pruneEmptiedDirectories, readAnchor, readExpectation, readFileHex, readFileText, readHostFloor, readHostManifest, readManifestEntry, readSnapshot, readSurfaceBaseline, readSurfaceCollisions, resolveContainedPath, resolveRealPath, stageBytes, stageHost, stageInventory };
|
|
4559
4611
|
|
|
4560
4612
|
//# sourceMappingURL=index.js.map
|