@orkestrel/scaffold 0.0.7 → 0.0.9

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.
@@ -2,7 +2,7 @@
2
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, relative } from "node:path";
4
4
  import * as tls from "node:tls";
5
- import { DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GROUPS, MAX_ARTIFACT_BYTES, NAME_PATTERN, ScaffoldError, blueprint, catalogNames, catalogToBlock, createCompiler, dependency, diffPlan, isScaffoldError, manifestToDependencies, manifestToName, planToSummary } from "../src/core/index.js";
5
+ import { DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GROUPS, MAX_ARTIFACT_BYTES, NAME_PATTERN, SERVICE_SCRIPT_PATH, ScaffoldError, blueprint, catalogNames, catalogToBlock, createCompiler, dependency, diffPlan, isScaffoldError, manifestToDependencies, manifestToName, planToSummary } from "../src/core/index.js";
6
6
  import { WriteTransaction, catalogPackages, commitWriteTransaction, createMaterializer, createSync, deriveBlueprint, digestFile, digestText, discardWriteTransaction, discoverPackages, hostRoot, hydratePlan, isFilesystemPath, isRealDirectory, isTerminalText, isVacant, locateHostSource, parseSyncOptions, pruneTargets, readFileText, readHostManifest, readManifest, readTarget, resolvePhysicalPath, validateWriteDirectories } from "../src/server/index.js";
7
7
  import { createReporter, createSpinner, createStyler } from "@orkestrel/console";
8
8
  import { createServerSink } from "@orkestrel/console/server";
@@ -49,6 +49,8 @@ var ACTION_LABEL = Object.freeze({
49
49
  });
50
50
  /** Repair's deliberately limited ownership boundary. */
51
51
  var REPAIR_SCOPE = "repair scope: shared host-owned artifacts only — starter and generated files are never touched";
52
+ /** Repair's opt-in generated-canon ownership boundary. */
53
+ var REPAIR_GENERATED_SCOPE = "repair scope: shared host-owned and generated artifacts — starter files and package.json are never touched";
52
54
  /** The dry-run note for `new`. */
53
55
  var NEW_DRY_RUN_NOTE = "dry run — pass --apply to write";
54
56
  /** The fallback message for a malformed command line without an error message. */
@@ -91,7 +93,7 @@ var VERB_SUMMARY = Object.freeze({
91
93
  new: "scaffold a workspace into ./<name>",
92
94
  pull: "refresh vendored guides/versions, report drift",
93
95
  audit: "whole-plan conformance report",
94
- repair: "restore the shared host-owned set",
96
+ repair: "restore host-owned files, plus generated canon with --generated",
95
97
  fleet: "audit/repair every workspace under the cwd's immediate children",
96
98
  catalog: "regenerate the fleet package-catalog table"
97
99
  });
