@orkestrel/scaffold 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bin/main.js +45 -4
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +40 -11
- package/dist/host/claude/agents/orkestrel.md +63 -48
- package/dist/host/claude/rules/documentation.md +1 -1
- package/dist/host/guides/guide.md +211 -100
- package/dist/host/guides/scaffold.md +165 -10
- package/dist/host/manifest.json +5 -5
- package/dist/host/tests/config.test.ts +30 -7
- package/dist/src/core/index.cjs +242 -149
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +185 -98
- package/dist/src/core/index.d.ts +185 -98
- package/dist/src/core/index.js +240 -150
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +171 -66
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +140 -26
- package/dist/src/server/index.d.ts +140 -26
- package/dist/src/server/index.js +173 -69
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/src/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { andOf, arrayOf, attempt, boundsOf, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf } from "@orkestrel/contract";
|
|
2
|
-
import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, ScaffoldError, bytesToHex, cloneValue, computeBytes, contentToHex, inferGroup, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isMirror, isPath, isPlan, isSnapshot, nameToGuide, planToFindings } from "../core/index.js";
|
|
2
|
+
import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_TOTAL_ARTIFACT_BYTES, ScaffoldError, bytesToHex, cloneValue, computeBytes, contentToHex, inferGroup, isAudit, isCatalogEntry, isCollection, isDependency, isDependencyName, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, nameToGuide, planToFindings } from "../core/index.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, parse, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { Emitter } from "@orkestrel/emitter";
|
|
@@ -130,12 +130,23 @@ var MANIFEST_NAME = "manifest.json";
|
|
|
130
130
|
* the byte ceiling. The character ceiling is read first so an oversized string is
|
|
131
131
|
* refused before it is split.
|
|
132
132
|
*
|
|
133
|
+
* The two spellings of an empty segment are answered differently. A trailing
|
|
134
|
+
* separator terminates a directory rather than opening a segment, and every
|
|
135
|
+
* supported filesystem and every Node path API reads `project/` and `project` as
|
|
136
|
+
* one location, so it is admitted. A doubled separator is a genuine empty
|
|
137
|
+
* segment, so `project//src` is refused. Nothing normalizes the argument first —
|
|
138
|
+
* every server entry point guards the caller's text and resolves it afterwards —
|
|
139
|
+
* so a directory taken from a shell completion arrives carrying the separator the
|
|
140
|
+
* shell appended and names the directory it appears to name.
|
|
141
|
+
*
|
|
133
142
|
* @example
|
|
134
143
|
* ```ts
|
|
135
144
|
* import { isFilesystemPath } from '@orkestrel/scaffold/server'
|
|
136
145
|
*
|
|
137
146
|
* isFilesystemPath('C:/Users/sample/project') // true
|
|
138
147
|
* isFilesystemPath('../sibling') // true
|
|
148
|
+
* isFilesystemPath('project/') // true
|
|
149
|
+
* isFilesystemPath('project//src') // false
|
|
139
150
|
* isFilesystemPath('project/nul') // false
|
|
140
151
|
* ```
|
|
141
152
|
*/
|
|
@@ -144,7 +155,8 @@ function isFilesystemPath(value) {
|
|
|
144
155
|
if (!isString(value) || value.length === 0 || value.length > MAX_PATH_LENGTH) return false;
|
|
145
156
|
if (CONTROL_CHARACTER_PATTERN.test(value)) return false;
|
|
146
157
|
const normalized = value.replaceAll("\\", "/");
|
|
147
|
-
const
|
|
158
|
+
const rooted = normalized.startsWith("//") ? normalized.slice(2) : normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
159
|
+
const segments = (rooted.endsWith("/") ? rooted.slice(0, -1) : rooted).split("/");
|
|
148
160
|
if (segments.length > 64) return false;
|
|
149
161
|
for (const [index, segment] of segments.entries()) {
|
|
150
162
|
if (segment === "." || segment === "..") continue;
|
|
@@ -509,6 +521,32 @@ function matchesSensitivePath(path) {
|
|
|
509
521
|
return /(?:^|\/)(?:(?:\.ssh|\.aws|\.azure|\.docker|\.kube|\.gnupg|\.env(?:\.[^/]*)?)(?:\/|$)|(?:\.npmrc|\.pypirc|\.netrc|\.git-credentials|settings\.local\.json|auth\.json|credentials(?:\.json)?|application_default_credentials\.json|id_rsa|id_ed25519|kubeconfig)$|\.config\/(?:gh|gcloud)(?:\/|$)|\.local\/share\/keyrings(?:\/|$)|[^/]*service-account[^/]*\.json$|[^/]*\.(?:jks|key|p12|pem|pfx|pkcs12)$)/i.test(normalized);
|
|
510
522
|
}
|
|
511
523
|
/**
|
|
524
|
+
* Test whether a vendored path is one a target receives executable.
|
|
525
|
+
*
|
|
526
|
+
* @param path - The target-relative path to classify; either separator is read.
|
|
527
|
+
* @returns `true` when the path is declared in {@link EXECUTABLE_PATHS}.
|
|
528
|
+
*
|
|
529
|
+
* @remarks
|
|
530
|
+
* The declaration is the whole answer, and deliberately so. Reading the staging
|
|
531
|
+
* host's mode instead makes the manifest depend on where the package was built:
|
|
532
|
+
* Windows carries no executable bit, so a host staged there declares every entry
|
|
533
|
+
* non-executable and every target it later fills receives hooks at `0644`. One
|
|
534
|
+
* checkout stages one manifest on every host because this predicate never
|
|
535
|
+
* consults the filesystem.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```ts
|
|
539
|
+
* import { matchesExecutablePath } from '@orkestrel/scaffold/server'
|
|
540
|
+
*
|
|
541
|
+
* matchesExecutablePath('scripts/codex.sh') // true
|
|
542
|
+
* matchesExecutablePath('scripts\\deps.sh') // true
|
|
543
|
+
* matchesExecutablePath('AGENTS.md') // false
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
function matchesExecutablePath(path) {
|
|
547
|
+
return EXECUTABLE_PATHS.includes(path.replaceAll("\\", "/"));
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
512
550
|
* Project a target-relative path to the storage name a vendored host holds it under.
|
|
513
551
|
*
|
|
514
552
|
* @param path - The target-relative path the file is written to.
|
|
@@ -727,9 +765,10 @@ function computeFileDigest(path) {
|
|
|
727
765
|
* Resolve a path through the real filesystem, keeping the part that does not exist yet.
|
|
728
766
|
*
|
|
729
767
|
* @param path - The absolute or relative host path to resolve.
|
|
730
|
-
* @returns The path with its existing prefix
|
|
731
|
-
* `undefined` when the text is not a host path,
|
|
732
|
-
* resolves,
|
|
768
|
+
* @returns The lexical resolution of `path`, with its existing prefix then
|
|
769
|
+
* resolved through every link, or `undefined` when the text is not a host path,
|
|
770
|
+
* no bounded existing ancestor resolves, a link target cannot be read, a link
|
|
771
|
+
* target carries a `..` segment, or an ancestor cannot be read.
|
|
733
772
|
*
|
|
734
773
|
* @remarks
|
|
735
774
|
* A containment decision has to be made about a destination that does not exist
|
|
@@ -739,6 +778,30 @@ function computeFileDigest(path) {
|
|
|
739
778
|
* it. The climb is bounded by the path-depth ceiling, so an adversarial path
|
|
740
779
|
* cannot make it walk indefinitely.
|
|
741
780
|
*
|
|
781
|
+
* The caller's own text is collapsed first, which is what `resolve` does with a
|
|
782
|
+
* `..` the caller wrote: it cancels the segment before it as text, before any
|
|
783
|
+
* link in that segment is read. So `<root>/hop/..` answers `<root>` even where
|
|
784
|
+
* `hop` links elsewhere, rather than the directory holding what `hop` points at.
|
|
785
|
+
* The collapse only ever shortens the caller's path, so nothing reaches outside
|
|
786
|
+
* it by this; the answer is that lexical location resolved through links, not
|
|
787
|
+
* the physical location the links lead to. {@link resolveContainedPath} passes
|
|
788
|
+
* its `root` through here, so a root written with a parent segment is contained
|
|
789
|
+
* against its collapsed spelling.
|
|
790
|
+
*
|
|
791
|
+
* `realpath` answers `ENOENT` both for a name that is not there and for a link
|
|
792
|
+
* whose target is not there. The name is therefore inspected without following
|
|
793
|
+
* it: a dangling link redirects the walk to its target, while a genuinely absent
|
|
794
|
+
* name is retained as one segment of the unresolved suffix. A dangling link
|
|
795
|
+
* target containing a `..` segment is refused. Resolving that target as one
|
|
796
|
+
* lexical string could discard a preceding link before the filesystem gives
|
|
797
|
+
* `..` its physical meaning.
|
|
798
|
+
*
|
|
799
|
+
* That target is split on both separators on every host, which is the reading
|
|
800
|
+
* `isPath` already gives a planned path. A POSIX filename legally containing a
|
|
801
|
+
* backslash is therefore refused with it: `weird\..\name` is one name to the
|
|
802
|
+
* host and three segments here. The package keeps one separator law rather than
|
|
803
|
+
* a host-dependent second one, and this is the conservative side of it.
|
|
804
|
+
*
|
|
742
805
|
* @example
|
|
743
806
|
* ```ts
|
|
744
807
|
* import { resolveRealPath } from '@orkestrel/scaffold/server'
|
|
@@ -758,6 +821,16 @@ function resolveRealPath(path) {
|
|
|
758
821
|
return physical;
|
|
759
822
|
}
|
|
760
823
|
if (!matchesMissingPath(real.error)) return void 0;
|
|
824
|
+
const status = attempt(() => lstatSync(current));
|
|
825
|
+
if (status.success) {
|
|
826
|
+
if (!status.value.isSymbolicLink()) return void 0;
|
|
827
|
+
const target = attempt(() => readlinkSync(current));
|
|
828
|
+
if (!target.success) return void 0;
|
|
829
|
+
if (target.value.split(/[\\/]/u).includes("..")) return void 0;
|
|
830
|
+
current = resolve(dirname(current), target.value);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (!matchesMissingPath(status.error)) return void 0;
|
|
761
834
|
const parent = dirname(current);
|
|
762
835
|
if (parent === current) return void 0;
|
|
763
836
|
pending.unshift(relative(parent, current));
|
|
@@ -774,20 +847,31 @@ function resolveRealPath(path) {
|
|
|
774
847
|
*
|
|
775
848
|
* @remarks
|
|
776
849
|
* The containment law, and the one door every read in this module goes through.
|
|
777
|
-
* Both sides are resolved through the real filesystem before they are compared
|
|
778
|
-
*
|
|
779
|
-
* answer is then the lexical join
|
|
780
|
-
*
|
|
850
|
+
* Both sides are resolved through the real filesystem before they are compared.
|
|
851
|
+
* A dangling link is followed only when its raw target contains no parent
|
|
852
|
+
* traversal. The answer is then the lexical join of `root` and `path` — an
|
|
853
|
+
* absolute path under `root`, not a root-relative one — so the caller operates
|
|
854
|
+
* on the path it named rather than on a resolved form the target may not
|
|
855
|
+
* recognize. A `root` written with a parent segment is collapsed by that
|
|
856
|
+
* resolution before anything is read, so containment is measured against the
|
|
857
|
+
* directory the caller's text names.
|
|
781
858
|
*
|
|
782
859
|
* Comparison is exact text, which fails closed on a case-insensitive
|
|
783
860
|
* filesystem: a root and a path spelled with different case resolve to
|
|
784
861
|
* different strings there and are refused, never wrongly admitted.
|
|
785
862
|
*
|
|
863
|
+
* The answer describes the namespace this call read. The contract excludes a
|
|
864
|
+
* concurrent rename or link swap during the call or before the caller finishes
|
|
865
|
+
* using the returned path. This helper returns a string, not a filesystem
|
|
866
|
+
* handle, so it cannot bind its containment check to a later operation. A caller
|
|
867
|
+
* that admits hostile concurrent namespace mutation needs a handle-bound
|
|
868
|
+
* operation instead.
|
|
869
|
+
*
|
|
786
870
|
* @example
|
|
787
871
|
* ```ts
|
|
788
872
|
* import { resolveContainedPath } from '@orkestrel/scaffold/server'
|
|
789
873
|
*
|
|
790
|
-
* resolveContainedPath('/tmp/project', 'guides/router.md')
|
|
874
|
+
* resolveContainedPath('/tmp/project', 'guides/router.md')?.endsWith('router.md') // true
|
|
791
875
|
* resolveContainedPath('/tmp/project', '../secrets') // undefined
|
|
792
876
|
* ```
|
|
793
877
|
*/
|
|
@@ -864,7 +948,7 @@ function isVacant(target) {
|
|
|
864
948
|
* ```ts
|
|
865
949
|
* import { listFiles } from '@orkestrel/scaffold/server'
|
|
866
950
|
*
|
|
867
|
-
* listFiles('./dist/host') // ['AGENTS.md', '
|
|
951
|
+
* listFiles('./dist/host') // ['AGENTS.md', 'CLAUDE.md', 'LICENSE', …]
|
|
868
952
|
* ```
|
|
869
953
|
*/
|
|
870
954
|
function listFiles(root) {
|
|
@@ -1238,14 +1322,14 @@ function readHostManifest(host) {
|
|
|
1238
1322
|
*
|
|
1239
1323
|
* @remarks
|
|
1240
1324
|
* The one place the three declared fields are decided together, because they are
|
|
1241
|
-
* three readings of one
|
|
1242
|
-
* the destination is the path it answers for, and
|
|
1243
|
-
*
|
|
1325
|
+
* three readings of one path: {@link pathToStorage} decides where it is stored,
|
|
1326
|
+
* the destination is the path it answers for, and {@link matchesExecutablePath}
|
|
1327
|
+
* decides whether a target receives it executable.
|
|
1244
1328
|
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1329
|
+
* The bit is read from that declaration rather than from the source's mode, so
|
|
1330
|
+
* the entry does not depend on where the package was staged. A Windows host
|
|
1331
|
+
* reports no executable bit at all, and reading the mode there declared every
|
|
1332
|
+
* entry non-executable and shipped consumers hooks they could not run.
|
|
1249
1333
|
*
|
|
1250
1334
|
* @example
|
|
1251
1335
|
* ```ts
|
|
@@ -1262,7 +1346,7 @@ function readManifestEntry(destination, source) {
|
|
|
1262
1346
|
return {
|
|
1263
1347
|
storage: pathToStorage(destination),
|
|
1264
1348
|
destination,
|
|
1265
|
-
executable: (
|
|
1349
|
+
executable: matchesExecutablePath(destination)
|
|
1266
1350
|
};
|
|
1267
1351
|
}
|
|
1268
1352
|
/**
|
|
@@ -1432,8 +1516,8 @@ function stageHost(checkout, host) {
|
|
|
1432
1516
|
* @remarks
|
|
1433
1517
|
* Device and inode rather than the path, because the path is the thing that can
|
|
1434
1518
|
* be swapped underneath a write. An anchor captured before a mutation and
|
|
1435
|
-
* checked again after it
|
|
1436
|
-
*
|
|
1519
|
+
* checked again after it proves the directory written into sits where the
|
|
1520
|
+
* inspected one sat, not that it is the one that was inspected.
|
|
1437
1521
|
*
|
|
1438
1522
|
* @example
|
|
1439
1523
|
* ```ts
|
|
@@ -1459,9 +1543,13 @@ function readAnchor(path) {
|
|
|
1459
1543
|
* device and inode.
|
|
1460
1544
|
*
|
|
1461
1545
|
* @remarks
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1546
|
+
* This binds location rather than history. `true` means the path still resolves
|
|
1547
|
+
* to the same physical directory on the same device, so the next write lands
|
|
1548
|
+
* where the last one did. A path now holding nothing, a file, or a symlink
|
|
1549
|
+
* answers `false`; a directory swapped in by `rename` also answers `false`
|
|
1550
|
+
* because the replacement carries its own inode. A directory deleted and made
|
|
1551
|
+
* again under the same name can receive the old inode back and answers `true`,
|
|
1552
|
+
* which nothing here detects.
|
|
1465
1553
|
*
|
|
1466
1554
|
* @example
|
|
1467
1555
|
* ```ts
|
|
@@ -1607,6 +1695,12 @@ function matchesPrecondition(precondition) {
|
|
|
1607
1695
|
* - **No partly written destination.** Every file is written whole into the
|
|
1608
1696
|
* private root and digested there before commit, so a destination never
|
|
1609
1697
|
* receives bytes that were still being produced.
|
|
1698
|
+
* - **Containment, not continuity, of the directories it creates.** Every
|
|
1699
|
+
* ancestor is re-read between `mkdir` calls and again before the first
|
|
1700
|
+
* promotion, so an ancestor that became a file, a symlink, a directory
|
|
1701
|
+
* elsewhere, or nothing is refused. An ancestor deleted and recreated under
|
|
1702
|
+
* the same name can receive its old inode back and is indistinguishable here
|
|
1703
|
+
* from one that never moved.
|
|
1610
1704
|
* - **No crash atomicity across destinations.** A process killed between two
|
|
1611
1705
|
* promotions leaves the target holding some new files and some old ones, and
|
|
1612
1706
|
* leaves the private root behind. Nothing here is a journal, and the private
|
|
@@ -1701,7 +1795,7 @@ var WriteTransaction = class {
|
|
|
1701
1795
|
this.#stage = join(this.#root, "stage");
|
|
1702
1796
|
this.#backup = join(this.#root, "backup");
|
|
1703
1797
|
const opened = attempt(() => {
|
|
1704
|
-
this.#
|
|
1798
|
+
this.#establish(parent);
|
|
1705
1799
|
for (const directory of [
|
|
1706
1800
|
this.#root,
|
|
1707
1801
|
this.#stage,
|
|
@@ -1793,7 +1887,7 @@ var WriteTransaction = class {
|
|
|
1793
1887
|
const staged = this.#stagePath(path);
|
|
1794
1888
|
const copied = attempt(() => {
|
|
1795
1889
|
copyFileSync(source, staged, constants.COPYFILE_EXCL);
|
|
1796
|
-
|
|
1890
|
+
chmodSync(staged, executable ? 493 : 420);
|
|
1797
1891
|
});
|
|
1798
1892
|
if (!copied.success) throw new ScaffoldError("WRITE", `The staged copy at ${path} could not be made.`, {
|
|
1799
1893
|
path,
|
|
@@ -1825,8 +1919,12 @@ var WriteTransaction = class {
|
|
|
1825
1919
|
directory(path) {
|
|
1826
1920
|
this.#assertOpen();
|
|
1827
1921
|
if (this.#expectation(path).shape === "file") throw new ScaffoldError("TARGET", `The destination at ${path} holds a file.`, { path });
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1922
|
+
const established = attempt(() => this.#establish(this.#resolve(this.#target, path)));
|
|
1923
|
+
if (!established.success) throw new ScaffoldError("WRITE", `The directory at ${path} could not be established.`, {
|
|
1924
|
+
path,
|
|
1925
|
+
error: established.error
|
|
1926
|
+
});
|
|
1927
|
+
const result = established.value;
|
|
1830
1928
|
if (result.created.length > 0 && !this.#established.includes(path)) this.#established.push(path);
|
|
1831
1929
|
return result;
|
|
1832
1930
|
}
|
|
@@ -1870,14 +1968,8 @@ var WriteTransaction = class {
|
|
|
1870
1968
|
const taken = [];
|
|
1871
1969
|
const applied = attempt(() => {
|
|
1872
1970
|
this.#preflight();
|
|
1873
|
-
for (const path of this.#staged)
|
|
1874
|
-
|
|
1875
|
-
promoted.push(path);
|
|
1876
|
-
}
|
|
1877
|
-
for (const path of this.#taken) {
|
|
1878
|
-
this.#take(path);
|
|
1879
|
-
taken.push(path);
|
|
1880
|
-
}
|
|
1971
|
+
for (const path of this.#staged) this.#promote(path, promoted);
|
|
1972
|
+
for (const path of this.#taken) this.#take(path, taken);
|
|
1881
1973
|
});
|
|
1882
1974
|
this.#open = false;
|
|
1883
1975
|
if (!applied.success) {
|
|
@@ -1977,9 +2069,11 @@ var WriteTransaction = class {
|
|
|
1977
2069
|
ancestor: anchor.path
|
|
1978
2070
|
});
|
|
1979
2071
|
mkdirSync(segment);
|
|
2072
|
+
this.#created.push(segment);
|
|
1980
2073
|
const established = readAnchor(segment);
|
|
1981
2074
|
if (established === void 0) throw new ScaffoldError("WRITE", `The directory at ${segment} changed while writing.`, { path: destination });
|
|
1982
2075
|
anchor = established;
|
|
2076
|
+
this.#created[this.#created.length - 1] = established;
|
|
1983
2077
|
created.push(established);
|
|
1984
2078
|
}
|
|
1985
2079
|
return {
|
|
@@ -1988,10 +2082,16 @@ var WriteTransaction = class {
|
|
|
1988
2082
|
};
|
|
1989
2083
|
}
|
|
1990
2084
|
#preflight() {
|
|
1991
|
-
for (const anchor of this.#created)
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2085
|
+
for (const anchor of this.#created) {
|
|
2086
|
+
if (typeof anchor === "string") throw new ScaffoldError("WRITE", `The directory at ${anchor} changed while writing.`, {
|
|
2087
|
+
target: this.#target,
|
|
2088
|
+
path: anchor
|
|
2089
|
+
});
|
|
2090
|
+
if (!matchesAnchor(anchor)) throw new ScaffoldError("WRITE", `The directory at ${anchor.path} changed while writing.`, {
|
|
2091
|
+
target: this.#target,
|
|
2092
|
+
path: anchor.path
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
1995
2095
|
for (const path of [...this.#staged, ...this.#taken]) if (!matchesExpectation(this.#expectation(path))) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, {
|
|
1996
2096
|
target: this.#target,
|
|
1997
2097
|
path
|
|
@@ -2001,24 +2101,26 @@ var WriteTransaction = class {
|
|
|
2001
2101
|
path
|
|
2002
2102
|
});
|
|
2003
2103
|
}
|
|
2004
|
-
#promote(path) {
|
|
2104
|
+
#promote(path, promoted) {
|
|
2005
2105
|
const expectation = this.#expectation(path);
|
|
2006
2106
|
if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2007
2107
|
const staged = this.#resolve(this.#stage, path);
|
|
2008
|
-
if (expectation.shape === "absent") this.#
|
|
2108
|
+
if (expectation.shape === "absent") this.#establish(dirname(expectation.path));
|
|
2009
2109
|
else {
|
|
2010
2110
|
const backup = this.#resolve(this.#backup, path);
|
|
2011
2111
|
mkdirSync(dirname(backup), { recursive: true });
|
|
2012
2112
|
linkSync(expectation.path, backup);
|
|
2013
2113
|
}
|
|
2014
2114
|
renameSync(staged, expectation.path);
|
|
2115
|
+
promoted.push(path);
|
|
2015
2116
|
}
|
|
2016
|
-
#take(path) {
|
|
2117
|
+
#take(path, taken) {
|
|
2017
2118
|
const expectation = this.#expectation(path);
|
|
2018
2119
|
if (!matchesExpectation(expectation)) throw new ScaffoldError("WRITE", `The destination at ${path} moved while writing.`, { path });
|
|
2019
2120
|
const backup = this.#resolve(this.#backup, path);
|
|
2020
2121
|
mkdirSync(dirname(backup), { recursive: true });
|
|
2021
2122
|
renameSync(expectation.path, backup);
|
|
2123
|
+
taken.push(path);
|
|
2022
2124
|
}
|
|
2023
2125
|
#recover(promoted, taken) {
|
|
2024
2126
|
const residue = [];
|
|
@@ -2044,8 +2146,9 @@ var WriteTransaction = class {
|
|
|
2044
2146
|
}));
|
|
2045
2147
|
if (!cleared.success) residue.push(cleared.error);
|
|
2046
2148
|
}
|
|
2047
|
-
for (const
|
|
2048
|
-
const
|
|
2149
|
+
for (const created of [...this.#created].reverse()) {
|
|
2150
|
+
const path = typeof created === "string" ? created : created.path;
|
|
2151
|
+
const removed = attempt(() => rmdirSync(path));
|
|
2049
2152
|
if (!removed.success && !matchesMissingPath(removed.error)) residue.push(removed.error);
|
|
2050
2153
|
}
|
|
2051
2154
|
return residue;
|
|
@@ -2222,10 +2325,13 @@ var Materializer = class Materializer {
|
|
|
2222
2325
|
* The audit is a preview, not an instruction. The plan is hydrated and
|
|
2223
2326
|
* compared against the target again here, and the verdicts that produces must
|
|
2224
2327
|
* match the ones the audit carried for every path the plan owns; anything else
|
|
2225
|
-
* means the target moved, and the whole call is refused.
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2328
|
+
* means the target moved, and the whole call is refused. The audit is checked
|
|
2329
|
+
* for agreement rather than for plausibility, so a verdict the comparison could
|
|
2330
|
+
* not have produced — a birth-owned path reported stale, which the `Finding`
|
|
2331
|
+
* shape admits — disagrees with the derived one and is refused. A missing
|
|
2332
|
+
* destination is restored whatever its ownership; a stale one is replaced only
|
|
2333
|
+
* where the artifact claims its bytes, which is what leaves a presence-owned
|
|
2334
|
+
* file a consumer has edited exactly as it is.
|
|
2229
2335
|
*/
|
|
2230
2336
|
repair(plan, audit, target) {
|
|
2231
2337
|
this.#assertAlive();
|
|
@@ -2598,6 +2704,12 @@ var Materializer = class Materializer {
|
|
|
2598
2704
|
path: finding.path
|
|
2599
2705
|
});
|
|
2600
2706
|
if (other.drift === finding.drift && other.observed === finding.observed) continue;
|
|
2707
|
+
if (!matchesDriftReachability(finding.ownership, other)) throw this.#error("TARGET", `The path ${finding.path} carries an audit verdict this plan could not produce.`, {
|
|
2708
|
+
target,
|
|
2709
|
+
path: finding.path,
|
|
2710
|
+
ownership: finding.ownership,
|
|
2711
|
+
drift: other.drift
|
|
2712
|
+
});
|
|
2601
2713
|
throw this.#error("TARGET", `The path ${finding.path} moved since its audit.`, {
|
|
2602
2714
|
target,
|
|
2603
2715
|
path: finding.path
|
|
@@ -2846,6 +2958,7 @@ var Upstream = class Upstream {
|
|
|
2846
2958
|
static #defaultBudget = 16777216;
|
|
2847
2959
|
static #scope = "orkestrel";
|
|
2848
2960
|
static #unreadable = "the answer carries no readable latest version";
|
|
2961
|
+
static #packument = "application/vnd.npm.install-v1+json";
|
|
2849
2962
|
#emitter;
|
|
2850
2963
|
#guideBase;
|
|
2851
2964
|
#guideBranch;
|
|
@@ -3038,7 +3151,7 @@ var Upstream = class Upstream {
|
|
|
3038
3151
|
return parsed.href.replace(/\/+$/u, "");
|
|
3039
3152
|
}
|
|
3040
3153
|
async #release(dependency, allowance) {
|
|
3041
|
-
const outcome = await this.#read(this.#registryURL(dependency.name), this.#registryTimeout, allowance);
|
|
3154
|
+
const outcome = await this.#read(this.#registryURL(dependency.name), this.#registryTimeout, allowance, Upstream.#packument);
|
|
3042
3155
|
const latest = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
|
|
3043
3156
|
const release = latest === void 0 ? {
|
|
3044
3157
|
name: dependency.name,
|
|
@@ -3075,7 +3188,7 @@ var Upstream = class Upstream {
|
|
|
3075
3188
|
return mirror;
|
|
3076
3189
|
}
|
|
3077
3190
|
async #entry(name, allowance) {
|
|
3078
|
-
const outcome = await this.#read(this.#registryURL(name), this.#registryTimeout, allowance);
|
|
3191
|
+
const outcome = await this.#read(this.#registryURL(name), this.#registryTimeout, allowance, Upstream.#packument);
|
|
3079
3192
|
const version = outcome.lookup === "found" ? this.#latest(outcome.content) : void 0;
|
|
3080
3193
|
if (version !== void 0) return {
|
|
3081
3194
|
name,
|
|
@@ -3119,11 +3232,11 @@ var Upstream = class Upstream {
|
|
|
3119
3232
|
const repository = encodeURIComponent(name.slice(name.lastIndexOf("/") + 1));
|
|
3120
3233
|
return `${this.#guideBase}/${Upstream.#scope}/${repository}/refs/heads/${branch}/${nameToGuide(name)}`;
|
|
3121
3234
|
}
|
|
3122
|
-
async #read(url, timeout, allowance) {
|
|
3235
|
+
async #read(url, timeout, allowance, accept) {
|
|
3123
3236
|
let note = "";
|
|
3124
3237
|
for (let attempt = 0; attempt <= this.#retries; attempt += 1) {
|
|
3125
3238
|
this.#assertAlive();
|
|
3126
|
-
const outcome = await this.#request(url, timeout, allowance);
|
|
3239
|
+
const outcome = await this.#request(url, timeout, allowance, accept);
|
|
3127
3240
|
if (outcome.lookup !== "failed") return outcome;
|
|
3128
3241
|
note = outcome.note;
|
|
3129
3242
|
}
|
|
@@ -3137,7 +3250,7 @@ var Upstream = class Upstream {
|
|
|
3137
3250
|
note
|
|
3138
3251
|
};
|
|
3139
3252
|
}
|
|
3140
|
-
async #request(url, timeout, allowance) {
|
|
3253
|
+
async #request(url, timeout, allowance, accept) {
|
|
3141
3254
|
if (allowance.remaining <= 0) return {
|
|
3142
3255
|
lookup: "failed",
|
|
3143
3256
|
content: "",
|
|
@@ -3146,7 +3259,8 @@ var Upstream = class Upstream {
|
|
|
3146
3259
|
try {
|
|
3147
3260
|
const response = await fetch(url, {
|
|
3148
3261
|
signal: AbortSignal.any([this.#controller.signal, AbortSignal.timeout(timeout)]),
|
|
3149
|
-
redirect: "manual"
|
|
3262
|
+
redirect: "manual",
|
|
3263
|
+
...accept === void 0 ? {} : { headers: { accept } }
|
|
3150
3264
|
});
|
|
3151
3265
|
if (response.status === 404) {
|
|
3152
3266
|
await response.body?.cancel();
|
|
@@ -3183,21 +3297,11 @@ var Upstream = class Upstream {
|
|
|
3183
3297
|
}
|
|
3184
3298
|
}
|
|
3185
3299
|
async #body(response, allowance) {
|
|
3186
|
-
const declared = response.headers.get("content-length");
|
|
3187
|
-
const stated = declared === null ? void 0 : Number(declared);
|
|
3188
|
-
if (stated !== void 0 && Number.isFinite(stated) && (stated > this.#limit || stated > allowance.remaining)) {
|
|
3189
|
-
await response.body?.cancel();
|
|
3190
|
-
return {
|
|
3191
|
-
lookup: "failed",
|
|
3192
|
-
content: "",
|
|
3193
|
-
note: `the response declares ${String(stated)} bytes, past the ${String(Math.min(this.#limit, allowance.remaining))}-byte allowance`
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
3300
|
const body = response.body;
|
|
3197
3301
|
if (body === null) return {
|
|
3198
|
-
lookup: "
|
|
3302
|
+
lookup: "failed",
|
|
3199
3303
|
content: "",
|
|
3200
|
-
note:
|
|
3304
|
+
note: `HTTP ${String(response.status)}, and the answer carries no body`
|
|
3201
3305
|
};
|
|
3202
3306
|
const reader = body.getReader();
|
|
3203
3307
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -3332,6 +3436,6 @@ function createUpstream(options) {
|
|
|
3332
3436
|
return new Upstream(options);
|
|
3333
3437
|
}
|
|
3334
3438
|
//#endregion
|
|
3335
|
-
export { BRANCH_PATTERN, 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, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, createMaterializer, createUpstream, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHostManifest, isInventory, isManifestEntry, isMaterializerHooks, isMaterializerOptions, isMirrors, isPhysicalDirectory, isPhysicalFile, isRepository, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, listDirectories, listFiles, matchesAnchor, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageHost };
|
|
3439
|
+
export { BRANCH_PATTERN, 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, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, createMaterializer, createUpstream, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHostManifest, isInventory, isManifestEntry, isMaterializerHooks, isMaterializerOptions, isMirrors, isPhysicalDirectory, isPhysicalFile, isRepository, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageHost };
|
|
3336
3440
|
|
|
3337
3441
|
//# sourceMappingURL=index.js.map
|