@orkestrel/scaffold 0.0.23 → 0.0.25
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 +84 -99
- package/dist/bin/main.js +1094 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/host/CLAUDE.md +3 -1
- package/dist/host/agents/orchestration.md +61 -4
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/rules/architecture.md +45 -3
- package/dist/host/claude/rules/quality.md +4 -0
- package/dist/host/claude/rules/tests.md +57 -1
- package/dist/host/claude/rules/workspace.md +50 -17
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/configs/helpers.ts +762 -0
- package/dist/host/dotfiles/oxlintrc.json +2 -1
- package/dist/host/guides/scaffold.md +862 -0
- package/dist/host/manifest.json +40 -33
- package/dist/host/tests/config.test.ts +544 -0
- package/dist/host/tests/policy.test.ts +46 -0
- package/dist/host/tests/setupPolicy.ts +529 -701
- package/dist/src/core/index.cjs +3568 -10576
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2361 -2800
- package/dist/src/core/index.d.ts +2361 -2800
- package/dist/src/core/index.js +3512 -10440
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2855 -3765
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1915 -1330
- package/dist/src/server/index.d.ts +1915 -1330
- package/dist/src/server/index.js +2812 -3680
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -23
- package/dist/bin/scaffold.js +0 -1896
- package/dist/bin/scaffold.js.map +0 -1
- package/dist/host/guides/src/scaffold.md +0 -2922
- /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
|
@@ -1,1382 +1,1967 @@
|
|
|
1
1
|
import { Audit } from '@orkestrel/scaffold';
|
|
2
|
-
import { Blueprint } from '@orkestrel/scaffold';
|
|
3
|
-
import { BooleanShape } from '@orkestrel/contract';
|
|
4
2
|
import { CatalogEntry } from '@orkestrel/scaffold';
|
|
5
|
-
import { ContractInterface } from '@orkestrel/contract';
|
|
6
3
|
import { Dependency } from '@orkestrel/scaffold';
|
|
7
4
|
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
8
5
|
import { EmitterHooks } from '@orkestrel/emitter';
|
|
9
6
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { NumberShape } from '@orkestrel/contract';
|
|
13
|
-
import { ObjectShape } from '@orkestrel/contract';
|
|
14
|
-
import { OptionalShape } from '@orkestrel/contract';
|
|
7
|
+
import { Guard } from '@orkestrel/contract';
|
|
8
|
+
import { Mirror } from '@orkestrel/scaffold';
|
|
15
9
|
import { Plan } from '@orkestrel/scaffold';
|
|
16
|
-
import {
|
|
10
|
+
import { Release } from '@orkestrel/scaffold';
|
|
17
11
|
import { Snapshot } from '@orkestrel/scaffold';
|
|
18
|
-
import { StringShape } from '@orkestrel/contract';
|
|
19
|
-
import { SyncReport } from '@orkestrel/scaffold';
|
|
20
|
-
import { VersionSync } from '@orkestrel/scaffold';
|
|
21
|
-
|
|
22
|
-
/** One mutable aggregate entry allowance shared by fleet catalog roots. */
|
|
23
|
-
export declare type CatalogAllowance = Float64Array;
|
|
24
12
|
|
|
25
13
|
/**
|
|
26
|
-
*
|
|
27
|
-
* package discovered under each root, its description drawn from its own
|
|
28
|
-
* guide's FIRST blockquote.
|
|
14
|
+
* The Git branch syntax the guide endpoint accepts.
|
|
29
15
|
*
|
|
30
|
-
* @param roots - The fleet root directories to scan (each walked via `discoverPackages`).
|
|
31
16
|
* @remarks
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* TOP-LEVEL children in its `guides/src/<short>.md` (`<short>` = `name` with
|
|
37
|
-
* the `@orkestrel/` prefix stripped), parsed with `@orkestrel/markdown`'s
|
|
38
|
-
* `parseDocument` — a multi-paragraph blockquote overview yields only its
|
|
39
|
-
* FIRST paragraph, never the whole quote glued together; embedded newlines
|
|
40
|
-
* collapse to single spaces, and surrounding whitespace trims. A
|
|
41
|
-
* missing/unreadable guide, a guide carrying no blockquote, or a blockquote
|
|
42
|
-
* with no top-level paragraph child, yields `description: ''`, never a
|
|
43
|
-
* thrown error. Entries merge across `roots` (a later root's entry for a
|
|
44
|
-
* repeated `name` wins), then code-unit sort by `name`. An unreadable ROOT
|
|
45
|
-
* itself is NOT wrapped here — whatever `discoverPackages` throws for it
|
|
46
|
-
* propagates as-is; the bin layer is responsible for coding that failure
|
|
47
|
-
* `TARGET`.
|
|
48
|
-
* @returns The merged, sorted `CatalogEntry[]`.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```ts
|
|
52
|
-
* import { catalogPackages } from '@orkestrel/scaffold/server'
|
|
53
|
-
*
|
|
54
|
-
* catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
|
|
55
|
-
* ```
|
|
17
|
+
* A branch is caller-supplied and reaches a URL path, so it is closed to
|
|
18
|
+
* alphanumerics, dot, underscore, hyphen, and the separator, must open with an
|
|
19
|
+
* alphanumeric, and may carry no `..` anywhere. That last refusal is what stops
|
|
20
|
+
* a branch from walking out of the guide directory it addresses.
|
|
56
21
|
*/
|
|
57
|
-
export declare
|
|
22
|
+
export declare const BRANCH_PATTERN: RegExp;
|
|
58
23
|
|
|
59
24
|
/**
|
|
60
|
-
*
|
|
61
|
-
* any later promotion fails.
|
|
62
|
-
*
|
|
63
|
-
* @param transaction - Same-volume sibling staging state.
|
|
64
|
-
* @param paths - Portable target-relative files to promote.
|
|
65
|
-
* @throws `ScaffoldError('WRITE', ...)` with recovery details on failure.
|
|
66
|
-
*/
|
|
67
|
-
export declare function commitWriteTransaction(transaction: WriteTransaction, paths: readonly string[]): void;
|
|
68
|
-
|
|
69
|
-
/** Consume one aggregate fleet-catalog traversal slot. */
|
|
70
|
-
export declare function consumeCatalogAllowance(allowance: CatalogAllowance, root: string): void;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Create a `MaterializerInterface` (server) — the materialization entity,
|
|
74
|
-
* seeded from `MaterializerOptions`.
|
|
75
|
-
*
|
|
76
|
-
* @param options - Optional `host` root override, emitter hooks, and error handler
|
|
77
|
-
* @returns A {@link MaterializerInterface}
|
|
25
|
+
* Compute the SHA-256 digest of text.
|
|
78
26
|
*
|
|
79
|
-
* @
|
|
80
|
-
*
|
|
81
|
-
* import { createMaterializer } from '@orkestrel/scaffold/server'
|
|
82
|
-
*
|
|
83
|
-
* const materializer = createMaterializer()
|
|
84
|
-
* materializer.destroy()
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Create a `SyncInterface` (server) — the upstream-synchronization entity,
|
|
91
|
-
* seeded from `SyncOptions`.
|
|
27
|
+
* @param content - The text to digest.
|
|
28
|
+
* @returns Sixty-four lowercase hexadecimal digits.
|
|
92
29
|
*
|
|
93
|
-
* @
|
|
94
|
-
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* The identity the server face states, and the reason it differs from core's:
|
|
32
|
+
* core settled on a folded 64-bit identity because compilation is synchronous by
|
|
33
|
+
* contract and the only cryptographic digest a host-independent scope reaches is
|
|
34
|
+
* asynchronous. A Node host reaches the real one synchronously, and
|
|
35
|
+
* `HostManifest.digest` is documented as SHA-256, so this is what the server
|
|
36
|
+
* uses everywhere a digest is claimed.
|
|
95
37
|
*
|
|
96
38
|
* @example
|
|
97
39
|
* ```ts
|
|
98
|
-
* import {
|
|
40
|
+
* import { computeDigest } from '@orkestrel/scaffold/server'
|
|
99
41
|
*
|
|
100
|
-
*
|
|
101
|
-
* sync.destroy()
|
|
42
|
+
* computeDigest('hi\n') // '98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4'
|
|
102
43
|
* ```
|
|
103
44
|
*/
|
|
104
|
-
export declare function
|
|
45
|
+
export declare function computeDigest(content: string): string;
|
|
105
46
|
|
|
106
47
|
/**
|
|
107
|
-
*
|
|
48
|
+
* Compute the SHA-256 digest of one file's exact bytes.
|
|
108
49
|
*
|
|
109
|
-
* @param path - The
|
|
110
|
-
* @
|
|
111
|
-
*
|
|
112
|
-
*/
|
|
113
|
-
export declare function createWriteDirectory(path: string, boundary: string): WriteDirectoryResult;
|
|
114
|
-
|
|
115
|
-
/** Default cumulative response bytes retained by one Sync operation. */
|
|
116
|
-
export declare const DEFAULT_SYNC_BUDGET = 16777216;
|
|
117
|
-
|
|
118
|
-
/** Default simultaneous upstream requests used by `Sync`. */
|
|
119
|
-
export declare const DEFAULT_SYNC_CONCURRENCY = 6;
|
|
120
|
-
|
|
121
|
-
/** Default maximum dependencies or catalog entries retained by one Sync operation. */
|
|
122
|
-
export declare const DEFAULT_SYNC_ITEMS = 256;
|
|
123
|
-
|
|
124
|
-
/** Default per-response byte limit used by `Sync`. */
|
|
125
|
-
export declare const DEFAULT_SYNC_LIMIT = 5242880;
|
|
126
|
-
|
|
127
|
-
/** Default per-request timeout used by `Sync`, in milliseconds. */
|
|
128
|
-
export declare const DEFAULT_SYNC_TIMEOUT = 10000;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
|
|
132
|
-
* inverse `audit` / `repair` / `mirror` need to diff a live package against
|
|
133
|
-
* its own would-be scaffold, rather than a fresh, dependency-less stand-in.
|
|
50
|
+
* @param path - The resolved host path to digest.
|
|
51
|
+
* @returns The digest, or `undefined` when the path is not a physical file, is
|
|
52
|
+
* past the artifact ceiling, or moved while it was being read.
|
|
134
53
|
*
|
|
135
|
-
* @param target - The existing package directory to derive a `Blueprint` from.
|
|
136
54
|
* @remarks
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* and the
|
|
140
|
-
*
|
|
141
|
-
* `app/<environment>/`; a target with no environment on either axis is also a coded
|
|
142
|
-
* `TARGET` failure. The `bin` and `integration` facts probe physical
|
|
143
|
-
* directories at `src/bin` and `tests/integration`. `services` is the sorted
|
|
144
|
-
* set of direct vendor directories under `tests/service` that contain a test
|
|
145
|
-
* at any depth, while `global` probes the physical exact-case `tests/setupGlobal.ts` file and `showcase`
|
|
146
|
-
* probes the physical exact-case `configs/app/vite.showcase.config.ts` regular file; none is
|
|
147
|
-
* inferred from the workspace name. `dependencies` /
|
|
148
|
-
* `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
|
|
149
|
-
* `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
|
|
150
|
-
* .optional === true` carries `optional: true`). `extras` is EVERY entry of
|
|
151
|
-
* `manifest.devDependencies` (not only `@orkestrel/`-prefixed ones — an
|
|
152
|
-
* external extra like `zod` must round-trip too), EXCLUDING the complete
|
|
153
|
-
* generated dependency set `devDependenciesFor` emits for the reconstructed
|
|
154
|
-
* blueprint's actual environments and structural axes. A devDependency ALSO present in
|
|
155
|
-
* `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
|
|
156
|
-
* dev-installed for local testing) is likewise excluded from `extras` — it
|
|
157
|
-
* already appears as a `peer`/`dependency` above, and double-counting it as
|
|
158
|
-
* an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
|
|
159
|
-
* gate. `overrides` is always `[]` — derivation cannot recover a caller's
|
|
160
|
-
* artifact-override intent from repository state.
|
|
161
|
-
* @returns The reconstructed `Blueprint`.
|
|
162
|
-
* @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
|
|
163
|
-
* (via `readManifest`), is not valid JSON, its name is unsafe for its
|
|
164
|
-
* publication mode, or `target` carries no source or application environment.
|
|
165
|
-
* @throws `ScaffoldError('INVALID', …)` when service tests use the retired flat
|
|
166
|
-
* layout, a vendor directory has no test, or a service companion is missing.
|
|
55
|
+
* Read in bounded chunks rather than loaded whole, so digesting a large file
|
|
56
|
+
* costs one buffer instead of its size. The file's identity and size are
|
|
57
|
+
* measured before and after the read and a mismatch answers `undefined`, so a
|
|
58
|
+
* digest is either of one settled file or is not produced at all.
|
|
167
59
|
*
|
|
168
60
|
* @example
|
|
169
61
|
* ```ts
|
|
170
|
-
* import {
|
|
62
|
+
* import { computeFileDigest } from '@orkestrel/scaffold/server'
|
|
171
63
|
*
|
|
172
|
-
*
|
|
64
|
+
* computeFileDigest('/tmp/project/AGENTS.md') // the file's SHA-256
|
|
65
|
+
* computeFileDigest('/tmp/project/absent.md') // undefined
|
|
173
66
|
* ```
|
|
174
67
|
*/
|
|
175
|
-
export declare function
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Compute a bounded-memory SHA-256 digest for one file.
|
|
179
|
-
*
|
|
180
|
-
* @param path - The file to read.
|
|
181
|
-
* @returns Its lowercase SHA-256 digest.
|
|
182
|
-
*/
|
|
183
|
-
export declare function digestFile(path: string): string;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Compute SHA-256 from exact lowercase hexadecimal bytes without decoding the whole value at once.
|
|
187
|
-
*
|
|
188
|
-
* @param hex - Exact hexadecimal bytes.
|
|
189
|
-
* @returns Their lowercase SHA-256 digest.
|
|
190
|
-
*/
|
|
191
|
-
export declare function digestHex(hex: string): string;
|
|
68
|
+
export declare function computeFileDigest(path: string): string | undefined;
|
|
192
69
|
|
|
193
70
|
/**
|
|
194
|
-
*
|
|
71
|
+
* Compute the digest of a vendored host's declared membership.
|
|
195
72
|
*
|
|
196
73
|
* @param entries - The ordered file membership declarations.
|
|
197
74
|
* @param roots - The ordered directory membership declarations.
|
|
198
|
-
* @returns
|
|
199
|
-
*/
|
|
200
|
-
export declare function digestHostManifest(entries: readonly ManifestEntry[], roots: readonly string[]): string;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Compute SHA-256 from UTF-8 text.
|
|
204
|
-
*
|
|
205
|
-
* @param value - The text to hash.
|
|
206
|
-
* @returns Its lowercase SHA-256 digest.
|
|
207
|
-
*/
|
|
208
|
-
export declare function digestText(value: string): string;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Remove an uncommitted or already-committed write transaction's private residue.
|
|
212
|
-
*
|
|
213
|
-
* @param transaction - The transaction to discard.
|
|
214
|
-
* @throws `ScaffoldError('WRITE', ...)` when its private root cannot be removed.
|
|
215
|
-
*/
|
|
216
|
-
export declare function discardWriteTransaction(transaction: WriteTransaction): void;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* List a fleet root's `@orkestrel/*` package directories.
|
|
220
|
-
*
|
|
221
|
-
* @param root - The fleet root directory to scan.
|
|
222
|
-
* @returns Absolute, code-unit-sorted paths of `root`'s immediate child
|
|
223
|
-
* directories whose `package.json` parses and whose `name` starts with
|
|
224
|
-
* `@orkestrel/`. A child with an unreadable or unparsable `package.json`,
|
|
225
|
-
* or a non-`@orkestrel` name, is skipped silently — it simply is not a
|
|
226
|
-
* fleet member.
|
|
227
|
-
* @throws `ScaffoldError('TARGET', …)` when a child directory name contains
|
|
228
|
-
* terminal controls and therefore cannot be safely inspected or reported.
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
* ```ts
|
|
232
|
-
* import { discoverPackages } from '@orkestrel/scaffold/server'
|
|
233
|
-
*
|
|
234
|
-
* discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
|
|
235
|
-
* ```
|
|
236
|
-
*/
|
|
237
|
-
export declare function discoverPackages(root: string, allowance?: CatalogAllowance): readonly string[];
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Render the local pointer written when an upstream dependency guide is not vendored yet.
|
|
241
|
-
*
|
|
242
|
-
* @param source - The canonical `guides/src/<name>.md` source path.
|
|
243
|
-
* @returns The deterministic pointer content.
|
|
244
|
-
*/
|
|
245
|
-
export declare function guideStub(source: string): string;
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Extract the first paragraph from the first blockquote in a Markdown guide.
|
|
249
|
-
*
|
|
250
|
-
* @param text - The guide Markdown to traverse.
|
|
251
|
-
* @returns A normalized one-line description, or `undefined` when parsing
|
|
252
|
-
* fails, no blockquote paragraph exists, or the paragraph is empty.
|
|
253
|
-
*
|
|
254
|
-
* @example
|
|
255
|
-
* ```ts
|
|
256
|
-
* import { guideToDescription } from '@orkestrel/scaffold/server'
|
|
257
|
-
*
|
|
258
|
-
* guideToDescription('> A concise package description.\n>\n> More detail.')
|
|
259
|
-
* // 'A concise package description.'
|
|
260
|
-
* ```
|
|
261
|
-
*/
|
|
262
|
-
export declare function guideToDescription(text: string): string | undefined;
|
|
263
|
-
|
|
264
|
-
/** One validated guide update and its contained destination. */
|
|
265
|
-
export declare interface GuideWrite {
|
|
266
|
-
readonly guide: GuideSync;
|
|
267
|
-
readonly destination: string;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Reserved metadata file written at the root of every staged host. */
|
|
271
|
-
export declare const HOST_MANIFEST_PATH = "manifest.json";
|
|
272
|
-
|
|
273
|
-
/** A complete vendored-host inventory with file entries and declared directory roots. */
|
|
274
|
-
export declare interface HostManifest {
|
|
275
|
-
readonly entries: readonly ManifestEntry[];
|
|
276
|
-
readonly roots: readonly string[];
|
|
277
|
-
/** SHA-256 of the exact declared entry/root membership. */
|
|
278
|
-
readonly digest: string;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Locate this MODULE's own installed package root — the nearest ancestor of
|
|
283
|
-
* `import.meta.url` holding a `package.json` — and return its vendored
|
|
284
|
-
* `dist/host` data root. THE single source of truth for the default
|
|
285
|
-
* `Materializer` / `scaffold` bin host: once installed, walking up from the
|
|
286
|
-
* module's own file (not `process.cwd()`, which points at whichever project
|
|
287
|
-
* happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
|
|
288
|
-
* correct default host — the package ships its vendored data with itself.
|
|
289
|
-
* `dist/host` may not exist yet when this resolves from SOURCE under a test
|
|
290
|
-
* runner; that is fine — existence is checked at the point of use, not here.
|
|
291
|
-
*
|
|
292
|
-
* @returns The absolute vendored `dist/host` path.
|
|
293
|
-
* @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
|
|
294
|
-
* location holds a `package.json`.
|
|
295
|
-
*
|
|
296
|
-
* @example
|
|
297
|
-
* ```ts
|
|
298
|
-
* import { hostRoot } from '@orkestrel/scaffold/server'
|
|
299
|
-
*
|
|
300
|
-
* hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
|
|
301
|
-
* ```
|
|
302
|
-
*/
|
|
303
|
-
export declare function hostRoot(): string;
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Rehydrate a `Plan`'s `host`-origin artifacts with their exact byte hex read
|
|
307
|
-
* from `host` — manifest-aware, via `locateHostSource`.
|
|
308
|
-
*
|
|
309
|
-
* @param plan - The plan to hydrate.
|
|
310
|
-
* @param host - The resolved host root to read from.
|
|
311
|
-
* @returns A new `Plan` whose file-shaped `host` artifacts carry exact `hex`.
|
|
312
|
-
* Directory-shaped host artifacts expand into one byte-aware artifact
|
|
313
|
-
* per file, preserving their group and mapping the artifact/source prefixes.
|
|
314
|
-
* `template` / `computed` artifacts pass through untouched.
|
|
315
|
-
* @throws `ScaffoldError('TARGET', …)` when the host is not a readable
|
|
316
|
-
* directory, a present manifest is invalid/incomplete, or a required source
|
|
317
|
-
* is absent, escaping, not a file/directory as declared, or unreadable.
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```ts
|
|
321
|
-
* import { hydratePlan } from '@orkestrel/scaffold/server'
|
|
322
|
-
*
|
|
323
|
-
* const hydrated = hydratePlan(plan, './dist/host')
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
export declare function hydratePlan(plan: Plan, host: string): Plan;
|
|
327
|
-
|
|
328
|
-
/** Narrow one exact aggregate fleet traversal allowance. */
|
|
329
|
-
export declare function isCatalogAllowance(value: unknown): value is CatalogAllowance;
|
|
330
|
-
|
|
331
|
-
/** Whether a normalized catalog description is bounded and contains no controls. */
|
|
332
|
-
export declare function isCatalogDescription(value: unknown): value is string;
|
|
333
|
-
|
|
334
|
-
/** Whether one dependency is an exact data-property record safe to snapshot. */
|
|
335
|
-
export declare function isDependencyData(value: unknown): value is Dependency;
|
|
336
|
-
|
|
337
|
-
/** Whether one host filesystem path is bounded, non-empty, and safe to render in a terminal. */
|
|
338
|
-
export declare function isFilesystemPath(value: unknown): value is string;
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Narrow a value to one exact complete vendored-host manifest.
|
|
342
|
-
*
|
|
343
|
-
* @param value - The candidate manifest value.
|
|
344
|
-
* @returns `true` only for an exact `{ entries, roots, digest }` record with safe paths.
|
|
345
|
-
*/
|
|
346
|
-
export declare function isHostManifest(value: unknown): value is HostManifest;
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Narrow a value to one exact vendored-host manifest entry.
|
|
350
|
-
*
|
|
351
|
-
* @param value - The candidate raw manifest entry.
|
|
352
|
-
* @returns `true` only for the exact safe entry shape.
|
|
353
|
-
*/
|
|
354
|
-
export declare function isManifestEntry(value: unknown): value is ManifestEntry;
|
|
355
|
-
|
|
356
|
-
/** Narrow an exact initial-listener record for `Materializer`. */
|
|
357
|
-
export declare function isMaterializerEventHooks(value: unknown): value is EmitterHooks<MaterializerEventMap>;
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Determine whether a caught filesystem error reports an absent path.
|
|
361
|
-
*
|
|
362
|
-
* @param value - The caught value.
|
|
363
|
-
* @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
|
|
364
|
-
*/
|
|
365
|
-
export declare function isMissingPathError(value: unknown): boolean;
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Determine whether a value is a non-empty portable relative POSIX path.
|
|
369
|
-
*
|
|
370
|
-
* @param value - The candidate path.
|
|
371
|
-
* @returns `true` when every path segment is safe and portable.
|
|
372
|
-
*/
|
|
373
|
-
export declare function isPortablePath(value: unknown): value is string;
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Whether a path is an existing physical directory rather than a file or link.
|
|
377
|
-
*
|
|
378
|
-
* @param path - The filesystem path to inspect without following links.
|
|
379
|
-
* @returns `true` only for a successful `lstat` reporting a directory.
|
|
380
|
-
*/
|
|
381
|
-
export declare function isRealDirectory(path: string): boolean;
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Whether a path is an existing physical file rather than a directory or link.
|
|
385
|
-
*
|
|
386
|
-
* @param path - The filesystem path to inspect without following links.
|
|
387
|
-
* @returns `true` only for a successful `lstat` reporting a single-link file.
|
|
388
|
-
*/
|
|
389
|
-
export declare function isRealFile(path: string): boolean;
|
|
390
|
-
|
|
391
|
-
/** Determine whether a target-relative path addresses preserved repository metadata. */
|
|
392
|
-
export declare function isReservedTargetPath(value: string): boolean;
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Determine whether a host-relative path resembles local configuration or credentials.
|
|
396
|
-
*
|
|
397
|
-
* @param value - The portable candidate path.
|
|
398
|
-
* @returns `true` when the path must be excluded from vendored host output.
|
|
399
|
-
*/
|
|
400
|
-
export declare function isSensitiveHostPath(value: string): boolean;
|
|
401
|
-
|
|
402
|
-
/** Narrow an exact initial-listener record for `Sync`. */
|
|
403
|
-
export declare function isSyncEventHooks(value: unknown): value is EmitterHooks<SyncEventMap>;
|
|
404
|
-
|
|
405
|
-
/** Whether one externally supplied string is safe to render in terminal or JSON diagnostics. */
|
|
406
|
-
export declare function isTerminalText(value: unknown): value is string;
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Whether a target path is absent, empty, or contains nothing but a `.git`
|
|
410
|
-
* directory — the green-field target law `Materializer.materialize` enforces.
|
|
411
|
-
*
|
|
412
|
-
* @param target - The candidate target directory path.
|
|
413
|
-
* @returns `true` when `target` is safe to materialize a fresh package into.
|
|
414
|
-
*
|
|
415
|
-
* @example
|
|
416
|
-
* ```ts
|
|
417
|
-
* import { isVacant } from '@orkestrel/scaffold/server'
|
|
418
|
-
*
|
|
419
|
-
* isVacant('./packages/router-new') // true — absent, empty, or only a .git dir
|
|
420
|
-
* ```
|
|
421
|
-
*/
|
|
422
|
-
export declare function isVacant(target: string): boolean;
|
|
423
|
-
|
|
424
|
-
/** Narrow one exact transaction destination precondition. */
|
|
425
|
-
export declare function isWritePrecondition(value: unknown): value is WritePrecondition;
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Recursively list a directory's descendant directories.
|
|
429
|
-
*
|
|
430
|
-
* @param root - The directory to list.
|
|
431
|
-
* @returns Root-relative POSIX directory paths in code-unit order.
|
|
432
|
-
*/
|
|
433
|
-
export declare function listDirectories(root: string): readonly string[];
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Recursively list a directory's files as root-relative paths.
|
|
437
|
-
*
|
|
438
|
-
* @param root - The directory to list.
|
|
439
|
-
* @returns Root-relative file paths (posix-style `/` separators), code-unit
|
|
440
|
-
* sorted, or `[]` when `root` is absent.
|
|
441
|
-
*
|
|
442
|
-
* @example
|
|
443
|
-
* ```ts
|
|
444
|
-
* import { listFiles } from '@orkestrel/scaffold/server'
|
|
445
|
-
*
|
|
446
|
-
* listFiles('./dist/host/.claude/agents') // ['scout.md', 'builder.md', …]
|
|
447
|
-
* ```
|
|
448
|
-
*/
|
|
449
|
-
export declare function listFiles(root: string): readonly string[];
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Resolve the absolute host-storage path for a host-origin artifact's
|
|
453
|
-
* `source`, manifest-aware.
|
|
454
|
-
*
|
|
455
|
-
* @param manifest - The parsed complete host manifest, or `undefined`
|
|
456
|
-
* when the host carries none (raw-repo-root fallback).
|
|
457
|
-
* @param source - The artifact's `source` (or `path`) to resolve.
|
|
458
|
-
* @param host - The resolved host root the path is joined against.
|
|
459
|
-
* @returns `join(host, source)` when `manifest` is `undefined` (no vendored
|
|
460
|
-
* staging indirection); when `manifest` is present, `join(host,
|
|
461
|
-
* entries[0].storage)` for the SINGLE manifest entry whose `destination`
|
|
462
|
-
* equals `source`, or `undefined` when zero or more than one entry matches
|
|
463
|
-
* (`source` names a directory, or the manifest is ambiguous — no single
|
|
464
|
-
* storage file to point at).
|
|
465
|
-
*
|
|
466
|
-
* @example
|
|
467
|
-
* ```ts
|
|
468
|
-
* import { locateHostSource } from '@orkestrel/scaffold/server'
|
|
469
|
-
*
|
|
470
|
-
* locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
|
|
471
|
-
* locateHostSource(
|
|
472
|
-
* {
|
|
473
|
-
* entries: [{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }],
|
|
474
|
-
* roots: [],
|
|
475
|
-
* digest: '3b52f19450237ea4cfb39bc7a77b1e5094b52a8ff1fc9c02fd68b13ad088aa0f',
|
|
476
|
-
* },
|
|
477
|
-
* 'package.json',
|
|
478
|
-
* './dist/host',
|
|
479
|
-
* )
|
|
480
|
-
* // './dist/host/pkg.tmpl'
|
|
481
|
-
* ```
|
|
482
|
-
*/
|
|
483
|
-
export declare function locateHostSource(manifest: HostManifest | undefined, source: string, host: string): string | undefined;
|
|
484
|
-
|
|
485
|
-
/** One entry of the vendored host's `manifest.json` (server). */
|
|
486
|
-
export declare interface ManifestEntry {
|
|
487
|
-
readonly storage: string;
|
|
488
|
-
readonly destination: string;
|
|
489
|
-
readonly executable: boolean;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* The materialization entity (server) — the only impure environment in the
|
|
494
|
-
* package, writing a `Plan` to `node:fs` behind an explicit call.
|
|
495
|
-
*
|
|
496
|
-
* @remarks
|
|
497
|
-
* `materialize` is green-field: it refuses any target `isVacant` rejects
|
|
498
|
-
* (`ScaffoldError('TARGET', …)`), then byte-copies each `host` artifact from
|
|
499
|
-
* the `host` root and writes each `template` / `computed` artifact's rendered
|
|
500
|
-
* `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
|
|
501
|
-
* `repair` is into-existing: it skips the vacancy check and writes `missing`
|
|
502
|
-
* artifacts an `Audit` names. `stale` artifacts are report-only unless its
|
|
503
|
-
* `replace` switch is true, because byte replacement discards local changes.
|
|
504
|
-
* Hydrated directory-shaped host entries are expanded into
|
|
505
|
-
* file-shaped artifacts, so canonical skills and agent configuration are
|
|
506
|
-
* audited and repaired file by file. `prune` deletes stale files under
|
|
507
|
-
* `target/.claude/agents/`, `target/.codex/agents/`, and `target/scripts/`
|
|
508
|
-
* that the vendored `host` does not name. After `destroy()`
|
|
509
|
-
* every method throws `DESTROYED`; teardown is idempotent, emitter last.
|
|
510
|
-
* Before the vacancy check or any write, `materialize` and `repair` require a
|
|
511
|
-
* structural `isPlan` match and a valid `validatePlan` result. Contextual
|
|
512
|
-
* overrides that are missing from the artifact set, host-owned, or target
|
|
513
|
-
* `package.json` therefore fail closed instead of being silently ignored.
|
|
514
|
-
*
|
|
515
|
-
* @remarks
|
|
516
|
-
* `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
|
|
517
|
-
* carries a `manifest.json` (this package's own vendored `dist/host`), each
|
|
518
|
-
* artifact's `source` (a destination-relative path) is looked up in the
|
|
519
|
-
* manifest to find its un-dotted STORAGE path plus an `executable` bit
|
|
520
|
-
* (applied via `chmodSync` after the copy) — the vendored-package shape,
|
|
521
|
-
* where storage names avoid leading dots npm would otherwise mangle. When
|
|
522
|
-
* `host` carries no `manifest.json` (a caller-supplied raw repo root, e.g. a
|
|
523
|
-
* sibling checkout or a test fixture), `source` maps to `host` 1:1, exactly
|
|
524
|
-
* as before.
|
|
525
|
-
*
|
|
526
|
-
* @remarks
|
|
527
|
-
* A manifest-present `source` with ZERO matching entries degrades to a stub
|
|
528
|
-
* ONLY when that `source` is a dependency-guide pointer — one that starts
|
|
529
|
-
* with `guides/src/` and ends with `.md` (the `guides/src/<dep>.md` pointer
|
|
530
|
-
* `Compiler` emits for any dependency outside this package's vendored set).
|
|
531
|
-
* That is the ONLY zero-match case that is legitimate: every `HOST_PATHS`
|
|
532
|
-
* source is always staged by `stageHost`, so a non-guide zero-match means a
|
|
533
|
-
* corrupted or truncated `manifest.json`, not an intentionally-unvendored
|
|
534
|
-
* artifact. For a guide pointer, a short stub file is written at the
|
|
535
|
-
* destination and reported exactly like a successful copy. The READ path
|
|
536
|
-
* leaves that intentional pointer without canonical bytes, so `diffPlan`
|
|
537
|
-
* treats it as presence-owned rather than inventing a byte match.
|
|
538
|
-
* For every OTHER zero-match, the
|
|
539
|
-
* fail-closed `ScaffoldError('TARGET', …)` is thrown — degrading an
|
|
540
|
-
* unscoped zero-match would otherwise let a corrupted manifest silently stub
|
|
541
|
-
* an unrecoverable artifact (e.g. `AGENTS.md` — `pull` only ever fetches
|
|
542
|
-
* dependency guides) or write a FILE named `.claude` over what should be a
|
|
543
|
-
* directory artifact. The raw-root fallback (`manifest === undefined`, a
|
|
544
|
-
* caller-supplied `--from`) keeps its own throw regardless: an EXPLICITLY
|
|
545
|
-
* named source failing to resolve is a different, caller-error failure
|
|
546
|
-
* class, not a "not vendored" degrade.
|
|
547
|
-
*
|
|
548
|
-
* @remarks
|
|
549
|
-
* Defense in depth at the filesystem trust boundary: EVERY resolved
|
|
550
|
-
* destination (`materialize` and `repair`, both origins) is asserted to stay
|
|
551
|
-
* within `resolve(target)` before any write, and every `host`-origin copy
|
|
552
|
-
* source is asserted to stay within `resolve(host)` before any read — a
|
|
553
|
-
* traversal segment (`../`) in an artifact's `path` or `source` cannot escape
|
|
554
|
-
* either root, even if a gate upstream (e.g. an ungated `Plan` built by hand)
|
|
555
|
-
* let it through. A destination violation throws `ScaffoldError('WRITE', …)`;
|
|
556
|
-
* a source violation throws `ScaffoldError('TARGET', …)`.
|
|
557
|
-
*
|
|
558
|
-
* @remarks
|
|
559
|
-
* The containment check is REAL-PATH aware, not merely lexical: both the
|
|
560
|
-
* root (`target` / `host`) and the candidate destination/source are resolved
|
|
561
|
-
* through `realpathSync` on their DEEPEST EXISTING ancestor before the prefix
|
|
562
|
-
* comparison, so a symlinked subdirectory planted inside an otherwise
|
|
563
|
-
* legitimate root cannot smuggle a write (or read) outside it — `repair`,
|
|
564
|
-
* which has no `isVacant` gate, is covered exactly like `materialize`. A path
|
|
565
|
-
* segment that does not yet exist on disk (the still-to-be-created file/dir
|
|
566
|
-
* a write is about to create) is rejoined onto the resolved existing
|
|
567
|
-
* ancestor rather than realpath'd itself.
|
|
568
|
-
*
|
|
569
|
-
* @example
|
|
570
|
-
* ```ts
|
|
571
|
-
* import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
|
|
572
|
-
* import { createMaterializer } from '@orkestrel/scaffold/server'
|
|
573
|
-
*
|
|
574
|
-
* const plan = blueprintToPlan(blueprint('budget', { src: ['core'] }))
|
|
575
|
-
* const materializer = createMaterializer()
|
|
576
|
-
* materializer.materialize(plan, './packages/budget-new')
|
|
577
|
-
* materializer.destroy()
|
|
578
|
-
* ```
|
|
579
|
-
*/
|
|
580
|
-
export declare class Materializer implements MaterializerInterface {
|
|
581
|
-
#private;
|
|
582
|
-
constructor(options?: MaterializerOptions);
|
|
583
|
-
get emitter(): EmitterInterface<MaterializerEventMap>;
|
|
584
|
-
materialize(plan: Plan, target: string): MaterializeResult;
|
|
585
|
-
repair(plan: Plan, audit: Audit, target: string, replace?: boolean): MaterializeResult;
|
|
586
|
-
prune(target: string, expected: Readonly<Record<string, string>>): MaterializeResult;
|
|
587
|
-
destroy(): void;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** The outcome of one materialization (server). */
|
|
591
|
-
export declare interface MaterializeResult {
|
|
592
|
-
readonly target: string;
|
|
593
|
-
readonly written: readonly string[];
|
|
594
|
-
readonly copied: readonly string[];
|
|
595
|
-
readonly skipped: readonly string[];
|
|
596
|
-
readonly removed: readonly string[];
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/** `Materializer`'s push observation channel (AGENTS §13, server). */
|
|
600
|
-
export declare type MaterializerEventMap = {
|
|
601
|
-
readonly copy: readonly [path: string];
|
|
602
|
-
readonly write: readonly [path: string];
|
|
603
|
-
readonly remove: readonly [path: string];
|
|
604
|
-
readonly done: readonly [result: MaterializeResult];
|
|
605
|
-
readonly error: readonly [error: unknown];
|
|
606
|
-
readonly destroy: readonly [];
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
/** The materialization contract (server) — the only impure entity in the package. */
|
|
610
|
-
export declare interface MaterializerInterface {
|
|
611
|
-
readonly emitter: EmitterInterface<MaterializerEventMap>;
|
|
612
|
-
materialize(plan: Plan, target: string): MaterializeResult;
|
|
613
|
-
repair(plan: Plan, audit: Audit, target: string, replace?: boolean): MaterializeResult;
|
|
614
|
-
prune(target: string, expected: Snapshot): MaterializeResult;
|
|
615
|
-
destroy(): void;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Options for `createMaterializer` / the `Materializer` constructor (server).
|
|
620
|
-
*
|
|
621
|
-
* @remarks
|
|
622
|
-
* `host` is the vendored-data root `host`-origin artifacts are copied FROM;
|
|
623
|
-
* defaults to THIS PACKAGE'S OWN vendored data root (`dist/host`, resolved
|
|
624
|
-
* from the installed module's own location) — the package vendors its host
|
|
625
|
-
* data and ships it with itself, so the default host is never the caller's
|
|
626
|
-
* working directory. A caller-supplied `host` pointing at a raw repo root
|
|
627
|
-
* (no `manifest.json` alongside it) maps artifact paths 1:1 instead of
|
|
628
|
-
* through the manifest — the sibling-repo / test-fixture shape.
|
|
629
|
-
*/
|
|
630
|
-
export declare interface MaterializerOptions {
|
|
631
|
-
readonly host?: string;
|
|
632
|
-
readonly on?: EmitterHooks<MaterializerEventMap>;
|
|
633
|
-
readonly error?: EmitterErrorHandler;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/** The compiled, closed data-only `MaterializerOptions` contract. */
|
|
637
|
-
export declare const materializerOptionsContract: ContractInterface<Readonly<{} & {
|
|
638
|
-
host?: string;
|
|
639
|
-
}>>;
|
|
640
|
-
|
|
641
|
-
/** Build the closed data-only `MaterializerOptions` shape. */
|
|
642
|
-
export declare function materializerOptionsShape(): ObjectShape<{
|
|
643
|
-
host: OptionalShape<StringShape>;
|
|
644
|
-
}, false>;
|
|
645
|
-
|
|
646
|
-
/** Maximum normalized package-catalog description length. */
|
|
647
|
-
export declare const MAX_CATALOG_DESCRIPTION_LENGTH = 500;
|
|
648
|
-
|
|
649
|
-
/** Maximum caller-controlled filesystem path depth accepted before filesystem work. */
|
|
650
|
-
export declare const MAX_FILESYSTEM_DEPTH = 64;
|
|
651
|
-
|
|
652
|
-
/** Maximum UTF-8 bytes parsed from one local package guide. */
|
|
653
|
-
export declare const MAX_GUIDE_BYTES = 5242880;
|
|
654
|
-
|
|
655
|
-
/** Maximum directory nesting accepted by one vendored-host traversal. */
|
|
656
|
-
export declare const MAX_HOST_DEPTH = 32;
|
|
657
|
-
|
|
658
|
-
/** Maximum filesystem entries accepted by one vendored-host traversal. */
|
|
659
|
-
export declare const MAX_HOST_ENTRIES = 4096;
|
|
660
|
-
|
|
661
|
-
/** Maximum UTF-8 bytes accepted in one portable filesystem path segment. */
|
|
662
|
-
export declare const MAX_PATH_SEGMENT_BYTES = 255;
|
|
663
|
-
|
|
664
|
-
/** Maximum caller-supplied endpoint characters inspected before URL allocation. */
|
|
665
|
-
export declare const MAX_SYNC_BASE_LENGTH = 2048;
|
|
666
|
-
|
|
667
|
-
/** Maximum portable Git branch characters accepted by the guide endpoint. */
|
|
668
|
-
export declare const MAX_SYNC_BRANCH_LENGTH = 255;
|
|
669
|
-
|
|
670
|
-
/** Maximum configurable cumulative response bytes retained by one Sync operation. */
|
|
671
|
-
export declare const MAX_SYNC_BUDGET = 104857600;
|
|
672
|
-
|
|
673
|
-
/** Maximum simultaneous upstream requests accepted by `Sync`. */
|
|
674
|
-
export declare const MAX_SYNC_CONCURRENCY = 64;
|
|
675
|
-
|
|
676
|
-
/** Maximum configurable dependencies or catalog entries for one Sync operation. */
|
|
677
|
-
export declare const MAX_SYNC_ITEMS = 1000;
|
|
678
|
-
|
|
679
|
-
/** Maximum response body accepted by `Sync`, in bytes. */
|
|
680
|
-
export declare const MAX_SYNC_LIMIT = 5242880;
|
|
681
|
-
|
|
682
|
-
/** Maximum retry count accepted by `Sync`. */
|
|
683
|
-
export declare const MAX_SYNC_RETRIES = 5;
|
|
684
|
-
|
|
685
|
-
/** Maximum per-request timeout accepted by `Sync`, in milliseconds. */
|
|
686
|
-
export declare const MAX_SYNC_TIMEOUT = 300000;
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Remove the canonical Orkestrel scope from a dependency name.
|
|
690
|
-
*
|
|
691
|
-
* @param name - A dependency name.
|
|
692
|
-
* @returns Its unscoped member when canonical, otherwise the original name.
|
|
693
|
-
*/
|
|
694
|
-
export declare function packageShortName(name: string): string;
|
|
695
|
-
|
|
696
|
-
/** Parse a bounded dense array of host filesystem paths without invoking caller methods. */
|
|
697
|
-
export declare function parseFilesystemPaths(value: unknown, limit: number): readonly string[] | undefined;
|
|
698
|
-
|
|
699
|
-
/** Parse exact materializer options before allocating its emitter. */
|
|
700
|
-
export declare function parseMaterializerOptions(value: unknown): MaterializerOptions;
|
|
701
|
-
|
|
702
|
-
/** Parse a bounded dense array of portable paths without invoking caller methods. */
|
|
703
|
-
export declare function parsePortablePaths(value: unknown, limit: number): readonly string[] | undefined;
|
|
704
|
-
|
|
705
|
-
/** Parse and normalize one upstream HTTP(S) endpoint base. */
|
|
706
|
-
export declare function parseSyncBase(value: unknown): SyncBase;
|
|
707
|
-
|
|
708
|
-
/** Parse a safe Git branch path for the canonical raw-guide URL. */
|
|
709
|
-
export declare function parseSyncBranch(value: unknown): SyncBranch;
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Snapshot only declared guide references while enforcing per-file and cumulative byte limits.
|
|
713
|
-
*
|
|
714
|
-
* @param value - The caller-supplied guide content map.
|
|
715
|
-
* @param names - Exact dependency names eligible for selection.
|
|
716
|
-
* @param budget - Maximum cumulative UTF-8 bytes.
|
|
717
|
-
* @returns A frozen selected map, or `undefined` when no map was supplied.
|
|
718
|
-
*/
|
|
719
|
-
export declare function parseSyncCurrent(value: Readonly<Record<string, string>> | undefined, names: readonly string[], budget: number): Readonly<Record<string, string>> | undefined;
|
|
720
|
-
|
|
721
|
-
/** Parse and semantically validate dependency data before Sync performs network I/O. */
|
|
722
|
-
export declare function parseSyncDependencies(value: unknown, external: boolean): readonly Dependency[];
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Parse and semantically validate bare registry package names before Sync performs network I/O.
|
|
726
|
-
*
|
|
727
|
-
* @param value - Untrusted package-name collection.
|
|
728
|
-
* @returns A frozen owned snapshot of valid unique npm package names.
|
|
729
|
-
*/
|
|
730
|
-
export declare function parseSyncNames(value: unknown): readonly string[];
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Parse the exact bounded `SyncOptions` boundary before allocating resources.
|
|
734
|
-
*
|
|
735
|
-
* @param value - Caller-supplied options.
|
|
736
|
-
* @returns A fresh, validated options record.
|
|
737
|
-
*/
|
|
738
|
-
export declare function parseSyncOptions(value: unknown): SyncOptions;
|
|
739
|
-
|
|
740
|
-
/** Parse bounded exact transaction preconditions without invoking caller methods. */
|
|
741
|
-
export declare function parseWritePreconditions(value: unknown, limit: number): readonly WritePrecondition[] | undefined;
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* The prune-owned directories. Files outside these roots are never candidates
|
|
745
|
-
* for removal, including project-owned skills under `.agents/skills` and
|
|
746
|
-
* `.claude/skills`.
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* ```ts
|
|
750
|
-
* import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
|
|
751
|
-
*
|
|
752
|
-
* PRUNE_DIRECTORIES // ['.claude/agents', '.codex/agents', 'scripts']
|
|
753
|
-
* ```
|
|
754
|
-
*/
|
|
755
|
-
export declare const PRUNE_DIRECTORIES: readonly string[];
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* List the repo-relative POSIX paths under `target`'s prune directories
|
|
759
|
-
* (`.claude/agents`, `.codex/agents`, `scripts`) that the vendored `host`
|
|
760
|
-
* allowlist does NOT declare — THE single source of truth for prune drift, consumed by both
|
|
761
|
-
* `Materializer.prune` (which deletes exactly these paths) and the bin's
|
|
762
|
-
* audit/preview UX (which now shows them honestly instead of a
|
|
763
|
-
* structurally-always-zero `audit.foreign`).
|
|
764
|
-
*
|
|
765
|
-
* @param target - The target directory to scan for unexpected files.
|
|
766
|
-
* @param host - The vendored host root the allowlist is derived from.
|
|
767
|
-
* @returns The unexpected relative paths (e.g. `.claude/agents/rogue.md`); `[]`
|
|
768
|
-
* when a prune directory is absent under `target`, or when none of its
|
|
769
|
-
* files are unexpected. Pure read — never deletes anything.
|
|
770
|
-
* @throws `ScaffoldError('TARGET', …)` when `host` cannot positively
|
|
771
|
-
* establish a vendored allowlist for a prune directory that DOES exist
|
|
772
|
-
* under `target` (see `vendoredPruneSet`'s fail-closed remarks).
|
|
773
|
-
*
|
|
774
|
-
* @example
|
|
775
|
-
* ```ts
|
|
776
|
-
* import { pruneTargets } from '@orkestrel/scaffold/server'
|
|
777
|
-
*
|
|
778
|
-
* pruneTargets('./packages/router', hostRoot()) // ['.claude/agents/rogue.md']
|
|
779
|
-
* ```
|
|
780
|
-
*/
|
|
781
|
-
export declare function pruneTargets(target: string, host: string): readonly string[];
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Read one contained file as exact lowercase hexadecimal bytes.
|
|
785
|
-
*
|
|
786
|
-
* @param root - The declared containing root.
|
|
787
|
-
* @param path - The root-relative file path.
|
|
788
|
-
* @param code - The coded failure for containment or reading.
|
|
789
|
-
* @param boundary - The boundary name used in diagnostics.
|
|
790
|
-
* @returns The exact file bytes encoded as lowercase hexadecimal.
|
|
791
|
-
*/
|
|
792
|
-
export declare function readFileHex(root: string, path: string, code: ScaffoldErrorCode, boundary: string, limit?: number): string;
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Read one contained physical file as bounded UTF-8 text.
|
|
796
|
-
*
|
|
797
|
-
* @param root - The declared containing root.
|
|
798
|
-
* @param path - The root-relative file path.
|
|
799
|
-
* @param code - The coded failure for containment or reading.
|
|
800
|
-
* @param boundary - The boundary name used in diagnostics.
|
|
801
|
-
* @returns The exact file bytes decoded as UTF-8 text.
|
|
802
|
-
*/
|
|
803
|
-
export declare function readFileText(root: string, path: string, code: ScaffoldErrorCode, boundary: string, limit?: number): string;
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Read bounded physical local guide mirrors for package names.
|
|
807
|
-
*
|
|
808
|
-
* @param target - The package root.
|
|
809
|
-
* @param names - The package names whose mirrors are eligible.
|
|
810
|
-
* @returns Existing guide content keyed by package name.
|
|
811
|
-
*/
|
|
812
|
-
export declare function readGuideReferences(target: string, names: readonly string[]): Readonly<Record<string, string>>;
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Read and validate a vendored host root's `manifest.json`, when present.
|
|
816
|
-
*
|
|
817
|
-
* @param host - The host root to probe.
|
|
818
|
-
* @returns The parsed complete manifest, or `undefined` when `host` has no
|
|
819
|
-
* `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
|
|
820
|
-
* an artifact's `source` to `host` 1:1, no vendored staging indirection).
|
|
821
|
-
* @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
|
|
822
|
-
* unreadable, malformed, membership-corrupted, collision-prone,
|
|
823
|
-
* root-incomplete, or does not map bijectively and case-exactly onto real
|
|
824
|
-
* contained storage files.
|
|
825
|
-
*
|
|
826
|
-
* @example
|
|
827
|
-
* ```ts
|
|
828
|
-
* import { readHostManifest } from '@orkestrel/scaffold/server'
|
|
829
|
-
*
|
|
830
|
-
* readHostManifest('./dist/host') // HostManifest | undefined
|
|
831
|
-
* ```
|
|
832
|
-
*/
|
|
833
|
-
export declare function readHostManifest(host: string): HostManifest | undefined;
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Read `target/package.json` text — the read that feeds `manifestToDependencies`.
|
|
837
|
-
*
|
|
838
|
-
* @param target - The target directory to read the manifest from.
|
|
839
|
-
* @returns The manifest file's raw text.
|
|
840
|
-
* @throws `ScaffoldError('TARGET', …)` when the manifest is absent or
|
|
841
|
-
* unreadable (e.g. `EACCES` / `EPERM`) — carries the resolved `full` path
|
|
842
|
-
* in `context`.
|
|
843
|
-
*
|
|
844
|
-
* @example
|
|
845
|
-
* ```ts
|
|
846
|
-
* import { readManifest } from '@orkestrel/scaffold/server'
|
|
847
|
-
*
|
|
848
|
-
* readManifest('./packages/router') // '{ "name": "@orkestrel/router", … }'
|
|
849
|
-
* ```
|
|
850
|
-
*/
|
|
851
|
-
export declare function readManifest(target: string): string;
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Read a target's current bytes at a set of relative paths into a
|
|
855
|
-
* byte-exact hexadecimal {@link Snapshot} — the I/O that feeds `diffPlan`.
|
|
856
|
-
*
|
|
857
|
-
* @param target - The target directory to read from.
|
|
858
|
-
* @param paths - The plan-relative artifact paths to probe.
|
|
859
|
-
* @returns A snapshot keyed by path; each file maps to its exact lowercase
|
|
860
|
-
* hexadecimal bytes and a directly requested directory maps to `''`
|
|
861
|
-
* (presence only). An absent path is omitted entirely.
|
|
862
|
-
* @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
|
|
863
|
-
* (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
|
|
864
|
-
* the resolved `full` path) in `context`. An absent path is never an
|
|
865
|
-
* error — it is simply omitted, per the return contract above.
|
|
866
|
-
*
|
|
867
|
-
* @example
|
|
868
|
-
* ```ts
|
|
869
|
-
* import { readTarget } from '@orkestrel/scaffold/server'
|
|
870
|
-
*
|
|
871
|
-
* readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
|
|
872
|
-
* // { 'package.json': '7b226e616d65223a…', 'src/core/index.ts': '6578706f7274…' }
|
|
873
|
-
* ```
|
|
874
|
-
*/
|
|
875
|
-
export declare function readTarget(target: string, paths: readonly string[]): Snapshot;
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Map a manifest destination from an artifact's source prefix to its target prefix.
|
|
879
|
-
*
|
|
880
|
-
* @param artifact - The host artifact carrying the target path and optional source.
|
|
881
|
-
* @param destination - The matched manifest destination.
|
|
882
|
-
* @returns The exact target-relative path for the matched manifest file.
|
|
883
|
-
* @throws `ScaffoldError('INVALID', …)` when `destination` is outside the source prefix.
|
|
884
|
-
*/
|
|
885
|
-
export declare function remapArtifactPath(artifact: HostArtifact, destination: string): string;
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Atomically promote a staged sibling directory while preserving recoverable state.
|
|
889
|
-
*
|
|
890
|
-
* @param staging - The completed staging directory.
|
|
891
|
-
* @param target - The destination directory to replace.
|
|
892
|
-
* @param backup - The sibling path reserved for the prior target.
|
|
893
|
-
* @throws `ScaffoldError('WRITE', …)` with explicit `committed` and recovery paths.
|
|
894
|
-
*/
|
|
895
|
-
export declare function replaceDirectory(staging: string, target: string, backup: string): void;
|
|
896
|
-
|
|
897
|
-
/** Windows device names that remain reserved even when followed by an extension. */
|
|
898
|
-
export declare const RESERVED_PATH_SEGMENT_PATTERN: RegExp;
|
|
899
|
-
|
|
900
|
-
/** Existing repository metadata that a materialization target must never own. */
|
|
901
|
-
export declare const RESERVED_TARGET_PATH_PATTERN: RegExp;
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Resolve a path beneath a declared root and reject lexical or symlink escape.
|
|
905
|
-
*
|
|
906
|
-
* @param root - The containing filesystem root.
|
|
907
|
-
* @param path - The candidate path, relative or absolute.
|
|
908
|
-
* @param code - The coded error to raise on escape.
|
|
909
|
-
* @param boundary - The boundary name used in diagnostics.
|
|
910
|
-
* @returns The lexically resolved candidate after realpath-aware validation.
|
|
911
|
-
*/
|
|
912
|
-
export declare function resolveContainedPath(root: string, path: string, code: ScaffoldErrorCode, boundary: string): string;
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Resolve and completely preflight the canonical guide destinations a sync may write.
|
|
916
|
-
*
|
|
917
|
-
* @param guides - Structurally validated behind guide results.
|
|
918
|
-
* @param target - The repository root that owns `guides/src`.
|
|
919
|
-
* @returns Each guide paired with its contained destination.
|
|
920
|
-
* @throws `ScaffoldError('WRITE', ...)` before mutation for ownership, collision,
|
|
921
|
-
* containment, or existing filesystem-shape violations.
|
|
922
|
-
*/
|
|
923
|
-
export declare function resolveGuideWrites(guides: readonly GuideSync[], target: string): readonly GuideWrite[];
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Resolve a contained path whose existing ancestor chain contains no links.
|
|
927
|
-
*
|
|
928
|
-
* @param root - The trusted lexical and physical root.
|
|
929
|
-
* @param path - The portable root-relative path.
|
|
930
|
-
* @param code - The coded error to raise on failure.
|
|
931
|
-
* @param boundary - The boundary name used in diagnostics.
|
|
932
|
-
* @returns The contained destination.
|
|
933
|
-
* @throws `ScaffoldError` when containment fails or an ancestor is not a real directory.
|
|
934
|
-
*/
|
|
935
|
-
export declare function resolvePhysicalPath(root: string, path: string, code: ScaffoldErrorCode, boundary: string): string;
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* Resolve the deepest existing ancestor of a path through the real filesystem.
|
|
939
|
-
*
|
|
940
|
-
* @param path - The absolute or relative path to resolve.
|
|
941
|
-
* @returns A path whose existing prefix has been resolved through symlinks.
|
|
942
|
-
*/
|
|
943
|
-
export declare function resolveRealPath(path: string): string;
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Restore quarantined files to their original target-relative paths.
|
|
947
|
-
*
|
|
948
|
-
* @param transaction - The nominal transaction that owns the quarantine and target.
|
|
949
|
-
* @param paths - The relative paths to restore, in their original move order.
|
|
950
|
-
* @throws `ScaffoldError('WRITE', …)` after attempting every reverse-order
|
|
951
|
-
* restoration when one or more files could not be restored.
|
|
952
|
-
*/
|
|
953
|
-
export declare function restoreFiles(transaction: WriteTransaction, paths: readonly string[]): void;
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
|
|
957
|
-
* string values — the shared `dependencies` / `peerDependencies` /
|
|
958
|
-
* `devDependencies` reader `deriveBlueprint` uses for every dependency-shaped
|
|
959
|
-
* field.
|
|
960
|
-
*
|
|
961
|
-
* @param value - The candidate manifest field value (e.g. `parsed.dependencies`).
|
|
962
|
-
* @returns The `@orkestrel/`-prefixed `[name, range]` entries; `[]` when
|
|
963
|
-
* `value` is not a plain object (per `isRecord`).
|
|
964
|
-
*
|
|
965
|
-
* @example
|
|
966
|
-
* ```ts
|
|
967
|
-
* import { selectOrkestrelEntries } from '@orkestrel/scaffold/server'
|
|
968
|
-
*
|
|
969
|
-
* selectOrkestrelEntries({ '@orkestrel/core': '^1.0.0', lodash: '^4.0.0' })
|
|
970
|
-
* // [['@orkestrel/core', '^1.0.0']]
|
|
971
|
-
* ```
|
|
972
|
-
*/
|
|
973
|
-
export declare function selectOrkestrelEntries(value: unknown): readonly (readonly [string, string])[];
|
|
974
|
-
|
|
975
|
-
/** Local configuration and credential-like paths that must never enter a vendored host. */
|
|
976
|
-
export declare const SENSITIVE_HOST_PATH_PATTERN: RegExp;
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Stage the vendored host set (byte-preserved copies + `manifest.json`) from
|
|
980
|
-
* a repo root into an output directory — the BUILD-time primitive the
|
|
981
|
-
* `build:host` npm script now calls directly (replacing a standalone build
|
|
982
|
-
* script); `Materializer.materialize` is the RUNTIME reader of what this
|
|
983
|
-
* writes (via `hostRoot` / `readHostManifest`).
|
|
75
|
+
* @returns The SHA-256 of that exact membership, in that exact order.
|
|
984
76
|
*
|
|
985
|
-
* @param root - The repo root every `paths` entry resolves against.
|
|
986
|
-
* @param out - The output directory to replace after staging completes.
|
|
987
|
-
* @param paths - The repo-relative file/directory entries to stage; defaults
|
|
988
|
-
* to the package's own vendored set (`HOST_PATHS`) — a caller passes an
|
|
989
|
-
* explicit list only to stage an arbitrary/test set.
|
|
990
77
|
* @remarks
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
* @returns The written manifest's entries (`{ storage, destination, executable }`).
|
|
998
|
-
* @throws `ScaffoldError('TARGET', …)` for an invalid/escaping source or path
|
|
999
|
-
* collision, and `ScaffoldError('WRITE', …)` for staging/swap failures.
|
|
1000
|
-
*
|
|
1001
|
-
* @example
|
|
1002
|
-
* ```ts
|
|
1003
|
-
* import { stageHost } from '@orkestrel/scaffold/server'
|
|
1004
|
-
*
|
|
1005
|
-
* const entries = stageHost(process.cwd(), 'dist/host')
|
|
1006
|
-
* entries.length // number of files staged
|
|
1007
|
-
* ```
|
|
1008
|
-
*/
|
|
1009
|
-
export declare function stageHost(root: string, out: string, paths?: readonly string[]): readonly ManifestEntry[];
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* Map a repo-relative path to its vendored-host STAGING path, per the
|
|
1013
|
-
* dotfile-mapping rule `stageHost` writes into `manifest.json`.
|
|
1014
|
-
*
|
|
1015
|
-
* @param path - The repo-relative source path (e.g. `.claude/agents/reviewer.md`).
|
|
1016
|
-
* @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
|
|
1017
|
-
* `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
|
|
1018
|
-
* dot wherever it appears; an undotted path is unchanged.
|
|
78
|
+
* Independent of the manifest's own `digest` field, which is what lets a reader
|
|
79
|
+
* detect a membership edit that did not update it. Order is part of the claim
|
|
80
|
+
* rather than normalized away, because a staged manifest sorts its entries and
|
|
81
|
+
* roots once and a reordered copy is a different file. Each entry is projected
|
|
82
|
+
* to exactly the three declared fields, so a hand-added property cannot ride
|
|
83
|
+
* into the digest and cannot change it either.
|
|
1019
84
|
*
|
|
1020
85
|
* @example
|
|
1021
86
|
* ```ts
|
|
1022
|
-
* import {
|
|
87
|
+
* import { computeManifestDigest } from '@orkestrel/scaffold/server'
|
|
1023
88
|
*
|
|
1024
|
-
*
|
|
1025
|
-
* storagePath('.claude/agents/reviewer.md') // 'claude/agents/reviewer.md'
|
|
1026
|
-
* storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
|
|
1027
|
-
* storagePath('AGENTS.md') // 'AGENTS.md'
|
|
89
|
+
* computeManifestDigest([], []) // the digest of the empty membership
|
|
1028
90
|
* ```
|
|
1029
91
|
*/
|
|
1030
|
-
export declare function
|
|
92
|
+
export declare function computeManifestDigest(entries: readonly ManifestEntry[], roots: readonly string[]): string;
|
|
1031
93
|
|
|
1032
94
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
* `Materializer`, Promise-based and network-only.
|
|
1035
|
-
*
|
|
1036
|
-
* @remarks
|
|
1037
|
-
* Every method reads upstream over HTTPS with a 10-second per-request
|
|
1038
|
-
* `AbortSignal.timeout` and bounded `concurrency` (default 6, never an
|
|
1039
|
-
* unbounded `Promise.all`). The default COLLECT posture captures each
|
|
1040
|
-
* dependency's `freshness` (`404` → `missing`, transport / other non-2xx →
|
|
1041
|
-
* `failed`) into the result; `strict` mode instead throws
|
|
1042
|
-
* `ScaffoldError('FETCH', …)` naming the failing URL. `guides`'s optional
|
|
1043
|
-
* `current` parameter is a caller-supplied local-mirror content map keyed by
|
|
1044
|
-
* dependency NAME (the `diffPlan` caller-supplied-reference pattern): WITH the
|
|
1045
|
-
* map, a fetched guide byte-equal to its entry verdicts `current`, anything
|
|
1046
|
-
* differing or absent from the map verdicts `behind`; WITHOUT the map, every
|
|
1047
|
-
* successful fetch verdicts `behind` (no reference means it needs syncing).
|
|
1048
|
-
* `pull` builds that map itself from the TARGET's declared dependency mirrors;
|
|
1049
|
-
* `mirror` builds it from the registry's exact organization package list,
|
|
1050
|
-
* excluding the target's own guide and issuing no registry version reads. Both
|
|
1051
|
-
* verdicts are target-relative; `write` commits only the
|
|
1052
|
-
* `behind` guides (never `current`, `missing`, or `failed`, which carries no
|
|
1053
|
-
* trustworthy content) under the same realpath-anchored containment law
|
|
1054
|
-
* `Materializer` enforces. After `destroy()` every method throws `DESTROYED`;
|
|
1055
|
-
* teardown is idempotent, emitter last.
|
|
1056
|
-
*
|
|
1057
|
-
* @example
|
|
1058
|
-
* ```ts
|
|
1059
|
-
* import { createSync } from '@orkestrel/scaffold/server'
|
|
95
|
+
* Construct a {@link Materializer}.
|
|
1060
96
|
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
*/
|
|
1067
|
-
export declare class Sync implements SyncInterface {
|
|
1068
|
-
#private;
|
|
1069
|
-
constructor(options?: SyncOptions);
|
|
1070
|
-
get emitter(): EmitterInterface<SyncEventMap>;
|
|
1071
|
-
lookup(names: readonly string[]): Promise<readonly VersionLookup[]>;
|
|
1072
|
-
guides(deps: readonly Dependency[], current?: Readonly<Record<string, string>>): Promise<readonly GuideSync[]>;
|
|
1073
|
-
versions(deps: readonly Dependency[]): Promise<readonly VersionSync[]>;
|
|
1074
|
-
/**
|
|
1075
|
-
* The fleet package catalog, sourced from the npm registry — the
|
|
1076
|
-
* AUTHORITATIVE enumeration (never a caller-supplied root).
|
|
97
|
+
* @param options - The vendored host root, the initial listeners, and the listener-error handler.
|
|
98
|
+
* @returns The materializer, typed as the contract consumers program against.
|
|
99
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
|
|
100
|
+
* not an option bag the materializer accepts, and `TARGET` when the host carries
|
|
101
|
+
* a manifest that cannot be read or does not match what it stores.
|
|
1077
102
|
*
|
|
1078
|
-
* @
|
|
1079
|
-
*
|
|
1080
|
-
* @
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* catalog to build; (2) each name's own registry packument supplies
|
|
1086
|
-
* `version` (`dist-tags.latest`) and a registry-path `description`
|
|
1087
|
-
* fallback — a failed/malformed packument keeps the entry (degraded:
|
|
1088
|
-
* `version: ''`) rather than dropping it, since the org list already
|
|
1089
|
-
* proved the package exists; (3) each name's own guide
|
|
1090
|
-
* (`guides/src/<short>.md`, same canonical URL as `guides()`, fetched
|
|
1091
|
-
* unauthenticated — every fleet repo is public) supplies the PREFERRED
|
|
1092
|
-
* `description` — its first blockquote's first paragraph — falling back
|
|
1093
|
-
* to the packument description when the guide 404s, faults, or carries no
|
|
1094
|
-
* blockquote; a 404 STAYS LISTED (it is a reachability signal, not an
|
|
1095
|
-
* absence) with a note reading `guide unreachable (HTTP 404 — repo
|
|
1096
|
-
* private or guide missing?)`. Emits `package` once per entry with a
|
|
1097
|
-
* combined human-readable `note` (empty when both fetches succeeded)
|
|
1098
|
-
* alongside the existing `error` events for each degraded sub-fetch.
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { createMaterializer } from '@orkestrel/scaffold/server'
|
|
106
|
+
*
|
|
107
|
+
* const materializer = createMaterializer({ host: './dist/host' })
|
|
108
|
+
* materializer.destroy()
|
|
109
|
+
* ```
|
|
1099
110
|
*/
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
export declare
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
export declare
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
export declare function
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
/**
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
111
|
+
export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Construct an {@link Upstream}.
|
|
115
|
+
*
|
|
116
|
+
* @param options - The two endpoints, the request bounds, the initial listeners,
|
|
117
|
+
* and the listener-error handler.
|
|
118
|
+
* @returns The reader, typed as the contract consumers program against.
|
|
119
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but is
|
|
120
|
+
* not an option bag the reader accepts, or when either endpoint names a scheme,
|
|
121
|
+
* host, or form the reader will not request.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { createUpstream } from '@orkestrel/scaffold/server'
|
|
126
|
+
*
|
|
127
|
+
* const upstream = createUpstream({ guides: { branch: 'main' } })
|
|
128
|
+
* upstream.destroy()
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function createUpstream(options?: UpstreamOptions): UpstreamInterface;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The exact SHA-256 syntax a digest is stated in: sixty-four lowercase hexadecimal digits.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Fixed length, unlike the core byte encoding, because a digest is one value of
|
|
138
|
+
* one algorithm rather than a variable run of bytes. Lowercase only, so two
|
|
139
|
+
* spellings of one digest can never compare unequal.
|
|
140
|
+
*/
|
|
141
|
+
export declare const DIGEST_PATTERN: RegExp;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The drive prefix a Windows host path may open with.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* The one segment allowed to carry a colon. Every other segment is measured by
|
|
148
|
+
* {@link INVALID_SEGMENT_CHARACTER_PATTERN}, which refuses one, so a stream name
|
|
149
|
+
* such as `file.txt:stream` cannot be smuggled through a later segment.
|
|
150
|
+
*/
|
|
151
|
+
export declare const DRIVE_PATTERN: RegExp;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The complete vendored-host inventory.
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* `roots` is the sorted directory inventory, which is what distinguishes a
|
|
158
|
+
* declared empty directory. `digest` is the SHA-256 of that exact entry and
|
|
159
|
+
* root membership, so a membership edit that did not update the digest is
|
|
160
|
+
* detected. A self-consistent replacement manifest defines its own smaller
|
|
161
|
+
* membership; authenticating omitted membership is outside a checksum's
|
|
162
|
+
* contract.
|
|
163
|
+
*/
|
|
164
|
+
export declare interface HostManifest {
|
|
165
|
+
readonly entries: readonly ManifestEntry[];
|
|
166
|
+
readonly roots: readonly string[];
|
|
167
|
+
readonly digest: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Visible characters no host path segment may carry.
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* Narrower than the core path law by exactly one character: a backslash is a
|
|
175
|
+
* separator on a Windows host rather than a forbidden character, so it is
|
|
176
|
+
* normalized to `/` before the segments are measured instead of refused here.
|
|
177
|
+
*/
|
|
178
|
+
export declare const INVALID_SEGMENT_CHARACTER_PATTERN: RegExp;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Narrow a value to a Git branch the guide endpoint accepts.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* A branch reaches the guide URL's path, so the syntax is closed rather than
|
|
185
|
+
* merely bounded and no `..` is admitted anywhere in it.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* import { isBranch } from '@orkestrel/scaffold/server'
|
|
190
|
+
*
|
|
191
|
+
* isBranch('main') // true
|
|
192
|
+
* isBranch('main/../etc') // false
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export declare const isBranch: Guard<string>;
|
|
196
|
+
|
|
197
|
+
/** Narrow a value to a bounded list of fleet catalog rows. */
|
|
198
|
+
export declare const isCatalogEntries: Guard<readonly CatalogEntry[]>;
|
|
199
|
+
|
|
200
|
+
/** Narrow a value to a bounded list of declared runtime dependencies. */
|
|
201
|
+
export declare const isDependencies: Guard<readonly Dependency[]>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Narrow a value to a bounded list of `@orkestrel` package names.
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* Composed from the core collection and dependency-name guards rather than
|
|
208
|
+
* restated, so the scope law that keeps a derived guide mirror inside its
|
|
209
|
+
* directory has exactly one home.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* import { isDependencyNames } from '@orkestrel/scaffold/server'
|
|
214
|
+
*
|
|
215
|
+
* isDependencyNames(['@orkestrel/router']) // true
|
|
216
|
+
* isDependencyNames(['router']) // false
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export declare const isDependencyNames: Guard<readonly string[]>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Narrow a value to one exact SHA-256 digest.
|
|
223
|
+
*
|
|
224
|
+
* @remarks
|
|
225
|
+
* The identity a vendored host manifest and a write precondition are both stated
|
|
226
|
+
* in. Fixed at sixty-four lowercase digits, so the value either is a digest of
|
|
227
|
+
* that algorithm or is refused; there is no shorter or longer accepted form.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* import { isDigest } from '@orkestrel/scaffold/server'
|
|
232
|
+
*
|
|
233
|
+
* isDigest('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') // true
|
|
234
|
+
* isDigest('E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855') // false
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export declare const isDigest: Guard<string>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Narrow a value to a bounded upstream endpoint.
|
|
241
|
+
*
|
|
242
|
+
* @remarks
|
|
243
|
+
* Length only. Which schemes and hosts an endpoint may name is the reader's law,
|
|
244
|
+
* because it builds the request and can report why one was refused, where a
|
|
245
|
+
* guard has only `false` to say.
|
|
246
|
+
*/
|
|
247
|
+
export declare const isEndpoint: Guard<string>;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Test whether a path is a physical file with exact on-disk casing.
|
|
251
|
+
*
|
|
252
|
+
* @param path - The host path to inspect segment by segment.
|
|
253
|
+
* @returns `true` only for a physical file whose requested segments exactly
|
|
254
|
+
* match the names each parent directory stores.
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* A direct file lookup follows the host's case-folding rules on Windows and
|
|
258
|
+
* common macOS filesystems. Reading each parent directory supplies the stored
|
|
259
|
+
* names, so this predicate can enforce the package's exact-case structural
|
|
260
|
+
* contract on every supported host.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { isExactCaseFile } from '@orkestrel/scaffold/server'
|
|
265
|
+
*
|
|
266
|
+
* isExactCaseFile('/tmp/project/src/bin/main.ts') // true only for that exact spelling
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
export declare function isExactCaseFile(path: string): boolean;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Narrow a value to a path naming a location on this host.
|
|
273
|
+
*
|
|
274
|
+
* @param value - The candidate host path.
|
|
275
|
+
* @returns `true` for a bounded absolute or relative path whose every segment is
|
|
276
|
+
* portable across the supported filesystems.
|
|
277
|
+
*
|
|
278
|
+
* @remarks
|
|
279
|
+
* The counterpart to the core path law, not a copy of it. A target directory and
|
|
280
|
+
* the vendored host root are locations on the machine rather than paths inside a
|
|
281
|
+
* workspace, so a drive prefix, a UNC share, and a backslash separator are all
|
|
282
|
+
* admitted here and `..` is a legitimate way to name a sibling directory.
|
|
283
|
+
* Containment is still enforced, but by the core law over the artifact paths
|
|
284
|
+
* written beneath the target, not by this one.
|
|
285
|
+
*
|
|
286
|
+
* What it does refuse is a segment no supported filesystem can hold: an empty
|
|
287
|
+
* one, a reserved Windows device name, a trailing dot or space, a wildcard or
|
|
288
|
+
* redirection character, a colon anywhere but the drive prefix, and a name past
|
|
289
|
+
* the byte ceiling. The character ceiling is read first so an oversized string is
|
|
290
|
+
* refused before it is split.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```ts
|
|
294
|
+
* import { isFilesystemPath } from '@orkestrel/scaffold/server'
|
|
295
|
+
*
|
|
296
|
+
* isFilesystemPath('C:/Users/sample/project') // true
|
|
297
|
+
* isFilesystemPath('../sibling') // true
|
|
298
|
+
* isFilesystemPath('project/nul') // false
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
export declare function isFilesystemPath(value: unknown): value is string;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Narrow a value to one {@link HostManifest}.
|
|
305
|
+
*
|
|
306
|
+
* @remarks
|
|
307
|
+
* The manifest is read from a directory a caller named, so it is the least
|
|
308
|
+
* trusted value the server face handles and is guarded whole: every entry, every
|
|
309
|
+
* declared root, and the digest that authenticates their membership.
|
|
310
|
+
*/
|
|
311
|
+
export declare const isHostManifest: Guard<HostManifest>;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Narrow a value to a working-tree inventory within the limit one target may report.
|
|
315
|
+
*
|
|
316
|
+
* @param value - The candidate inventory.
|
|
317
|
+
* @returns `true` for an array of no more than `MAX_INVENTORY_PATHS` items.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* Compose this ahead of an element guard exactly as the core collection guard is
|
|
321
|
+
* composed, and for the same reason: the item count is settled before anything
|
|
322
|
+
* walks the items, and a hostile `length` accessor answers `false` rather than
|
|
323
|
+
* escaping as a throw. It exists beside that guard rather than reusing it
|
|
324
|
+
* because the two bound different things — one bounds what a caller may hand a
|
|
325
|
+
* public method, this one bounds what a checkout may contain.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { isInventory } from '@orkestrel/scaffold/server'
|
|
330
|
+
*
|
|
331
|
+
* isInventory(['AGENTS.md']) // true
|
|
332
|
+
* isInventory('AGENTS.md') // false
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export declare function isInventory(value: unknown): value is readonly unknown[];
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Narrow a value to one {@link ManifestEntry}.
|
|
339
|
+
*
|
|
340
|
+
* @remarks
|
|
341
|
+
* Both paths are measured by the core path law, because a vendored host's
|
|
342
|
+
* storage name and the destination it maps to are each a path inside a
|
|
343
|
+
* workspace. That is what stops a hand-edited manifest from mapping a vendored
|
|
344
|
+
* file to a destination outside the target.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* import { isManifestEntry } from '@orkestrel/scaffold/server'
|
|
349
|
+
*
|
|
350
|
+
* isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
export declare const isManifestEntry: Guard<ManifestEntry>;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Narrow a value to the materializer's initial listener record.
|
|
357
|
+
*
|
|
358
|
+
* @remarks
|
|
359
|
+
* Every event is optional and every declared value is a function. A key outside
|
|
360
|
+
* the materializer's event map is refused, so a listener wired to a misspelled
|
|
361
|
+
* event fails at construction instead of never firing.
|
|
362
|
+
*/
|
|
363
|
+
export declare const isMaterializerHooks: Guard<EmitterHooks<MaterializerEventMap>>;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Narrow a value to {@link MaterializerOptions}.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```ts
|
|
370
|
+
* import { isMaterializerOptions } from '@orkestrel/scaffold/server'
|
|
371
|
+
*
|
|
372
|
+
* isMaterializerOptions({}) // true
|
|
373
|
+
* isMaterializerOptions({ host: 'dist/host*' }) // false
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
export declare const isMaterializerOptions: Guard<MaterializerOptions>;
|
|
377
|
+
|
|
378
|
+
/** Narrow a value to a bounded list of fetched guide mirrors. */
|
|
379
|
+
export declare const isMirrors: Guard<readonly Mirror[]>;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Test whether a path is a physical directory this package will read or write into.
|
|
383
|
+
*
|
|
384
|
+
* @param path - The resolved host path to inspect, without following links.
|
|
385
|
+
* @returns `true` only for a directory that is not a link.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* A junction and a directory symbolic link both report as directories once
|
|
389
|
+
* followed, so the inspection deliberately does not follow: a redirected
|
|
390
|
+
* directory is refused here rather than silently accepted as the one the caller
|
|
391
|
+
* named.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```ts
|
|
395
|
+
* import { isPhysicalDirectory } from '@orkestrel/scaffold/server'
|
|
396
|
+
*
|
|
397
|
+
* isPhysicalDirectory('/tmp/project') // true for a plain directory
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
export declare function isPhysicalDirectory(path: string): boolean;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Test whether a path is a physical file this package will read or replace.
|
|
404
|
+
*
|
|
405
|
+
* @param path - The resolved host path to inspect, without following links.
|
|
406
|
+
* @returns `true` only for a regular file that is neither a link nor hard-linked
|
|
407
|
+
* elsewhere.
|
|
408
|
+
*
|
|
409
|
+
* @remarks
|
|
410
|
+
* The link tests are the point. A symbolic link is a path pointing somewhere
|
|
411
|
+
* else, so writing through one writes outside the target; a hard link means a
|
|
412
|
+
* second name shares the same bytes, so replacing them changes a file nobody
|
|
413
|
+
* asked about. Both are refused rather than followed.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```ts
|
|
417
|
+
* import { isPhysicalFile } from '@orkestrel/scaffold/server'
|
|
418
|
+
*
|
|
419
|
+
* isPhysicalFile('/tmp/project/AGENTS.md') // true for a plain file
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
export declare function isPhysicalFile(path: string): boolean;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Narrow a value to a {@link Repository}.
|
|
426
|
+
*
|
|
427
|
+
* @remarks
|
|
428
|
+
* Both path lists are target-relative, so both are measured by the core path
|
|
429
|
+
* law: a reported path that is not one this package could have planned is not a
|
|
430
|
+
* path it will delete. The inventory guard bounds the lists, because a checkout
|
|
431
|
+
* is legitimately far larger than any collection a caller hands a method.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* import { isRepository } from '@orkestrel/scaffold/server'
|
|
436
|
+
*
|
|
437
|
+
* isRepository({ tracked: ['AGENTS.md'], dirty: [] }) // true
|
|
438
|
+
* isRepository({ tracked: ['../secrets'], dirty: [] }) // false
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
export declare const isRepository: Guard<Repository>;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Narrow a value to a per-request timeout in milliseconds.
|
|
445
|
+
*
|
|
446
|
+
* @remarks
|
|
447
|
+
* A whole number of milliseconds, at least one and no more than
|
|
448
|
+
* {@link MAX_UPSTREAM_TIMEOUT}. Zero is refused because a request that may not
|
|
449
|
+
* take any time is a request that cannot succeed.
|
|
450
|
+
*/
|
|
451
|
+
export declare const isTimeout: Guard<number>;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Narrow a value to the upstream reader's initial listener record.
|
|
455
|
+
*
|
|
456
|
+
* @remarks
|
|
457
|
+
* Closed to the reader's own four events for the same reason the materializer's
|
|
458
|
+
* record is closed to its five.
|
|
459
|
+
*/
|
|
460
|
+
export declare const isUpstreamHooks: Guard<EmitterHooks<UpstreamEventMap>>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Narrow a value to {@link UpstreamOptions}.
|
|
464
|
+
*
|
|
465
|
+
* @remarks
|
|
466
|
+
* Each grouped endpoint is closed to its own leaves, so a setting written under
|
|
467
|
+
* the wrong entity is refused rather than ignored. Every numeric leaf is a whole
|
|
468
|
+
* number inside a ceiling: an unbounded concurrency, retry count, response
|
|
469
|
+
* limit, or call budget is a way to exhaust the caller, so the ceiling is stated
|
|
470
|
+
* here rather than left to the reader. The two byte ceilings are the core
|
|
471
|
+
* artifact and total-artifact limits, because a fetched guide is an artifact and
|
|
472
|
+
* a whole call retains no more than a whole plan.
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* import { isUpstreamOptions } from '@orkestrel/scaffold/server'
|
|
477
|
+
*
|
|
478
|
+
* isUpstreamOptions({ guides: { branch: 'main' }, concurrency: 4 }) // true
|
|
479
|
+
* isUpstreamOptions({ concurrency: 0 }) // false
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
export declare const isUpstreamOptions: Guard<UpstreamOptions>;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Test whether a target is safe to write a fresh workspace into.
|
|
486
|
+
*
|
|
487
|
+
* @param target - The candidate target directory.
|
|
488
|
+
* @returns `true` when the target is absent, empty, or holds nothing but its own
|
|
489
|
+
* `.git` directory.
|
|
490
|
+
*
|
|
491
|
+
* @remarks
|
|
492
|
+
* The green-field law. A checkout of an empty repository is where a new
|
|
493
|
+
* workspace legitimately starts, so that one directory is admitted and nothing
|
|
494
|
+
* else is; anything more means the caller is repairing a workspace rather than
|
|
495
|
+
* creating one. Only the first two entries are read, so the answer costs the
|
|
496
|
+
* same on an empty directory and on a full one.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```ts
|
|
500
|
+
* import { isVacant } from '@orkestrel/scaffold/server'
|
|
501
|
+
*
|
|
502
|
+
* isVacant('./packages/router-new') // true when absent, empty, or `.git` only
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
export declare function isVacant(target: string): boolean;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* List a directory's descendant directories as sorted root-relative paths.
|
|
509
|
+
*
|
|
510
|
+
* @param root - The directory to inventory.
|
|
511
|
+
* @returns Every descendant directory as a `/`-separated root-relative path, in
|
|
512
|
+
* code-unit order, and `[]` when `root` is absent.
|
|
513
|
+
* @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
|
|
514
|
+
* @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
|
|
515
|
+
* physical directory, cannot be read, holds a name this package could not plan,
|
|
516
|
+
* or carries more entries or more nesting than one inventory may report.
|
|
517
|
+
*
|
|
518
|
+
* @remarks
|
|
519
|
+
* The sibling of {@link listFiles}, under the same bounds and the same refusals,
|
|
520
|
+
* and it exists because a directory holding no file is invisible to a file walk.
|
|
521
|
+
* That is the half a vendored host's `roots` declares and the half a file
|
|
522
|
+
* inventory cannot check, so a stager needs both walks to state a complete
|
|
523
|
+
* membership.
|
|
524
|
+
*
|
|
525
|
+
* `root` itself is not listed, because the answer is root-relative and the root
|
|
526
|
+
* has no root-relative name. A redirected directory is not listed and is not
|
|
527
|
+
* walked into, so no traversal can leave the root and no cycle can form.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* import { listDirectories } from '@orkestrel/scaffold/server'
|
|
532
|
+
*
|
|
533
|
+
* listDirectories('./.claude') // ['agents', 'rules', 'skills', …]
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
export declare function listDirectories(root: string): readonly string[];
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* List a directory's files as sorted root-relative paths.
|
|
540
|
+
*
|
|
541
|
+
* @param root - The directory to inventory.
|
|
542
|
+
* @returns Every descendant file as a `/`-separated root-relative path, in
|
|
543
|
+
* code-unit order, and `[]` when `root` is absent.
|
|
544
|
+
* @throws `ScaffoldError('INVALID', …)` when `root` is not a host path.
|
|
545
|
+
* @throws `ScaffoldError('TARGET', …)` when `root` is present but is not a
|
|
546
|
+
* physical directory, cannot be read, holds a name this package could not plan,
|
|
547
|
+
* or carries more entries or more nesting than one inventory may report.
|
|
548
|
+
*
|
|
549
|
+
* @remarks
|
|
550
|
+
* A whole-tree answer throws where a single-path answer returns `undefined`, and
|
|
551
|
+
* the reason is that a partial inventory reads exactly like a complete one. A
|
|
552
|
+
* caller comparing a target against a plan would treat a truncated listing as
|
|
553
|
+
* proof that the missing files are not there.
|
|
554
|
+
*
|
|
555
|
+
* Absence is the one exception: nothing to inventory is a complete answer, so it
|
|
556
|
+
* is the empty list. Links are listed as files rather than followed, so no
|
|
557
|
+
* traversal can leave the root and no cycle can form.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* import { listFiles } from '@orkestrel/scaffold/server'
|
|
562
|
+
*
|
|
563
|
+
* listFiles('./dist/host') // ['AGENTS.md', 'claude/rules/names.md', …]
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
export declare function listFiles(root: string): readonly string[];
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The reserved metadata name a staged vendored host writes at its own root.
|
|
570
|
+
*
|
|
571
|
+
* @remarks
|
|
572
|
+
* The one name a vendored file may never claim, because the staged root holds
|
|
573
|
+
* the manifest under it. The producer refuses a storage name equal to it and the
|
|
574
|
+
* reader looks for exactly this file, so both sides read one constant rather
|
|
575
|
+
* than repeating a literal that only agrees by inspection.
|
|
576
|
+
*/
|
|
577
|
+
export declare const MANIFEST_NAME = "manifest.json";
|
|
578
|
+
|
|
579
|
+
/** One file record of the vendored host's manifest. */
|
|
580
|
+
export declare interface ManifestEntry {
|
|
581
|
+
readonly storage: string;
|
|
582
|
+
readonly destination: string;
|
|
583
|
+
readonly executable: boolean;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Test whether a captured directory is still the same directory.
|
|
588
|
+
*
|
|
589
|
+
* @param anchor - The identity captured earlier.
|
|
590
|
+
* @returns `true` when the path still holds a physical directory of that exact
|
|
591
|
+
* device and inode.
|
|
592
|
+
*
|
|
593
|
+
* @remarks
|
|
594
|
+
* The check a write repeats between steps. A directory replaced by another
|
|
595
|
+
* directory of the same name answers `false` here, which is the case a path
|
|
596
|
+
* comparison alone cannot see.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* import { matchesAnchor, readAnchor } from '@orkestrel/scaffold/server'
|
|
601
|
+
*
|
|
602
|
+
* const anchor = readAnchor('/tmp/project')
|
|
603
|
+
* anchor !== undefined && matchesAnchor(anchor) // true while it is untouched
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
export declare function matchesAnchor(anchor: WriteAnchor): boolean;
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Test whether a destination still holds what was captured of it.
|
|
610
|
+
*
|
|
611
|
+
* @param expectation - The state captured earlier.
|
|
612
|
+
* @returns `true` when re-reading the destination now produces that same state.
|
|
613
|
+
*
|
|
614
|
+
* @remarks
|
|
615
|
+
* Compared field for field against a fresh {@link readExpectation}, so an
|
|
616
|
+
* expectation recorded without a digest matches only a destination that still
|
|
617
|
+
* has no digest to give. That is what keeps the comparison honest in both
|
|
618
|
+
* directions: nothing is treated as satisfied because it was never measured.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* import { matchesExpectation, readExpectation } from '@orkestrel/scaffold/server'
|
|
623
|
+
*
|
|
624
|
+
* const expectation = readExpectation('/tmp/project/AGENTS.md')
|
|
625
|
+
* expectation !== undefined && matchesExpectation(expectation) // true while untouched
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
export declare function matchesExpectation(expectation: WriteExpectation): boolean;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Test whether a path addresses a target's own repository metadata.
|
|
632
|
+
*
|
|
633
|
+
* @param path - The path to classify; either separator is read.
|
|
634
|
+
* @returns `true` for `.git` and for anything beneath it.
|
|
635
|
+
*
|
|
636
|
+
* @remarks
|
|
637
|
+
* The one home of the `.git` membership rule, read from two directions. A target
|
|
638
|
+
* holding nothing but this directory is still vacant, because a checkout of an
|
|
639
|
+
* empty repository is where a fresh workspace legitimately starts. A path
|
|
640
|
+
* beneath it is never removed and never vendored, because deleting a target's
|
|
641
|
+
* history is not a repair.
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* ```ts
|
|
645
|
+
* import { matchesGitPath } from '@orkestrel/scaffold/server'
|
|
646
|
+
*
|
|
647
|
+
* matchesGitPath('.git') // true
|
|
648
|
+
* matchesGitPath('.git/config') // true
|
|
649
|
+
* matchesGitPath('.gitignore') // false
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
export declare function matchesGitPath(path: string): boolean;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Test whether a caught filesystem error reports an absent path.
|
|
656
|
+
*
|
|
657
|
+
* @param error - The caught value.
|
|
658
|
+
* @returns `true` only for an `Error` whose `code` is exactly `ENOENT`.
|
|
659
|
+
*
|
|
660
|
+
* @remarks
|
|
661
|
+
* The one place absence is told apart from failure. Every read here answers
|
|
662
|
+
* `undefined` or an empty result for a path that is not there and reports a path
|
|
663
|
+
* that is there but unreadable, so the two must never be read from the same
|
|
664
|
+
* caught value by eye. Total for any caught value, including a hostile one.
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```ts
|
|
668
|
+
* import { matchesMissingPath } from '@orkestrel/scaffold/server'
|
|
669
|
+
*
|
|
670
|
+
* matchesMissingPath(Object.assign(new Error('gone'), { code: 'ENOENT' })) // true
|
|
671
|
+
* matchesMissingPath(new Error('gone')) // false
|
|
672
|
+
* ```
|
|
673
|
+
*/
|
|
674
|
+
export declare function matchesMissingPath(error: unknown): boolean;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Test whether a destination still matches the narrower state a caller observed.
|
|
678
|
+
*
|
|
679
|
+
* @param precondition - The caller-observed state the write is held to.
|
|
680
|
+
* @returns `true` when the destination is absent as stated, or holds a physical
|
|
681
|
+
* file whose bytes digest to the stated value.
|
|
682
|
+
*
|
|
683
|
+
* @remarks
|
|
684
|
+
* Narrower than {@link matchesExpectation} on purpose. A caller observed bytes,
|
|
685
|
+
* not inodes and timestamps, so binding a write to a device identity it never
|
|
686
|
+
* saw would refuse writes that are perfectly safe — a file rewritten to
|
|
687
|
+
* identical bytes by an editor is still the file the caller read. A precondition
|
|
688
|
+
* that states no digest claims presence only.
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```ts
|
|
692
|
+
* import { matchesPrecondition } from '@orkestrel/scaffold/server'
|
|
693
|
+
*
|
|
694
|
+
* matchesPrecondition({ path: '/tmp/project/new.md', shape: 'absent' }) // true while absent
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
export declare function matchesPrecondition(precondition: WritePrecondition): boolean;
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Test whether a target-relative path is one no verb may delete.
|
|
701
|
+
*
|
|
702
|
+
* @param path - The target-relative path to classify.
|
|
703
|
+
* @returns `true` when the path must survive every verb this package runs.
|
|
704
|
+
*
|
|
705
|
+
* @remarks
|
|
706
|
+
* The deletion deny-list, stated as a rule over paths rather than as a list of
|
|
707
|
+
* directories. It is the inversion the contract asks for: the candidate set
|
|
708
|
+
* comes from an audit's foreign findings narrowed by what git tracks, and this
|
|
709
|
+
* is what that set is then measured against. Repository metadata is protected
|
|
710
|
+
* because losing history is not a repair, and a target's own `src` and `app`
|
|
711
|
+
* trees are protected because a workspace's source is the one thing scaffold
|
|
712
|
+
* never plans and never owns, whatever an audit reports about it.
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```ts
|
|
716
|
+
* import { matchesProtectedPath } from '@orkestrel/scaffold/server'
|
|
717
|
+
*
|
|
718
|
+
* matchesProtectedPath('src/core/index.ts') // true
|
|
719
|
+
* matchesProtectedPath('.git/config') // true
|
|
720
|
+
* matchesProtectedPath('.claude/agents/rogue.md') // false
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
export declare function matchesProtectedPath(path: string): boolean;
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Test whether a path names local configuration or a credential.
|
|
727
|
+
*
|
|
728
|
+
* @param path - The path to classify; either separator is read.
|
|
729
|
+
* @returns `true` when the path must never be copied into a vendored host.
|
|
730
|
+
*
|
|
731
|
+
* @remarks
|
|
732
|
+
* The vendoring deny-list. A host root is staged from a real checkout, so the
|
|
733
|
+
* refusal is stated over the path rather than over the file's content: a
|
|
734
|
+
* credential is recognizable by where it sits and what it is called long before
|
|
735
|
+
* anything reads it. Repository metadata is included through
|
|
736
|
+
* {@link matchesGitPath}, so one call answers the whole question and no caller
|
|
737
|
+
* has to remember to ask twice.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```ts
|
|
741
|
+
* import { matchesSensitivePath } from '@orkestrel/scaffold/server'
|
|
742
|
+
*
|
|
743
|
+
* matchesSensitivePath('.npmrc') // true
|
|
744
|
+
* matchesSensitivePath('.claude/settings.local.json') // true
|
|
745
|
+
* matchesSensitivePath('.claude/settings.json') // false
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
export declare function matchesSensitivePath(path: string): boolean;
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* The mutation spine: read the vendored host, re-derive the target, stage, swap.
|
|
752
|
+
*
|
|
753
|
+
* @remarks
|
|
754
|
+
* Every verb runs the same three steps. It snapshots each caller-supplied value
|
|
755
|
+
* and guards the snapshot, so a property backed by an accessor never reaches a
|
|
756
|
+
* decision. It re-derives what it is about to touch and compares that against
|
|
757
|
+
* the observation the caller handed in, refusing the whole call when anything
|
|
758
|
+
* moved. Only then does it open a {@link WriteTransaction}, which stages every
|
|
759
|
+
* byte in a private sibling directory and swaps them into place.
|
|
760
|
+
*
|
|
761
|
+
* The vendored host is read once, at construction, and cross-checked once. Its
|
|
762
|
+
* manifest authenticates its own membership, which is all a checksum sitting
|
|
763
|
+
* beside the data can do; matching that membership against the files actually
|
|
764
|
+
* stored is this class's job. The comparison is exact text and therefore exact
|
|
765
|
+
* case, so a manifest naming `agents.md` for a stored `AGENTS.md` is refused on
|
|
766
|
+
* a case-insensitive filesystem rather than silently resolved.
|
|
767
|
+
*
|
|
768
|
+
* What a mutation guarantees is exactly what {@link WriteTransaction}
|
|
769
|
+
* guarantees, and no more: a caught failure part way through a commit rolls the
|
|
770
|
+
* whole commit back, no destination ever receives half-written bytes, and a
|
|
771
|
+
* process killed mid-commit leaves a mixed target. This is not a journal and
|
|
772
|
+
* does not claim to be one.
|
|
773
|
+
*
|
|
774
|
+
* Every error is emitted on `error` immediately before it is thrown, so an
|
|
775
|
+
* observer sees a refusal even where the caller catches it.
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```ts
|
|
779
|
+
* import type { Plan } from '@orkestrel/scaffold'
|
|
780
|
+
* import { Materializer } from '@orkestrel/scaffold/server'
|
|
781
|
+
*
|
|
782
|
+
* declare const plan: Plan
|
|
783
|
+
*
|
|
784
|
+
* const materializer = new Materializer({ host: './dist/host' })
|
|
785
|
+
* materializer.materialize(plan, './packages/router')
|
|
786
|
+
* materializer.destroy()
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
export declare class Materializer implements MaterializerInterface {
|
|
790
|
+
#private;
|
|
791
|
+
/**
|
|
792
|
+
* Construct a materializer over one vendored host root.
|
|
793
|
+
*
|
|
794
|
+
* @param options - The vendored host root, the initial listeners, and the
|
|
795
|
+
* listener-error handler.
|
|
796
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
|
|
797
|
+
* is not an option bag this materializer accepts, and `TARGET` when the host
|
|
798
|
+
* carries a manifest that cannot be read or does not match what it stores.
|
|
799
|
+
*
|
|
800
|
+
* @remarks
|
|
801
|
+
* `host` defaults to this package's own vendored root, resolved from this
|
|
802
|
+
* module's own location so it never depends on the caller's working
|
|
803
|
+
* directory. A host carrying no manifest is read as a raw checkout and every
|
|
804
|
+
* artifact maps onto it one to one.
|
|
805
|
+
*
|
|
806
|
+
* The host is read here rather than on first use, so a broken vendored root
|
|
807
|
+
* fails at construction where the caller can still act on it, and so nothing
|
|
808
|
+
* has to carry a second flag recording whether the read has happened yet.
|
|
809
|
+
*/
|
|
810
|
+
constructor(options?: MaterializerOptions);
|
|
811
|
+
/** The materializer's observation channel. */
|
|
812
|
+
get emitter(): EmitterInterface<MaterializerEventMap>;
|
|
813
|
+
/**
|
|
814
|
+
* Compare a plan with a target through the vendored host that will repair it.
|
|
815
|
+
*
|
|
816
|
+
* @param plan - The compiled plan to compare.
|
|
817
|
+
* @param target - The directory to inspect.
|
|
818
|
+
* @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
|
|
819
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
820
|
+
* exact shape, `TARGET` when the host or target cannot be read within its
|
|
821
|
+
* bounds, and `DESTROYED` after teardown.
|
|
822
|
+
*
|
|
823
|
+
* @remarks
|
|
824
|
+
* Host directories expand before the target is read, so this method and
|
|
825
|
+
* {@link repair} compare the same paths with the same ownership. Foreign
|
|
826
|
+
* candidates are files beneath those expanded roots only; a root file never
|
|
827
|
+
* becomes a deletion candidate merely because its group is selected.
|
|
828
|
+
*/
|
|
829
|
+
audit(plan: Plan, target: string): Audit;
|
|
830
|
+
/**
|
|
831
|
+
* Write a plan into a vacant target.
|
|
832
|
+
*
|
|
833
|
+
* @param plan - The compiled plan to write.
|
|
834
|
+
* @param target - The directory to write into; it must hold nothing the plan would collide with.
|
|
835
|
+
* @returns The paths written and skipped.
|
|
836
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
837
|
+
* exact shape, `TARGET` when the target is not vacant or the host does not
|
|
838
|
+
* carry a planned artifact, `WRITE` when the write cannot be staged or
|
|
839
|
+
* committed, and `DESTROYED` after teardown.
|
|
840
|
+
*
|
|
841
|
+
* @remarks
|
|
842
|
+
* The plan's own bytes are not trusted: every host-origin artifact is re-read
|
|
843
|
+
* from the vendored root, so what lands is what this package ships rather than
|
|
844
|
+
* what a caller-built plan claimed it ships. A vendored directory expands into
|
|
845
|
+
* one artifact per file beneath it, and a vendored directory holding no file
|
|
846
|
+
* at all is created as an empty directory.
|
|
847
|
+
*/
|
|
848
|
+
materialize(plan: Plan, target: string): MaterializeResult;
|
|
849
|
+
/**
|
|
850
|
+
* Write a plan into an existing target, guided by an audit of it.
|
|
851
|
+
*
|
|
852
|
+
* @param plan - The compiled plan to write.
|
|
853
|
+
* @param audit - The preview returned by this materializer's `audit` method.
|
|
854
|
+
* @param target - The directory to write into.
|
|
855
|
+
* @returns The paths written and skipped, each decided by its artifact's ownership.
|
|
856
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
857
|
+
* exact shape, `TARGET` when the target moved since its audit, `WRITE` when
|
|
858
|
+
* the write cannot be staged or committed, and `DESTROYED` after teardown.
|
|
859
|
+
*
|
|
860
|
+
* @remarks
|
|
861
|
+
* The audit is a preview, not an instruction. The plan is hydrated and
|
|
862
|
+
* compared against the target again here, and the verdicts that produces must
|
|
863
|
+
* match the ones the audit carried for every path the plan owns; anything else
|
|
864
|
+
* means the target moved, and the whole call is refused. A missing destination
|
|
865
|
+
* is restored whatever its ownership; a stale one is replaced only where the
|
|
866
|
+
* artifact claims its bytes, which is what leaves a presence-owned file a
|
|
867
|
+
* consumer has edited exactly as it is.
|
|
868
|
+
*/
|
|
869
|
+
repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
|
|
870
|
+
/**
|
|
871
|
+
* Write fetched dependency guides to their local mirrors.
|
|
872
|
+
*
|
|
873
|
+
* @param mirrors - The fetched guides; each carries the local bytes its write is held to.
|
|
874
|
+
* @param target - The directory to write into.
|
|
875
|
+
* @returns The mirror paths written and skipped; a mirror already current is skipped.
|
|
876
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
877
|
+
* exact shape, `TARGET` when a mirror moved since it was fetched, `WRITE` when
|
|
878
|
+
* the write cannot be staged or committed, and `DESTROYED` after teardown.
|
|
879
|
+
*
|
|
880
|
+
* @remarks
|
|
881
|
+
* A verdict carrying no bytes carries a cause instead, so it is skipped rather
|
|
882
|
+
* than written: one unreachable package never costs the caller the rest of the
|
|
883
|
+
* fetch, and it never empties a mirror it could not replace.
|
|
884
|
+
*/
|
|
885
|
+
mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
|
|
886
|
+
/**
|
|
887
|
+
* Rewrite the marker-bounded package table in the target's catalog agent file.
|
|
888
|
+
*
|
|
889
|
+
* @param entries - The published packages the table should list.
|
|
890
|
+
* @param target - The directory to write into.
|
|
891
|
+
* @returns The catalog path, written when the region's bytes moved and skipped otherwise.
|
|
892
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
893
|
+
* exact shape, `TARGET` when the file is unreadable or carries no marked
|
|
894
|
+
* region, `WRITE` when the write cannot be staged or committed, and
|
|
895
|
+
* `DESTROYED` after teardown.
|
|
896
|
+
*
|
|
897
|
+
* @remarks
|
|
898
|
+
* Only the text between the two markers is replaced, so every word a consumer
|
|
899
|
+
* wrote around the table survives the call. A row whose lookup produced no
|
|
900
|
+
* version prints the cause it carries instead, because dropping the row would
|
|
901
|
+
* hide a package the organization publishes behind one failed request.
|
|
902
|
+
*/
|
|
903
|
+
catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
|
|
904
|
+
/**
|
|
905
|
+
* Rewrite the `@orkestrel/*` range set in the target's manifest.
|
|
906
|
+
*
|
|
907
|
+
* @param dependencies - The names and ranges the manifest should declare.
|
|
908
|
+
* @param target - The directory to write into.
|
|
909
|
+
* @returns The manifest path, written when a declared range moved and skipped otherwise.
|
|
910
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
911
|
+
* exact shape or names a package the manifest does not declare, `TARGET` when
|
|
912
|
+
* the manifest is unreadable, `WRITE` when the write cannot be staged or
|
|
913
|
+
* committed, and `DESTROYED` after teardown.
|
|
914
|
+
*
|
|
915
|
+
* @remarks
|
|
916
|
+
* No other part of the manifest is read back out or rewritten, so a consumer's
|
|
917
|
+
* own description, keywords, scripts, and formatting survive the call. Only a
|
|
918
|
+
* range already declared is rewritten: inserting a package would mean
|
|
919
|
+
* re-serializing the whole manifest, which is exactly the edit this verb
|
|
920
|
+
* promises not to make, so an undeclared name is refused by name instead.
|
|
921
|
+
*/
|
|
922
|
+
declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
|
|
923
|
+
/**
|
|
924
|
+
* Delete the files the plan does not own.
|
|
925
|
+
*
|
|
926
|
+
* @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
|
|
927
|
+
* @param repository - The target's git state; only a tracked path is ever deleted.
|
|
928
|
+
* @param target - The directory to delete from.
|
|
929
|
+
* @returns The paths removed.
|
|
930
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
|
|
931
|
+
* exact shape, `TARGET` when the tree carries uncommitted changes or a
|
|
932
|
+
* candidate moved since its audit, `WRITE` when the deletion cannot be staged
|
|
933
|
+
* or committed, and `DESTROYED` after teardown.
|
|
934
|
+
*
|
|
935
|
+
* @remarks
|
|
936
|
+
* The candidate set is re-derived and compared against the audit before
|
|
937
|
+
* anything moves, and every file is quarantined and re-verified rather than
|
|
938
|
+
* unlinked, so a failure part way through restores what it already took. The
|
|
939
|
+
* package's own source and application trees are never candidates, whatever
|
|
940
|
+
* the audit reports, and neither is anything git does not track: git is the
|
|
941
|
+
* recovery mechanism, so a path it cannot restore is not one this verb takes.
|
|
942
|
+
* A tree carrying uncommitted work is refused whole for the same reason.
|
|
943
|
+
*/
|
|
944
|
+
remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
|
|
945
|
+
/**
|
|
946
|
+
* Tear the materializer down. Every later call throws, and teardown is idempotent.
|
|
947
|
+
*
|
|
948
|
+
* @returns Nothing.
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```ts
|
|
952
|
+
* import { Materializer } from '@orkestrel/scaffold/server'
|
|
953
|
+
*
|
|
954
|
+
* const materializer = new Materializer()
|
|
955
|
+
* materializer.destroy()
|
|
956
|
+
* materializer.emitter.destroyed // true
|
|
957
|
+
* ```
|
|
958
|
+
*/
|
|
959
|
+
destroy(): void;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* The outcome of one mutation of a target.
|
|
964
|
+
*
|
|
965
|
+
* @remarks
|
|
966
|
+
* `written` names every path this call created or replaced. `skipped` names
|
|
967
|
+
* every path it considered and left alone, whether because the target already
|
|
968
|
+
* matched or because the artifact's ownership forbade touching it. `removed`
|
|
969
|
+
* names every path it deleted.
|
|
970
|
+
*/
|
|
971
|
+
export declare interface MaterializeResult {
|
|
972
|
+
readonly target: string;
|
|
973
|
+
readonly written: readonly string[];
|
|
974
|
+
readonly skipped: readonly string[];
|
|
975
|
+
readonly removed: readonly string[];
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** The materializer's observation channel. */
|
|
979
|
+
export declare type MaterializerEventMap = {
|
|
980
|
+
readonly write: readonly [path: string];
|
|
981
|
+
readonly remove: readonly [path: string];
|
|
982
|
+
readonly finish: readonly [result: MaterializeResult];
|
|
983
|
+
readonly error: readonly [error: unknown];
|
|
984
|
+
readonly destroy: readonly [];
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* The mutation contract: the package's only filesystem writer.
|
|
989
|
+
*
|
|
990
|
+
* @remarks
|
|
991
|
+
* Every method binds to the observation it was given. It re-derives what it is
|
|
992
|
+
* about to touch, compares that against the supplied preview, and refuses the
|
|
993
|
+
* whole call when membership or bytes moved, rather than racing to be the last
|
|
994
|
+
* writer.
|
|
995
|
+
*/
|
|
996
|
+
export declare interface MaterializerInterface {
|
|
997
|
+
readonly emitter: EmitterInterface<MaterializerEventMap>;
|
|
998
|
+
/**
|
|
999
|
+
* Compare a plan with a target through the vendored host that will repair it.
|
|
1000
|
+
*
|
|
1001
|
+
* @param plan - The compiled plan to compare.
|
|
1002
|
+
* @param target - The directory to inspect.
|
|
1003
|
+
* @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
|
|
1004
|
+
*/
|
|
1005
|
+
audit(plan: Plan, target: string): Audit;
|
|
1006
|
+
/**
|
|
1007
|
+
* Write a plan into a vacant target.
|
|
1008
|
+
*
|
|
1009
|
+
* @param plan - The compiled plan to write.
|
|
1010
|
+
* @param target - The directory to write into; it must hold nothing the plan would collide with.
|
|
1011
|
+
* @returns The paths written and skipped.
|
|
1012
|
+
*/
|
|
1013
|
+
materialize(plan: Plan, target: string): MaterializeResult;
|
|
1014
|
+
/**
|
|
1015
|
+
* Write a plan into an existing target, guided by an audit of it.
|
|
1016
|
+
*
|
|
1017
|
+
* @param plan - The compiled plan to write.
|
|
1018
|
+
* @param audit - The preview returned by this materializer's `audit` method.
|
|
1019
|
+
* @param target - The directory to write into.
|
|
1020
|
+
* @returns The paths written and skipped, each decided by its artifact's ownership.
|
|
1021
|
+
*/
|
|
1022
|
+
repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
|
|
1023
|
+
/**
|
|
1024
|
+
* Write fetched dependency guides to their local mirrors.
|
|
1025
|
+
*
|
|
1026
|
+
* @param mirrors - The fetched guides; each carries the local bytes its write is held to.
|
|
1027
|
+
* @param target - The directory to write into.
|
|
1028
|
+
* @returns The mirror paths written and skipped; a mirror already current is skipped.
|
|
1029
|
+
*/
|
|
1030
|
+
mirror(mirrors: readonly Mirror[], target: string): MaterializeResult;
|
|
1031
|
+
/**
|
|
1032
|
+
* Rewrite the marker-bounded package table in the target's catalog agent file.
|
|
1033
|
+
*
|
|
1034
|
+
* @param entries - The published packages the table should list.
|
|
1035
|
+
* @param target - The directory to write into.
|
|
1036
|
+
* @returns The catalog path, written when the region's bytes moved and skipped otherwise.
|
|
1037
|
+
*/
|
|
1038
|
+
catalog(entries: readonly CatalogEntry[], target: string): MaterializeResult;
|
|
1039
|
+
/**
|
|
1040
|
+
* Rewrite the `@orkestrel/*` range set in the target's manifest.
|
|
1041
|
+
*
|
|
1042
|
+
* @param dependencies - The names and ranges the manifest should declare.
|
|
1043
|
+
* @param target - The directory to write into.
|
|
1044
|
+
* @returns The manifest path, written when a declared range moved and skipped otherwise.
|
|
1045
|
+
*
|
|
1046
|
+
* @remarks
|
|
1047
|
+
* No other part of the manifest is read back out or rewritten, so a
|
|
1048
|
+
* consumer's own description, keywords, and scripts survive the call.
|
|
1049
|
+
*/
|
|
1050
|
+
declare(dependencies: readonly Dependency[], target: string): MaterializeResult;
|
|
1051
|
+
/**
|
|
1052
|
+
* Delete the files the plan does not own.
|
|
1053
|
+
*
|
|
1054
|
+
* @param audit - The preview returned by this materializer's `audit` method; its foreign findings are the candidate set.
|
|
1055
|
+
* @param repository - The target's git state; only a tracked path is ever deleted.
|
|
1056
|
+
* @param target - The directory to delete from.
|
|
1057
|
+
* @returns The paths removed.
|
|
1058
|
+
*
|
|
1059
|
+
* @remarks
|
|
1060
|
+
* The candidate set is re-derived and compared against the audit before
|
|
1061
|
+
* anything moves, and every file is quarantined and re-verified rather than
|
|
1062
|
+
* unlinked, so a failure part way through restores what it already took.
|
|
1063
|
+
* The package's own source and application trees are never candidates,
|
|
1064
|
+
* whatever the audit reports.
|
|
1065
|
+
*/
|
|
1066
|
+
remove(audit: Audit, repository: Repository, target: string): MaterializeResult;
|
|
1067
|
+
/**
|
|
1068
|
+
* Tear the materializer down. Every later call throws, and teardown is idempotent.
|
|
1069
|
+
*
|
|
1070
|
+
* @returns Nothing.
|
|
1071
|
+
*/
|
|
1072
|
+
destroy(): void;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Options for the materializer.
|
|
1077
|
+
*
|
|
1078
|
+
* @remarks
|
|
1079
|
+
* `host` is the vendored data root host-origin artifacts are copied from. It
|
|
1080
|
+
* defaults to this package's own vendored root, resolved from the installed
|
|
1081
|
+
* module's location rather than the caller's working directory. A host that
|
|
1082
|
+
* carries no manifest beside it maps artifact paths one to one instead of
|
|
1083
|
+
* through the manifest.
|
|
1084
|
+
*/
|
|
1085
|
+
export declare interface MaterializerOptions {
|
|
1086
|
+
readonly host?: string;
|
|
1087
|
+
readonly on?: EmitterHooks<MaterializerEventMap>;
|
|
1088
|
+
readonly error?: EmitterErrorHandler;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** Maximum characters one guide branch may carry. */
|
|
1092
|
+
export declare const MAX_BRANCH_LENGTH = 255;
|
|
1093
|
+
|
|
1094
|
+
/** Maximum characters one caller-supplied upstream endpoint may carry. */
|
|
1095
|
+
export declare const MAX_ENDPOINT_LENGTH = 2048;
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Maximum paths one target's working-tree inventory may report.
|
|
1099
|
+
*
|
|
1100
|
+
* @remarks
|
|
1101
|
+
* Far above the core collection ceiling, and deliberately so. A tracked or dirty
|
|
1102
|
+
* path list is the target repository's own fact rather than an argument a caller
|
|
1103
|
+
* authored, so measuring it against the ceiling that bounds a public collection
|
|
1104
|
+
* would read a legitimately large checkout as hostile and refuse the deletion
|
|
1105
|
+
* verb on it.
|
|
1106
|
+
*/
|
|
1107
|
+
export declare const MAX_INVENTORY_PATHS = 100000;
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Maximum segments one host path may carry.
|
|
1111
|
+
*
|
|
1112
|
+
* @remarks
|
|
1113
|
+
* Bounds the work a path decision costs before any filesystem call is made. With
|
|
1114
|
+
* {@link MAX_PATH_SEGMENT_BYTES} it is also the real length ceiling of an
|
|
1115
|
+
* accepted path, well inside the core character ceiling.
|
|
1116
|
+
*/
|
|
1117
|
+
export declare const MAX_PATH_DEPTH = 64;
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Maximum UTF-8 bytes one host path segment may encode to.
|
|
1121
|
+
*
|
|
1122
|
+
* @remarks
|
|
1123
|
+
* The limit every supported filesystem shares for a single name. It is a byte
|
|
1124
|
+
* count rather than a character count, because that is the unit the filesystem
|
|
1125
|
+
* imposes it in: eighty-six three-byte characters are already past it.
|
|
1126
|
+
*/
|
|
1127
|
+
export declare const MAX_PATH_SEGMENT_BYTES = 255;
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Maximum simultaneous upstream requests.
|
|
1131
|
+
*
|
|
1132
|
+
* @remarks
|
|
1133
|
+
* A ceiling rather than a default: the reader picks what it opens by, and this
|
|
1134
|
+
* is only what a caller may raise it to.
|
|
1135
|
+
*/
|
|
1136
|
+
export declare const MAX_UPSTREAM_CONCURRENCY = 64;
|
|
1137
|
+
|
|
1138
|
+
/** Maximum retries one upstream request may be given after a transport fault. */
|
|
1139
|
+
export declare const MAX_UPSTREAM_RETRIES = 5;
|
|
1140
|
+
|
|
1141
|
+
/** Maximum timeout one upstream request may be given, in milliseconds. */
|
|
1142
|
+
export declare const MAX_UPSTREAM_TIMEOUT = 300000;
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Project a target-relative path to the storage name a vendored host holds it under.
|
|
1146
|
+
*
|
|
1147
|
+
* @param path - The target-relative path the file is written to.
|
|
1148
|
+
* @returns The storage name beneath the host root.
|
|
1149
|
+
*
|
|
1150
|
+
* @remarks
|
|
1151
|
+
* A staged host is a plain directory that npm packs, and npm's own ignore rules
|
|
1152
|
+
* would drop a leading-dot entry from the tarball. So every dot that opens a
|
|
1153
|
+
* segment comes off, and a dotted file at the root moves under `dotfiles/` to
|
|
1154
|
+
* keep it from colliding with an undotted sibling of the same name. The mapping
|
|
1155
|
+
* is one direction only: a staged host's manifest records the destination each
|
|
1156
|
+
* storage name answers for, so the reader never re-derives this.
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* ```ts
|
|
1160
|
+
* import { pathToStorage } from '@orkestrel/scaffold/server'
|
|
1161
|
+
*
|
|
1162
|
+
* pathToStorage('.gitignore') // 'dotfiles/gitignore'
|
|
1163
|
+
* pathToStorage('.claude/rules/names.md') // 'claude/rules/names.md'
|
|
1164
|
+
* pathToStorage('AGENTS.md') // 'AGENTS.md'
|
|
1165
|
+
* ```
|
|
1166
|
+
*/
|
|
1167
|
+
export declare function pathToStorage(path: string): string;
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Capture one directory's physical identity.
|
|
1171
|
+
*
|
|
1172
|
+
* @param path - The resolved directory path to capture.
|
|
1173
|
+
* @returns The anchor, or `undefined` when the path is not a physical directory.
|
|
1174
|
+
*
|
|
1175
|
+
* @remarks
|
|
1176
|
+
* Device and inode rather than the path, because the path is the thing that can
|
|
1177
|
+
* be swapped underneath a write. An anchor captured before a mutation and
|
|
1178
|
+
* checked again after it is what proves the directory written into is the
|
|
1179
|
+
* directory that was inspected.
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* ```ts
|
|
1183
|
+
* import { readAnchor } from '@orkestrel/scaffold/server'
|
|
1184
|
+
*
|
|
1185
|
+
* readAnchor('/tmp/project') // { path: '/tmp/project', device: 1, inode: 2 }
|
|
1186
|
+
* ```
|
|
1187
|
+
*/
|
|
1188
|
+
export declare function readAnchor(path: string): WriteAnchor | undefined;
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Capture what one destination holds before a write.
|
|
1192
|
+
*
|
|
1193
|
+
* @param path - The resolved destination path to capture.
|
|
1194
|
+
* @returns The expectation, or `undefined` when the destination is a link or a
|
|
1195
|
+
* shape this package will not write over.
|
|
1196
|
+
*
|
|
1197
|
+
* @remarks
|
|
1198
|
+
* Absence is a captured state rather than a failure, because most writes expect
|
|
1199
|
+
* exactly that. Each shape carries only the facts it supplies: a directory
|
|
1200
|
+
* carries its identity, a file carries its identity, size, and bytes, and an
|
|
1201
|
+
* absent destination carries nothing at all. A file past the artifact ceiling
|
|
1202
|
+
* carries no digest and is bound by its identity, size, and modification time
|
|
1203
|
+
* alone, which is the strongest honest claim about bytes nobody read.
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* ```ts
|
|
1207
|
+
* import { readExpectation } from '@orkestrel/scaffold/server'
|
|
1208
|
+
*
|
|
1209
|
+
* readExpectation('/tmp/project/absent.md') // { path: …, shape: 'absent' }
|
|
1210
|
+
* ```
|
|
1211
|
+
*/
|
|
1212
|
+
export declare function readExpectation(path: string): WriteExpectation | undefined;
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Read one contained file as its exact bytes in lowercase hexadecimal.
|
|
1216
|
+
*
|
|
1217
|
+
* @param root - The containing host directory.
|
|
1218
|
+
* @param path - The portable root-relative file path.
|
|
1219
|
+
* @param limit - The most bytes this read accepts; the artifact ceiling by default.
|
|
1220
|
+
* @returns The exact bytes as hexadecimal, or `undefined` when the file is
|
|
1221
|
+
* absent, is not a physical readable file, is past `limit`, or moved while it
|
|
1222
|
+
* was being read.
|
|
1223
|
+
* @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
|
|
1224
|
+
* `path` leaves `root`.
|
|
1225
|
+
*
|
|
1226
|
+
* @remarks
|
|
1227
|
+
* Hexadecimal rather than text, because this is what a byte comparison is stated
|
|
1228
|
+
* in everywhere in this package: a plan's artifact, an audit finding, and a
|
|
1229
|
+
* snapshot all compare as the same digits. The file's identity and size are
|
|
1230
|
+
* measured before and after the read, and one extra byte is requested past the
|
|
1231
|
+
* declared size, so a file that grew or was replaced mid-read answers
|
|
1232
|
+
* `undefined` rather than half of two files.
|
|
1233
|
+
*
|
|
1234
|
+
* @example
|
|
1235
|
+
* ```ts
|
|
1236
|
+
* import { readFileHex } from '@orkestrel/scaffold/server'
|
|
1237
|
+
*
|
|
1238
|
+
* readFileHex('/tmp/project', 'AGENTS.md') // '2320416765…'
|
|
1239
|
+
* ```
|
|
1240
|
+
*/
|
|
1241
|
+
export declare function readFileHex(root: string, path: string, limit?: number): string | undefined;
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Read one contained file as bounded UTF-8 text.
|
|
1245
|
+
*
|
|
1246
|
+
* @param root - The containing host directory.
|
|
1247
|
+
* @param path - The portable root-relative file path.
|
|
1248
|
+
* @param limit - The most bytes this read accepts; the artifact ceiling by default.
|
|
1249
|
+
* @returns The decoded text, or `undefined` when {@link readFileHex} answers
|
|
1250
|
+
* nothing or the bytes are not valid UTF-8.
|
|
1251
|
+
* @throws `ScaffoldError('INVALID', …)` when the arguments are off contract or
|
|
1252
|
+
* `path` leaves `root`.
|
|
1253
|
+
*
|
|
1254
|
+
* @remarks
|
|
1255
|
+
* Decoding is strict, so a file carrying an invalid sequence answers `undefined`
|
|
1256
|
+
* rather than text carrying replacement characters. That matters because the
|
|
1257
|
+
* text is parsed next: a manifest silently repaired into valid JSON by lossy
|
|
1258
|
+
* decoding would be trusted.
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* ```ts
|
|
1262
|
+
* import { readFileText } from '@orkestrel/scaffold/server'
|
|
1263
|
+
*
|
|
1264
|
+
* readFileText('/tmp/project', 'package.json') // '{ "name": "@orkestrel/router", … }'
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
export declare function readFileText(root: string, path: string, limit?: number): string | undefined;
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Read a vendored host's manifest, when it carries one.
|
|
1271
|
+
*
|
|
1272
|
+
* @param host - The vendored host root to read.
|
|
1273
|
+
* @returns The manifest, or `undefined` when the host carries none.
|
|
1274
|
+
* @throws `ScaffoldError('INVALID', …)` when `host` is not a host path.
|
|
1275
|
+
* @throws `ScaffoldError('TARGET', …)` when the manifest is there but cannot be
|
|
1276
|
+
* read, is not the declared shape, or does not match its own membership.
|
|
1277
|
+
*
|
|
1278
|
+
* @remarks
|
|
1279
|
+
* The two failures are held apart deliberately. A host with no manifest is a
|
|
1280
|
+
* raw checkout, and a caller reads it by mapping each path one to one. A host
|
|
1281
|
+
* with a manifest that does not verify is a staged host that has been edited,
|
|
1282
|
+
* and answering `undefined` there would degrade it to that same one-to-one
|
|
1283
|
+
* mapping — which is how an edited manifest would get a caller to read files it
|
|
1284
|
+
* never declared. So absence answers and corruption throws.
|
|
1285
|
+
*
|
|
1286
|
+
* Verification here is the manifest's own self-consistency: the digest against
|
|
1287
|
+
* the exact membership beside it. Whether that membership matches the files
|
|
1288
|
+
* actually stored is a separate question, and it belongs to the reader that
|
|
1289
|
+
* walks the host.
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```ts
|
|
1293
|
+
* import { readHostManifest } from '@orkestrel/scaffold/server'
|
|
1294
|
+
*
|
|
1295
|
+
* readHostManifest('./dist/host') // the manifest, or undefined for a raw root
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
export declare function readHostManifest(host: string): HostManifest | undefined;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Derive one vendored-host manifest entry from a file in a checkout.
|
|
1302
|
+
*
|
|
1303
|
+
* @param destination - The target-relative path the file is written to.
|
|
1304
|
+
* @param source - The resolved host path the bytes are read from.
|
|
1305
|
+
* @returns The entry, or `undefined` when `source` is not a physical file this
|
|
1306
|
+
* package will vendor or carries more bytes than one artifact may.
|
|
1307
|
+
*
|
|
1308
|
+
* @remarks
|
|
1309
|
+
* The one place the three declared fields are decided together, because they are
|
|
1310
|
+
* three readings of one file: {@link pathToStorage} decides where it is stored,
|
|
1311
|
+
* the destination is the path it answers for, and the executable bit is read
|
|
1312
|
+
* from the source's own mode.
|
|
1313
|
+
*
|
|
1314
|
+
* That mode is the honest limit of this reading. A Windows host reports no
|
|
1315
|
+
* executable bit at all, so a host staged there declares every entry
|
|
1316
|
+
* non-executable and a consumer receives scripts without it. Staging on a POSIX
|
|
1317
|
+
* host is what carries the bit through.
|
|
1318
|
+
*
|
|
1319
|
+
* @example
|
|
1320
|
+
* ```ts
|
|
1321
|
+
* import { readManifestEntry } from '@orkestrel/scaffold/server'
|
|
1322
|
+
*
|
|
1323
|
+
* readManifestEntry('.gitignore', '/tmp/checkout/.gitignore')
|
|
1324
|
+
* // { storage: 'dotfiles/gitignore', destination: '.gitignore', executable: false }
|
|
1325
|
+
* ```
|
|
1326
|
+
*/
|
|
1327
|
+
export declare function readManifestEntry(destination: string, source: string): ManifestEntry | undefined;
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Read a target's current bytes at the paths a plan claims.
|
|
1331
|
+
*
|
|
1332
|
+
* @param target - The target directory to read.
|
|
1333
|
+
* @param paths - The plan-relative paths to probe.
|
|
1334
|
+
* @returns One entry per path that is there: a file maps to its exact bytes as
|
|
1335
|
+
* hexadecimal and a directory maps to `''`, which records presence with no bytes
|
|
1336
|
+
* to compare. An absent path is omitted.
|
|
1337
|
+
* @throws `ScaffoldError('INVALID', …)` when `target` is not a host path or
|
|
1338
|
+
* `paths` is not a bounded list of plannable paths.
|
|
1339
|
+
* @throws `ScaffoldError('TARGET', …)` when a path that is there cannot be read,
|
|
1340
|
+
* or when the whole read would retain more bytes than one plan may.
|
|
1341
|
+
*
|
|
1342
|
+
* @remarks
|
|
1343
|
+
* The one door from a real directory into the vocabulary an audit compares in.
|
|
1344
|
+
* Absence is omission rather than an empty value, because core reads a missing
|
|
1345
|
+
* key as a missing destination and an empty string as a present directory; the
|
|
1346
|
+
* two are different verdicts. A path that is there but unreadable throws instead
|
|
1347
|
+
* of being omitted, because omission would report it as missing and a repair
|
|
1348
|
+
* would then overwrite whatever is actually sitting there.
|
|
1349
|
+
*
|
|
1350
|
+
* @example
|
|
1351
|
+
* ```ts
|
|
1352
|
+
* import { readSnapshot } from '@orkestrel/scaffold/server'
|
|
1353
|
+
*
|
|
1354
|
+
* readSnapshot('./packages/router', ['package.json', 'guides'])
|
|
1355
|
+
* // { 'package.json': '7b226e…', guides: '' }
|
|
1356
|
+
* ```
|
|
1357
|
+
*/
|
|
1358
|
+
export declare function readSnapshot(target: string, paths: readonly string[]): Snapshot;
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* What git reports about a target's working tree.
|
|
1362
|
+
*
|
|
1363
|
+
* @remarks
|
|
1364
|
+
* `tracked` is the only set a deletion may draw from: git does not report the
|
|
1365
|
+
* loss of an untracked path and `git diff` cannot restore it, so an ignored
|
|
1366
|
+
* path such as an installed dependency tree, a build output, or an editor
|
|
1367
|
+
* directory survives every verb. `dirty` is every path carrying an uncommitted
|
|
1368
|
+
* change, taken repo-wide rather than over a write set, because deletion makes
|
|
1369
|
+
* the write set the whole workspace. A clean tree is an empty `dirty`. A target
|
|
1370
|
+
* that is not a git repository yields no `Repository` at all, so the caller
|
|
1371
|
+
* decides what to do about that rather than reading it out of an invented
|
|
1372
|
+
* empty value.
|
|
1373
|
+
*/
|
|
1374
|
+
export declare interface Repository {
|
|
1375
|
+
readonly tracked: readonly string[];
|
|
1376
|
+
readonly dirty: readonly string[];
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* The Windows device names that stay reserved even when an extension follows.
|
|
1381
|
+
*
|
|
1382
|
+
* @remarks
|
|
1383
|
+
* Refused on every host rather than only on Windows. A generated workspace is
|
|
1384
|
+
* checked out on all of them, so a directory this package writes on Linux must
|
|
1385
|
+
* still be a name Windows can hold.
|
|
1386
|
+
*/
|
|
1387
|
+
export declare const RESERVED_SEGMENT_PATTERN: RegExp;
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Resolve a root-relative path and refuse one that leaves its root.
|
|
1391
|
+
*
|
|
1392
|
+
* @param root - The containing host directory.
|
|
1393
|
+
* @param path - The portable root-relative path.
|
|
1394
|
+
* @returns The destination as this package will address it, or `undefined` when
|
|
1395
|
+
* either argument is off contract or the destination lies outside `root`.
|
|
1396
|
+
*
|
|
1397
|
+
* @remarks
|
|
1398
|
+
* The containment law, and the one door every read in this module goes through.
|
|
1399
|
+
* Both sides are resolved through the real filesystem before they are compared,
|
|
1400
|
+
* so a link planted inside the root cannot smuggle a destination out of it; the
|
|
1401
|
+
* answer is then the lexical join, so the caller operates on the path it named
|
|
1402
|
+
* rather than on a resolved form the target may not recognize.
|
|
1403
|
+
*
|
|
1404
|
+
* Comparison is exact text, which fails closed on a case-insensitive
|
|
1405
|
+
* filesystem: a root and a path spelled with different case resolve to
|
|
1406
|
+
* different strings there and are refused, never wrongly admitted.
|
|
1407
|
+
*
|
|
1408
|
+
* @example
|
|
1409
|
+
* ```ts
|
|
1410
|
+
* import { resolveContainedPath } from '@orkestrel/scaffold/server'
|
|
1411
|
+
*
|
|
1412
|
+
* resolveContainedPath('/tmp/project', 'guides/router.md') // '/tmp/project/guides/router.md'
|
|
1413
|
+
* resolveContainedPath('/tmp/project', '../secrets') // undefined
|
|
1414
|
+
* ```
|
|
1415
|
+
*/
|
|
1416
|
+
export declare function resolveContainedPath(root: string, path: string): string | undefined;
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Resolve a path through the real filesystem, keeping the part that does not exist yet.
|
|
1420
|
+
*
|
|
1421
|
+
* @param path - The absolute or relative host path to resolve.
|
|
1422
|
+
* @returns The path with its existing prefix resolved through every link, or
|
|
1423
|
+
* `undefined` when the text is not a host path, no bounded existing ancestor
|
|
1424
|
+
* resolves, or an ancestor cannot be read.
|
|
1425
|
+
*
|
|
1426
|
+
* @remarks
|
|
1427
|
+
* A containment decision has to be made about a destination that does not exist
|
|
1428
|
+
* yet, and a lexical answer is not enough: a link anywhere in the existing
|
|
1429
|
+
* prefix moves the destination somewhere the text never named. So the deepest
|
|
1430
|
+
* existing ancestor is resolved and the remaining segments are re-joined onto
|
|
1431
|
+
* it. The climb is bounded by the path-depth ceiling, so an adversarial path
|
|
1432
|
+
* cannot make it walk indefinitely.
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* ```ts
|
|
1436
|
+
* import { resolveRealPath } from '@orkestrel/scaffold/server'
|
|
1437
|
+
*
|
|
1438
|
+
* resolveRealPath('./packages/new/src') // the real path of `packages`, plus `new/src`
|
|
1439
|
+
* ```
|
|
1440
|
+
*/
|
|
1441
|
+
export declare function resolveRealPath(path: string): string | undefined;
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Stage a vendored host root from a real checkout.
|
|
1445
|
+
*
|
|
1446
|
+
* @param checkout - The checkout the vendored paths are read from.
|
|
1447
|
+
* @param host - The vendored host root to fill; it must be absent or empty.
|
|
1448
|
+
* @returns One entry per staged file, sorted by storage name.
|
|
1449
|
+
* @throws `ScaffoldError('INVALID', …)` when either argument is not a host path
|
|
1450
|
+
* or a vendored path leaves the checkout or the host root.
|
|
1451
|
+
* @throws `ScaffoldError('TARGET', …)` when the checkout is not a directory, the
|
|
1452
|
+
* host root is not vacant, the checkout does not carry every vendored path, two
|
|
1453
|
+
* vendored files claim one storage name, a vendored file is not a plain file
|
|
1454
|
+
* within the artifact ceiling, or the staged manifest does not read back.
|
|
1455
|
+
* @throws `ScaffoldError('WRITE', …)` when a staged file or the manifest cannot
|
|
1456
|
+
* be written.
|
|
1457
|
+
*
|
|
1458
|
+
* @remarks
|
|
1459
|
+
* This is the producer half of the vendored host, and it is not the mutation
|
|
1460
|
+
* contract `MaterializerInterface` states. That contract owns **target**
|
|
1461
|
+
* writes: it materializes a compiled plan into a consumer's workspace, binds
|
|
1462
|
+
* every destination to what the caller observed, and rolls a failed commit back.
|
|
1463
|
+
* This reads this package's own checkout at build time and fills its own build
|
|
1464
|
+
* output. Different direction, different lifetime, no consumer target involved,
|
|
1465
|
+
* so the two do not overlap and neither one belongs inside the other.
|
|
1466
|
+
*
|
|
1467
|
+
* Staging is plain rather than transactional for the same reason. A
|
|
1468
|
+
* `WriteTransaction` exists to hold a directory that already holds work
|
|
1469
|
+
* still; a build output holds nothing, is deleted whole before every build, and
|
|
1470
|
+
* has no concurrent reader. What replaces it is refusing early and ordering the
|
|
1471
|
+
* writes: the whole membership is derived before anything is created, so a
|
|
1472
|
+
* checkout this refuses leaves no host root at all, and `manifest.json` is
|
|
1473
|
+
* written last, so a stage that failed part way through leaves a root every
|
|
1474
|
+
* reader treats as a raw checkout and fails loudly on.
|
|
1475
|
+
*
|
|
1476
|
+
* A missing vendored path is refused rather than staged around. A partial root
|
|
1477
|
+
* is not detectably partial: it fails later, in a consumer's terminal, on
|
|
1478
|
+
* whichever path the plan reached first. Refusing here fails the build that
|
|
1479
|
+
* produced it, where the maintainer can act, and it names every missing path at
|
|
1480
|
+
* once. A directory is the same case — declaring an absent directory as an empty
|
|
1481
|
+
* root would create an empty directory in every generated workspace.
|
|
1482
|
+
*
|
|
1483
|
+
* The vendoring deny-list applies to what the walk discovers beneath a vendored
|
|
1484
|
+
* directory, where a maintainer's local credential can legitimately sit, and
|
|
1485
|
+
* such a path is skipped. A path `HOST_PATHS` names itself is curated data
|
|
1486
|
+
* rather than discovery, so it is staged or the stage is refused.
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* ```ts
|
|
1490
|
+
* import { stageHost } from '@orkestrel/scaffold/server'
|
|
1491
|
+
*
|
|
1492
|
+
* stageHost(process.cwd(), 'dist/host').length // the files staged
|
|
1493
|
+
* ```
|
|
1494
|
+
*/
|
|
1495
|
+
export declare function stageHost(checkout: string, host: string): readonly ManifestEntry[];
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* The reading spine: one bounded, unauthenticated, redirect-free request per answer.
|
|
1499
|
+
*
|
|
1500
|
+
* @remarks
|
|
1501
|
+
* This is the package's only network reader, and it never writes. Every call
|
|
1502
|
+
* opens one byte allowance and spends it across every read the call makes, so a
|
|
1503
|
+
* caller is bounded twice over: `limit` refuses one oversized answer and
|
|
1504
|
+
* `budget` refuses many small ones. The two are separate exhaustion routes and
|
|
1505
|
+
* neither bound covers the other.
|
|
1506
|
+
*
|
|
1507
|
+
* A per-package failure never escapes as a throw. It is projected into the
|
|
1508
|
+
* verdict that package's row already carries — `missing` for an upstream `404`,
|
|
1509
|
+
* which is a definite answer, and `failed` for a transport fault, which is no
|
|
1510
|
+
* answer at all — so one unreachable package never costs the caller the rest of
|
|
1511
|
+
* the answer. The organization package list is the one exception: without it
|
|
1512
|
+
* there is no fleet to report, so an unreachable or malformed list is a coded
|
|
1513
|
+
* `FETCH` failure.
|
|
1514
|
+
*
|
|
1515
|
+
* Requests are unauthenticated because every fleet repository is public, and
|
|
1516
|
+
* they follow no redirect, so a misconfigured or hostile endpoint cannot move a
|
|
1517
|
+
* read to another host. Each one is bounded by its endpoint's timeout and by the
|
|
1518
|
+
* reader's own abort signal, so {@link Upstream.destroy} cancels what is in
|
|
1519
|
+
* flight instead of waiting for it.
|
|
1520
|
+
*
|
|
1521
|
+
* The allowance is threaded through the private reads as a mutable
|
|
1522
|
+
* `{ remaining: number }` carrier rather than held on the instance, because
|
|
1523
|
+
* concurrent calls each own their own budget and must not spend each other's.
|
|
1524
|
+
*
|
|
1525
|
+
* @example
|
|
1526
|
+
* ```ts
|
|
1527
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
1528
|
+
*
|
|
1529
|
+
* const upstream = new Upstream({ registry: { timeout: 5_000 } })
|
|
1530
|
+
* const releases = await upstream.lookup([{ name: '@orkestrel/emitter', range: '^0.0.5' }])
|
|
1531
|
+
* upstream.destroy()
|
|
1532
|
+
* ```
|
|
1533
|
+
*/
|
|
1534
|
+
export declare class Upstream implements UpstreamInterface {
|
|
1535
|
+
#private;
|
|
1536
|
+
/**
|
|
1537
|
+
* Construct a reader over one guide host and one registry.
|
|
1538
|
+
*
|
|
1539
|
+
* @param options - The two endpoints, the request bounds, the initial
|
|
1540
|
+
* listeners, and the listener-error handler.
|
|
1541
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `options` is present but
|
|
1542
|
+
* is not an option bag this reader accepts, or when either endpoint names a
|
|
1543
|
+
* scheme, host, or form this reader will not request.
|
|
1544
|
+
*
|
|
1545
|
+
* @remarks
|
|
1546
|
+
* `isEndpoint` bounds an endpoint's length and nothing else, so the scheme and
|
|
1547
|
+
* host law is settled here, where a refusal can say which endpoint was refused
|
|
1548
|
+
* and why. An endpoint must be HTTPS, or HTTP to a loopback host — the one
|
|
1549
|
+
* place an unencrypted request has no network between the two ends. That
|
|
1550
|
+
* refuses `file:`, `data:`, and plain HTTP to a real host, and it is what
|
|
1551
|
+
* keeps a fixture reachable without weakening transport security anywhere a
|
|
1552
|
+
* real request goes. An endpoint carrying credentials, a query, or a fragment
|
|
1553
|
+
* is refused too: this reader authenticates nothing and appends its own path.
|
|
1554
|
+
*/
|
|
1555
|
+
constructor(options?: UpstreamOptions);
|
|
1556
|
+
/** The upstream reader's observation channel. */
|
|
1557
|
+
get emitter(): EmitterInterface<UpstreamEventMap>;
|
|
1558
|
+
/**
|
|
1559
|
+
* Look up the registry's latest release for each declared dependency.
|
|
1560
|
+
*
|
|
1561
|
+
* @param dependencies - The declared dependencies to look up.
|
|
1562
|
+
* @returns One release verdict per dependency, in input order.
|
|
1563
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `dependencies` is not a
|
|
1564
|
+
* bounded list of declared dependencies, and `DESTROYED` when the reader is
|
|
1565
|
+
* torn down before or during the call.
|
|
1566
|
+
*
|
|
1567
|
+
* @remarks
|
|
1568
|
+
* Whether the declared range already admits the reported version is not
|
|
1569
|
+
* decided here and is not stored on the verdict: it is a function of the
|
|
1570
|
+
* `range` and `latest` sitting beside each other, and one centralized helper
|
|
1571
|
+
* answers it for every caller.
|
|
1572
|
+
*
|
|
1573
|
+
* @example
|
|
1574
|
+
* ```ts
|
|
1575
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
1576
|
+
*
|
|
1577
|
+
* const upstream = new Upstream()
|
|
1578
|
+
* await upstream.lookup([{ name: '@orkestrel/router', range: '^0.0.8' }])
|
|
1579
|
+
* upstream.destroy()
|
|
1580
|
+
* ```
|
|
1581
|
+
*/
|
|
1582
|
+
lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
|
|
1583
|
+
/**
|
|
1584
|
+
* Fetch each named package's guide, beside the local mirror it answers for.
|
|
1585
|
+
*
|
|
1586
|
+
* @param names - The packages to fetch: the target's declared set, or the whole organization.
|
|
1587
|
+
* @param current - The target's local mirrors as exact bytes, keyed by mirror path.
|
|
1588
|
+
* @returns One mirror verdict per name, in input order.
|
|
1589
|
+
* @throws {@link ScaffoldError} coded `INVALID` when `names` is not a bounded
|
|
1590
|
+
* list of fleet package names or `current` is not a snapshot, and `DESTROYED`
|
|
1591
|
+
* when the reader is torn down before or during the call.
|
|
1592
|
+
*
|
|
1593
|
+
* @remarks
|
|
1594
|
+
* The mirror path is derived from the package name by the same helper the plan
|
|
1595
|
+
* derives it with, and the fetched URL ends in that exact path, so a verdict
|
|
1596
|
+
* always answers for the file it names. `observed` is carried through from
|
|
1597
|
+
* `current` whatever the lookup produced, because it is the precondition the
|
|
1598
|
+
* later write is held to and a failed fetch does not change what the target
|
|
1599
|
+
* currently holds.
|
|
1600
|
+
*
|
|
1601
|
+
* @example
|
|
1602
|
+
* ```ts
|
|
1603
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
1604
|
+
*
|
|
1605
|
+
* const upstream = new Upstream()
|
|
1606
|
+
* await upstream.fetch(['@orkestrel/router'], { 'guides/router.md': '2320526f75746572' })
|
|
1607
|
+
* upstream.destroy()
|
|
1608
|
+
* ```
|
|
1609
|
+
*/
|
|
1610
|
+
fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
|
|
1611
|
+
/**
|
|
1612
|
+
* Catalog the published fleet from the registry's organization package list.
|
|
1613
|
+
*
|
|
1614
|
+
* @returns One row per published package, sorted by name.
|
|
1615
|
+
* @throws {@link ScaffoldError} coded `FETCH` when the organization package
|
|
1616
|
+
* list is unreachable, malformed, empty, or larger than one bounded
|
|
1617
|
+
* collection, and `DESTROYED` when the reader is torn down before or during
|
|
1618
|
+
* the call.
|
|
1619
|
+
*
|
|
1620
|
+
* @remarks
|
|
1621
|
+
* The organization list is the exact membership the registry publishes, not a
|
|
1622
|
+
* relevance search, and it is the whole reason this answer can claim to be the
|
|
1623
|
+
* fleet. So it fails hard where a per-package lookup fails soft: an empty or
|
|
1624
|
+
* unreadable list would otherwise be written into a target's catalog table as
|
|
1625
|
+
* a fleet with no packages in it. A package whose own version lookup then
|
|
1626
|
+
* fails still keeps its row, carrying the cause instead of a version, because
|
|
1627
|
+
* the list already proved the package is published.
|
|
1628
|
+
*
|
|
1629
|
+
* @example
|
|
1630
|
+
* ```ts
|
|
1631
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
1632
|
+
*
|
|
1633
|
+
* const upstream = new Upstream()
|
|
1634
|
+
* const entries = await upstream.catalog()
|
|
1635
|
+
* upstream.destroy()
|
|
1636
|
+
* ```
|
|
1637
|
+
*/
|
|
1638
|
+
catalog(): Promise<readonly CatalogEntry[]>;
|
|
1639
|
+
/**
|
|
1640
|
+
* Tear the reader down, aborting every request in flight. Teardown is idempotent.
|
|
1641
|
+
*
|
|
1642
|
+
* @returns Nothing.
|
|
1643
|
+
*
|
|
1644
|
+
* @remarks
|
|
1645
|
+
* A call still in flight rejects with a `DESTROYED` error rather than
|
|
1646
|
+
* resolving to a partial answer, because half a fleet reads exactly like a
|
|
1647
|
+
* whole one.
|
|
1648
|
+
*
|
|
1649
|
+
* @example
|
|
1650
|
+
* ```ts
|
|
1651
|
+
* import { Upstream } from '@orkestrel/scaffold/server'
|
|
1652
|
+
*
|
|
1653
|
+
* const upstream = new Upstream()
|
|
1654
|
+
* upstream.destroy()
|
|
1655
|
+
* upstream.emitter.destroyed // true
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|
|
1658
|
+
destroy(): void;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* The upstream reader's observation channel.
|
|
1663
|
+
*
|
|
1664
|
+
* @remarks
|
|
1665
|
+
* Each verdict is published whole rather than as a name beside a summary, so a
|
|
1666
|
+
* listener reads the same value the call returns and a failed lookup is told
|
|
1667
|
+
* apart from a successful one by the verdict's own discriminant rather than by
|
|
1668
|
+
* which event carried it.
|
|
1669
|
+
*/
|
|
1670
|
+
export declare type UpstreamEventMap = {
|
|
1671
|
+
readonly release: readonly [release: Release];
|
|
1672
|
+
readonly mirror: readonly [mirror: Mirror];
|
|
1673
|
+
readonly error: readonly [error: unknown];
|
|
1674
|
+
readonly destroy: readonly [];
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* The upstream contract: the package's only network reader, and it never writes.
|
|
1679
|
+
*
|
|
1680
|
+
* @remarks
|
|
1681
|
+
* A per-package failure is collected as a verdict carrying its cause, not
|
|
1682
|
+
* thrown, so one unreachable package never costs the caller the rest of the
|
|
1683
|
+
* answer. The organization list is the exception: without it there is no fleet
|
|
1684
|
+
* to report, so an unreachable or malformed list is a coded failure.
|
|
1685
|
+
*/
|
|
1686
|
+
export declare interface UpstreamInterface {
|
|
1687
|
+
readonly emitter: EmitterInterface<UpstreamEventMap>;
|
|
1688
|
+
/**
|
|
1689
|
+
* Look up the registry's latest release for each declared dependency.
|
|
1690
|
+
*
|
|
1691
|
+
* @param dependencies - The declared dependencies to look up.
|
|
1692
|
+
* @returns One release verdict per dependency, in input order.
|
|
1693
|
+
*/
|
|
1694
|
+
lookup(dependencies: readonly Dependency[]): Promise<readonly Release[]>;
|
|
1695
|
+
/**
|
|
1696
|
+
* Fetch each named package's guide, beside the local mirror it answers for.
|
|
1697
|
+
*
|
|
1698
|
+
* @param names - The packages to fetch: the target's declared set, or the whole organization.
|
|
1699
|
+
* @param current - The target's local mirrors as exact bytes, keyed by mirror path.
|
|
1700
|
+
* @returns One mirror verdict per name, in input order.
|
|
1701
|
+
*/
|
|
1702
|
+
fetch(names: readonly string[], current: Snapshot): Promise<readonly Mirror[]>;
|
|
1703
|
+
/**
|
|
1704
|
+
* Catalog the published fleet from the registry's organization package list.
|
|
1705
|
+
*
|
|
1706
|
+
* @returns One row per published package, sorted by name.
|
|
1707
|
+
*/
|
|
1708
|
+
catalog(): Promise<readonly CatalogEntry[]>;
|
|
1709
|
+
/**
|
|
1710
|
+
* Tear the reader down, aborting every request in flight. Teardown is idempotent.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns Nothing.
|
|
1713
|
+
*/
|
|
1714
|
+
destroy(): void;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Options for the upstream reader.
|
|
1719
|
+
*
|
|
1720
|
+
* @remarks
|
|
1721
|
+
* The two endpoints are grouped under the entity each configures: `guides`
|
|
1722
|
+
* takes the guide host's `base`, its `branch`, and its `timeout`; `registry`
|
|
1723
|
+
* takes the registry's `base` and `timeout`. `concurrency` bounds requests in
|
|
1724
|
+
* flight and `retries` opts into per-request retry on a transport fault.
|
|
1725
|
+
* `limit` bounds the bytes read from one response body and `budget` bounds the
|
|
1726
|
+
* bytes read across a whole call, so neither one oversized answer nor many
|
|
1727
|
+
* small ones can exhaust the caller. Every request is unauthenticated and
|
|
1728
|
+
* follows no redirect.
|
|
1729
|
+
*/
|
|
1730
|
+
export declare interface UpstreamOptions {
|
|
1731
|
+
readonly guides?: {
|
|
1732
|
+
readonly base?: string;
|
|
1733
|
+
readonly branch?: string;
|
|
1734
|
+
readonly timeout?: number;
|
|
1735
|
+
};
|
|
1736
|
+
readonly registry?: {
|
|
1737
|
+
readonly base?: string;
|
|
1738
|
+
readonly timeout?: number;
|
|
1739
|
+
};
|
|
1740
|
+
readonly concurrency?: number;
|
|
1741
|
+
readonly retries?: number;
|
|
1742
|
+
readonly limit?: number;
|
|
1743
|
+
readonly budget?: number;
|
|
1744
|
+
readonly on?: EmitterHooks<UpstreamEventMap>;
|
|
1745
|
+
readonly error?: EmitterErrorHandler;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/** One physical directory identity captured across a write transaction. */
|
|
1749
|
+
export declare interface WriteAnchor {
|
|
1750
|
+
readonly path: string;
|
|
1751
|
+
readonly device: number;
|
|
1752
|
+
readonly inode: number;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** The final directory anchor of a write transaction and the subset one call created. */
|
|
1756
|
+
export declare interface WriteDirectoryResult {
|
|
1757
|
+
readonly anchor: WriteAnchor;
|
|
1758
|
+
readonly created: readonly WriteAnchor[];
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* One destination snapshot captured before a write and required to survive it.
|
|
1763
|
+
*
|
|
1764
|
+
* @remarks
|
|
1765
|
+
* `device`, `inode`, `modified`, `size`, and `digest` are present only where
|
|
1766
|
+
* the observed shape supplies them.
|
|
1767
|
+
*/
|
|
1768
|
+
export declare interface WriteExpectation {
|
|
1769
|
+
readonly path: string;
|
|
1770
|
+
readonly shape: 'absent' | 'file' | 'directory';
|
|
1771
|
+
readonly device?: number;
|
|
1772
|
+
readonly inode?: number;
|
|
1773
|
+
readonly modified?: number;
|
|
1774
|
+
readonly size?: number;
|
|
1775
|
+
readonly digest?: string;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/** The narrower caller-observed destination state a write transaction must still match. */
|
|
1779
|
+
export declare interface WritePrecondition {
|
|
1780
|
+
readonly path: string;
|
|
1781
|
+
readonly shape: 'absent' | 'file';
|
|
1782
|
+
readonly digest?: string;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* One staged, reversible mutation of one target directory.
|
|
1787
|
+
*
|
|
1788
|
+
* @remarks
|
|
1789
|
+
* The transaction owns a private root beside the target — a sibling directory on
|
|
1790
|
+
* the same volume, so every promotion is a rename rather than a copy. Staging
|
|
1791
|
+
* writes go into that root and nothing else, so a call that fails while staging
|
|
1792
|
+
* has not touched the target at all. Commit is the only step that mutates the
|
|
1793
|
+
* target, and it is the only step that can need rolling back.
|
|
1794
|
+
*
|
|
1795
|
+
* Two bindings hold a destination still. A **precondition** is what the caller
|
|
1796
|
+
* observed earlier and is checked once, at construction, so a target that moved
|
|
1797
|
+
* between the caller's read and this transaction fails before anything is
|
|
1798
|
+
* created. An **expectation** is captured here, at construction, and re-checked
|
|
1799
|
+
* at commit, so a target that moves while the write is being staged fails before
|
|
1800
|
+
* anything is promoted.
|
|
1801
|
+
*
|
|
1802
|
+
* What this provides, exactly:
|
|
1803
|
+
*
|
|
1804
|
+
* - **Across destinations, staged-then-swapped with rollback on a caught
|
|
1805
|
+
* failure.** A failure part way through commit restores every destination it
|
|
1806
|
+
* already promoted, restores every file it already took, and removes every
|
|
1807
|
+
* directory it created, then reports what recovery could not undo. This is
|
|
1808
|
+
* measured: a promotion is driven to fail after an earlier one landed, and the
|
|
1809
|
+
* earlier destination is read back.
|
|
1810
|
+
* - **No partly written destination.** Every file is written whole into the
|
|
1811
|
+
* private root and digested there before commit, so a destination never
|
|
1812
|
+
* receives bytes that were still being produced.
|
|
1813
|
+
* - **No crash atomicity across destinations.** A process killed between two
|
|
1814
|
+
* promotions leaves the target holding some new files and some old ones, and
|
|
1815
|
+
* leaves the private root behind. Nothing here is a journal, and the private
|
|
1816
|
+
* root's name is the only record a later run could read.
|
|
1817
|
+
*
|
|
1818
|
+
* A destination is preserved by hard link and then replaced by a single
|
|
1819
|
+
* `rename`, rather than moved aside and replaced, so the path continues to name
|
|
1820
|
+
* the old file right up to the swap. That is a property of `rename` on the host,
|
|
1821
|
+
* not one this package's tests measure: telling it apart from move-then-replace
|
|
1822
|
+
* needs a reader observing the destination inside the swap, and no test here
|
|
1823
|
+
* does that. Read the claim as the mechanism it describes, not as a proven
|
|
1824
|
+
* guarantee about a concurrent reader.
|
|
1825
|
+
*
|
|
1826
|
+
* A path names a file this transaction writes, takes, or establishes as a
|
|
1827
|
+
* directory; every path is target-relative and is measured by the portable-path
|
|
1828
|
+
* law. An expectation and a precondition both name the resolved destination
|
|
1829
|
+
* instead, because that is the path each is re-read at.
|
|
1830
|
+
*
|
|
1831
|
+
* @example
|
|
1832
|
+
* ```ts
|
|
1833
|
+
* import { WriteTransaction } from '@orkestrel/scaffold/server'
|
|
1834
|
+
*
|
|
1835
|
+
* const transaction = new WriteTransaction('./packages/router', ['AGENTS.md'])
|
|
1836
|
+
* try {
|
|
1837
|
+
* transaction.write('AGENTS.md', '# Agents\n')
|
|
1838
|
+
* transaction.commit() // ['AGENTS.md']
|
|
1839
|
+
* } finally {
|
|
1840
|
+
* transaction.discard()
|
|
1841
|
+
* }
|
|
1842
|
+
* ```
|
|
1843
|
+
*/
|
|
1844
|
+
export declare class WriteTransaction {
|
|
1845
|
+
#private;
|
|
1846
|
+
/**
|
|
1847
|
+
* Open a transaction over one target directory.
|
|
1848
|
+
*
|
|
1849
|
+
* @param target - The directory every path is written beneath.
|
|
1850
|
+
* @param paths - Every target-relative path this transaction may touch.
|
|
1851
|
+
* @param preconditions - The caller-observed destination states the whole
|
|
1852
|
+
* transaction is held to, each naming a resolved destination of `paths`.
|
|
1853
|
+
* @throws {@link ScaffoldError} coded `INVALID` when an argument is off
|
|
1854
|
+
* contract, `TARGET` when a destination is a shape this package will not write
|
|
1855
|
+
* over or no longer matches its precondition, and `WRITE` when the private
|
|
1856
|
+
* root cannot be established.
|
|
1857
|
+
*
|
|
1858
|
+
* @remarks
|
|
1859
|
+
* Nothing is created until every destination has been inspected and every
|
|
1860
|
+
* precondition has held, so a refused transaction leaves no residue at all.
|
|
1861
|
+
* The private root is created last and carries a random name, so two
|
|
1862
|
+
* transactions over one target never collide.
|
|
1863
|
+
*/
|
|
1864
|
+
constructor(target: string, paths: readonly string[], preconditions?: readonly WritePrecondition[]);
|
|
1865
|
+
/** The resolved directory every path is written beneath. */
|
|
1866
|
+
get target(): string;
|
|
1867
|
+
/** What each destination held when the transaction opened, in path order. */
|
|
1868
|
+
get expectations(): readonly WriteExpectation[];
|
|
1869
|
+
/** Whether the transaction can still be committed or discarded. */
|
|
1870
|
+
get open(): boolean;
|
|
1871
|
+
/**
|
|
1872
|
+
* Stage one text file.
|
|
1873
|
+
*
|
|
1874
|
+
* @param path - The target-relative path to write.
|
|
1875
|
+
* @param content - The exact UTF-8 text the destination should hold.
|
|
1876
|
+
* @returns Nothing.
|
|
1877
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
1878
|
+
* transaction opened or is already staged, `TARGET` when the destination holds
|
|
1879
|
+
* a directory, and `WRITE` when the staged file cannot be written or does not
|
|
1880
|
+
* carry the bytes it was given.
|
|
1881
|
+
*
|
|
1882
|
+
* @remarks
|
|
1883
|
+
* The staged file is read back and digested against the text it was given, so
|
|
1884
|
+
* a partial or interrupted write is refused here rather than promoted later.
|
|
1885
|
+
*/
|
|
1886
|
+
write(path: string, content: string): void;
|
|
1887
|
+
/**
|
|
1888
|
+
* Stage one byte-for-byte copy of a file that already exists on this host.
|
|
1889
|
+
*
|
|
1890
|
+
* @param path - The target-relative path to write.
|
|
1891
|
+
* @param source - The resolved absolute path to copy the bytes from.
|
|
1892
|
+
* @param executable - Whether the destination should carry the executable bit.
|
|
1893
|
+
* @returns Nothing.
|
|
1894
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
1895
|
+
* transaction opened or is already staged, `TARGET` when the destination holds
|
|
1896
|
+
* a directory or the source is not a physical file, and `WRITE` when the copy
|
|
1897
|
+
* cannot be made or does not digest to the source's bytes.
|
|
1898
|
+
*
|
|
1899
|
+
* @remarks
|
|
1900
|
+
* The source is digested before the copy and the copy is digested after it, so
|
|
1901
|
+
* a source that changed mid-copy is refused instead of staged.
|
|
1902
|
+
*/
|
|
1903
|
+
copy(path: string, source: string, executable: boolean): void;
|
|
1904
|
+
/**
|
|
1905
|
+
* Establish one directory inside the target, one segment at a time.
|
|
1906
|
+
*
|
|
1907
|
+
* @param path - The target-relative directory to establish.
|
|
1908
|
+
* @returns The directory's identity and every segment this call created.
|
|
1909
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
1910
|
+
* transaction opened, `TARGET` when the destination holds a file, and `WRITE`
|
|
1911
|
+
* when a segment cannot be created or changed while it was being created.
|
|
1912
|
+
*
|
|
1913
|
+
* @remarks
|
|
1914
|
+
* A directory is created immediately rather than staged, because creating one
|
|
1915
|
+
* destroys nothing: rollback removes exactly the segments this call created,
|
|
1916
|
+
* innermost first, and leaves every segment that was already there. Each
|
|
1917
|
+
* created segment is captured by device and inode, so a segment swapped
|
|
1918
|
+
* underneath the transaction is detected rather than written into.
|
|
1919
|
+
*/
|
|
1920
|
+
directory(path: string): WriteDirectoryResult;
|
|
1921
|
+
/**
|
|
1922
|
+
* Mark one file for deletion at commit.
|
|
1923
|
+
*
|
|
1924
|
+
* @param path - The target-relative file to delete.
|
|
1925
|
+
* @returns Nothing.
|
|
1926
|
+
* @throws {@link ScaffoldError} coded `INVALID` when the path is not one this
|
|
1927
|
+
* transaction opened or is already claimed, and `TARGET` when the destination
|
|
1928
|
+
* does not hold a file.
|
|
1929
|
+
*
|
|
1930
|
+
* @remarks
|
|
1931
|
+
* Nothing moves here. Commit renames the file into the private backup rather
|
|
1932
|
+
* than unlinking it, so a later failure in the same commit puts it back.
|
|
1933
|
+
*/
|
|
1934
|
+
remove(path: string): void;
|
|
1935
|
+
/**
|
|
1936
|
+
* Promote every staged file and take every marked file, or roll the whole call back.
|
|
1937
|
+
*
|
|
1938
|
+
* @returns Every target-relative path whose destination changed: the files
|
|
1939
|
+
* promoted, then the directories established, then the files taken.
|
|
1940
|
+
* @throws {@link ScaffoldError} coded `WRITE` when the transaction is closed,
|
|
1941
|
+
* when a destination moved since the transaction opened, or when the commit
|
|
1942
|
+
* failed; a failure reports what rollback could not undo in its context.
|
|
1943
|
+
*
|
|
1944
|
+
* @remarks
|
|
1945
|
+
* Every destination and every directory this transaction created is re-checked
|
|
1946
|
+
* before anything moves, so the common failure moves nothing at all. That
|
|
1947
|
+
* check runs inside the same rollback as the promotions, because a transaction
|
|
1948
|
+
* that refuses before it starts still has a private root and created
|
|
1949
|
+
* directories to clear. The transaction is closed either way: a committed one
|
|
1950
|
+
* has nothing left to undo and a failed one has already been rolled back.
|
|
1951
|
+
*/
|
|
1952
|
+
commit(): readonly string[];
|
|
1953
|
+
/**
|
|
1954
|
+
* Abandon the transaction and remove everything it created.
|
|
1955
|
+
*
|
|
1956
|
+
* @returns Nothing.
|
|
1957
|
+
* @throws {@link ScaffoldError} coded `WRITE` when residue could not be
|
|
1958
|
+
* removed, naming the private root that still holds it.
|
|
1959
|
+
*
|
|
1960
|
+
* @remarks
|
|
1961
|
+
* Idempotent, and a no-op on a transaction that already committed or already
|
|
1962
|
+
* failed, so a caller can put it in a `finally` beside the work it guards.
|
|
1963
|
+
*/
|
|
1964
|
+
discard(): void;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
export { }
|