@@ -99,9 +101,9 @@ var VERB_SUMMARY = Object.freeze({
99
101
  var VERB_FLAGS = Object.freeze({
100
102
  new: "--src a,b --app a,b --deps x,y --apply --yes --target <path> --from <path>",
101
103
  pull: "--target . --deps x,y --apply --yes --strict",
102
- audit: "--target . --live --from <path> --groups a,b",
103
- repair: "--target . --apply --yes --prune --from <path>",
104
- fleet: "--apply --yes --prune --from <path>",
104
+ audit: "--target . --live --generated --from <path> --groups a,b",
105
+ repair: "--target . --generated --apply --yes --prune --from <path>",
106
+ fleet: "--generated --apply --yes --prune --from <path>",
105
107
  catalog: "--from <path> ... --target <repo> --offline --apply --yes"
106
108
  });
107
109
  /** Plain-language command flag descriptions. */
@@ -125,17 +127,20 @@ var VERB_FLAG_HELP = Object.freeze({
125
127
  audit: [
126
128
  ["--target .", "directory to audit (default: current directory)"],
127
129
  ["--live", "also check upstream freshness over the network"],
130
+ ["--generated", "include generated canon if the repair hand-off is accepted"],
128
131
  ["--from <path>", "read the template from a local path instead of the bundled one"],
129
132
  ["--groups a,b", "limit the audit to these artifact groups"]
130
133
  ],
131
134
  repair: [
132
135
  ["--target .", "directory to repair (default: current directory)"],
136
+ ["--generated", "also restore generated canon except package.json"],
133
137
  ["--apply", "write the fixes (default is a dry run)"],
134
138
  ["--yes", "skip the confirmation question"],
135
139
  ["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts"],
136
140
  ["--from <path>", "read the template from a local path instead of the bundled one"]
137
141
  ],
138
142
  fleet: [
143
+ ["--generated", "also restore generated canon except package.json in every package"],
139
144
  ["--apply", "write fixes across every package (default is a dry run)"],
140
145
  ["--yes", "skip the confirmation question"],
141
146
  ["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts, per package"],
@@ -322,15 +327,28 @@ function auditTable(audit, plan) {
322
327
  rows: findingRows(audit.findings, plan)
323
328
  };
324
329
  }
325
- /** Render drift outside repair's ownership boundary. */
326
- function scopeNote(count) {
330
+ /**
331
+ * Render drift outside repair's selected ownership boundary.
332
+ *
333
+ * @param count - The number of findings outside the selected scope.
334
+ * @param generated - Whether generated canon was included in the repair scope.
335
+ * @returns The scope guidance line, or `undefined` when no findings remain outside scope.
336
+ */
337
+ function scopeNote(count, generated) {
327
338
  if (count === 0) return void 0;
328
- return `note: ${countPart(count, "finding")} outside repair's scope — run 'audit' for the list; generated files are yours to edit`;
339
+ return generated ? `note: ${countPart(count, "finding")} outside host-owned and generated repair scope — run 'audit' for the list; starter files and package.json remain protected` : `note: ${countPart(count, "finding")} outside host-owned repair scope — run 'audit' for the list`;
329
340
  }
330
- /** Render repair's dry-run verdict. */
331
- function repairVerdict(audit) {
332
- if (audit.clean) return `repair: ${countPart(audit.findings.length, "host-owned artifact")} aligned — nothing to write`;
333
- return `repair: ${bucketText(audit)} pass --apply to write`;
341
+ /**
342
+ * Render repair's dry-run verdict.
343
+ *
344
+ * @param audit - The audit over the selected repair plan.
345
+ * @param generated - Whether generated canon was included in the repair scope.
346
+ * @returns The scope-aware clean or drifted verdict.
347
+ */
348
+ function repairVerdict(audit, generated) {
349
+ const scope = generated ? "host-owned and generated" : "host-owned";
350
+ if (audit.clean) return `repair: ${countPart(audit.findings.length, `${scope} artifact`)} aligned — nothing to write`;
351
+ return `repair: ${scope}: ${bucketText(audit)} — pass --apply to write`;
334
352
  }
335
353
  /** Render repair's materialization tally. */
336
354
  function repairSuccess(result, removed) {
@@ -537,7 +555,28 @@ function unresolvedVersion(names) {
537
555
  }
538
556
  /** Render drift that belongs to generated artifacts. */
539
557
  function generatedNote(count) {
540
- return `${countPart(count, "finding")} in generated files — these are regenerated, not hand-edited; repair does not touch them`;
558
+ return `${countPart(count, "finding")} in generated files — these are regenerated, not hand-edited; run 'scaffold repair --generated' to restore them`;
559
+ }
560
+ /**
561
+ * Render honest repair guidance for computed and protected manifest drift.
562
+ *
563
+ * @param findings - The audit findings to classify.
564
+ * @param plan - The plan that owns each computed artifact.
565
+ * @returns One line for repairable computed drift and one for protected manifest drift when present.
566
+ */
567
+ function renderComputedNotes(findings, plan) {
568
+ const origins = new Map(plan.artifacts.map((artifact) => [artifact.path, artifact.origin]));
569
+ let computed = 0;
570
+ let manifest = 0;
571
+ for (const finding of findings) {
572
+ if (finding.drift === "aligned" || origins.get(finding.path) !== "computed") continue;
573
+ if (finding.path === "package.json") manifest += 1;
574
+ else computed += 1;
575
+ }
576
+ const notes = [];
577
+ if (computed > 0) notes.push(generatedNote(computed));
578
+ if (manifest > 0) notes.push(`${countPart(manifest, "finding")} in package.json — repair does not rewrite protected publication metadata; review and edit it directly`);
579
+ return notes;
541
580
  }
542
581
  /** Render live dependency freshness tallies. */
543
582
  function auditLiveNote(current, behind, failed) {
@@ -604,6 +643,10 @@ function parseArguments(argv) {
604
643
  type: "boolean",
605
644
  default: false
606
645
  },
646
+ generated: {
647
+ type: "boolean",
648
+ default: false
649
+ },
607
650
  strict: {
608
651
  type: "boolean",
609
652
  default: false
@@ -766,17 +809,22 @@ var CLI = class {
766
809
  compiler.destroy();
767
810
  }
768
811
  }
812
+ #prunePaths(target, host, service) {
813
+ const seam = service ? SERVICE_SCRIPT_PATH : void 0;
814
+ return pruneTargets(target, host).filter((path) => path !== seam);
815
+ }
769
816
  /**
770
- * Merge a `pruneTargets` scan into `audit` as `foreign` findings — pure
817
+ * Merge the physical prune scan into `audit` as `foreign` findings — pure
771
818
  * object-spread composition in the BIN only (`src/core`'s `diffPlan` is never
772
- * modified/reimplemented). Every unexpected path becomes one
773
- * `'orchestration'`-group `foreign` finding (the `Group` every `PRUNE_DIRECTORIES`
774
- * entry—`.claude/agents`, `.codex/agents`, `scripts`—belongs to); any scan hit makes the
775
- * merged audit unclean, so an "unexpected file" is honestly counted as drift
776
- * (exit 1) instead of the structurally-always-zero `diffPlan.foreign`.
819
+ * modified/reimplemented). Every unexpected path except the declared service
820
+ * workspace's exact `SERVICE_SCRIPT_PATH` becomes one `'orchestration'`-group
821
+ * `foreign` finding (the `Group` every `PRUNE_DIRECTORIES` entry—`.claude/agents`,
822
+ * `.codex/agents`, `scripts`—belongs to); any scan hit makes the merged audit
823
+ * unclean, so an "unexpected file" is honestly counted as drift (exit 1)
824
+ * instead of the structurally-always-zero `diffPlan.foreign`.
777
825
  */
778
- #scan(audit, target, host) {
779
- const paths = pruneTargets(target, host);
826
+ #scan(audit, target, host, service) {
827
+ const paths = this.#prunePaths(target, host, service);
780
828
  if (paths.length === 0) return audit;
781
829
  const findings = paths.map((path) => ({
782
830
  path,
@@ -795,8 +843,8 @@ var CLI = class {
795
843
  * When fail-closed allowlist discovery raises a coded `TARGET` failure, retain
796
844
  * the existing findings and mark the audit incomplete instead of crashing.
797
845
  */
798
- #scanSafe(audit, target, host) {
799
- const scanned = attempt(() => this.#scan(audit, target, host));
846
+ #scanSafe(audit, target, host, service) {
847
+ const scanned = attempt(() => this.#scan(audit, target, host, service));
800
848
  if (scanned.success) return {
801
849
  audit: scanned.value,
802
850
  skipped: false
@@ -817,9 +865,9 @@ var CLI = class {
817
865
  throw scanned.error;
818
866
  }
819
867
  /**
820
- * `repair`'s own scope is host-only, but the caller compiles the FULL plan anyway — diff it too
821
- * (cheap, local, no network) so a clean host verdict can point at drift OUTSIDE repair's reach.
822
- * The count feeds the shared `scopeNote` renderer.
868
+ * `repair` scopes to host-owned artifacts by default and optionally generated canon, but the
869
+ * caller compiles the full plan anyway. Diff it too so a clean scoped verdict can point at drift
870
+ * outside the selected boundary. The count feeds the shared `scopeNote` renderer.
823
871
  */
824
872
  #outside(compiled, target) {
825
873
  const full = diffPlan(compiled, readTarget(target, compiled.artifacts.map((artifact) => artifact.path)));
@@ -1093,7 +1141,7 @@ var CLI = class {
1093
1141
  ...diffPlan(plan, readTarget(target, artifactPaths)),
1094
1142
  questions
1095
1143
  };
1096
- const scanned = this.#scanSafe(rawAudit, target, host);
1144
+ const scanned = this.#scanSafe(rawAudit, target, host, spec.service);
1097
1145
  const audit = scanned.audit;
1098
1146
  let drifted = !audit.clean;
1099
1147
  let live;
@@ -1134,7 +1182,6 @@ var CLI = class {
1134
1182
  if (!audit.clean) {
1135
1183
  const split = partitionFindings(audit.findings, plan);
1136
1184
  const ownedCount = split.owned.drifted + split.owned.missing;
1137
- const computedCount = split.generated.drifted + split.generated.missing;
1138
1185
  const pruneRequested = values.prune === true;
1139
1186
  const offerHandoff = this.#tty && (ownedCount > 0 || audit.foreign > 0 && pruneRequested);
1140
1187
  let handoffAccepted = false;
@@ -1148,19 +1195,19 @@ var CLI = class {
1148
1195
  if (handoffAccepted) {
1149
1196
  await this.#repair(values, false);
1150
1197
  const rawFinal = diffPlan(plan, readTarget(target, artifactPaths));
1151
- const finalScanned = this.#scanSafe(rawFinal, target, host);
1198
+ const finalScanned = this.#scanSafe(rawFinal, target, host, spec.service);
1152
1199
  process.exitCode = finalScanned.audit.clean ? 0 : 1;
1153
1200
  return;
1154
1201
  }
1155
1202
  }
1156
1203
  if (!handoffAccepted) {
1157
1204
  if (audit.foreign > 0 && !pruneRequested) this.#reporter.line(FOREIGN_HINT);
1158
- if (computedCount > 0) this.#reporter.line(generatedNote(computedCount));
1205
+ for (const note of renderComputedNotes(audit.findings, plan)) this.#reporter.line(note);
1159
1206
  }
1160
1207
  }
1161
1208
  process.exitCode = drifted ? 1 : 0;
1162
1209
  }
1163
- /** `scaffold repair` — restore the shared host-owned set for one target. */
1210
+ /** `scaffold repair` — restore host-owned files and optionally generated canon for one target. */
1164
1211
  async #repair(values, json) {
1165
1212
  const target = this.#contain(values.target ?? ".", json);
1166
1213
  let spec;
@@ -1170,13 +1217,14 @@ var CLI = class {
1170
1217
  this.#error(error, json);
1171
1218
  }
1172
1219
  const [compiled] = this.#compile(spec, json);
1220
+ const generated = values.generated === true;
1173
1221
  const scoped = {
1174
1222
  ...compiled,
1175
1223
  blueprint: {
1176
1224
  ...compiled.blueprint,
1177
1225
  overrides: []
1178
1226
  },
1179
- artifacts: compiled.artifacts.filter((artifact) => artifact.origin === "host")
1227
+ artifacts: compiled.artifacts.filter((artifact) => artifact.origin === "host" || generated && artifact.origin === "computed" && artifact.path !== "package.json")
1180
1228
  };
1181
1229
  const host = values.from?.[0] ?? hostRoot();
1182
1230
  let plan;
@@ -1192,23 +1240,23 @@ var CLI = class {
1192
1240
  this.#error(error, json);
1193
1241
  }
1194
1242
  if (!json) {
1195
- this.#reporter.line(REPAIR_SCOPE);
1243
+ this.#reporter.line(generated ? REPAIR_GENERATED_SCOPE : REPAIR_SCOPE);
1196
1244
  this.#reporter.section("Audit");
1197
1245
  this.#reporter.table(auditTable(audit, plan));
1198
1246
  }
1199
- const prunePaths = values.prune ? pruneTargets(target, host) : [];
1247
+ const prunePaths = values.prune ? this.#prunePaths(target, host, spec.service) : [];
1200
1248
  const pruneSnapshot = readTarget(target, prunePaths);
1201
1249
  if (audit.clean && prunePaths.length === 0) {
1202
1250
  if (json) this.#write(audit);
1203
1251
  else {
1204
- this.#reporter.line(repairVerdict(audit));
1205
- const note = scopeNote(this.#outside(compiled, target));
1252
+ this.#reporter.line(repairVerdict(audit, generated));
1253
+ const note = scopeNote(this.#outside(compiled, target), generated);
1206
1254
  if (note !== void 0) this.#reporter.line(note);
1207
1255
  }
1208
1256
  process.exitCode = 0;
1209
1257
  return;
1210
1258
  }
1211
- if (!json) this.#reporter.line(repairVerdict(audit));
1259
+ if (!json) this.#reporter.line(repairVerdict(audit, generated));
1212
1260
  const terminal = createTerminal();
1213
1261
  let proceed = true;
1214
1262
  if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(audit.drifted + audit.missing + audit.foreign), values, json);
@@ -1241,6 +1289,7 @@ var CLI = class {
1241
1289
  /** `scaffold fleet` — audit/repair every `@orkestrel` package beneath the current directory's immediate children. */
1242
1290
  async #fleet(values, json) {
1243
1291
  const root = this.#contain(".", json);
1292
+ const generated = values.generated === true;
1244
1293
  const packages = discoverPackages(root);
1245
1294
  if (packages.length === 0) this.#fail(`no @orkestrel packages under "${root}" — fleet scans the immediate children of the current directory; stand in the folder that contains your checkouts (cd ..), or use 'repair' to true up just this repo.`, json);
1246
1295
  const host = values.from?.[0] ?? hostRoot();
@@ -1262,7 +1311,7 @@ var CLI = class {
1262
1311
  ...scaffolding.plan.blueprint,
1263
1312
  overrides: []
1264
1313
  },
1265
- artifacts: scaffolding.plan.artifacts.filter((artifact) => artifact.origin === "host")
1314
+ artifacts: scaffolding.plan.artifacts.filter((artifact) => artifact.origin === "host" || generated && artifact.origin === "computed" && artifact.path !== "package.json")
1266
1315
  };
1267
1316
  questions = scaffolding.questions;
1268
1317
  } finally {
@@ -1273,7 +1322,7 @@ var CLI = class {
1273
1322
  ...diffPlan(plan, readTarget(directory, plan.artifacts.map((artifact) => artifact.path))),
1274
1323
  questions
1275
1324
  };
1276
- const audit = this.#scan(rawAudit, directory, host);
1325
+ const audit = this.#scan(rawAudit, directory, host, plan.blueprint.service);
1277
1326
  repos.push({
1278
1327
  name,
1279
1328
  directory,
@@ -1313,7 +1362,7 @@ var CLI = class {
1313
1362
  const terminal = createTerminal();
1314
1363
  const proceed = await this.#apply(terminal, applyConfirmMessage(fileCount, dirty.length), values, json);
1315
1364
  const pruneSets = proceed && values.prune ? new Map(dirty.map((repo) => {
1316
- const paths = pruneTargets(repo.directory, host);
1365
+ const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.service);
1317
1366
  return [repo.name, readTarget(repo.directory, paths)];
1318
1367
  })) : /* @__PURE__ */ new Map();
1319
1368
  const prunePaths = dirty.flatMap((repo) => Object.keys(pruneSets.get(repo.name) ?? {}).map((path) => `${repo.name}/${path}`));
@@ -1336,7 +1385,7 @@ var CLI = class {
1336
1385
  if (doPrune) materializer.prune(repo.directory, pruneSets.get(repo.name) ?? {});
1337
1386
  const paths = repo.plan.artifacts.map((artifact) => artifact.path);
1338
1387
  const rawFinal = diffPlan(repo.plan, readTarget(repo.directory, paths));
1339
- const finalAudit = this.#scan(rawFinal, repo.directory, host);
1388
+ const finalAudit = this.#scan(rawFinal, repo.directory, host, repo.plan.blueprint.service);
1340
1389
  if (!finalAudit.clean) drifted += 1;
1341
1390
  entries.push(fleetEntryOf(repo.name, finalAudit, false));
1342
1391
  if (!json) this.#reporter.line(fleetRepoLine(repo.name, {