@orkestrel/scaffold 0.0.60 → 0.0.61

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 (61) hide show
  1. package/README.md +13 -10
  2. package/dist/bin/main.js +632 -320
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/CLAUDE.md +5 -1
  5. package/dist/host/agents/orchestration.md +44 -19
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +5 -5
  7. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
  8. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +10 -9
  9. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +21 -11
  10. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +20 -2
  11. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +1 -1
  13. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +1 -1
  14. package/dist/host/agents/templates/brief.md +16 -7
  15. package/dist/host/agents/transports/claude.md +4 -2
  16. package/dist/host/agents/transports/codex.md +4 -1
  17. package/dist/host/claude/agents/analyst.md +3 -1
  18. package/dist/host/claude/agents/application.md +1 -1
  19. package/dist/host/claude/agents/builder.md +3 -3
  20. package/dist/host/claude/agents/checker.md +5 -0
  21. package/dist/host/claude/agents/grok.md +15 -5
  22. package/dist/host/claude/agents/implementer.md +1 -1
  23. package/dist/host/claude/agents/orkestrel.md +2 -2
  24. package/dist/host/claude/agents/planner.md +10 -0
  25. package/dist/host/claude/agents/reviewer.md +14 -8
  26. package/dist/host/claude/agents/sol.md +3 -1
  27. package/dist/host/claude/agents/verifier.md +2 -4
  28. package/dist/host/claude/rules/architecture.md +7 -5
  29. package/dist/host/claude/rules/documentation.md +1 -0
  30. package/dist/host/claude/rules/names.md +23 -5
  31. package/dist/host/claude/rules/patterns.md +1 -0
  32. package/dist/host/claude/rules/quality.md +1 -1
  33. package/dist/host/claude/rules/tests.md +3 -3
  34. package/dist/host/claude/rules/typescript.md +4 -1
  35. package/dist/host/claude/rules/writing.md +2 -2
  36. package/dist/host/codex/agents/builder.toml +6 -6
  37. package/dist/host/codex/agents/checker.toml +2 -1
  38. package/dist/host/codex/agents/grok.toml +12 -5
  39. package/dist/host/codex/agents/implementer.toml +2 -2
  40. package/dist/host/codex/agents/opus.toml +6 -1
  41. package/dist/host/codex/agents/planner.toml +11 -6
  42. package/dist/host/codex/agents/reviewer.toml +8 -6
  43. package/dist/host/guides/scaffold.md +39 -14
  44. package/dist/host/manifest.json +41 -41
  45. package/dist/host/scripts/codex.sh +0 -0
  46. package/dist/host/scripts/cursor.sh +0 -0
  47. package/dist/host/scripts/deps.sh +0 -0
  48. package/dist/host/scripts/ollama.sh +0 -0
  49. package/dist/src/core/index.cjs +420 -278
  50. package/dist/src/core/index.cjs.map +1 -1
  51. package/dist/src/core/index.d.cts +361 -220
  52. package/dist/src/core/index.d.ts +361 -220
  53. package/dist/src/core/index.js +417 -279
  54. package/dist/src/core/index.js.map +1 -1
  55. package/dist/src/server/index.cjs +208 -170
  56. package/dist/src/server/index.cjs.map +1 -1
  57. package/dist/src/server/index.d.cts +276 -152
  58. package/dist/src/server/index.d.ts +276 -152
  59. package/dist/src/server/index.js +200 -172
  60. package/dist/src/server/index.js.map +1 -1
  61. package/package.json +4 -3
package/dist/bin/main.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { align, renderTable, strip, stripControls, width } from "@orkestrel/console";
3
- import { attempt, isRecord, isString, parseJSON } from "@orkestrel/contract";
3
+ import { attempt, isError, isRecord, isString, parseJSON } from "@orkestrel/contract";
4
+ import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isFloorPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
5
+ import { Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readSnapshot, resolveContainedPath } from "../src/server/index.js";
4
6
  import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
5
7
  import { executeSync } from "@orkestrel/process/server";
6
- import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isCanonPath, isDeferredPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
7
- import { Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readSnapshot, resolveContainedPath } from "../src/server/index.js";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/bin/constants.ts
10
10
  /**
11
- * The name the executable installs as.
11
+ * Names the command the executable installs as.
12
12
  *
13
13
  * @remarks
14
14
  * `package.json`'s `bin` field is the authority for what the command is called;
@@ -17,7 +17,7 @@ import { parseArgs } from "node:util";
17
17
  */
18
18
  var EXECUTABLE_NAME = "scaffold";
19
19
  /**
20
- * The {@link Verb} values in usage order, frozen.
20
+ * Lists the {@link Verb} values in usage order, frozen.
21
21
  *
22
22
  * @remarks
23
23
  * The order the type declares them in, which is also the order usage lists them:
@@ -32,7 +32,7 @@ var VERBS = Object.freeze([
32
32
  "overwrite"
33
33
  ]);
34
34
  /**
35
- * What each exit code means, frozen.
35
+ * Describes what each exit code means, frozen.
36
36
  *
37
37
  * @remarks
38
38
  * Keyed by the code constants rather than by literals, so the usage block
@@ -44,7 +44,7 @@ var EXIT_SUMMARY = Object.freeze({
44
44
  [2]: "usage error"
45
45
  });
46
46
  /**
47
- * The machine-readable code a malformed command line reports.
47
+ * Names the machine-readable code a malformed command line reports.
48
48
  *
49
49
  * @remarks
50
50
  * The executable contributes its own codes to the failure envelope, which is why
@@ -53,12 +53,12 @@ var EXIT_SUMMARY = Object.freeze({
53
53
  * operation could.
54
54
  */
55
55
  var USAGE_CODE = "USAGE";
56
- /** The machine-readable code a failure carrying no code of its own reports. */
56
+ /** Names the machine-readable code a failure carrying no code of its own reports. */
57
57
  var FAILED_CODE = "FAILED";
58
- /** What the failure envelope says when the raised value carried no message. */
58
+ /** Holds what the failure envelope says when the raised value carried no message. */
59
59
  var FAILED_MESSAGE = "The command failed for an unrecognized reason";
60
60
  /**
61
- * The positional argument `new` alone takes, as usage writes it.
61
+ * Names the positional argument `new` alone takes, as usage writes it.
62
62
  *
63
63
  * @remarks
64
64
  * The workspace name is the only positional argument any verb takes, so it is
@@ -66,7 +66,7 @@ var FAILED_MESSAGE = "The command failed for an unrecognized reason";
66
66
  */
67
67
  var NAME_ARGUMENT = "<name>";
68
68
  /**
69
- * Every option the executable accepts, as `node:util` parses them, frozen.
69
+ * Declares every option the executable accepts, as `node:util` parses them, frozen.
70
70
  *
71
71
  * @remarks
72
72
  * One table for every verb rather than one per verb, because the verb an option
@@ -95,7 +95,7 @@ var COMMAND_OPTIONS = Object.freeze({
95
95
  json: Object.freeze({ type: "boolean" })
96
96
  });
97
97
  /**
98
- * What each option does, keyed by the token usage prints, frozen.
98
+ * Describes what each option does, keyed by the token usage prints, frozen.
99
99
  *
100
100
  * @remarks
101
101
  * The key order is the glossary order. A key is the whole displayed token,
@@ -118,7 +118,7 @@ var OPTION_SUMMARY = Object.freeze({
118
118
  ORKESTREL_SCAFFOLD_REPOSITORY: "the repository base mapped to upstream.repository.base"
119
119
  });
120
120
  /**
121
- * The options each verb takes, in usage order, frozen.
121
+ * Lists the options each verb takes, in usage order, frozen.
122
122
  *
123
123
  * @remarks
124
124
  * The executable's half of the frozen command union: every option a branch
@@ -167,7 +167,7 @@ var VERB_OPTIONS = Object.freeze({
167
167
  ])
168
168
  });
169
169
  /**
170
- * What each verb does, in one line, frozen.
170
+ * Describes what each verb does, in one line, frozen.
171
171
  *
172
172
  * @remarks
173
173
  * Each line names what the verb writes, because authority is the verb's: a
@@ -183,7 +183,7 @@ var VERB_SUMMARY = Object.freeze({
183
183
  //#endregion
184
184
  //#region src/bin/errors.ts
185
185
  /**
186
- * The error raised when a command line is not a command.
186
+ * Represents the error raised when a command line is not a command.
187
187
  *
188
188
  * @remarks
189
189
  * Distinct from `ScaffoldError` because they answer different questions and
@@ -210,7 +210,7 @@ var VERB_SUMMARY = Object.freeze({
210
210
  var UsageError = class extends Error {
211
211
  code;
212
212
  /**
213
- * Construct a usage error.
213
+ * Constructs a usage error.
214
214
  *
215
215
  * @param message - What was wrong with the command line, in one sentence.
216
216
  */
