@orkestrel/scaffold 0.0.1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/bin/scaffold.js +1539 -0
  4. package/dist/bin/scaffold.js.map +1 -0
  5. package/dist/host/AGENTS.md +939 -0
  6. package/dist/host/CLAUDE.md +495 -0
  7. package/dist/host/LICENSE +21 -0
  8. package/dist/host/claude/agents/builder.md +48 -0
  9. package/dist/host/claude/agents/checker.md +37 -0
  10. package/dist/host/claude/agents/composer.md +64 -0
  11. package/dist/host/claude/agents/grok.md +50 -0
  12. package/dist/host/claude/agents/orkestrel.md +236 -0
  13. package/dist/host/claude/agents/planner.md +44 -0
  14. package/dist/host/claude/agents/researcher.md +38 -0
  15. package/dist/host/claude/agents/reviewer.md +47 -0
  16. package/dist/host/claude/agents/scout.md +35 -0
  17. package/dist/host/claude/agents/verifier.md +34 -0
  18. package/dist/host/claude/settings.json +26 -0
  19. package/dist/host/dotfiles/editorconfig +17 -0
  20. package/dist/host/dotfiles/gitattributes +3 -0
  21. package/dist/host/dotfiles/gitignore +40 -0
  22. package/dist/host/dotfiles/oxfmtrc.json +18 -0
  23. package/dist/host/dotfiles/oxlintignore +20 -0
  24. package/dist/host/dotfiles/oxlintrc.json +58 -0
  25. package/dist/host/dotfiles/prettierignore +5 -0
  26. package/dist/host/github/workflows/ci.yml +64 -0
  27. package/dist/host/guides/src/guide.md +312 -0
  28. package/dist/host/guides/src/scaffold.md +2152 -0
  29. package/dist/host/manifest.json +137 -0
  30. package/dist/host/scripts/cursor.sh +74 -0
  31. package/dist/host/scripts/deps.sh +38 -0
  32. package/dist/host/scripts/ollama.sh +163 -0
  33. package/dist/src/core/index.cjs +3728 -0
  34. package/dist/src/core/index.cjs.map +1 -0
  35. package/dist/src/core/index.d.cts +1941 -0
  36. package/dist/src/core/index.d.ts +1941 -0
  37. package/dist/src/core/index.js +3636 -0
  38. package/dist/src/core/index.js.map +1 -0
  39. package/dist/src/server/index.cjs +1595 -0
  40. package/dist/src/server/index.cjs.map +1 -0
  41. package/dist/src/server/index.d.cts +779 -0
  42. package/dist/src/server/index.d.ts +779 -0
  43. package/dist/src/server/index.js +1572 -0
  44. package/dist/src/server/index.js.map +1 -0
  45. package/package.json +113 -0
