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