@@ -221,10 +221,10 @@ var UsageError = class extends Error {
221
221
  }
222
222
  };
223
223
  /**
224
- * Narrow a caught value to a {@link UsageError}.
224
+ * Narrows a caught value to a {@link UsageError}.
225
225
  *
226
226
  * @param value - The caught value to narrow.
227
- * @returns `true` when `value` is a {@link UsageError}.
227
+ * @returns True if `value` is a {@link UsageError}; false otherwise.
228
228
  *
229
229
  * @example
230
230
  * ```ts
@@ -240,7 +240,7 @@ function isUsageError(value) {
240
240
  //#endregion
241
241
  //#region src/bin/helpers.ts
242
242
  /**
243
- * Read the option name out of the token usage displays it as.
243
+ * Reads the option name out of the token usage displays it as.
244
244
  *
245
245
  * @param option - The displayed token, such as `--from <path>`.
246
246
  * @returns The bare name `node:util` parses the option under.
@@ -263,7 +263,7 @@ function optionToName(option) {
263
263
  return token.startsWith("--") ? token.slice(2) : token;
264
264
  }
265
265
  /**
266
- * Project process environment endpoints into upstream reader options.
266
+ * Projects process environment endpoints into upstream reader options.
267
267
  *
268
268
  * @param environment - The process environment to read.
269
269
  * @returns The configured endpoint groups, or `undefined` when neither endpoint
@@ -279,7 +279,7 @@ function environmentToUpstream(environment) {
279
279
  };
280
280
  }
281
281
  /**
282
- * Render one verb's synopsis.
282
+ * Renders one verb's synopsis.
283
283
  *
284
284
  * @param verb - The verb to describe.
285
285
  * @returns The command line this verb accepts, with every option bracketed.
@@ -300,7 +300,7 @@ function verbToSyntax(verb) {
300
300
  return `${EXECUTABLE_NAME} ${verb}${verb === "new" ? ` ${NAME_ARGUMENT}` : ""} ${VERB_OPTIONS[verb].map((option) => `[${option}]`).join(" ")}`;
301
301
  }
302
302
  /**
303
- * Render the whole command reference.
303
+ * Renders the whole command reference.
304
304
  *
305
305
  * @returns One line per output call: the synopsis, every verb, the option glossary, and the exit codes.
306
306
  *
@@ -327,7 +327,7 @@ function renderUsage() {
327
327
  ];
328
328
  }
329
329
  /**
330
- * Read one command out of the arguments following the executable's own name.
330
+ * Reads one command out of the arguments following the executable's own name.
331
331
  *
332
332
  * @param argv - The arguments the executable was given.
333
333
  * @returns The command the arguments denote.
@@ -364,7 +364,7 @@ function argvToCommand(argv) {
364
364
  }));
365
365
  if (!parsed.success) {
366
366
  const cause = parsed.error;
367
- throw new UsageError(cause instanceof Error ? cause.message : `Could not read the arguments to '${verb}'.`);
367
+ throw new UsageError(isError(cause) ? cause.message : `Could not read the arguments to '${verb}'.`);
368
368
  }
369
369
  const { positionals, values } = parsed.value;
370
370
  const accepted = VERB_OPTIONS[verb].map((option) => optionToName(option));
@@ -436,7 +436,7 @@ function argvToCommand(argv) {
436
436
  }
437
437
  }
438
438
  /**
439
- * Read the exit code an audit reports.
439
+ * Reads the exit code an audit reports.
440
440
  *
441
441
  * @param audit - The comparison of a plan against a target.
442
442
  * @returns `EXIT_CLEAN` when the target matched the plan, `EXIT_DRIFT` otherwise.
@@ -463,7 +463,7 @@ function auditToExit(audit) {
463
463
  return blocked || drifted ? 1 : 0;
464
464
  }
465
465
  /**
466
- * Read the runtime and development rows a writing verb may raise.
466
+ * Reads the runtime and development rows a writing verb may raise.
467
467
  *
468
468
  * @param manifest - The target manifest text.
469
469
  * @param blueprint - The workspace shape that supplies the planned tool set.
@@ -502,7 +502,7 @@ function manifestToWritableDependencies(manifest, blueprint) {
502
502
  };
503
503
  }
504
504
  /**
505
- * Keep only dependencies published by the Orkestrel fleet.
505
+ * Keeps only dependencies published by the Orkestrel fleet.
506
506
  *
507
507
  * @param dependencies - The declared dependencies to inspect.
508
508
  * @returns The fleet declarations in their original order.
@@ -511,7 +511,7 @@ function dependenciesToFleet(dependencies) {
511
511
  return dependencies.filter((dependency) => dependency.name.startsWith("@orkestrel/"));
512
512
  }
513
513
  /**
514
- * Project declared concrete ranges to their distributed release floors.
514
+ * Projects declared concrete ranges to their distributed release floors.
515
515
  *
516
516
  * @param dependencies - The declarations whose floor versions to read.
517
517
  * @returns One found floor per declaration, or `undefined` when any range names
@@ -531,7 +531,7 @@ function dependenciesToFloors(dependencies) {
531
531
  return releases;
532
532
  }
533
533
  /**
534
- * Read the exit code registry release evidence earns.
534
+ * Reads the exit code registry release evidence earns.
535
535
  *
536
536
  * @param releases - The release verdicts to measure.
537
537
  * @returns `EXIT_DRIFT` for a failed lookup or an exact fleet mismatch; `EXIT_CLEAN` otherwise.
@@ -540,7 +540,7 @@ function releasesToExit(releases) {
540
540
  return releases.some((release) => release.lookup !== "found" || release.name.startsWith("@orkestrel/") && release.range !== `^${release.latest}`) ? 1 : 0;
541
541
  }
542
542
  /**
543
- * Project foreign floor and supported-major drift into audit questions.
543
+ * Projects foreign floor and supported-major drift into audit questions.
544
544
  *
545
545
  * @param releases - The release verdicts to measure.
546
546
  * @param served - Alternate release verdicts used to detect a newer served major.
@@ -570,7 +570,7 @@ function releasesToQuestions(releases, served = releases) {
570
570
  return questions;
571
571
  }
572
572
  /**
573
- * Project an audit into the human summary of its outcome.
573
+ * Projects an audit into the human summary of its outcome.
574
574
  *
575
575
  * @param audit - The comparison to summarize.
576
576
  * @returns The refusal, or the planned-path outcome, its grounds, and any foreign-path count.
@@ -607,7 +607,7 @@ function auditToSummary(audit) {
607
607
  return foreign === 0 ? summary : `${summary} The plan does not own ${String(foreign)} further path${foreign === 1 ? "" : "s"} beneath its groups.`;
608
608
  }
609
609
  /**
610
- * Project a raised value into the machine-readable failure envelope.
610
+ * Projects a raised value into the machine-readable failure envelope.
611
611
  *
612
612
  * @param error - The value a command raised.
613
613
  * @returns The envelope naming the coded reason and what went wrong.
@@ -633,13 +633,556 @@ function errorToEnvelope(error) {
633
633
  } };
634
634
  return { error: {
635
635
  code: FAILED_CODE,
636
- message: error instanceof Error ? error.message : FAILED_MESSAGE
636
+ message: isError(error) ? error.message : FAILED_MESSAGE
637
637
  } };
638
638
  }
639
+ /**
640
+ * Writes one report line to the process output stream.
641
+ *
642
+ * @param line - The already-sanitized line to write.
643
+ *
644
+ * @remarks
645
+ * The destination a terminal caller means for the report. It is the executable's
646
+ * default `output` handler and the only place this package names
647
+ * `process.stdout`, so a caller driving the executable from inside another
648
+ * process replaces the whole write path by supplying its own handler.
649
+ *
650
+ * @example
651
+ * ```ts
652
+ * import { writeOutput } from './helpers.js'
653
+ *
654
+ * writeOutput('0 written, 0 unchanged, 0 removed in ./target.')
655
+ * ```
656
+ */
657
+ function writeOutput(line) {
658
+ process.stdout.write(`${line}\n`);
659
+ }
660
+ /**
661
+ * Writes one diagnostic line to the process error stream.
662
+ *
663
+ * @param line - The already-sanitized line to write.
664
+ *
665
+ * @remarks
666
+ * The destination a terminal caller means for everything that must stay off the
667
+ * report, so a piped machine-readable value is never polluted by a warning. It
668
+ * is the executable's default `diagnostic` handler and the only place this
669
+ * package names `process.stderr`.
670
+ *
671
+ * @example
672
+ * ```ts
673
+ * import { writeDiagnostic } from './helpers.js'
674
+ *
675
+ * writeDiagnostic('FAILED: the target is not a git repository.')
676
+ * ```
677
+ */
678
+ function writeDiagnostic(line) {
679
+ process.stderr.write(`${line}\n`);
680
+ }
681
+ /**
682
+ * Strips one line of everything a terminal would act on rather than print.
683
+ *
684
+ * @param line - The line to sanitize.
685
+ * @returns The line with ANSI escapes and control characters removed and every
686
+ * break folded to a space.
687
+ *
688
+ * @remarks
689
+ * The single write path's cleaner, and the only place hostile bytes are answered
690
+ * for. A refusal quotes the argument that caused it, so an escape sequence, a
691
+ * bell, or a forged second line arrives here inside otherwise ordinary prose.
692
+ * ANSI escapes go first because they are what repaints a terminal, control
693
+ * characters next, and what remains is folded onto one line because a handler
694
+ * takes one line and a caller writing a record per call must get one record.
695
+ * A lone carriage return is folded as a break too, because a terminal repaints
696
+ * the line already printed on one. The fold is a replacement rather than a line
697
+ * split, so no bare `\r` is read as a line terminator here.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * import { sanitizeLine } from './helpers.js'
702
+ *
703
+ * sanitizeLine('first\nsecond') // 'first second'
704
+ * ```
705
+ */
706
+ function sanitizeLine(line) {
707
+ return stripControls(strip(line)).replace(/\r\n|\r|\n/gu, " ");
708
+ }
709
+ /**
710
+ * Projects an incomplete version resolution into the refusal it earns.
711
+ *
712
+ * @param versions - The resolution to measure.
713
+ * @returns The refusal to throw, or `undefined` when the resolution is complete.
714
+ *
715
+ * @remarks
716
+ * Read before a caller opens a write, so a run that could not name every floor
717
+ * refuses instead of writing a partial pin set.
718
+ *
719
+ * @example
720
+ * ```ts
721
+ * import { versionsToRefusal } from './helpers.js'
722
+ *
723
+ * versionsToRefusal({ releases: [], pins: { runtime: [], development: [] }, forced: false, complete: true })
724
+ * // undefined
725
+ * ```
726
+ */
727
+ function versionsToRefusal(versions) {
728
+ if (versions.complete) return void 0;
729
+ const names = versions.releases.filter((release) => release.lookup !== "found").map((release) => release.name);
730
+ return new ScaffoldError("FETCH", names.length === 0 ? "A declared dependency names no concrete floor." : `The registry named no release for ${names.join(", ")}.`, { names: names.length });
731
+ }
732
+ /**
733
+ * Projects an incomplete catalog fetch into the refusal it earns.
734
+ *
735
+ * @param entries - The catalog rows the fetch produced.
736
+ * @param mirrors - The guide mirrors the fetch produced.
737
+ * @returns The refusal to throw, or `undefined` when every row answered.
738
+ *
739
+ * @remarks
740
+ * A catalog transaction starts only after every packument answered. A mirror the
741
+ * host could not serve — a failed read or an absent guide, which is what a
742
+ * published package with a private repository answers with — is skipped and
743
+ * reported rather than refusing every other write, because one unreachable
744
+ * package never costs the caller the rest of the fetch.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * import { fetchToRefusal } from './helpers.js'
749
+ *
750
+ * fetchToRefusal([], []) // undefined
751
+ * ```
752
+ */
753
+ function fetchToRefusal(entries, mirrors) {
754
+ const failed = [...entries.filter((entry) => entry.lookup !== "found").map((entry) => entry.name), ...mirrors.filter((mirror) => mirror.lookup !== "found" && mirror.lookup !== "failed" && mirror.lookup !== "missing").map((mirror) => mirror.name)];
755
+ if (failed.length === 0) return void 0;
756
+ return new ScaffoldError("FETCH", `Upstream produced no complete catalog answer for ${failed.join(", ")}.`, { names: failed.length });
757
+ }
758
+ /**
759
+ * Projects the catalog packuments already read into declared release evidence.
760
+ *
761
+ * @param declared - The declared dependencies to answer for, in declaration order.
762
+ * @param entries - The catalog rows the fetch produced.
763
+ * @returns One release verdict per declaration, in input order.
764
+ *
765
+ * @remarks
766
+ * The catalog verb already holds every fleet packument, so a declared range is
767
+ * measured against what that read returned rather than against a second request.
768
+ *
769
+ * @example
770
+ * ```ts
771
+ * import { entriesToReleases } from './helpers.js'
772
+ *
773
+ * entriesToReleases([{ name: '@orkestrel/emitter', range: '^0.0.5' }], [])
774
+ * // [{ name: '@orkestrel/emitter', range: '^0.0.5', lookup: 'missing', note: '…' }]
775
+ * ```
776
+ */
777
+ function entriesToReleases(declared, entries) {
778
+ return declared.map((dependency) => {
779
+ const entry = entries.find((candidate) => candidate.name === dependency.name);
780
+ if (entry?.lookup === "found") return {
781
+ ...dependency,
782
+ lookup: "found",
783
+ latest: entry.version
784
+ };
785
+ return {
786
+ ...dependency,
787
+ lookup: entry?.lookup ?? "missing",
788
+ note: entry?.note ?? "the organization catalog does not list the declared package"
789
+ };
790
+ });
791
+ }
792
+ /**
793
+ * Projects release evidence into the complete pin set a write may apply.
794
+ *
795
+ * @param releases - The release verdicts, one per declared dependency, in the
796
+ * order the declarations were looked up.
797
+ * @param declared - The runtime and development declarations the verdicts answer for.
798
+ * @returns The runtime and development pins, each at the caret of its release.
799
+ * @throws `ScaffoldError('FETCH', …)` when a verdict named no release, when a
800
+ * verdict has no matching declaration, or when the two sets differ in length.
801
+ *
802
+ * @remarks
803
+ * All or nothing: a caller never writes a partial pin set, because a manifest
804
+ * carrying some raised ranges and some stale ones is harder to recover from than
805
+ * one that was not written at all.
806
+ *
807
+ * @example
808
+ * ```ts
809
+ * import { releasesToPins } from './helpers.js'
810
+ *
811
+ * releasesToPins(
812
+ * [{ name: '@orkestrel/emitter', range: '^0.0.5', lookup: 'found', latest: '0.0.6' }],
813
+ * { runtime: [{ name: '@orkestrel/emitter', range: '^0.0.5' }], development: [] },
814
+ * ) // { runtime: [{ name: '@orkestrel/emitter', range: '^0.0.6' }], development: [] }
815
+ * ```
816
+ */
817
+ function releasesToPins(releases, declared) {
818
+ const refused = releases.filter((release) => release.lookup !== "found");
819
+ if (refused.length > 0) throw new ScaffoldError("FETCH", `The registry named no release for ${refused.map((release) => release.name).join(", ")}.`, { names: refused.length });
820
+ const dependencies = [...declared.runtime, ...declared.development];
821
+ const pins = [];
822
+ for (let index = 0; index < releases.length; index += 1) {
823
+ const release = releases[index];
824
+ const dependency = dependencies[index];
825
+ if (release === void 0 || dependency === void 0) throw new ScaffoldError("FETCH", "The release answer has no matching declaration.");
826
+ if (release.lookup !== "found") throw new ScaffoldError("FETCH", `The registry named no release for ${release.name}.`);
827
+ pins.push({
828
+ name: dependency.name,
829
+ range: `^${release.latest}`
830
+ });
831
+ }
832
+ if (pins.length !== dependencies.length) throw new ScaffoldError("FETCH", "The release answer does not match the declaration set.");
833
+ return {
834
+ runtime: pins.slice(0, declared.runtime.length),
835
+ development: pins.slice(declared.runtime.length)
836
+ };
837
+ }
838
+ /**
839
+ * Reads the literal Vitest projects and npm run scripts one shell command invokes.
840
+ *
841
+ * @param script - The manifest script text to read.
842
+ * @returns The invoked projects and scripts, or `undefined` when the command
843
+ * cannot be read literally.
844
+ *
845
+ * @remarks
846
+ * Quotes group a token but do not hide the option, while shell expansions make
847
+ * its value unresolved and therefore refuse the write that asked the question.
848
+ * An unterminated quote, a trailing escape, and an unresolved `--project` value
849
+ * all answer `undefined` rather than a partial reading.
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * import { scriptToInvocations } from './helpers.js'
854
+ *
855
+ * scriptToInvocations('vitest run --project src:core')
856
+ * // { projects: ['src:core'], scripts: [] }
857
+ * ```
858
+ */
859
+ function scriptToInvocations(script) {
860
+ const tokens = [];
861
+ let value = "";
862
+ let resolved = true;
863
+ let started = false;
864
+ let quote;
865
+ for (let index = 0; index < script.length; index += 1) {
866
+ const character = script[index];
867
+ if (character === void 0) return void 0;
868
+ if (quote === void 0) {
869
+ if (/\s/.test(character)) {
870
+ if (started) tokens.push({
871
+ value,
872
+ resolved
873
+ });
874
+ value = "";
875
+ resolved = true;
876
+ started = false;
877
+ continue;
878
+ }
879
+ if (";&|()".includes(character)) {
880
+ if (started) tokens.push({
881
+ value,
882
+ resolved
883
+ });
884
+ const paired = script[index + 1] === character && (character === "&" || character === "|");
885
+ tokens.push({
886
+ value: paired ? `${character}${character}` : character,
887
+ resolved: true
888
+ });
889
+ value = "";
890
+ resolved = true;
891
+ started = false;
892
+ if (paired) index += 1;
893
+ continue;
894
+ }
895
+ if (character === "\"" || character === "'") {
896
+ quote = character;
897
+ started = true;
898
+ continue;
899
+ }
900
+ if (character === "\\") {
901
+ const escaped = script[index + 1];
902
+ if (escaped === void 0) return void 0;
903
+ value += escaped;
904
+ started = true;
905
+ index += 1;
906
+ continue;
907
+ }
908
+ if (character === "$" || character === "`" || character === "%") resolved = false;
909
+ value += character;
910
+ started = true;
911
+ continue;
912
+ }
913
+ if (character === quote) {
914
+ quote = void 0;
915
+ continue;
916
+ }
917
+ if (character === "\\" && quote === "\"") {
918
+ const escaped = script[index + 1];
919
+ if (escaped === void 0) return void 0;
920
+ value += escaped;
921
+ index += 1;
922
+ continue;
923
+ }
924
+ if (quote === "\"" && (character === "$" || character === "`" || character === "%")) resolved = false;
925
+ value += character;
926
+ started = true;
927
+ }
928
+ if (quote !== void 0) return void 0;
929
+ if (started) tokens.push({
930
+ value,
931
+ resolved
932
+ });
933
+ const projects = [];
934
+ const scripts = [];
935
+ for (let index = 0; index < tokens.length; index += 1) {
936
+ const token = tokens[index];
937
+ if (token === void 0) return void 0;
938
+ if (token.value === "npm" && tokens[index + 1]?.value === "run") {
939
+ const name = tokens[index + 2];
940
+ if (name === void 0 || !name.resolved || name.value.length === 0 || [
941
+ "&&",
942
+ "||",
943
+ ";",
944
+ "|",
945
+ "&",
946
+ "(",
947
+ ")"
948
+ ].includes(name.value)) return void 0;
949
+ scripts.push(name.value);
950
+ index += 2;
951
+ continue;
952
+ }
953
+ if (token.value === "--project") {
954
+ const project = tokens[index + 1];
955
+ if (project === void 0 || !project.resolved || project.value.length === 0 || [
956
+ "&&",
957
+ "||",
958
+ ";",
959
+ "|",
960
+ "&",
961
+ "(",
962
+ ")"
963
+ ].includes(project.value)) return void 0;
964
+ projects.push(project.value);
965
+ index += 1;
966
+ continue;
967
+ }
968
+ if (token.value.startsWith("--project=")) {
969
+ const project = token.value.slice(10);
970
+ if (!token.resolved || project.length === 0) return void 0;
971
+ projects.push(project);
972
+ continue;
973
+ }
974
+ if (!token.resolved && token.value.includes("--project")) return void 0;
975
+ }
976
+ return {
977
+ projects,
978
+ scripts
979
+ };
980
+ }
981
+ /**
982
+ * Reads the environments one axis of a target physically ships.
983
+ *
984
+ * @param target - The target directory to read.
985
+ * @param axis - The axis directory, `src` or `app`.
986
+ * @returns The environments that axis holds as directories, in declared order.
987
+ *
988
+ * @remarks
989
+ * Read as directories rather than declared, because a directory is the fact and
990
+ * a declaration would be a second copy of it free to disagree.
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * import { targetToEnvironments } from './helpers.js'
995
+ *
996
+ * targetToEnvironments('./packages/router', 'src') // ['core', 'server']
997
+ * ```
998
+ */
999
+ function targetToEnvironments(target, axis) {
1000
+ return ENVIRONMENTS.filter((environment) => {
1001
+ const full = resolveContainedPath(target, `${axis}/${environment}`);
1002
+ return full !== void 0 && isPhysicalDirectory(full);
1003
+ });
1004
+ }
1005
+ /**
1006
+ * Reads the packages a target's catalog table listed before this run.
1007
+ *
1008
+ * @param target - The target directory holding the catalog agent file.
1009
+ * @returns The package names the table's first column carries, in table order,
1010
+ * and no names when the file is absent.
1011
+ *
1012
+ * @remarks
1013
+ * Read through the declared markdown parser rather than by pattern, so a row is
1014
+ * a row because the document says so.
1015
+ *
1016
+ * @example
1017
+ * ```ts
1018
+ * import { catalogToNames } from './helpers.js'
1019
+ *
1020
+ * catalogToNames('./packages/router') // ['@orkestrel/contract', '@orkestrel/emitter']
1021
+ * ```
1022
+ */
1023
+ function catalogToNames(target) {
1024
+ const text = readFileText(target, CATALOG_AGENT_PATH);
1025
+ if (text === void 0) return [];
1026
+ const names = [];
1027
+ for (const table of createMarkdown(text).filter(isTableNode)) for (const row of table.rows) {
1028
+ const [cell] = row;
1029
+ if (cell === void 0) continue;
1030
+ const name = cell.map(flattenText).join("").trim();
1031
+ if (DEPENDENCY_NAME_PATTERN.test(name)) names.push(name);
1032
+ }
1033
+ return names;
1034
+ }
1035
+ /**
1036
+ * Reads one git query as its NUL-separated records.
1037
+ *
1038
+ * @param target - The repository the query runs in.
1039
+ * @param args - The git arguments, without the program name.
1040
+ * @returns The non-empty records git wrote, in git's own order.
1041
+ * @throws `ScaffoldError('TARGET', …)` when the query fails, which is what a
1042
+ * directory that is not a git repository answers with.
1043
+ *
1044
+ * @remarks
1045
+ * Git is asked rather than reimplemented, because the tracked set and the dirty
1046
+ * set are git's own answers and nothing else can give them. `executeSync`
1047
+ * resolves the bare `git` name against `PATH` and `PATHEXT` on Windows, never
1048
+ * through a shell, so the query runs without an extension of its own. A failed
1049
+ * run resolves instead of throwing there, so git's own refusal is buffered into
1050
+ * the result rather than written onto a stream nobody chose.
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * import { readGitRecords } from './helpers.js'
1055
+ *
1056
+ * readGitRecords('./packages/router', ['ls-files', '-z']) // ['package.json', 'src/core/index.ts']
1057
+ * ```
1058
+ */
1059
+ function readGitRecords(target, args) {
1060
+ const result = executeSync({
1061
+ file: "git",
1062
+ arguments: [...args]
1063
+ }, {
1064
+ workspace: target,
1065
+ limit: MAX_MANIFEST_BYTES,
1066
+ strict: false
1067
+ });
1068
+ if (result.failed) throw new ScaffoldError("TARGET", `The target at ${target} is not a git repository.`, { target });
1069
+ return result.stdout.split("\0").filter((record) => record.length > 0);
1070
+ }
1071
+ /**
1072
+ * Reads the environments a comma-separated selection names.
1073
+ *
1074
+ * @param selection - The selection text, or `undefined` for no selection.
1075
+ * @param axis - The axis the option configures, quoted in a refusal.
1076
+ * @returns The named environments in declared order, and none for no selection.
1077
+ * @throws `UsageError` when the selection names something that is not an environment.
1078
+ *
1079
+ * @example
1080
+ * ```ts
1081
+ * import { selectionToEnvironments } from './helpers.js'
1082
+ *
1083
+ * selectionToEnvironments('server,core', 'src') // ['core', 'server']
1084
+ * ```
1085
+ */
1086
+ function selectionToEnvironments(selection, axis) {
1087
+ if (selection === void 0) return [];
1088
+ const requested = selection.split(",");
1089
+ const refused = requested.filter((name) => !ENVIRONMENTS.some((environment) => environment === name));
1090
+ if (refused.length > 0) throw new UsageError(`'--${axis}' does not take ${refused.join(", ")}. It takes ${ENVIRONMENTS.join(", ")}.`);
1091
+ return ENVIRONMENTS.filter((environment) => requested.includes(environment));
1092
+ }
1093
+ /**
1094
+ * Reads the artifact groups a comma-separated selection names.
1095
+ *
1096
+ * @param selection - The selection text, or `undefined` for no selection.
1097
+ * @returns The named groups in plan order, and `undefined` for no selection,
1098
+ * which covers every group.
1099
+ * @throws `UsageError` when the selection names something that is not a group.
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * import { selectionToGroups } from './helpers.js'
1104
+ *
1105
+ * selectionToGroups('tests,manifest') // ['manifest', 'tests']
1106
+ * ```
1107
+ */
1108
+ function selectionToGroups(selection) {
1109
+ if (selection === void 0) return void 0;
1110
+ const requested = selection.split(",");
1111
+ const refused = requested.filter((name) => !GROUPS.some((group) => group === name));
1112
+ if (refused.length > 0) throw new UsageError(`'--groups' does not take ${refused.join(", ")}. It takes ${GROUPS.join(", ")}.`);
1113
+ return GROUPS.filter((group) => requested.includes(group));
1114
+ }
1115
+ /**
1116
+ * Reads the fleet packages a comma-separated selection names.
1117
+ *
1118
+ * @param selection - The selection text, or `undefined` for no selection.
1119
+ * @returns The named packages in selection order, and none for no selection.
1120
+ * @throws `UsageError` when a name is not a published `@orkestrel` package name.
1121
+ *
1122
+ * @example
1123
+ * ```ts
1124
+ * import { selectionToPackages } from './helpers.js'
1125
+ *
1126
+ * selectionToPackages('@orkestrel/emitter') // ['@orkestrel/emitter']
1127
+ * ```
1128
+ */
1129
+ function selectionToPackages(selection) {
1130
+ if (selection === void 0) return [];
1131
+ const requested = selection.split(",");
1132
+ const refused = requested.filter((name) => !DEPENDENCY_NAME_PATTERN.test(name));
1133
+ if (refused.length > 0) throw new UsageError(`'--deps' does not take ${refused.join(", ")}. Every name is a published @orkestrel package.`);
1134
+ return requested;
1135
+ }
1136
+ /**
1137
+ * Merges the results of two mutations of one target into one result.
1138
+ *
1139
+ * @param first - The earlier result, which fixes the reported target.
1140
+ * @param second - The later result.
1141
+ * @returns One result carrying both path lists, first's paths ahead of second's.
1142
+ *
1143
+ * @remarks
1144
+ * Written and skipped never overlap across the calls a verb makes, because each
1145
+ * call answers for its own paths.
1146
+ *
1147
+ * @example
1148
+ * ```ts
1149
+ * import { mergeResults } from './helpers.js'
1150
+ *
1151
+ * mergeResults(
1152
+ * { target: './t', written: ['a'], skipped: [], removed: [] },
1153
+ * { target: './t', written: ['b'], skipped: [], removed: [] },
1154
+ * ) // { target: './t', written: ['a', 'b'], skipped: [], removed: [] }
1155
+ * ```
1156
+ */
1157
+ function mergeResults(first, second) {
1158
+ return {
1159
+ target: first.target,
1160
+ written: [...first.written, ...second.written],
1161
+ skipped: [...first.skipped, ...second.skipped],
1162
+ removed: [...first.removed, ...second.removed]
1163
+ };
1164
+ }
1165
+ /**
1166
+ * Projects one mutation result into the line that states what it did.
1167
+ *
1168
+ * @param result - The result to state.
1169
+ * @returns One line naming the written, unchanged, and removed tallies and the target.
1170
+ *
1171
+ * @example
1172
+ * ```ts
1173
+ * import { resultToTally } from './helpers.js'
1174
+ *
1175
+ * resultToTally({ target: './t', written: ['a'], skipped: [], removed: [] })
1176
+ * // '1 written, 0 unchanged, 0 removed in ./t.'
1177
+ * ```
1178
+ */
1179
+ function resultToTally(result) {
1180
+ return `${String(result.written.length)} written, ${String(result.skipped.length)} unchanged, ${String(result.removed.length)} removed in ${result.target}.`;
1181
+ }
639
1182
  //#endregion