@@ -0,0 +1,1595 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_url = require("node:url");
5
+ let _orkestrel_markdown = require("@orkestrel/markdown");
6
+ let _src_core = require("../core/index.cjs");
7
+ let _orkestrel_emitter = require("@orkestrel/emitter");
8
+ //#region src/server/helpers.ts
9
+ /**
10
+ * Locate this MODULE's own installed package root — the nearest ancestor of
11
+ * `import.meta.url` holding a `package.json` — and return its vendored
12
+ * `dist/host` data root. THE single source of truth for the default
13
+ * `Materializer` / `scaffold` bin host: once installed, walking up from the
14
+ * module's own file (not `process.cwd()`, which points at whichever project
15
+ * happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
16
+ * correct default host — the package ships its vendored data with itself.
17
+ * `dist/host` may not exist yet when this resolves from SOURCE under a test
18
+ * runner; that is fine — existence is checked at the point of use, not here.
19
+ *
20
+ * @returns The absolute vendored `dist/host` path.
21
+ * @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
22
+ * location holds a `package.json`.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { hostRoot } from '@orkestrel/scaffold/server'
27
+ *
28
+ * hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
29
+ * ```
30
+ */
31
+ function hostRoot() {
32
+ let dir = (0, node_path.dirname)((0, node_url.fileURLToPath)({}.url));
33
+ for (;;) {
34
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return (0, node_path.join)(dir, "dist", "host");
35
+ const parent = (0, node_path.dirname)(dir);
36
+ if (parent === dir) throw new _src_core.ScaffoldError("TARGET", "No package root found above the module location", { module: {}.url });
37
+ dir = parent;
38
+ }
39
+ }
40
+ /**
41
+ * Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
42
+ * string values — the shared `dependencies` / `peerDependencies` /
43
+ * `devDependencies` reader `deriveBlueprint` uses for every dependency-shaped
44
+ * field.
45
+ *
46
+ * @param value - The candidate manifest field value (e.g. `parsed.dependencies`).
47
+ * @returns The `@orkestrel/`-prefixed `[name, range]` entries; `[]` when
48
+ * `value` is not a plain object (per `isRecord`).
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { selectOrkestrelEntries } from '@orkestrel/scaffold/server'
53
+ *
54
+ * selectOrkestrelEntries({ '@orkestrel/core': '^1.0.0', lodash: '^4.0.0' })
55
+ * // [['@orkestrel/core', '^1.0.0']]
56
+ * ```
57
+ */
58
+ function selectOrkestrelEntries(value) {
59
+ if (!isRecord(value)) return [];
60
+ return Object.entries(value).filter((entry) => typeof entry[1] === "string" && entry[0].startsWith("@orkestrel/"));
61
+ }
62
+ /**
63
+ * Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
64
+ * inverse `audit` / `repair` / `mirror` need to diff a live package against
65
+ * its own would-be scaffold, rather than a fresh, dependency-less stand-in.
66
+ *
67
+ * @param target - The existing package directory to derive a `Blueprint` from.
68
+ * @remarks
69
+ * `name` strips the `@orkestrel/` prefix off `manifest.name` — a non-`@orkestrel`
70
+ * name is a coded `TARGET` failure, since this tool derives only `@orkestrel`
71
+ * packages. `surfaces` is read off the LIVE line: every surface the package
72
+ * carries has a `src/<surface>/` directory, so each of `'core' | 'browser' |
73
+ * 'server'` is included iff that directory exists at `target`; a target with
74
+ * NONE of the three is also a coded `TARGET` failure. `dependencies` /
75
+ * `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
76
+ * `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
77
+ * .optional === true` carries `optional: true`). `extras` is EVERY entry of
78
+ * `manifest.devDependencies` (not only `@orkestrel/`-prefixed ones — an
79
+ * external extra like `zod` must round-trip too), EXCLUDING the generated
80
+ * devDependency baseline (`devDependenciesFor([])`'s keys, which already
81
+ * cover `@orkestrel/guide` and `@orkestrel/scaffold`) every scaffolded
82
+ * package already carries, never a package-specific extra. A devDependency
83
+ * ALSO present in
84
+ * `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
85
+ * dev-installed for local testing) is likewise excluded from `extras` — it
86
+ * already surfaces as a `peer`/`dependency` above, and double-counting it as
87
+ * an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
88
+ * gate. `overrides` is always `[]` — derivation cannot know a caller's
89
+ * template-override intent.
90
+ * @returns The reconstructed `Blueprint`.
91
+ * @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
92
+ * (via `readManifest`), is not valid JSON, its `name` is not `@orkestrel`-
93
+ * prefixed, or `target` carries none of the three surface directories.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { deriveBlueprint } from '@orkestrel/scaffold/server'
98
+ *
99
+ * deriveBlueprint('./packages/router') // { name: 'router', surfaces: ['core', 'server'], … }
100
+ * ```
101
+ */
102
+ function deriveBlueprint(target) {
103
+ const BASELINE_EXTRAS = /* @__PURE__ */ new Set([
104
+ ...Object.keys((0, _src_core.devDependenciesFor)([])),
105
+ "@orkestrel/guide",
106
+ "@orkestrel/scaffold"
107
+ ]);
108
+ const text = readManifest(target);
109
+ let parsed;
110
+ try {
111
+ parsed = JSON.parse(text);
112
+ } catch (error) {
113
+ throw new _src_core.ScaffoldError("TARGET", `Manifest at ${target} is not valid JSON`, {
114
+ target,
115
+ error
116
+ });
117
+ }
118
+ if (!isRecord(parsed)) throw new _src_core.ScaffoldError("TARGET", `Manifest at ${target} is not a JSON object`, { target });
119
+ const rawName = parsed.name;
120
+ if (typeof rawName !== "string" || !rawName.startsWith("@orkestrel/")) throw new _src_core.ScaffoldError("TARGET", `Manifest name "${String(rawName)}" is not an @orkestrel package`, {
121
+ target,
122
+ name: rawName
123
+ });
124
+ const name = rawName.slice(11);
125
+ const description = typeof parsed.description === "string" ? parsed.description : void 0;
126
+ const keywords = Array.isArray(parsed.keywords) && parsed.keywords.every((word) => typeof word === "string") ? parsed.keywords : [];
127
+ const version = typeof parsed.version === "string" ? parsed.version : _src_core.DEFAULT_VERSION;
128
+ const engines = isRecord(parsed.engines) && typeof parsed.engines.node === "string" ? parsed.engines.node : _src_core.DEFAULT_ENGINES;
129
+ const surfaces = [
130
+ "core",
131
+ "browser",
132
+ "server"
133
+ ].filter((surface) => (0, node_fs.existsSync)((0, node_path.join)(target, "src", surface)));
134
+ if (surfaces.length === 0) throw new _src_core.ScaffoldError("TARGET", `No surface directory found under ${target}/src`, { target });
135
+ const dependencies = selectOrkestrelEntries(parsed.dependencies).map(([depName, range]) => ({
136
+ name: depName,
137
+ range
138
+ }));
139
+ const peersMeta = isRecord(parsed.peerDependenciesMeta) ? parsed.peerDependenciesMeta : void 0;
140
+ const peers = selectOrkestrelEntries(parsed.peerDependencies).map(([depName, range]) => {
141
+ const meta = peersMeta !== void 0 ? peersMeta[depName] : void 0;
142
+ return isRecord(meta) && meta.optional === true ? {
143
+ name: depName,
144
+ range,
145
+ optional: true
146
+ } : {
147
+ name: depName,
148
+ range
149
+ };
150
+ });
151
+ const peerAndDependencyNames = /* @__PURE__ */ new Set([...selectOrkestrelEntries(parsed.peerDependencies).map(([depName]) => depName), ...selectOrkestrelEntries(parsed.dependencies).map(([depName]) => depName)]);
152
+ const devDependencies = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
153
+ return (0, _src_core.blueprint)(name, {
154
+ description,
155
+ keywords,
156
+ surfaces,
157
+ dependencies,
158
+ peers,
159
+ extras: Object.entries(devDependencies).filter((entry) => typeof entry[1] === "string").filter(([depName]) => !BASELINE_EXTRAS.has(depName) && !peerAndDependencyNames.has(depName)).map(([depName, range]) => ({
160
+ name: depName,
161
+ range
162
+ })),
163
+ version,
164
+ engines,
165
+ overrides: []
166
+ });
167
+ }
168
+ /**
169
+ * Whether a target path is absent, empty, or contains nothing but a `.git`
170
+ * directory — the green-field target law `Materializer.materialize` enforces.
171
+ *
172
+ * @param target - The candidate target directory path.
173
+ * @returns `true` when `target` is safe to materialize a fresh package into.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * import { isVacant } from '@orkestrel/scaffold/server'
178
+ *
179
+ * isVacant('./packages/router-new') // true — absent, empty, or only a .git dir
180
+ * ```
181
+ */
182
+ function isVacant(target) {
183
+ if (!(0, node_fs.existsSync)(target)) return true;
184
+ if (!(0, node_fs.statSync)(target).isDirectory()) return false;
185
+ const entries = (0, node_fs.readdirSync)(target);
186
+ return entries.length === 0 || entries.length === 1 && entries[0] === ".git";
187
+ }
188
+ /**
189
+ * Read a target's current content at a set of relative paths into a
190
+ * `Record<string, string>` — the I/O that feeds the pure `diffPlan`.
191
+ *
192
+ * @param target - The target directory to read from.
193
+ * @param paths - The plan-relative artifact paths to probe.
194
+ * @returns A record keyed by path; a directory entry maps to `''` (presence
195
+ * only — a `host`-origin directory artifact is audited by presence, never
196
+ * content), an absent path is OMITTED entirely (never an empty-string
197
+ * placeholder for a missing file, so `diffPlan` reports it `missing`).
198
+ * @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
199
+ * (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
200
+ * the resolved `full` path) in `context`. An absent path is never an
201
+ * error — it is simply omitted, per the return contract above.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * import { readTarget } from '@orkestrel/scaffold/server'
206
+ *
207
+ * readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
208
+ * // { 'package.json': '{ "name": … }', 'src/core/index.ts': '…' }
209
+ * ```
210
+ */
211
+ function readTarget(target, paths) {
212
+ const current = {};
213
+ for (const path of paths) {
214
+ const full = (0, node_path.join)(target, path);
215
+ if (!(0, node_fs.existsSync)(full)) continue;
216
+ try {
217
+ current[path] = (0, node_fs.statSync)(full).isDirectory() ? "" : (0, node_fs.readFileSync)(full, "utf8");
218
+ } catch (error) {
219
+ throw new _src_core.ScaffoldError("TARGET", `Failed to read target file at ${path}`, {
220
+ path,
221
+ full,
222
+ error
223
+ });
224
+ }
225
+ }
226
+ return current;
227
+ }
228
+ /**
229
+ * Read `target/package.json` text — the read that feeds `manifestToDependencies`.
230
+ *
231
+ * @param target - The target directory to read the manifest from.
232
+ * @returns The manifest file's raw text.
233
+ * @throws `ScaffoldError('TARGET', …)` when the manifest is absent or
234
+ * unreadable (e.g. `EACCES` / `EPERM`) — carries the resolved `full` path
235
+ * in `context`.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * import { readManifest } from '@orkestrel/scaffold/server'
240
+ *
241
+ * readManifest('./packages/router') // '{ "name": "@orkestrel/router", … }'
242
+ * ```
243
+ */
244
+ function readManifest(target) {
245
+ const full = (0, node_path.join)(target, "package.json");
246
+ try {
247
+ return (0, node_fs.readFileSync)(full, "utf8");
248
+ } catch (error) {
249
+ throw new _src_core.ScaffoldError("TARGET", `Failed to read manifest at ${full}`, {
250
+ target,
251
+ full,
252
+ error
253
+ });
254
+ }
255
+ }
256
+ /**
257
+ * Whether `value` is a plain object (not `null`, not an array).
258
+ *
259
+ * @param value - The candidate value.
260
+ * @returns `true` when `value` narrows to `Record<string, unknown>`.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { isRecord } from '@orkestrel/scaffold/server'
265
+ *
266
+ * isRecord({ a: 1 }) // true
267
+ * isRecord(null) // false
268
+ * ```
269
+ */
270
+ function isRecord(value) {
271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
+ }
273
+ /**
274
+ * Whether `value` is a well-formed `host/manifest.json` entry — a string
275
+ * `storage`, a string `destination`, and a boolean `executable`.
276
+ *
277
+ * @param value - The candidate raw manifest entry.
278
+ * @returns `true` when `value` narrows to `ManifestEntry`.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * import { isManifestEntry } from '@orkestrel/scaffold/server'
283
+ *
284
+ * isManifestEntry({ storage: 'a', destination: 'b', executable: false }) // true
285
+ * isManifestEntry({ storage: 'a', destination: 'b' }) // false — missing `executable`
286
+ * ```
287
+ */
288
+ function isManifestEntry(value) {
289
+ if (!isRecord(value)) return false;
290
+ return typeof value.storage === "string" && typeof value.destination === "string" && typeof value.executable === "boolean";
291
+ }
292
+ /**
293
+ * Read and validate a vendored host root's `manifest.json`, when present.
294
+ *
295
+ * @param host - The host root to probe.
296
+ * @returns The parsed entries, or `undefined` when `host` has no
297
+ * `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
298
+ * an artifact's `source` to `host` 1:1, no vendored staging indirection).
299
+ * @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
300
+ * unreadable, is not valid JSON, or is not an array of `ManifestEntry`.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * import { readHostManifest } from '@orkestrel/scaffold/server'
305
+ *
306
+ * readHostManifest('./dist/host') // readonly ManifestEntry[] | undefined
307
+ * ```
308
+ */
309
+ function readHostManifest(host) {
310
+ const full = (0, node_path.join)(host, "manifest.json");
311
+ if (!(0, node_fs.existsSync)(full)) return void 0;
312
+ let text;
313
+ try {
314
+ text = (0, node_fs.readFileSync)(full, "utf8");
315
+ } catch (error) {
316
+ throw new _src_core.ScaffoldError("TARGET", `Failed to read host manifest at ${full}`, {
317
+ host,
318
+ full,
319
+ error
320
+ });
321
+ }
322
+ let parsed;
323
+ try {
324
+ parsed = JSON.parse(text);
325
+ } catch (error) {
326
+ throw new _src_core.ScaffoldError("TARGET", `Host manifest at ${full} is not valid JSON`, {
327
+ host,
328
+ full,
329
+ error
330
+ });
331
+ }
332
+ if (!Array.isArray(parsed) || !parsed.every(isManifestEntry)) throw new _src_core.ScaffoldError("TARGET", `Host manifest at ${full} is not an array of manifest entries`, {
333
+ host,
334
+ full
335
+ });
336
+ return parsed;
337
+ }
338
+ /**
339
+ * Recursively list a directory's files as root-relative paths.
340
+ *
341
+ * @param root - The directory to list.
342
+ * @returns Root-relative file paths (posix-style `/` separators), or `[]`
343
+ * when `root` is absent.
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * import { listFiles } from '@orkestrel/scaffold/server'
348
+ *
349
+ * listFiles('./dist/host/.claude/agents') // ['scout.md', 'builder.md', …]
350
+ * ```
351
+ */
352
+ function listFiles(root) {
353
+ if (!(0, node_fs.existsSync)(root)) return [];
354
+ const files = [];
355
+ for (const entry of (0, node_fs.readdirSync)(root, { withFileTypes: true })) {
356
+ const full = (0, node_path.join)(root, entry.name);
357
+ if (entry.isDirectory()) for (const nested of listFiles(full)) files.push(`${entry.name}/${nested}`);
358
+ else files.push(entry.name);
359
+ }
360
+ return files;
361
+ }
362
+ /**
363
+ * Map a repo-relative path to its vendored-host STAGING path, per the
364
+ * dotfile-mapping rule `stageHost` writes into `manifest.json`.
365
+ *
366
+ * @param path - The repo-relative source path (e.g. `.claude/agents/scout.md`).
367
+ * @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
368
+ * `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
369
+ * dot wherever it appears; an undotted path is unchanged.
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * import { storagePath } from '@orkestrel/scaffold/server'
374
+ *
375
+ * storagePath('.gitignore') // 'dotfiles/gitignore'
376
+ * storagePath('.claude/agents/scout.md') // 'claude/agents/scout.md'
377
+ * storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
378
+ * storagePath('AGENTS.md') // 'AGENTS.md'
379
+ * ```
380
+ */
381
+ function storagePath(path) {
382
+ const segments = path.split("/");
383
+ if (segments.length === 1) {
384
+ const name = segments[0];
385
+ return name.startsWith(".") ? `dotfiles/${name.slice(1)}` : name;
386
+ }
387
+ return segments.map((segment) => segment.startsWith(".") ? segment.slice(1) : segment).join("/");
388
+ }
389
+ /**
390
+ * Stage the vendored host set (byte-preserved copies + `manifest.json`) from
391
+ * a repo root into an output directory — the BUILD-time primitive the
392
+ * `build:host` npm script now calls directly (replacing a standalone build
393
+ * script); `Materializer.materialize` is the RUNTIME reader of what this
394
+ * writes (via `hostRoot` / `readHostManifest`).
395
+ *
396
+ * @param root - The repo root every `paths` entry resolves against.
397
+ * @param out - The output directory to wipe and stage into (typically `dist/host`).
398
+ * @param paths - The repo-relative file/directory entries to stage; defaults
399
+ * to the package's own vendored set (`HOST_PATHS`) — a caller passes an
400
+ * explicit list only to stage an arbitrary/test set.
401
+ * @remarks
402
+ * `out` is wiped (`rmSync(out, { recursive: true, force: true })`) BEFORE
403
+ * staging, so a stale file left over from a prior run never lingers. Each
404
+ * `paths` entry is walked to its per-file leaves (a directory recursively,
405
+ * via `listFiles`; a file, itself) and copied byte-for-byte
406
+ * (`copyFileSync`) to `<out>/<storagePath(path)>`. The `executable` flag
407
+ * `entries` reports is derived from the `.sh` suffix on `destination` —
408
+ * deterministic on every build platform (Windows `stat` carries no execute
409
+ * bit); all vendored executables are shell scripts by construction.
410
+ * `manifest.json` is written LAST, as `entries` code-unit sorted by
411
+ * `destination`, tab-indented JSON with a trailing newline.
412
+ * @returns The written manifest's entries (`{ storage, destination, executable }`).
413
+ * @throws `ScaffoldError('TARGET', …)` naming the offending path when a
414
+ * `paths` entry has no source under `root`, or naming BOTH colliding
415
+ * destinations when two entries map to the same `storagePath` (the guard
416
+ * run BEFORE `manifest.json` is written).
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * import { stageHost } from '@orkestrel/scaffold/server'
421
+ *
422
+ * const entries = stageHost(process.cwd(), 'dist/host')
423
+ * entries.length // number of files staged
424
+ * ```
425
+ */
426
+ function stageHost(root, out, paths = _src_core.HOST_PATHS) {
427
+ const destinations = [];
428
+ for (const path of paths) {
429
+ const absolute = (0, node_path.join)(root, path);
430
+ if (!(0, node_fs.existsSync)(absolute)) throw new _src_core.ScaffoldError("TARGET", `Missing host source at ${path}`, {
431
+ path,
432
+ root
433
+ });
434
+ if ((0, node_fs.statSync)(absolute).isDirectory()) for (const nested of listFiles(absolute)) destinations.push(`${path}/${nested}`);
435
+ else destinations.push(path);
436
+ }
437
+ (0, node_fs.rmSync)(out, {
438
+ recursive: true,
439
+ force: true
440
+ });
441
+ const entries = [];
442
+ for (const destination of destinations) {
443
+ const storage = storagePath(destination);
444
+ const sourceAbsolute = (0, node_path.join)(root, destination);
445
+ const destinationAbsolute = (0, node_path.join)(out, storage);
446
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(destinationAbsolute), { recursive: true });
447
+ (0, node_fs.copyFileSync)(sourceAbsolute, destinationAbsolute);
448
+ const executable = destination.endsWith(".sh");
449
+ entries.push({
450
+ storage,
451
+ destination,
452
+ executable
453
+ });
454
+ }
455
+ entries.sort((a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0);
456
+ const byStorage = /* @__PURE__ */ new Map();
457
+ for (const entry of entries) {
458
+ const existing = byStorage.get(entry.storage);
459
+ if (existing !== void 0) throw new _src_core.ScaffoldError("TARGET", `Storage path collision at "${entry.storage}" — destinations "${existing}" and "${entry.destination}" both map to it`, {
460
+ storage: entry.storage,
461
+ destinations: [existing, entry.destination]
462
+ });
463
+ byStorage.set(entry.storage, entry.destination);
464
+ }
465
+ (0, node_fs.mkdirSync)(out, { recursive: true });
466
+ (0, node_fs.writeFileSync)((0, node_path.join)(out, "manifest.json"), `${JSON.stringify(entries, null, " ")}\n`);
467
+ return entries;
468
+ }
469
+ /**
470
+ * Resolve the absolute host-storage path for a host-origin artifact's
471
+ * `source`, manifest-aware.
472
+ *
473
+ * @param manifest - The host's parsed `manifest.json` entries, or `undefined`
474
+ * when the host carries none (raw-repo-root fallback).
475
+ * @param source - The artifact's `source` (or `path`) to resolve.
476
+ * @param host - The resolved host root the path is joined against.
477
+ * @returns `join(host, source)` when `manifest` is `undefined` (no vendored
478
+ * staging indirection); when `manifest` is present, `join(host,
479
+ * entries[0].storage)` for the SINGLE manifest entry whose `destination`
480
+ * equals `source`, or `undefined` when zero or more than one entry matches
481
+ * (`source` names a directory, or the manifest is ambiguous — no single
482
+ * storage file to point at).
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * import { locateHostSource } from '@orkestrel/scaffold/server'
487
+ *
488
+ * locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
489
+ * locateHostSource([{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }], 'package.json', './dist/host')
490
+ * // './dist/host/pkg.tmpl'
491
+ * ```
492
+ */
493
+ function locateHostSource(manifest, source, host) {
494
+ if (manifest === void 0) return (0, node_path.join)(host, source);
495
+ const entries = manifest.filter((entry) => entry.destination === source);
496
+ if (entries.length !== 1) return void 0;
497
+ return (0, node_path.join)(host, entries[0].storage);
498
+ }
499
+ /**
500
+ * Rehydrate a `Plan`'s `host`-origin artifacts with their real byte content
501
+ * read from `host` — manifest-aware, via `locateHostSource`.
502
+ *
503
+ * @param plan - The plan to hydrate.
504
+ * @param host - The resolved host root to read from.
505
+ * @returns A new `Plan` whose file-shaped `host` artifacts carry `content`;
506
+ * `template` / `computed` artifacts and directory-shaped `host` artifacts
507
+ * (no single storage file to read) pass through untouched.
508
+ * @throws `ScaffoldError('TARGET', …)` when a resolved, existing host source
509
+ * file fails to read.
510
+ *
511
+ * @example
512
+ * ```ts
513
+ * import { hydratePlan } from '@orkestrel/scaffold/server'
514
+ *
515
+ * const hydrated = hydratePlan(plan, './dist/host')
516
+ * ```
517
+ */
518
+ function hydratePlan(plan, host) {
519
+ const manifest = readHostManifest(host);
520
+ const artifacts = plan.artifacts.map((artifact) => {
521
+ if (artifact.origin !== "host") return artifact;
522
+ const source = artifact.source ?? artifact.path;
523
+ const full = locateHostSource(manifest, source, host);
524
+ if (full === void 0 || !(0, node_fs.existsSync)(full) || (0, node_fs.statSync)(full).isDirectory()) return artifact;
525
+ try {
526
+ return {
527
+ ...artifact,
528
+ content: (0, node_fs.readFileSync)(full, "utf8")
529
+ };
530
+ } catch (error) {
531
+ throw new _src_core.ScaffoldError("TARGET", `Failed to read host artifact at ${source}`, {
532
+ source,
533
+ full,
534
+ error
535
+ });
536
+ }
537
+ });
538
+ return {
539
+ ...plan,
540
+ artifacts
541
+ };
542
+ }
543
+ /**
544
+ * The `prune`-owned directories — a hard allowlist; `pruneTargets` (and the
545
+ * `Materializer.prune` that consumes it) never scans anything outside these two.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
550
+ *
551
+ * PRUNE_DIRECTORIES // ['.claude/agents', 'scripts']
552
+ * ```
553
+ */
554
+ var PRUNE_DIRECTORIES = [".claude/agents", "scripts"];
555
+ /**
556
+ * The vendored set of destination-relative paths under `directory` (one of
557
+ * `PRUNE_DIRECTORIES`) that `pruneTargets` must NOT report — read from the
558
+ * manifest's `destination`s when `host` has one, else listed straight off
559
+ * `host/<directory>`.
560
+ *
561
+ * @param host - The vendored host root to establish the allowlist from.
562
+ * @param directory - The prune directory (one of `PRUNE_DIRECTORIES`) to scope the allowlist to.
563
+ * @remarks
564
+ * FAIL CLOSED: before returning any allowlist (even an empty one), the
565
+ * vendored source must be POSITIVELY established, or a caller would treat an
566
+ * unresolved host as "vendors nothing" and report every file under
567
+ * `target/<directory>` as unexpected. A missing `host` root, or (no
568
+ * `manifest.json` AND no `host/<directory>`), is a coded `TARGET` failure —
569
+ * the distinction this guards is missing-host vs genuinely-empty-vendor: a
570
+ * `host` that EXISTS and vendors zero files in `directory` (an existing empty
571
+ * dir, or a manifest with zero entries for it) remains a valid empty allowlist.
572
+ * @returns The allowed destination-relative paths under `directory`.
573
+ * @throws `ScaffoldError('TARGET', …)` when `host` does not exist, or when
574
+ * `host` has no `manifest.json` and no `host/<directory>` either.
575
+ *
576
+ * @example
577
+ * ```ts
578
+ * import { vendoredPruneSet } from '@orkestrel/scaffold/server'
579
+ *
580
+ * vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/scout.md', … }
581
+ * ```
582
+ */
583
+ function vendoredPruneSet(host, directory) {
584
+ if (!(0, node_fs.existsSync)(host)) throw new _src_core.ScaffoldError("TARGET", `Cannot establish vendored source for prune: host root not found at ${host}`, {
585
+ host,
586
+ directory
587
+ });
588
+ const manifest = readHostManifest(host);
589
+ if (manifest !== void 0) return new Set(manifest.filter((entry) => entry.destination.startsWith(`${directory}/`)).map((entry) => entry.destination));
590
+ const hostDirectory = (0, node_path.join)(host, directory);
591
+ if (!(0, node_fs.existsSync)(hostDirectory)) throw new _src_core.ScaffoldError("TARGET", `Cannot establish vendored source for prune: no manifest.json and no host directory at ${hostDirectory}`, {
592
+ host,
593
+ directory
594
+ });
595
+ return new Set(listFiles(hostDirectory).map((relative) => `${directory}/${relative}`));
596
+ }
597
+ /**
598
+ * List the repo-relative POSIX paths under `target`'s prune directories
599
+ * (`.claude/agents`, `scripts`) that the vendored `host` allowlist does NOT
600
+ * declare — THE single source of truth for prune drift, consumed by both
601
+ * `Materializer.prune` (which deletes exactly these paths) and the bin's
602
+ * audit/preview UX (which now shows them honestly instead of a
603
+ * structurally-always-zero `audit.foreign`).
604
+ *
605
+ * @param target - The target directory to scan for unexpected files.
606
+ * @param host - The vendored host root the allowlist is derived from.
607
+ * @returns The unexpected relative paths (e.g. `.claude/agents/rogue.md`); `[]`
608
+ * when a prune directory is absent under `target`, or when none of its
609
+ * files are unexpected. Pure read — never deletes anything.
610
+ * @throws `ScaffoldError('TARGET', …)` when `host` cannot positively
611
+ * establish a vendored allowlist for a prune directory that DOES exist
612
+ * under `target` (see `vendoredPruneSet`'s fail-closed remarks).
613
+ *
614
+ * @example
615
+ * ```ts
616
+ * import { pruneTargets } from '@orkestrel/scaffold/server'
617
+ *
618
+ * pruneTargets('./packages/router', hostRoot()) // ['.claude/agents/rogue.md']
619
+ * ```
620
+ */
621
+ function pruneTargets(target, host) {
622
+ const paths = [];
623
+ for (const directory of PRUNE_DIRECTORIES) {
624
+ const root = (0, node_path.join)(target, directory);
625
+ if (!(0, node_fs.existsSync)(root)) continue;
626
+ const allowed = vendoredPruneSet(host, directory);
627
+ for (const relative of listFiles(root)) {
628
+ const path = `${directory}/${relative}`;
629
+ if (!allowed.has(path)) paths.push(path);
630
+ }
631
+ }
632
+ return paths;
633
+ }
634
+ /**
635
+ * List a fleet root's `@orkestrel/*` package directories.
636
+ *
637
+ * @param root - The fleet root directory to scan.
638
+ * @returns Absolute, code-unit-sorted paths of `root`'s immediate child
639
+ * directories whose `package.json` parses and whose `name` starts with
640
+ * `@orkestrel/`. A child with an unreadable or unparsable `package.json`,
641
+ * or a non-`@orkestrel` name, is skipped silently — it simply is not a
642
+ * fleet member.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * import { discoverPackages } from '@orkestrel/scaffold/server'
647
+ *
648
+ * discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
649
+ * ```
650
+ */
651
+ function discoverPackages(root) {
652
+ const packages = [];
653
+ for (const entry of (0, node_fs.readdirSync)(root, { withFileTypes: true })) {
654
+ if (!entry.isDirectory()) continue;
655
+ const directory = (0, node_path.join)(root, entry.name);
656
+ const manifestPath = (0, node_path.join)(directory, "package.json");
657
+ if (!(0, node_fs.existsSync)(manifestPath)) continue;
658
+ let parsed;
659
+ try {
660
+ parsed = JSON.parse((0, node_fs.readFileSync)(manifestPath, "utf8"));
661
+ } catch {
662
+ continue;
663
+ }
664
+ if (!isRecord(parsed)) continue;
665
+ const name = parsed.name;
666
+ if (typeof name === "string" && name.startsWith("@orkestrel/")) packages.push(directory);
667
+ }
668
+ return packages.sort();
669
+ }
670
+ /**
671
+ * Build the fleet package catalog — one `CatalogEntry` per `@orkestrel/*`
672
+ * package discovered under each root, its description drawn from its own
673
+ * guide's FIRST blockquote.
674
+ *
675
+ * @param roots - The fleet root directories to scan (each walked via `discoverPackages`).
676
+ * @remarks
677
+ * Per discovered package directory: `name` / `version` come from its own
678
+ * `package.json`; `description` is the first paragraph of the guide's opening
679
+ * blockquote — the flattened text of the FIRST `ParagraphNode` among the
680
+ * FIRST `BlockquoteNode` found's (depth-first, pre-order, via `walkNodes`)
681
+ * TOP-LEVEL children in its `guides/src/<short>.md` (`<short>` = `name` with
682
+ * the `@orkestrel/` prefix stripped), parsed with `@orkestrel/markdown`'s
683
+ * `parseDocument` — a multi-paragraph blockquote overview yields only its
684
+ * FIRST paragraph, never the whole quote glued together; embedded newlines
685
+ * collapse to single spaces, and surrounding whitespace trims. A
686
+ * missing/unreadable guide, a guide carrying no blockquote, or a blockquote
687
+ * with no top-level paragraph child, yields `description: ''`, never a
688
+ * thrown error. Entries merge across `roots` (a later root's entry for a
689
+ * repeated `name` wins), then code-unit sort by `name`. An unreadable ROOT
690
+ * itself is NOT wrapped here — whatever `discoverPackages` throws for it
691
+ * propagates as-is; the bin layer is responsible for coding that failure
692
+ * `TARGET`.
693
+ * @returns The merged, sorted `CatalogEntry[]`.
694
+ *
695
+ * @example
696
+ * ```ts
697
+ * import { catalogPackages } from '@orkestrel/scaffold/server'
698
+ *
699
+ * catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
700
+ * ```
701
+ */
702
+ function catalogPackages(roots) {
703
+ const merged = /* @__PURE__ */ new Map();
704
+ for (const root of roots) for (const directory of discoverPackages(root)) {
705
+ let parsed;
706
+ try {
707
+ parsed = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(directory, "package.json"), "utf8"));
708
+ } catch {
709
+ continue;
710
+ }
711
+ if (!isRecord(parsed)) continue;
712
+ const name = parsed.name;
713
+ if (typeof name !== "string" || !name.startsWith("@orkestrel/")) continue;
714
+ const version = typeof parsed.version === "string" ? parsed.version : _src_core.DEFAULT_VERSION;
715
+ const guidePath = (0, node_path.join)(directory, "guides", "src", `${name.slice(11)}.md`);
716
+ let description = "";
717
+ if ((0, node_fs.existsSync)(guidePath)) try {
718
+ const document = (0, _orkestrel_markdown.parseDocument)((0, node_fs.readFileSync)(guidePath, "utf8"));
719
+ let quote;
720
+ for (const node of (0, _orkestrel_markdown.walkNodes)(document)) if ((0, _orkestrel_markdown.isBlockquoteNode)(node)) {
721
+ quote = node;
722
+ break;
723
+ }
724
+ if (quote !== void 0) {
725
+ const paragraph = quote.children.find((child) => (0, _orkestrel_markdown.isParagraphNode)(child));
726
+ if (paragraph !== void 0) description = (0, _orkestrel_markdown.flattenText)(paragraph).replace(/\s+/g, " ").trim();
727
+ }
728
+ } catch {
729
+ description = "";
730
+ }
731
+ merged.set(name, {
732
+ name,
733
+ version,
734
+ description
735
+ });
736
+ }
737
+ return [...merged.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
738
+ }
739
+ //#endregion
740
+ //#region src/server/Materializer.ts
741
+ /**
742
+ * The materialization entity (server) — the only impure surface in the
743
+ * package, writing a `Plan` to `node:fs` behind an explicit call.
744
+ *
745
+ * @remarks
746
+ * `materialize` is green-field: it refuses any target `isVacant` rejects
747
+ * (`ScaffoldError('TARGET', …)`), then byte-copies each `host` artifact from
748
+ * the `host` root and writes each `template` / `computed` artifact's rendered
749
+ * `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
750
+ * `repair` is into-existing: it skips the vacancy check and writes ONLY the
751
+ * `missing` / `stale` artifacts an `Audit` names, leaving `aligned` ones
752
+ * untouched. `prune` deletes stale files under `target/.claude/agents/` and
753
+ * `target/scripts/` that the vendored `host` no longer names — the retired
754
+ * `mirror.sh`/`scaffold.sh` cleanup step, now a method. After `destroy()`
755
+ * every method throws `DESTROYED`; teardown is idempotent, emitter last.
756
+ *
757
+ * @remarks
758
+ * `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
759
+ * carries a `manifest.json` (this package's own vendored `dist/host`), each
760
+ * artifact's `source` (a destination-relative path) is looked up in the
761
+ * manifest to find its un-dotted STORAGE path plus an `executable` bit
762
+ * (applied via `chmodSync` after the copy) — the vendored-package shape,
763
+ * where storage names avoid leading dots npm would otherwise mangle. When
764
+ * `host` carries no `manifest.json` (a caller-supplied raw repo root, e.g. a
765
+ * sibling checkout or a test fixture), `source` maps to `host` 1:1, exactly
766
+ * as before.
767
+ *
768
+ * @remarks
769
+ * A manifest-present `source` with ZERO matching entries degrades to a stub
770
+ * ONLY when that `source` is a dependency-guide pointer — one that starts
771
+ * with `guides/src/` and ends with `.md` (the `guides/src/<dep>.md` pointer
772
+ * `Compiler` emits for any dependency outside this package's vendored set).
773
+ * That is the ONLY zero-match case that is legitimate: every `HOST_PATHS`
774
+ * source is always staged by `stageHost`, so a non-guide zero-match means a
775
+ * corrupted or truncated `manifest.json`, not an intentionally-unvendored
776
+ * artifact. For a guide pointer, a short stub file is written at the
777
+ * destination and reported exactly like a successful copy, mirroring the
778
+ * READ path (`hydratePlan` leaves such artifacts `content`-undefined, so
779
+ * `diffPlan` audits them by PRESENCE only). For every OTHER zero-match, the
780
+ * fail-closed `ScaffoldError('TARGET', …)` is thrown — degrading an
781
+ * unscoped zero-match would otherwise let a corrupted manifest silently stub
782
+ * an unrecoverable artifact (e.g. `AGENTS.md` — `pull` only ever fetches
783
+ * dependency guides) or write a FILE named `.claude` over what should be a
784
+ * directory artifact. The raw-root fallback (`manifest === undefined`, a
785
+ * caller-supplied `--from`) keeps its own throw regardless: an EXPLICITLY
786
+ * named source failing to resolve is a different, caller-error failure
787
+ * class, not a "not vendored" degrade.
788
+ *
789
+ * @remarks
790
+ * Defense in depth at the filesystem trust boundary: EVERY resolved
791
+ * destination (`materialize` and `repair`, both origins) is asserted to stay
792
+ * within `resolve(target)` before any write, and every `host`-origin copy
793
+ * source is asserted to stay within `resolve(host)` before any read — a
794
+ * traversal segment (`../`) in an artifact's `path` or `source` cannot escape
795
+ * either root, even if a gate upstream (e.g. an ungated `Plan` built by hand)
796
+ * let it through. A destination violation throws `ScaffoldError('WRITE', …)`;
797
+ * a source violation throws `ScaffoldError('TARGET', …)`.
798
+ *
799
+ * @remarks
800
+ * The containment check is REAL-PATH aware, not merely lexical: both the
801
+ * root (`target` / `host`) and the candidate destination/source are resolved
802
+ * through `realpathSync` on their DEEPEST EXISTING ancestor before the prefix
803
+ * comparison, so a symlinked subdirectory planted inside an otherwise
804
+ * legitimate root cannot smuggle a write (or read) outside it — `repair`,
805
+ * which has no `isVacant` gate, is covered exactly like `materialize`. A path
806
+ * segment that does not yet exist on disk (the still-to-be-created file/dir
807
+ * a write is about to create) is rejoined onto the resolved existing
808
+ * ancestor rather than realpath'd itself.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
813
+ * import { createMaterializer } from '@orkestrel/scaffold/server'
814
+ *
815
+ * const plan = blueprintToPlan(blueprint('budget', { surfaces: ['core'] }))
816
+ * const materializer = createMaterializer()
817
+ * materializer.materialize(plan, './packages/budget-new')
818
+ * materializer.destroy()
819
+ * ```
820
+ */
821
+ var Materializer = class Materializer {
822
+ #emitter;
823
+ #host;
824
+ #manifestLoaded = false;
825
+ #manifestEntries;
826
+ #destroyed = false;
827
+ constructor(options) {
828
+ this.#emitter = new _orkestrel_emitter.Emitter({
829
+ on: options?.on,
830
+ error: options?.error
831
+ });
832
+ this.#host = options?.host ?? hostRoot();
833
+ }
834
+ get emitter() {
835
+ return this.#emitter;
836
+ }
837
+ materialize(plan, target) {
838
+ this.#ensureAlive();
839
+ if (!isVacant(target)) throw new _src_core.ScaffoldError("TARGET", "materialize requires a vacant target", { target });
840
+ const written = [];
841
+ const copied = [];
842
+ for (const artifact of plan.artifacts) if (artifact.origin === "host") {
843
+ this.#copy(artifact, target);
844
+ copied.push(artifact.path);
845
+ } else {
846
+ this.#write(artifact, target);
847
+ written.push(artifact.path);
848
+ }
849
+ const result = {
850
+ target,
851
+ written,
852
+ copied,
853
+ skipped: [],
854
+ removed: []
855
+ };
856
+ this.#emitter.emit("done", result);
857
+ return result;
858
+ }
859
+ repair(plan, audit, target) {
860
+ this.#ensureAlive();
861
+ const drifted = new Map(audit.findings.map((finding) => [finding.path, finding.drift]));
862
+ const written = [];
863
+ const copied = [];
864
+ const skipped = [];
865
+ for (const artifact of plan.artifacts) {
866
+ const drift = drifted.get(artifact.path);
867
+ if (drift !== "missing" && drift !== "stale") {
868
+ skipped.push(artifact.path);
869
+ continue;
870
+ }
871
+ if (artifact.origin === "host") {
872
+ this.#copy(artifact, target);
873
+ copied.push(artifact.path);
874
+ } else {
875
+ this.#write(artifact, target);
876
+ written.push(artifact.path);
877
+ }
878
+ }
879
+ const result = {
880
+ target,
881
+ written,
882
+ copied,
883
+ skipped,
884
+ removed: []
885
+ };
886
+ this.#emitter.emit("done", result);
887
+ return result;
888
+ }
889
+ prune(target) {
890
+ this.#ensureAlive();
891
+ const removed = [];
892
+ for (const path of pruneTargets(target, this.#host)) {
893
+ (0, node_fs.unlinkSync)(Materializer.#assertContained(target, path, "WRITE", path));
894
+ removed.push(path);
895
+ this.#emitter.emit("remove", path);
896
+ }
897
+ const result = {
898
+ target,
899
+ written: [],
900
+ copied: [],
901
+ skipped: [],
902
+ removed
903
+ };
904
+ this.#emitter.emit("done", result);
905
+ return result;
906
+ }
907
+ destroy() {
908
+ if (this.#destroyed) return;
909
+ this.#destroyed = true;
910
+ this.#emitter.emit("destroy");
911
+ this.#emitter.destroy();
912
+ }
913
+ #copy(artifact, target) {
914
+ const source = artifact.source ?? artifact.path;
915
+ const manifest = this.#manifest();
916
+ if (manifest === void 0) {
917
+ const from = Materializer.#assertContained(this.#host, source, "TARGET", artifact.path);
918
+ const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
919
+ try {
920
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
921
+ (0, node_fs.cpSync)(from, to, { recursive: true });
922
+ } catch (error) {
923
+ this.#emitter.emit("error", error);
924
+ throw new _src_core.ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
925
+ path: artifact.path,
926
+ error
927
+ });
928
+ }
929
+ this.#emitter.emit("copy", artifact.path);
930
+ return;
931
+ }
932
+ const entries = manifest.filter((entry) => entry.destination === source || entry.destination.startsWith(`${source}/`));
933
+ if (entries.length === 0) {
934
+ if (!source.startsWith("guides/src/") || !source.endsWith(".md")) throw new _src_core.ScaffoldError("TARGET", `Manifest entry for "${source}" is missing — the vendored manifest may be corrupted or truncated`, {
935
+ target,
936
+ source
937
+ });
938
+ const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
939
+ try {
940
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
941
+ (0, node_fs.writeFileSync)(to, Materializer.#stub(source), "utf8");
942
+ } catch (error) {
943
+ this.#emitter.emit("error", error);
944
+ throw new _src_core.ScaffoldError("WRITE", `Failed to write host artifact stub at ${artifact.path}`, {
945
+ path: artifact.path,
946
+ error
947
+ });
948
+ }
949
+ this.#emitter.emit("copy", artifact.path);
950
+ return;
951
+ }
952
+ for (const entry of entries) {
953
+ const from = Materializer.#assertContained(this.#host, entry.storage, "TARGET", artifact.path);
954
+ const to = Materializer.#assertContained(target, entry.destination, "WRITE", artifact.path);
955
+ try {
956
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
957
+ (0, node_fs.cpSync)(from, to);
958
+ if (entry.executable) (0, node_fs.chmodSync)(to, 493);
959
+ } catch (error) {
960
+ this.#emitter.emit("error", error);
961
+ throw new _src_core.ScaffoldError("WRITE", `Failed to copy host artifact at ${artifact.path}`, {
962
+ path: artifact.path,
963
+ error
964
+ });
965
+ }
966
+ }
967
+ this.#emitter.emit("copy", artifact.path);
968
+ }
969
+ #manifest() {
970
+ if (!this.#manifestLoaded) {
971
+ this.#manifestEntries = readHostManifest(this.#host);
972
+ this.#manifestLoaded = true;
973
+ }
974
+ return this.#manifestEntries;
975
+ }
976
+ #write(artifact, target) {
977
+ const to = Materializer.#assertContained(target, artifact.path, "WRITE", artifact.path);
978
+ try {
979
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
980
+ (0, node_fs.writeFileSync)(to, artifact.content ?? "", "utf8");
981
+ } catch (error) {
982
+ this.#emitter.emit("error", error);
983
+ throw new _src_core.ScaffoldError("WRITE", `Failed to write artifact at ${artifact.path}`, {
984
+ path: artifact.path,
985
+ error
986
+ });
987
+ }
988
+ this.#emitter.emit("write", artifact.path);
989
+ }
990
+ static #assertContained(root, relative, code, path) {
991
+ const resolvedRoot = Materializer.#resolveReal((0, node_path.resolve)(root));
992
+ const resolvedCandidate = Materializer.#resolveReal((0, node_path.resolve)(root, relative));
993
+ if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + node_path.sep)) throw new _src_core.ScaffoldError(code, `Artifact path "${path}" escapes the ${code === "WRITE" ? "target" : "host"} root`, {
994
+ path,
995
+ root
996
+ });
997
+ return (0, node_path.resolve)(root, relative);
998
+ }
999
+ static #stub(source) {
1000
+ return `> Vendored guide for @orkestrel/${source.slice(11, source.length - 3)} — run \`scaffold pull\` to fetch it.\n`;
1001
+ }
1002
+ static #resolveReal(path) {
1003
+ if ((0, node_fs.existsSync)(path)) return (0, node_fs.realpathSync)(path);
1004
+ const parent = (0, node_path.dirname)(path);
1005
+ if (parent === path) return path;
1006
+ return (0, node_path.join)(Materializer.#resolveReal(parent), (0, node_path.relative)(parent, path));
1007
+ }
1008
+ #ensureAlive() {
1009
+ if (this.#destroyed) throw new _src_core.ScaffoldError("DESTROYED", "Materializer has been destroyed");
1010
+ }
1011
+ };
1012
+ //#endregion
1013
+ //#region src/server/Sync.ts
1014
+ /**
1015
+ * The upstream-synchronization entity (server) — the impure FETCH sibling of
1016
+ * `Materializer`, Promise-based and network-only.
1017
+ *
1018
+ * @remarks
1019
+ * Every method reads upstream over HTTPS with a 10-second per-request
1020
+ * `AbortSignal.timeout` and bounded `concurrency` (default 6, never an
1021
+ * unbounded `Promise.all`). The default COLLECT posture captures each
1022
+ * dependency's `freshness` (`404` → `missing`, transport / other non-2xx →
1023
+ * `failed`) into the result; `strict` mode instead throws
1024
+ * `ScaffoldError('FETCH', …)` naming the failing URL. `guides`'s optional
1025
+ * `current` parameter is a caller-supplied local-mirror content map keyed by
1026
+ * dependency NAME (the `diffPlan` caller-supplied-reference pattern): WITH the
1027
+ * map, a fetched guide byte-equal to its entry verdicts `current`, anything
1028
+ * differing or absent from the map verdicts `behind`; WITHOUT the map, every
1029
+ * successful fetch verdicts `behind` (no reference means it needs syncing).
1030
+ * `pull` builds that map itself from the TARGET's own `guides/src/<short>.md`
1031
+ * mirrors, so its verdicts are target-relative; `write` commits only the
1032
+ * `behind` guides (never `current`, `missing`, or `failed`, which carries no
1033
+ * trustworthy content) under the same realpath-anchored containment law
1034
+ * `Materializer` enforces. After `destroy()` every method throws `DESTROYED`;
1035
+ * teardown is idempotent, emitter last.
1036
+ *
1037
+ * @example
1038
+ * ```ts
1039
+ * import { createSync } from '@orkestrel/scaffold/server'
1040
+ *
1041
+ * const sync = createSync()
1042
+ * const report = await sync.pull('.')
1043
+ * if (report.failed === 0) await sync.write(report, '.')
1044
+ * sync.destroy()
1045
+ * ```
1046
+ */
1047
+ var Sync = class Sync {
1048
+ static #DEFAULT_TIMEOUT = 1e4;
1049
+ static #DEFAULT_CONCURRENCY = 6;
1050
+ static #DEFAULT_LIMIT = 5242880;
1051
+ #emitter;
1052
+ #guidesBase;
1053
+ #branch;
1054
+ #guidesTimeout;
1055
+ #registryBase;
1056
+ #registryTimeout;
1057
+ #concurrency;
1058
+ #retries;
1059
+ #strict;
1060
+ #limit;
1061
+ #destroyed = false;
1062
+ constructor(options) {
1063
+ this.#emitter = new _orkestrel_emitter.Emitter({
1064
+ on: options?.on,
1065
+ error: options?.error
1066
+ });
1067
+ this.#guidesBase = options?.guides?.base ?? "raw.githubusercontent.com";
1068
+ this.#branch = options?.guides?.branch ?? "main";
1069
+ this.#guidesTimeout = options?.guides?.timeout ?? Sync.#DEFAULT_TIMEOUT;
1070
+ this.#registryBase = options?.registry?.base ?? "registry.npmjs.org";
1071
+ this.#registryTimeout = options?.registry?.timeout ?? Sync.#DEFAULT_TIMEOUT;
1072
+ this.#concurrency = options?.concurrency ?? Sync.#DEFAULT_CONCURRENCY;
1073
+ this.#retries = options?.retries ?? 0;
1074
+ this.#strict = options?.strict ?? false;
1075
+ this.#limit = options?.limit ?? Sync.#DEFAULT_LIMIT;
1076
+ }
1077
+ get emitter() {
1078
+ return this.#emitter;
1079
+ }
1080
+ async guides(deps, current) {
1081
+ this.#ensureAlive();
1082
+ return Sync.#runPool(deps, this.#concurrency, async (dep) => {
1083
+ const short = Sync.#shortName(dep.name);
1084
+ const url = this.#guideUrl(short);
1085
+ const outcome = await Sync.#fetchText(url, this.#guidesTimeout, this.#retries, this.#limit);
1086
+ const guide = Sync.#toGuideSync(dep.name, short, outcome, current);
1087
+ if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
1088
+ this.#emitter.emit("guide", dep.name);
1089
+ if (this.#strict && (guide.freshness === "missing" || guide.freshness === "failed")) throw new _src_core.ScaffoldError("FETCH", `Failed to fetch guide at ${url}`, {
1090
+ url,
1091
+ name: dep.name
1092
+ });
1093
+ return guide;
1094
+ });
1095
+ }
1096
+ async versions(deps) {
1097
+ this.#ensureAlive();
1098
+ return Sync.#runPool(deps, this.#concurrency, async (dep) => {
1099
+ const url = this.#registryUrl(dep.name);
1100
+ const outcome = await Sync.#fetchText(url, this.#registryTimeout, this.#retries, this.#limit);
1101
+ const version = Sync.#toVersionSync(dep, outcome);
1102
+ if (outcome.kind === "failed") this.#emitter.emit("error", outcome.error);
1103
+ this.#emitter.emit("version", dep.name);
1104
+ if (this.#strict && (version.freshness === "missing" || version.freshness === "failed")) throw new _src_core.ScaffoldError("FETCH", `Failed to fetch registry version at ${url}`, {
1105
+ url,
1106
+ name: dep.name
1107
+ });
1108
+ return version;
1109
+ });
1110
+ }
1111
+ /**
1112
+ * The fleet package catalog, sourced from the npm registry — the
1113
+ * AUTHORITATIVE enumeration (never a caller-supplied root).
1114
+ *
1115
+ * @returns One {@link CatalogEntry} per `@orkestrel/*` package the registry
1116
+ * lists, code-unit sorted by `name`.
1117
+ * @remarks
1118
+ * Three fetches build each entry: (1) the org's exact package-list
1119
+ * (`-/org/orkestrel/package`) enumerates every published name — an
1120
+ * unreachable or malformed response throws a coded `ScaffoldError('FETCH')`
1121
+ * UNCONDITIONALLY (never gated by `strict`), since without it there is no
1122
+ * catalog to build; (2) each name's own registry packument supplies
1123
+ * `version` (`dist-tags.latest`) and a registry-path `description`
1124
+ * fallback — a failed/malformed packument keeps the entry (degraded:
1125
+ * `version: ''`) rather than dropping it, since the org list already
1126
+ * proved the package exists; (3) each name's own guide
1127
+ * (`guides/src/<short>.md`, same canonical URL as `guides()`, fetched
1128
+ * unauthenticated — every fleet repo is public) supplies the PREFERRED
1129
+ * `description` — its first blockquote's first paragraph — falling back
1130
+ * to the packument description when the guide 404s, faults, or carries no
1131
+ * blockquote; a 404 STAYS LISTED (it is a reachability signal, not an
1132
+ * absence) with a note reading `guide unreachable (HTTP 404 — repo
1133
+ * private or guide missing?)`. Emits `package` once per entry with a
1134
+ * combined human-readable `note` (empty when both fetches succeeded)
1135
+ * alongside the existing `error` events for each degraded sub-fetch.
1136
+ */
1137
+ async catalog() {
1138
+ this.#ensureAlive();
1139
+ const orgUrl = this.#orgUrl();
1140
+ const orgOutcome = await Sync.#fetchText(orgUrl, this.#registryTimeout, this.#retries, this.#limit);
1141
+ const names = Sync.#toOrgPackages(orgOutcome, orgUrl);
1142
+ return [...await Sync.#runPool(names, this.#concurrency, async (name) => {
1143
+ const packumentOutcome = await Sync.#fetchText(this.#registryUrl(name), this.#registryTimeout, this.#retries, this.#limit);
1144
+ const packument = Sync.#toPackument(packumentOutcome);
1145
+ const short = Sync.#shortName(name);
1146
+ const guideOutcome = await Sync.#fetchText(this.#guideUrl(short), this.#guidesTimeout, this.#retries, this.#limit);
1147
+ const guide = Sync.#toGuideDescription(guideOutcome);
1148
+ if (packumentOutcome.kind === "failed") this.#emitter.emit("error", packumentOutcome.error);
1149
+ if (guideOutcome.kind === "failed") this.#emitter.emit("error", guideOutcome.error);
1150
+ const description = guide.description ?? packument.description;
1151
+ const note = Sync.#combineNote(guide.note, packument.note);
1152
+ this.#emitter.emit("package", name, note);
1153
+ return {
1154
+ name,
1155
+ version: packument.version,
1156
+ description
1157
+ };
1158
+ })].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1159
+ }
1160
+ async pull(target) {
1161
+ this.#ensureAlive();
1162
+ const deps = (0, _src_core.manifestToDependencies)(readManifest(target));
1163
+ const current = {};
1164
+ for (const dep of deps) {
1165
+ const full = (0, node_path.join)(target, "guides", "src", `${Sync.#shortName(dep.name)}.md`);
1166
+ if (!(0, node_fs.existsSync)(full)) continue;
1167
+ try {
1168
+ current[dep.name] = (0, node_fs.readFileSync)(full, "utf8");
1169
+ } catch {}
1170
+ }
1171
+ const guides = await this.guides(deps, current);
1172
+ const versions = await this.versions(deps);
1173
+ const failed = [...guides, ...versions].filter((entry) => entry.freshness === "missing" || entry.freshness === "failed").length;
1174
+ const report = {
1175
+ target,
1176
+ guides,
1177
+ versions,
1178
+ clean: failed === 0 && guides.every((guide) => guide.freshness === "current") && versions.every((version) => version.freshness === "current"),
1179
+ failed
1180
+ };
1181
+ this.#emitter.emit("done", report);
1182
+ return report;
1183
+ }
1184
+ async write(report, target) {
1185
+ this.#ensureAlive();
1186
+ const written = [];
1187
+ for (const guide of report.guides) {
1188
+ if (guide.freshness !== "behind") continue;
1189
+ const to = Sync.#assertContained(target, guide.path);
1190
+ try {
1191
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(to), { recursive: true });
1192
+ (0, node_fs.writeFileSync)(to, guide.content, "utf8");
1193
+ } catch (error) {
1194
+ this.#emitter.emit("error", error);
1195
+ throw new _src_core.ScaffoldError("WRITE", `Failed to write guide at ${guide.path}`, {
1196
+ path: guide.path,
1197
+ error
1198
+ });
1199
+ }
1200
+ written.push(guide.path);
1201
+ this.#emitter.emit("write", guide.path);
1202
+ }
1203
+ return written;
1204
+ }
1205
+ destroy() {
1206
+ if (this.#destroyed) return;
1207
+ this.#destroyed = true;
1208
+ this.#emitter.emit("destroy");
1209
+ this.#emitter.destroy();
1210
+ }
1211
+ #guideUrl(short) {
1212
+ return `${Sync.#normalizeBase(this.#guidesBase)}/orkestrel/${short}/refs/heads/${this.#branch}/guides/src/${short}.md`;
1213
+ }
1214
+ #registryUrl(name) {
1215
+ return `${Sync.#normalizeBase(this.#registryBase)}/${name.replace("/", "%2F")}`;
1216
+ }
1217
+ #orgUrl() {
1218
+ return `${Sync.#normalizeBase(this.#registryBase)}/-/org/orkestrel/package`;
1219
+ }
1220
+ static #normalizeBase(base) {
1221
+ return /^https?:\/\//.test(base) ? base : `https://${base}`;
1222
+ }
1223
+ static #shortName(name) {
1224
+ return name.startsWith("@orkestrel/") ? name.slice(11) : name;
1225
+ }
1226
+ static #isRecord(value) {
1227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1228
+ }
1229
+ static async #runPool(items, concurrency, worker) {
1230
+ const results = new Array(items.length);
1231
+ const state = {
1232
+ cursor: 0,
1233
+ stopped: false,
1234
+ firstError: void 0
1235
+ };
1236
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => Sync.#drain({
1237
+ items,
1238
+ worker,
1239
+ results,
1240
+ state
1241
+ }));
1242
+ await Promise.allSettled(workers);
1243
+ if (state.firstError !== void 0) throw state.firstError.error;
1244
+ return results;
1245
+ }
1246
+ static async #drain(pool) {
1247
+ for (;;) {
1248
+ if (pool.state.stopped) return;
1249
+ const index = pool.state.cursor;
1250
+ pool.state.cursor += 1;
1251
+ if (index >= pool.items.length) return;
1252
+ try {
1253
+ pool.results[index] = await pool.worker(pool.items[index]);
1254
+ } catch (error) {
1255
+ if (pool.state.firstError === void 0) pool.state.firstError = { error };
1256
+ pool.state.stopped = true;
1257
+ return;
1258
+ }
1259
+ }
1260
+ }
1261
+ static async #fetchText(url, timeout, retries, limit) {
1262
+ let lastError;
1263
+ let lastNote = "";
1264
+ for (let attempt = 0; attempt <= retries; attempt += 1) try {
1265
+ const response = await fetch(url, {
1266
+ signal: AbortSignal.timeout(timeout),
1267
+ redirect: "manual"
1268
+ });
1269
+ if (response.status === 404) return { kind: "missing" };
1270
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
1271
+ lastNote = "redirected (redirect following is disabled)";
1272
+ lastError = /* @__PURE__ */ new Error(`Redirect blocked for ${url}`);
1273
+ continue;
1274
+ }
1275
+ if (!response.ok) {
1276
+ lastNote = `HTTP ${String(response.status)}`;
1277
+ lastError = /* @__PURE__ */ new Error(`Unexpected HTTP status ${response.status} for ${url}`);
1278
+ continue;
1279
+ }
1280
+ const read = await Sync.#readBounded(response, url, limit);
1281
+ if (!read.ok) {
1282
+ lastError = read.error;
1283
+ lastNote = read.note;
1284
+ continue;
1285
+ }
1286
+ return {
1287
+ kind: "ok",
1288
+ text: read.text
1289
+ };
1290
+ } catch (error) {
1291
+ lastError = error;
1292
+ lastNote = Sync.#transportNote(error);
1293
+ }
1294
+ return {
1295
+ kind: "failed",
1296
+ error: lastError,
1297
+ note: lastNote
1298
+ };
1299
+ }
1300
+ static #transportNote(error) {
1301
+ if (!(error instanceof Error)) return String(error);
1302
+ const code = Sync.#causeCode(error);
1303
+ return code !== void 0 ? `${error.message}: ${code}` : error.message;
1304
+ }
1305
+ static #causeCode(error) {
1306
+ const cause = error.cause;
1307
+ if (typeof cause !== "object" || cause === null) return void 0;
1308
+ if (!("code" in cause)) return void 0;
1309
+ return typeof cause.code === "string" ? cause.code : void 0;
1310
+ }
1311
+ static async #readBounded(response, url, limit) {
1312
+ const declared = response.headers.get("content-length");
1313
+ if (declared !== null) {
1314
+ const declaredBytes = Number(declared);
1315
+ if (Number.isFinite(declaredBytes) && declaredBytes > limit) return {
1316
+ ok: false,
1317
+ error: /* @__PURE__ */ new Error(`Response body for ${url} declares ${String(declaredBytes)} bytes, exceeding the ${String(limit)}-byte limit`),
1318
+ note: `response exceeded limit (${String(limit)} bytes)`
1319
+ };
1320
+ }
1321
+ const body = response.body;
1322
+ if (body === null) return {
1323
+ ok: true,
1324
+ text: ""
1325
+ };
1326
+ const reader = body.getReader();
1327
+ const decoder = new TextDecoder();
1328
+ const chunks = [];
1329
+ let total = 0;
1330
+ try {
1331
+ for (;;) {
1332
+ const { done, value } = await reader.read();
1333
+ if (done) break;
1334
+ total += value.byteLength;
1335
+ if (total > limit) return {
1336
+ ok: false,
1337
+ error: /* @__PURE__ */ new Error(`Response body for ${url} exceeded the ${String(limit)}-byte limit`),
1338
+ note: `response exceeded limit (${String(limit)} bytes)`
1339
+ };
1340
+ chunks.push(decoder.decode(value, { stream: true }));
1341
+ }
1342
+ chunks.push(decoder.decode());
1343
+ } finally {
1344
+ reader.releaseLock();
1345
+ }
1346
+ return {
1347
+ ok: true,
1348
+ text: chunks.join("")
1349
+ };
1350
+ }
1351
+ static #toGuideSync(name, short, outcome, current) {
1352
+ const path = `guides/src/${short}.md`;
1353
+ if (outcome.kind === "missing") return {
1354
+ name,
1355
+ path,
1356
+ content: "",
1357
+ freshness: "missing",
1358
+ note: "HTTP 404"
1359
+ };
1360
+ if (outcome.kind === "failed") return {
1361
+ name,
1362
+ path,
1363
+ content: "",
1364
+ freshness: "failed",
1365
+ note: outcome.note
1366
+ };
1367
+ const freshness = current?.[name] === outcome.text ? "current" : "behind";
1368
+ return {
1369
+ name,
1370
+ path,
1371
+ content: outcome.text,
1372
+ freshness
1373
+ };
1374
+ }
1375
+ static #toVersionSync(dep, outcome) {
1376
+ if (outcome.kind === "missing") return {
1377
+ name: dep.name,
1378
+ range: dep.range,
1379
+ latest: "",
1380
+ freshness: "missing",
1381
+ note: "HTTP 404"
1382
+ };
1383
+ if (outcome.kind === "failed") return {
1384
+ name: dep.name,
1385
+ range: dep.range,
1386
+ latest: "",
1387
+ freshness: "failed",
1388
+ note: outcome.note
1389
+ };
1390
+ const latest = Sync.#parseLatest(outcome.text);
1391
+ if (latest === void 0) return {
1392
+ name: dep.name,
1393
+ range: dep.range,
1394
+ latest: "",
1395
+ freshness: "failed",
1396
+ note: "malformed registry response (missing dist-tags.latest)"
1397
+ };
1398
+ return {
1399
+ name: dep.name,
1400
+ range: dep.range,
1401
+ latest,
1402
+ freshness: (0, _src_core.rangeToFreshness)(dep.range, latest)
1403
+ };
1404
+ }
1405
+ static #toOrgPackages(outcome, url) {
1406
+ if (outcome.kind === "missing" || outcome.kind === "failed") {
1407
+ const note = outcome.kind === "missing" ? "HTTP 404" : outcome.note;
1408
+ throw new _src_core.ScaffoldError("FETCH", `Failed to fetch the package list at ${url}`, {
1409
+ url,
1410
+ note
1411
+ });
1412
+ }
1413
+ let parsed;
1414
+ try {
1415
+ parsed = JSON.parse(outcome.text);
1416
+ } catch {
1417
+ throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1418
+ }
1419
+ if (!Sync.#isRecord(parsed)) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1420
+ const names = Object.keys(parsed).filter((name) => name.startsWith("@orkestrel/"));
1421
+ if (names.length === 0) throw new _src_core.ScaffoldError("FETCH", `Malformed package list response at ${url}`, { url });
1422
+ return names;
1423
+ }
1424
+ static #toPackument(outcome) {
1425
+ if (outcome.kind === "missing") return {
1426
+ version: "",
1427
+ description: "",
1428
+ note: "HTTP 404"
1429
+ };
1430
+ if (outcome.kind === "failed") return {
1431
+ version: "",
1432
+ description: "",
1433
+ note: outcome.note
1434
+ };
1435
+ let parsed;
1436
+ try {
1437
+ parsed = JSON.parse(outcome.text);
1438
+ } catch {
1439
+ return {
1440
+ version: "",
1441
+ description: "",
1442
+ note: "malformed registry response (invalid JSON)"
1443
+ };
1444
+ }
1445
+ if (!Sync.#isRecord(parsed)) return {
1446
+ version: "",
1447
+ description: "",
1448
+ note: "malformed registry response (not an object)"
1449
+ };
1450
+ const distTags = parsed["dist-tags"];
1451
+ const version = Sync.#isRecord(distTags) && typeof distTags.latest === "string" ? distTags.latest : "";
1452
+ return {
1453
+ version,
1454
+ description: typeof parsed.description === "string" ? parsed.description : "",
1455
+ note: version === "" ? "malformed registry response (missing dist-tags.latest)" : ""
1456
+ };
1457
+ }
1458
+ static #toGuideDescription(outcome) {
1459
+ if (outcome.kind === "missing") return {
1460
+ description: void 0,
1461
+ note: "guide unreachable (HTTP 404 — repo private or guide missing?)"
1462
+ };
1463
+ if (outcome.kind === "failed") return {
1464
+ description: void 0,
1465
+ note: outcome.note
1466
+ };
1467
+ const description = Sync.#firstBlockquoteParagraph(outcome.text);
1468
+ return description !== void 0 ? {
1469
+ description,
1470
+ note: ""
1471
+ } : {
1472
+ description: void 0,
1473
+ note: "guide has no blockquote description"
1474
+ };
1475
+ }
1476
+ static #firstBlockquoteParagraph(text) {
1477
+ let document;
1478
+ try {
1479
+ document = (0, _orkestrel_markdown.parseDocument)(text);
1480
+ } catch {
1481
+ return;
1482
+ }
1483
+ let quote;
1484
+ for (const node of (0, _orkestrel_markdown.walkNodes)(document)) if ((0, _orkestrel_markdown.isBlockquoteNode)(node)) {
1485
+ quote = node;
1486
+ break;
1487
+ }
1488
+ if (quote === void 0) return void 0;
1489
+ const paragraph = quote.children.find((child) => (0, _orkestrel_markdown.isParagraphNode)(child));
1490
+ if (paragraph === void 0) return void 0;
1491
+ const flattened = (0, _orkestrel_markdown.flattenText)(paragraph).replace(/\s+/g, " ").trim();
1492
+ return flattened.length > 0 ? flattened : void 0;
1493
+ }
1494
+ static #combineNote(guideNote, packumentNote) {
1495
+ const parts = [];
1496
+ if (packumentNote !== "") parts.push(`version unavailable — ${packumentNote}`);
1497
+ if (guideNote !== "") parts.push(`description from registry — ${guideNote}`);
1498
+ return parts.join("; ");
1499
+ }
1500
+ static #parseLatest(text) {
1501
+ let parsed;
1502
+ try {
1503
+ parsed = JSON.parse(text);
1504
+ } catch {
1505
+ return;
1506
+ }
1507
+ if (!Sync.#isRecord(parsed)) return void 0;
1508
+ const distTags = parsed["dist-tags"];
1509
+ if (!Sync.#isRecord(distTags)) return void 0;
1510
+ const latest = distTags.latest;
1511
+ return typeof latest === "string" ? latest : void 0;
1512
+ }
1513
+ static #assertContained(target, relative) {
1514
+ const resolvedRoot = Sync.#resolveReal((0, node_path.resolve)(target));
1515
+ const resolvedCandidate = Sync.#resolveReal((0, node_path.resolve)(target, relative));
1516
+ if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(resolvedRoot + node_path.sep)) throw new _src_core.ScaffoldError("WRITE", `Guide path "${relative}" escapes the target root`, {
1517
+ path: relative,
1518
+ target
1519
+ });
1520
+ return (0, node_path.resolve)(target, relative);
1521
+ }
1522
+ static #resolveReal(path) {
1523
+ if ((0, node_fs.existsSync)(path)) return (0, node_fs.realpathSync)(path);
1524
+ const parent = (0, node_path.dirname)(path);
1525
+ if (parent === path) return path;
1526
+ return (0, node_path.join)(Sync.#resolveReal(parent), (0, node_path.relative)(parent, path));
1527
+ }
1528
+ #ensureAlive() {
1529
+ if (this.#destroyed) throw new _src_core.ScaffoldError("DESTROYED", "Sync has been destroyed");
1530
+ }
1531
+ };
1532
+ //#endregion
1533
+ //#region src/server/factories.ts
1534
+ /**
1535
+ * Create a `MaterializerInterface` (server) — the materialization entity,
1536
+ * seeded from `MaterializerOptions`.
1537
+ *
1538
+ * @param options - Optional `host` root override, emitter hooks, and error handler
1539
+ * @returns A {@link MaterializerInterface}
1540
+ *
1541
+ * @example
1542
+ * ```ts
1543
+ * import { createMaterializer } from '@orkestrel/scaffold/server'
1544
+ *
1545
+ * const materializer = createMaterializer()
1546
+ * materializer.destroy()
1547
+ * ```
1548
+ */
1549
+ function createMaterializer(options) {
1550
+ return new Materializer(options);
1551
+ }
1552
+ /**
1553
+ * Create a `SyncInterface` (server) — the upstream-synchronization entity,
1554
+ * seeded from `SyncOptions`.
1555
+ *
1556
+ * @param options - Optional endpoint bases/branch, concurrency, retries, strict, emitter hooks, and error handler
1557
+ * @returns A {@link SyncInterface}
1558
+ *
1559
+ * @example
1560
+ * ```ts
1561
+ * import { createSync } from '@orkestrel/scaffold/server'
1562
+ *
1563
+ * const sync = createSync()
1564
+ * sync.destroy()
1565
+ * ```
1566
+ */
1567
+ function createSync(options) {
1568
+ return new Sync(options);
1569
+ }
1570
+ //#endregion
1571
+ exports.Materializer = Materializer;
1572
+ exports.PRUNE_DIRECTORIES = PRUNE_DIRECTORIES;
1573
+ exports.Sync = Sync;
1574
+ exports.catalogPackages = catalogPackages;
1575
+ exports.createMaterializer = createMaterializer;
1576
+ exports.createSync = createSync;
1577
+ exports.deriveBlueprint = deriveBlueprint;
1578
+ exports.discoverPackages = discoverPackages;
1579
+ exports.hostRoot = hostRoot;
1580
+ exports.hydratePlan = hydratePlan;
1581
+ exports.isManifestEntry = isManifestEntry;
1582
+ exports.isRecord = isRecord;
1583
+ exports.isVacant = isVacant;
1584
+ exports.listFiles = listFiles;
1585
+ exports.locateHostSource = locateHostSource;
1586
+ exports.pruneTargets = pruneTargets;
1587
+ exports.readHostManifest = readHostManifest;
1588
+ exports.readManifest = readManifest;
1589
+ exports.readTarget = readTarget;
1590
+ exports.selectOrkestrelEntries = selectOrkestrelEntries;
1591
+ exports.stageHost = stageHost;
1592
+ exports.storagePath = storagePath;
1593
+ exports.vendoredPruneSet = vendoredPruneSet;
1594
+
1595
+ //# sourceMappingURL=index.cjs.map