@orkestrel/scaffold 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/scaffold.js +90 -41
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/guides/src/scaffold.md +234 -101
- package/dist/src/core/index.cjs +478 -298
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +167 -31
- package/dist/src/core/index.d.ts +167 -31
- package/dist/src/core/index.js +469 -299
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +45 -14
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +16 -6
- package/dist/src/server/index.d.ts +16 -6
- package/dist/src/server/index.js +46 -16
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -3
package/dist/bin/scaffold.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
326
|
-
|
|
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
|
|
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
|
-
/**
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
|
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
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
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 =
|
|
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`
|
|
821
|
-
*
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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 =
|
|
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, {
|