640
1183
  //#region src/bin/CLI.ts
641
1184
  /**
642
- * The executable: one command line in, one exit code out.
1185
+ * Represents the executable: one command line in, one exit code out.
643
1186
  *
644
1187
  * @remarks
645
1188
  * Every destination this class writes to is a handler it was given, and the run
@@ -672,25 +1215,23 @@ function errorToEnvelope(error) {
672
1215
  * code // 0
673
1216
  * ```
674
1217
  */
675
- var CLI = class CLI {
676
- static #stdout = (line) => void process.stdout.write(`${line}\n`);
677
- static #stderr = (line) => void process.stderr.write(`${line}\n`);
1218
+ var CLI = class {
678
1219
  #output;
679
1220
  #diagnostic;
680
1221
  #upstream;
681
1222
  /**
682
- * Construct the executable over the destinations it writes to.
1223
+ * Constructs the executable over the destinations it writes to.
683
1224
  *
684
1225
  * @param options - The report and diagnostic handlers and the upstream
685
1226
  * endpoints; the process streams and the published endpoints when absent.
686
1227
  */
687
1228
  constructor(options) {
688
- this.#output = options?.output ?? CLI.#stdout;
689
- this.#diagnostic = options?.diagnostic ?? CLI.#stderr;
1229
+ this.#output = options?.output ?? writeOutput;
1230
+ this.#diagnostic = options?.diagnostic ?? writeDiagnostic;
690
1231
  this.#upstream = options?.upstream;
691
1232
  }
692
1233
  /**
693
- * Run one command line to completion and report through the configured output.
1234
+ * Runs one command line to completion and reports through the configured output.
694
1235
  *
695
1236
  * @param argv - The arguments following the executable's own name.
696
1237
  * @returns The exit code: `0` clean, `1` drift or failure, `2` a usage error.
@@ -734,11 +1275,11 @@ var CLI = class CLI {
734
1275
  async #create(command) {
735
1276
  const target = command.target ?? command.name;
736
1277
  const blueprint = createBlueprint(command.name, {
737
- src: this.#environments(command.src, "src"),
738
- app: this.#environments(command.app, "app"),
1278
+ src: selectionToEnvironments(command.src, "src"),
1279
+ app: selectionToEnvironments(command.app, "app"),
739
1280
  bin: command.bin === true,
740
1281
  setup: false,
741
- dependencies: this.#packages(command.dependencies).map((name) => ({
1282
+ dependencies: selectionToPackages(command.dependencies).map((name) => ({
742
1283
  name,
743
1284
  range: "^0.0.0"
744
1285
  }))
@@ -751,7 +1292,8 @@ var CLI = class CLI {
751
1292
  runtime: dependenciesToFleet(declarations.runtime),
752
1293
  development: dependenciesToFleet(declarations.development)
753
1294
  }, command.offline === true);
754
- this.#assertVersions(versions);
1295
+ const refusal = versionsToRefusal(versions);
1296
+ if (refusal !== void 0) throw refusal;
755
1297
  const resolved = replacePlanRanges(plan, versions.pins);
756
1298
  if (resolved === void 0) throw new ScaffoldError("BLOCKED", "The compiled plan ranges could not be replaced.");
757
1299
  const host = await this.#host(command.from, void 0, command.offline === true);
@@ -767,7 +1309,7 @@ var CLI = class CLI {
767
1309
  if (command.json === true) this.#report(outcome);
768
1310
  else {
769
1311
  this.#say(`Scaffolded ${blueprint.name} into ${result.target}.`);
770
- this.#say(this.#tally(result));
1312
+ this.#say(resultToTally(result));
771
1313
  }
772
1314
  if (versions.forced || host.forced) this.#reportBaselines(outcome.provenance);
773
1315
  return 0;
@@ -779,7 +1321,7 @@ var CLI = class CLI {
779
1321
  const target = command.target ?? ".";
780
1322
  const manifest = this.#manifest(target);
781
1323
  const blueprint = this.#derive(target);
782
- const groups = this.#groups(command.groups);
1324
+ const groups = selectionToGroups(command.groups);
783
1325
  const declared = manifestToWritableDependencies(manifest, blueprint);
784
1326
  const versions = await this.#versions(declared, command.offline === true);
785
1327
  const questions = [...this.#targetQuestions(target, blueprint, groups), ...releasesToQuestions(versions.releases)];
@@ -812,7 +1354,7 @@ var CLI = class CLI {
812
1354
  }
813
1355
  async #restore(command) {
814
1356
  const target = command.target ?? ".";
815
- const groups = this.#groups(command.groups);
1357
+ const groups = selectionToGroups(command.groups);
816
1358
  const blueprint = this.#derive(target);
817
1359
  this.#assertTarget(target, blueprint, groups);
818
1360
  const host = await this.#host(command.from, target, command.offline === true);
@@ -827,8 +1369,9 @@ var CLI = class CLI {
827
1369
  return 1;
828
1370
  }
829
1371
  const versions = await this.#versions(manifestToWritableDependencies(this.#manifest(target), blueprint), command.offline === true);
830
- this.#assertVersions(versions);
831
- const result = this.#merge(host.materializer.repair(plan, audit, target), host.materializer.declare({
1372
+ const refusal = versionsToRefusal(versions);
1373
+ if (refusal !== void 0) throw refusal;
1374
+ const result = mergeResults(host.materializer.repair(plan, audit, target), host.materializer.declare({
832
1375
  pins: versions.pins,
833
1376
  scripts: blueprintToWritableScripts(blueprint)
834
1377
  }, target));
@@ -848,7 +1391,7 @@ var CLI = class CLI {
848
1391
  else {
849
1392
  this.#present(terminal);
850
1393
  this.#reportReplacements(audit, terminal);
851
- this.#say(this.#tally(result));
1394
+ this.#say(resultToTally(result));
852
1395
  }
853
1396
  if (versions.forced || host.forced) this.#reportBaselines(outcome.provenance);
854
1397
  if (command.offline !== true && (versions.forced || host.forced)) return 1;
@@ -861,21 +1404,22 @@ var CLI = class CLI {
861
1404
  const target = command.target ?? ".";
862
1405
  const [host, ...extra] = command.from ?? [];
863
1406
  if (extra.length > 0) this.#warn(`Read the data root from ${String(host)}. The other ${String(extra.length)} local root${extra.length === 1 ? "" : "s"} named by --from reach nothing this run does.`);
864
- const previous = this.#previous(target);
1407
+ const previous = catalogToNames(target);
865
1408
  const fetched = await this.#fetch(target, command.all === true);
866
1409
  const declarations = manifestToDependencies(this.#manifest(target));
867
1410
  const writable = {
868
1411
  runtime: dependenciesToFleet(declarations.runtime),
869
1412
  development: dependenciesToFleet(declarations.development)
870
1413
  };
871
- const releases = this.#catalogReleases([...writable.runtime, ...writable.development], fetched.entries);
872
- const pins = this.#pin(releases, writable);
873
- this.#assertFetched(fetched.entries, fetched.mirrors);
1414
+ const releases = entriesToReleases([...writable.runtime, ...writable.development], fetched.entries);
1415
+ const pins = releasesToPins(releases, writable);
1416
+ const refusal = fetchToRefusal(fetched.entries, fetched.mirrors);
1417
+ if (refusal !== void 0) throw refusal;
874
1418
  const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup !== "found") ? "floor" : "live";
875
1419
  const materializer = new Materializer({ host: host ?? readHostFloor() });
876
1420
  let result;
877
1421
  try {
878
- result = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
1422
+ result = mergeResults(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
879
1423
  pins,
880
1424
  scripts: []
881
1425
  }, target));
@@ -899,7 +1443,7 @@ var CLI = class CLI {
899
1443
  }
900
1444
  async #replace(command) {
901
1445
  const target = command.target ?? ".";
902
- const groups = this.#groups(command.groups);
1446
+ const groups = selectionToGroups(command.groups);
903
1447
  const blueprint = this.#derive(target);
904
1448
  this.#assertTarget(target, blueprint, groups);
905
1449
  const worktree = this.#worktree(target);
@@ -922,18 +1466,16 @@ var CLI = class CLI {
922
1466
  pins: manifestToWritableDependencies(this.#manifest(target), blueprint),
923
1467
  scripts: blueprintToWritableScripts(blueprint)
924
1468
  };
925
- const repaired = host.materializer.repair(plan, audit, target);
926
- const removed = host.materializer.remove(plan, audit, command.dirty === true ? {
1469
+ const local = mergeResults(host.materializer.repair(plan, audit, target), host.materializer.remove(plan, audit, command.dirty === true ? {
927
1470
  tracked: worktree.tracked,
928
1471
  dirty: []
929
- } : worktree, target);
930
- const offline = this.#merge(repaired, removed);
931
- const online = command.offline === true ? await this.#offline(host.materializer, target, declared, host.baseline) : await this.#reconcile(host.materializer, target, declared, host.baseline, host.forced);
1472
+ } : worktree, target));
1473
+ const remainder = command.offline === true ? await this.#declare(host.materializer, target, declared, host.baseline) : await this.#reconcile(host.materializer, target, declared, host.baseline, host.forced);
932
1474
  const [measured] = this.#survey(host.materializer, blueprint, target, groups);
933
1475
  const terminal = this.#appendQuestions(measured, target, blueprint, groups);
934
1476
  const outcome = {
935
- ...online,
936
- ...this.#merge(offline, online),
1477
+ ...remainder,
1478
+ ...mergeResults(local, remainder),
937
1479
  audit: terminal
938
1480
  };
939
1481
  if (command.json === true) this.#report(outcome);
@@ -941,17 +1483,18 @@ var CLI = class CLI {
941
1483
  this.#present(terminal);
942
1484
  this.#reportReplacements(audit, terminal);
943
1485
  this.#recount(outcome);
944
- if (online.note !== void 0) this.#warn(online.note);
1486
+ if (remainder.note !== void 0) this.#warn(remainder.note);
945
1487
  }
946
- if (online.note !== void 0) return 1;
1488
+ if (remainder.note !== void 0) return 1;
947
1489
  return auditToExit(terminal);
948
1490
  } finally {
949
1491
  host.materializer.destroy();
950
1492
  }
951
1493
  }
952
- async #offline(materializer, target, declared, host) {
1494
+ async #declare(materializer, target, declared, host) {
953
1495
  const versions = await this.#versions(declared.pins, true);
954
- this.#assertVersions(versions);
1496
+ const refusal = versionsToRefusal(versions);
1497
+ if (refusal !== void 0) throw refusal;
955
1498
  return {
956
1499
  ...materializer.declare({
957
1500
  pins: versions.pins,
@@ -969,7 +1512,7 @@ var CLI = class CLI {
969
1512
  };
970
1513
  }
971
1514
  async #reconcile(materializer, target, declared, host, hostForced) {
972
- const previous = this.#previous(target);
1515
+ const previous = catalogToNames(target);
973
1516
  let releases = [];
974
1517
  let provenance = { ...host === void 0 ? {} : { host } };
975
1518
  try {
@@ -979,16 +1522,18 @@ var CLI = class CLI {
979
1522
  ...versions.baseline === void 0 ? {} : { versions: versions.baseline },
980
1523
  ...host === void 0 ? {} : { host }
981
1524
  };
982
- this.#assertVersions(versions);
1525
+ const refused = versionsToRefusal(versions);
1526
+ if (refused !== void 0) throw refused;
983
1527
  const fetched = await this.#fetch(target, false);
984
- this.#assertFetched(fetched.entries, fetched.mirrors);
1528
+ const incomplete = fetchToRefusal(fetched.entries, fetched.mirrors);
1529
+ if (incomplete !== void 0) throw incomplete;
985
1530
  const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup === "failed") ? "floor" : "live";
986
1531
  provenance = {
987
1532
  ...versions.baseline === void 0 ? {} : { versions: versions.baseline },
988
1533
  ...guides === void 0 ? {} : { guides },
989
1534
  ...host === void 0 ? {} : { host }
990
1535
  };
991
- const written = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
1536
+ const written = mergeResults(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
992
1537
  pins: versions.pins,
993
1538
  scripts: declared.scripts
994
1539
  }, target));
@@ -1031,7 +1576,7 @@ var CLI = class CLI {
1031
1576
  baseline: "floor",
1032
1577
  forced: false
1033
1578
  };
1034
- const paths = floor.manifest.entries.filter((entry) => !isDeferredPath(entry.destination) && !isCanonPath(entry.destination)).map((entry) => entry.destination);
1579
+ const paths = floor.manifest.entries.filter((entry) => !isFloorPath(entry.destination)).map((entry) => entry.destination);
1035
1580
  const current = target === void 0 ? floor.bytes : readSnapshot(target, paths);
1036
1581
  const upstream = new Upstream(this.#upstream);
1037
1582
  try {
@@ -1119,17 +1664,12 @@ var CLI = class CLI {
1119
1664
  }
1120
1665
  return {
1121
1666
  releases,
1122
- pins: this.#pin(releases, declared),
1667
+ pins: releasesToPins(releases, declared),
1123
1668
  baseline: "live",
1124
1669
  forced: false,
1125
1670
  complete: true
1126
1671
  };
1127
1672
  }
1128
- #assertVersions(versions) {
1129
- if (versions.complete) return;
1130
- const names = versions.releases.filter((release) => release.lookup !== "found").map((release) => release.name);
1131
- throw new ScaffoldError("FETCH", names.length === 0 ? "A declared dependency names no concrete floor." : `The registry named no release for ${names.join(", ")}.`, { names: names.length });
1132
- }
1133
1673
  async #lookup(declared) {
1134
1674
  if (declared.length === 0) return [];
1135
1675
  const upstream = new Upstream(this.#upstream);
@@ -1179,48 +1719,7 @@ var CLI = class CLI {
1179
1719
  }
1180
1720
  }
1181
1721
  #publish(materializer, target, entries, mirrors) {
1182
- return this.#merge(materializer.mirror(mirrors, target), materializer.catalog(entries, target));
1183
- }
1184
- #catalogReleases(declared, entries) {
1185
- return declared.map((dependency) => {
1186
- const entry = entries.find((candidate) => candidate.name === dependency.name);
1187
- if (entry?.lookup === "found") return {
1188
- ...dependency,
1189
- lookup: "found",
1190
- latest: entry.version
1191
- };
1192
- return {
1193
- ...dependency,
1194
- lookup: entry?.lookup ?? "missing",
1195
- note: entry?.note ?? "the organization catalog does not list the declared package"
1196
- };
1197
- });
1198
- }
1199
- #assertFetched(entries, mirrors) {
1200
- const failed = [...entries.filter((entry) => entry.lookup !== "found").map((entry) => entry.name), ...mirrors.filter((mirror) => mirror.lookup !== "found" && mirror.lookup !== "failed" && mirror.lookup !== "missing").map((mirror) => mirror.name)];
1201
- if (failed.length === 0) return;
1202
- throw new ScaffoldError("FETCH", `Upstream produced no complete catalog answer for ${failed.join(", ")}.`, { names: failed.length });
1203
- }
1204
- #pin(releases, declared) {
1205
- const refused = releases.filter((release) => release.lookup !== "found");
1206
- if (refused.length > 0) throw new ScaffoldError("FETCH", `The registry named no release for ${refused.map((release) => release.name).join(", ")}.`, { names: refused.length });
1207
- const dependencies = [...declared.runtime, ...declared.development];
1208
- const pins = [];
1209
- for (let index = 0; index < releases.length; index += 1) {
1210
- const release = releases[index];
1211
- const dependency = dependencies[index];
1212
- if (release === void 0 || dependency === void 0) throw new ScaffoldError("FETCH", "The release answer has no matching declaration.");
1213
- if (release.lookup !== "found") throw new ScaffoldError("FETCH", `The registry named no release for ${release.name}.`);
1214
- pins.push({
1215
- name: dependency.name,
1216
- range: `^${release.latest}`
1217
- });
1218
- }
1219
- if (pins.length !== dependencies.length) throw new ScaffoldError("FETCH", "The release answer does not match the declaration set.");
1220
- return {
1221
- runtime: pins.slice(0, declared.runtime.length),
1222
- development: pins.slice(declared.runtime.length)
1223
- };
1722
+ return mergeResults(materializer.mirror(mirrors, target), materializer.catalog(entries, target));
1224
1723
  }
1225
1724
  #compile(blueprint, groups) {
1226
1725
  const compiler = new Compiler();
@@ -1259,8 +1758,8 @@ var CLI = class CLI {
1259
1758
  const global = resolveContainedPath(target, GLOBAL_SETUP_PATH);
1260
1759
  const showcase = resolveContainedPath(target, SHOWCASE_CONFIG_PATH);
1261
1760
  return createBlueprint(declared.slice(declared.lastIndexOf("/") + 1), {
1262
- src: this.#probe(target, "src"),
1263
- app: this.#probe(target, "app"),
1761
+ src: targetToEnvironments(target, "src"),
1762
+ app: targetToEnvironments(target, "app"),
1264
1763
  dependencies: manifestToDependencies(manifest).runtime,
1265
1764
  bin: bin !== void 0 && isExactCaseFile(bin),
1266
1765
  setup: tests !== void 0 && listFiles(tests).some((path) => {
@@ -1276,128 +1775,6 @@ var CLI = class CLI {
1276
1775
  showcase: showcase !== void 0 && isExactCaseFile(showcase)
1277
1776
  });
1278
1777
  }
1279
- #invocations(script) {
1280
- const tokens = [];
1281
- let value = "";
1282
- let resolved = true;
1283
- let started = false;
1284
- let quote;
1285
- for (let index = 0; index < script.length; index += 1) {
1286
- const character = script[index];
1287
- if (character === void 0) return void 0;
1288
- if (quote === void 0) {
1289
- if (/\s/.test(character)) {
1290
- if (started) tokens.push({
1291
- value,
1292
- resolved
1293
- });
1294
- value = "";
1295
- resolved = true;
1296
- started = false;
1297
- continue;
1298
- }
1299
- if (";&|()".includes(character)) {
1300
- if (started) tokens.push({
1301
- value,
1302
- resolved
1303
- });
1304
- const paired = script[index + 1] === character && (character === "&" || character === "|");
1305
- tokens.push({
1306
- value: paired ? `${character}${character}` : character,
1307
- resolved: true
1308
- });
1309
- value = "";
1310
- resolved = true;
1311
- started = false;
1312
- if (paired) index += 1;
1313
- continue;
1314
- }
1315
- if (character === "\"" || character === "'") {
1316
- quote = character;
1317
- started = true;
1318
- continue;
1319
- }
1320
- if (character === "\\") {
1321
- const escaped = script[index + 1];
1322
- if (escaped === void 0) return void 0;
1323
- value += escaped;
1324
- started = true;
1325
- index += 1;
1326
- continue;
1327
- }
1328
- if (character === "$" || character === "`" || character === "%") resolved = false;
1329
- value += character;
1330
- started = true;
1331
- continue;
1332
- }
1333
- if (character === quote) {
1334
- quote = void 0;
1335
- continue;
1336
- }
1337
- if (character === "\\" && quote === "\"") {
1338
- const escaped = script[index + 1];
1339
- if (escaped === void 0) return void 0;
1340
- value += escaped;
1341
- index += 1;
1342
- continue;
1343
- }
1344
- if (quote === "\"" && (character === "$" || character === "`" || character === "%")) resolved = false;
1345
- value += character;
1346
- started = true;
1347
- }
1348
- if (quote !== void 0) return void 0;
1349
- if (started) tokens.push({
1350
- value,
1351
- resolved
1352
- });
1353
- const projects = [];
1354
- const scripts = [];
1355
- for (let index = 0; index < tokens.length; index += 1) {
1356
- const token = tokens[index];
1357
- if (token === void 0) return void 0;
1358
- if (token.value === "npm" && tokens[index + 1]?.value === "run") {
1359
- const name = tokens[index + 2];
1360
- if (name === void 0 || !name.resolved || name.value.length === 0 || [
1361
- "&&",
1362
- "||",
1363
- ";",
1364
- "|",
1365
- "&",
1366
- "(",
1367
- ")"
1368
- ].includes(name.value)) return void 0;
1369
- scripts.push(name.value);
1370
- index += 2;
1371
- continue;
1372
- }
1373
- if (token.value === "--project") {
1374
- const project = tokens[index + 1];
1375
- if (project === void 0 || !project.resolved || project.value.length === 0 || [
1376
- "&&",
1377
- "||",
1378
- ";",
1379
- "|",
1380
- "&",
1381
- "(",
1382
- ")"
1383
- ].includes(project.value)) return void 0;
1384
- projects.push(project.value);
1385
- index += 1;
1386
- continue;
1387
- }
1388
- if (token.value.startsWith("--project=")) {
1389
- const project = token.value.slice(10);
1390
- if (!token.resolved || project.length === 0) return void 0;
1391
- projects.push(project);
1392
- continue;
1393
- }
1394
- if (!token.resolved && token.value.includes("--project")) return void 0;
1395
- }
1396
- return {
1397
- projects,
1398
- scripts
1399
- };
1400
- }
1401
1778
  #projectQuestion(target, blueprint, writing = false) {
1402
1779
  const text = this.#manifest(target);
1403
1780
  const manifest = replaceManifestScripts(text, blueprintToWritableScripts(blueprint)) ?? text;
@@ -1410,7 +1787,7 @@ var CLI = class CLI {
1410
1787
  let unresolved = false;
1411
1788
  for (const script of Object.values(scripts)) {
1412
1789
  if (!isString(script) || !script.includes("vitest")) continue;
1413
- const invoked = this.#invocations(script);
1790
+ const invoked = scriptToInvocations(script);
1414
1791
  if (invoked === void 0) {
1415
1792
  unresolved = true;
1416
1793
  continue;
@@ -1434,7 +1811,7 @@ var CLI = class CLI {
1434
1811
  const expectedLines = /* @__PURE__ */ new Map();
1435
1812
  for (const [name, script] of Object.entries(expected)) {
1436
1813
  if (!name.startsWith("test:")) continue;
1437
- const invoked = this.#invocations(script);
1814
+ const invoked = scriptToInvocations(script);
1438
1815
  if (invoked === void 0) continue;
1439
1816
  for (const project of invoked.projects) {
1440
1817
  if (project === "probe") continue;
@@ -1452,7 +1829,7 @@ var CLI = class CLI {
1452
1829
  visited.add(name);
1453
1830
  const script = scripts[name];
1454
1831
  if (!isString(script)) continue;
1455
- const invoked = this.#invocations(script);
1832
+ const invoked = scriptToInvocations(script);
1456
1833
  if (invoked === void 0) {
1457
1834
  unresolved = true;
1458
1835
  continue;
@@ -1594,27 +1971,9 @@ var CLI = class CLI {
1594
1971
  if (manifest === void 0) throw new ScaffoldError("TARGET", `The target at ${target} carries no readable manifest.`, { target });
1595
1972
  return manifest;
1596
1973
  }
1597
- #probe(target, axis) {
1598
- return ENVIRONMENTS.filter((environment) => {
1599
- const full = resolveContainedPath(target, `${axis}/${environment}`);
1600
- return full !== void 0 && isPhysicalDirectory(full);
1601
- });
1602
- }
1603
- #previous(target) {
1604
- const text = readFileText(target, CATALOG_AGENT_PATH);
1605
- if (text === void 0) return [];
1606
- const names = [];
1607
- for (const table of createMarkdown(text).filter(isTableNode)) for (const row of table.rows) {
1608
- const [cell] = row;
1609
- if (cell === void 0) continue;
1610
- const name = cell.map(flattenText).join("").trim();
1611
- if (DEPENDENCY_NAME_PATTERN.test(name)) names.push(name);
1612
- }
1613
- return names;
1614
- }
1615
1974
  #worktree(target) {
1616
- const tracked = this.#inventory(target, ["ls-files", "-z"]);
1617
- const dirty = this.#inventory(target, [
1975
+ const tracked = readGitRecords(target, ["ls-files", "-z"]);
1976
+ const dirty = readGitRecords(target, [
1618
1977
  "status",
1619
1978
  "--porcelain=v1",
1620
1979
  "--untracked-files=all",
@@ -1631,47 +1990,6 @@ var CLI = class CLI {
1631
1990
  });
1632
1991
  return state;
1633
1992
  }
1634
- #inventory(target, args) {
1635
- const result = executeSync({
1636
- file: "git",
1637
- arguments: [...args]
1638
- }, {
1639
- workspace: target,
1640
- limit: MAX_MANIFEST_BYTES,
1641
- strict: false
1642
- });
1643
- if (result.failed) throw new ScaffoldError("TARGET", `The target at ${target} is not a git repository.`, { target });
1644
- return result.stdout.split("\0").filter((record) => record.length > 0);
1645
- }
1646
- #environments(selection, axis) {
1647
- if (selection === void 0) return [];
1648
- const requested = selection.split(",");
1649
- const refused = requested.filter((name) => !ENVIRONMENTS.some((environment) => environment === name));
1650
- if (refused.length > 0) throw new UsageError(`'--${axis}' does not take ${refused.join(", ")}. It takes ${ENVIRONMENTS.join(", ")}.`);
1651
- return ENVIRONMENTS.filter((environment) => requested.includes(environment));
1652
- }
1653
- #groups(selection) {
1654
- if (selection === void 0) return void 0;
1655
- const requested = selection.split(",");
1656
- const refused = requested.filter((name) => !GROUPS.some((group) => group === name));
1657
- if (refused.length > 0) throw new UsageError(`'--groups' does not take ${refused.join(", ")}. It takes ${GROUPS.join(", ")}.`);
1658
- return GROUPS.filter((group) => requested.includes(group));
1659
- }
1660
- #packages(selection) {
1661
- if (selection === void 0) return [];
1662
- const requested = selection.split(",");
1663
- const refused = requested.filter((name) => !DEPENDENCY_NAME_PATTERN.test(name));
1664
- if (refused.length > 0) throw new UsageError(`'--deps' does not take ${refused.join(", ")}. Every name is a published @orkestrel package.`);
1665
- return requested;
1666
- }
1667
- #merge(first, second) {
1668
- return {
1669
- target: first.target,
1670
- written: [...first.written, ...second.written],
1671
- skipped: [...first.skipped, ...second.skipped],
1672
- removed: [...first.removed, ...second.removed]
1673
- };
1674
- }
1675
1993
  #reportBaselines(provenance) {
1676
1994
  const baselines = [];
1677
1995
  if (provenance.versions !== void 0) baselines.push(`versions=${provenance.versions}`);
@@ -1714,8 +2032,8 @@ var CLI = class CLI {
1714
2032
  if (finding.drift !== "stale") continue;
1715
2033
  const current = terminal.get(finding.path);
1716
2034
  if (current?.drift !== "aligned" || current.observed === void 0) continue;
1717
- const previousLines = Buffer.from(finding.observed, "hex").toString("utf8").split(/\r\n|\r|\n/u).length;
1718
- const delta = Buffer.from(current.observed, "hex").toString("utf8").split(/\r\n|\r|\n/u).length - previousLines;
2035
+ const previousLines = Buffer.from(finding.observed, "hex").toString("utf8").split(/\r\n|\n/u).length;
2036
+ const delta = Buffer.from(current.observed, "hex").toString("utf8").split(/\r\n|\n/u).length - previousLines;
1719
2037
  if (delta === 0) {
1720
2038
  this.#say(`${finding.path} replaced (0-line delta).`);
1721
2039
  continue;
@@ -1725,13 +2043,10 @@ var CLI = class CLI {
1725
2043
  }
1726
2044
  }
1727
2045
  #recount(result) {
1728
- this.#say(this.#tally(result));
2046
+ this.#say(resultToTally(result));
1729
2047
  this.#say(`${String(result.entries.length)} published, ${String(result.mirrors.filter((mirror) => mirror.lookup === "found").length)} guide${result.mirrors.length === 1 ? "" : "s"} fetched, ${String(result.dropped.length)} no longer listed.`);
1730
2048
  for (const mirror of result.mirrors) if (mirror.lookup !== "found") this.#warn(`${mirror.name}: ${mirror.note}`);
1731
2049
  }
1732
- #tally(result) {
1733
- return `${String(result.written.length)} written, ${String(result.skipped.length)} unchanged, ${String(result.removed.length)} removed in ${result.target}.`;
1734
- }
1735
2050
  #report(value) {
1736
2051
  this.#say(JSON.stringify(value));
1737
2052
  }
@@ -1742,13 +2057,10 @@ var CLI = class CLI {
1742
2057
  return isUsageError(error) ? 2 : 1;
1743
2058
  }
1744
2059
  #say(line) {
1745
- this.#output(this.#sanitize(line));
2060
+ this.#output(sanitizeLine(line));
1746
2061
  }
1747
2062
  #warn(line) {
1748
- this.#diagnostic(this.#sanitize(line));
1749
- }
1750
- #sanitize(line) {
1751
- return stripControls(strip(line)).split(/\r?\n/).join(" ");
2063
+ this.#diagnostic(sanitizeLine(line));
1752
2064
  }
1753
2065
  };
1754
2066
  //#endregion