@orkestrel/scaffold 0.0.21 → 0.0.22
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 +371 -140
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +61 -42
- package/dist/host/CLAUDE.md +39 -408
- package/dist/host/agents/orchestration.md +454 -0
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +25 -7
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +42 -31
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +167 -0
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +98 -0
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +148 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +5 -5
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +11 -6
- package/dist/host/claude/agents/analyst.md +59 -0
- package/dist/host/claude/agents/application.md +1 -1
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +5 -1
- package/dist/host/claude/agents/codex.md +120 -94
- package/dist/host/claude/agents/grok.md +44 -32
- package/dist/host/claude/agents/implementer.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +1 -1
- package/dist/host/claude/agents/planner.md +9 -3
- package/dist/host/claude/agents/researcher.md +6 -5
- package/dist/host/claude/agents/reviewer.md +12 -6
- package/dist/host/claude/agents/scout.md +7 -4
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +17 -16
- package/dist/host/claude/rules/architecture.md +41 -22
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/names.md +7 -6
- package/dist/host/claude/rules/quality.md +65 -18
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +12 -6
- package/dist/host/claude/rules/typescript.md +1 -1
- package/dist/host/claude/rules/workspace.md +6 -3
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +13 -4
- package/dist/host/codex/agents/checker.toml +1 -1
- package/dist/host/codex/agents/grok.toml +2 -1
- package/dist/host/codex/agents/opus.toml +1 -1
- package/dist/host/codex/agents/planner.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/reviewer.toml +1 -1
- package/dist/host/codex/agents/scout.toml +2 -2
- package/dist/host/codex/config.toml +23 -70
- package/dist/host/cursor/rules/orchestration.mdc +33 -0
- package/dist/host/guides/src/scaffold.md +301 -123
- package/dist/host/manifest.json +47 -1
- package/dist/host/tests/setupPolicy.ts +83 -3
- package/dist/src/core/index.cjs +404 -63
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +119 -41
- package/dist/src/core/index.d.ts +119 -41
- package/dist/src/core/index.js +401 -64
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +83 -33
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +32 -16
- package/dist/src/server/index.d.ts +32 -16
- package/dist/src/server/index.js +84 -35
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/scaffold.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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, SERVICE_SCRIPT_PATH, ScaffoldError, blueprint, catalogNames, catalogToBlock, createCompiler, dependency, diffPlan, isScaffoldError, manifestToDependencies, manifestToName, planToSummary } from "../src/core/index.js";
|
|
5
|
+
import { CATALOG_AGENT_PATH, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GROUPS, MAX_ARTIFACT_BYTES, NAME_PATTERN, SERVICE_SCRIPT_PATH, ScaffoldError, blueprint, catalogNames, catalogToBlock, createCompiler, dependency, diffPlan, isScaffoldError, manifestToDependencies, manifestToName, ownDataValue, 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";
|
|
9
|
-
import { attempt } from "@orkestrel/contract";
|
|
9
|
+
import { attempt, isRecord, parseJSON } from "@orkestrel/contract";
|
|
10
10
|
import { isTerminalError } from "@orkestrel/terminal";
|
|
11
11
|
import { createTerminal } from "@orkestrel/terminal/server";
|
|
12
12
|
import { parseArgs } from "node:util";
|
|
@@ -48,10 +48,10 @@ var ACTION_LABEL = Object.freeze({
|
|
|
48
48
|
skipped: "unchanged",
|
|
49
49
|
removed: "removed"
|
|
50
50
|
});
|
|
51
|
-
/**
|
|
52
|
-
var REPAIR_SCOPE = "
|
|
53
|
-
/**
|
|
54
|
-
var REPAIR_GENERATED_SCOPE = "
|
|
51
|
+
/** The deliberately limited ownership boundary every write verb repairs within. */
|
|
52
|
+
var REPAIR_SCOPE = "shared host-owned artifacts and absent service seams — missing files are restored; drifted files change only with --replace, which discards local changes; present starter and generated files are never touched";
|
|
53
|
+
/** The opt-in generated-canon ownership boundary `--generated` widens that write to. */
|
|
54
|
+
var REPAIR_GENERATED_SCOPE = "shared host-owned and generated artifacts plus service manifest scripts — missing files are restored; drifted files change only with --replace, which discards local changes; present starter files and package publication metadata are never touched";
|
|
55
55
|
/** The dry-run note for `new`. */
|
|
56
56
|
var NEW_DRY_RUN_NOTE = "dry run — pass --apply to write";
|
|
57
57
|
/** The fallback message for a malformed command line without an error message. */
|
|
@@ -95,7 +95,7 @@ var VERB_SUMMARY = Object.freeze({
|
|
|
95
95
|
pull: "refresh vendored guides/versions, report drift",
|
|
96
96
|
mirror: "refresh every published Orkestrel package guide",
|
|
97
97
|
audit: "whole-plan conformance report",
|
|
98
|
-
repair: "restore host-owned files
|
|
98
|
+
repair: "restore missing host-owned files; replace drifted bytes only with --replace",
|
|
99
99
|
fleet: "audit/repair every workspace under the cwd's immediate children",
|
|
100
100
|
catalog: "regenerate the fleet package-catalog table"
|
|
101
101
|
});
|
|
@@ -104,9 +104,9 @@ var VERB_FLAGS = Object.freeze({
|
|
|
104
104
|
new: "--src a,b --app a,b --deps x,y --apply --yes --target <path> --from <path>",
|
|
105
105
|
pull: "--target . --deps x,y --apply --yes --strict",
|
|
106
106
|
mirror: "--target . --apply --yes --strict",
|
|
107
|
-
audit: "--target . --live --generated --from <path> --groups a,b",
|
|
108
|
-
repair: "--target . --generated --apply --yes --prune --from <path>",
|
|
109
|
-
fleet: "--generated --apply --yes --prune --from <path>",
|
|
107
|
+
audit: "--target . --live --generated --replace --from <path> --groups a,b",
|
|
108
|
+
repair: "--target . --generated --replace --apply --yes --prune --from <path>",
|
|
109
|
+
fleet: "--generated --replace --apply --yes --prune --from <path>",
|
|
110
110
|
catalog: "--from <path> ... --target <repo> --offline --apply --yes"
|
|
111
111
|
});
|
|
112
112
|
/** Plain-language command flag descriptions. */
|
|
@@ -136,20 +136,23 @@ var VERB_FLAG_HELP = Object.freeze({
|
|
|
136
136
|
audit: [
|
|
137
137
|
["--target .", "directory to audit (default: current directory)"],
|
|
138
138
|
["--live", "also check upstream freshness over the network"],
|
|
139
|
-
["--generated", "
|
|
139
|
+
["--generated", "widen an accepted repair hand-off to generated files"],
|
|
140
|
+
["--replace", "let the repair hand-off discard local changes in the drifted files it names"],
|
|
140
141
|
["--from <path>", "read the template from a local path instead of the bundled one"],
|
|
141
142
|
["--groups a,b", "limit the audit to these artifact groups"]
|
|
142
143
|
],
|
|
143
144
|
repair: [
|
|
144
145
|
["--target .", "directory to repair (default: current directory)"],
|
|
145
|
-
["--generated", "
|
|
146
|
+
["--generated", "widen the scope to generated files, except package.json"],
|
|
147
|
+
["--replace", "discard local changes in the drifted files named by the repair report"],
|
|
146
148
|
["--apply", "write the fixes (default is a dry run)"],
|
|
147
149
|
["--yes", "skip the confirmation question"],
|
|
148
150
|
["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts"],
|
|
149
151
|
["--from <path>", "read the template from a local path instead of the bundled one"]
|
|
150
152
|
],
|
|
151
153
|
fleet: [
|
|
152
|
-
["--generated", "
|
|
154
|
+
["--generated", "widen the scope to generated files in every package, except package.json"],
|
|
155
|
+
["--replace", "discard local changes in the drifted files named in each package"],
|
|
153
156
|
["--apply", "write fixes across every package (default is a dry run)"],
|
|
154
157
|
["--yes", "skip the confirmation question"],
|
|
155
158
|
["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts, per package"],
|
|
@@ -186,19 +189,17 @@ var VERB_EXAMPLE = Object.freeze({
|
|
|
186
189
|
/** Message used when a prune scan has no candidates. */
|
|
187
190
|
var PRUNE_EMPTY = "no unexpected files to delete";
|
|
188
191
|
/** Guidance for unexpected files outside a non-pruning repair handoff. */
|
|
189
|
-
var FOREIGN_HINT = "unexpected files
|
|
192
|
+
var FOREIGN_HINT = "unexpected files are never deleted by default — run 'scaffold repair --prune --apply' to delete them; a file you added yourself is unexpected too, so check the paths above first";
|
|
190
193
|
/** Interactive dependency prompt. */
|
|
191
194
|
var ORKESTREL_DEPS_PROMPT = "@orkestrel dependencies (comma-separated short names, e.g. contract, emitter — installed as dependencies)";
|
|
192
195
|
/** Catalog-degraded validation note. */
|
|
193
196
|
var CATALOG_UNRESOLVED_NOTE = "couldn't resolve the vendored @orkestrel catalog — validating names by shape only";
|
|
194
|
-
/** Catalog block destination in a scaffolded repository. */
|
|
195
|
-
var CATALOG_AGENT_PATH = ".claude/agents/orkestrel.md";
|
|
196
197
|
/** Opening marker for the generated package catalog block. */
|
|
197
198
|
var CATALOG_START_MARKER = "<!-- catalog:start -->";
|
|
198
199
|
/** Closing marker for the generated package catalog block. */
|
|
199
200
|
var CATALOG_END_MARKER = "<!-- catalog:end -->";
|
|
200
201
|
/** Non-terminal prune safety note. */
|
|
201
|
-
var PRUNE_SKIPPED = "prune skipped —
|
|
202
|
+
var PRUNE_SKIPPED = "prune skipped — pass --apply to authorize deletion; --yes only skips confirmation";
|
|
202
203
|
/** Degraded unexpected-file scan note. */
|
|
203
204
|
var SCAN_SKIPPED = "unexpected-file scanning skipped — couldn't establish the template source";
|
|
204
205
|
//#endregion
|
|
@@ -213,49 +214,57 @@ var CLIExitError = class extends Error {
|
|
|
213
214
|
};
|
|
214
215
|
//#endregion
|
|
215
216
|
//#region src/bin/shapers.ts
|
|
216
|
-
/** Partition findings by whether repair owns their artifact origin. */
|
|
217
|
+
/** Partition findings by whether repair owns their artifact origin. The `owned` and `generated` buckets carry no foreign findings by construction. */
|
|
217
218
|
function partitionFindings(findings, plan) {
|
|
218
219
|
const origins = new Map(plan.artifacts.map((artifact) => [artifact.path, artifact.origin]));
|
|
219
220
|
let ownedDrifted = 0;
|
|
220
221
|
let ownedMissing = 0;
|
|
221
|
-
let ownedForeign = 0;
|
|
222
222
|
let generatedDrifted = 0;
|
|
223
223
|
let generatedMissing = 0;
|
|
224
|
-
let
|
|
224
|
+
let foreign = 0;
|
|
225
225
|
for (const finding of findings) {
|
|
226
226
|
const origin = origins.get(finding.path);
|
|
227
227
|
const owned = origin === "host" || origin === "template";
|
|
228
228
|
if (finding.drift === "aligned") continue;
|
|
229
|
+
if (finding.drift === "foreign") {
|
|
230
|
+
foreign += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
229
233
|
if (finding.drift === "stale") {
|
|
230
234
|
if (owned) ownedDrifted += 1;
|
|
231
235
|
else generatedDrifted += 1;
|
|
232
236
|
} else if (finding.drift === "missing") {
|
|
233
237
|
if (owned) ownedMissing += 1;
|
|
234
238
|
else generatedMissing += 1;
|
|
235
|
-
}
|
|
236
|
-
else generatedForeign += 1;
|
|
239
|
+
}
|
|
237
240
|
}
|
|
238
241
|
return {
|
|
239
242
|
owned: {
|
|
240
243
|
drifted: ownedDrifted,
|
|
241
244
|
missing: ownedMissing,
|
|
242
|
-
foreign:
|
|
245
|
+
foreign: 0
|
|
243
246
|
},
|
|
244
247
|
generated: {
|
|
245
248
|
drifted: generatedDrifted,
|
|
246
249
|
missing: generatedMissing,
|
|
247
|
-
foreign:
|
|
250
|
+
foreign: 0
|
|
251
|
+
},
|
|
252
|
+
foreign: {
|
|
253
|
+
drifted: 0,
|
|
254
|
+
missing: 0,
|
|
255
|
+
foreign
|
|
248
256
|
}
|
|
249
257
|
};
|
|
250
258
|
}
|
|
251
259
|
/** Create one deterministic fleet result entry. */
|
|
252
|
-
function fleetEntryOf(name, counts, failed) {
|
|
260
|
+
function fleetEntryOf(name, counts, failed, outside = 0) {
|
|
253
261
|
return {
|
|
254
262
|
name,
|
|
255
263
|
drifted: counts?.drifted ?? 0,
|
|
256
264
|
missing: counts?.missing ?? 0,
|
|
257
265
|
foreign: counts?.foreign ?? 0,
|
|
258
|
-
failed
|
|
266
|
+
failed,
|
|
267
|
+
outside
|
|
259
268
|
};
|
|
260
269
|
}
|
|
261
270
|
/** Create the command-line interface's machine-readable failure envelope. */
|
|
@@ -277,7 +286,14 @@ function summaryToNewResult(summary, applied) {
|
|
|
277
286
|
applied
|
|
278
287
|
};
|
|
279
288
|
}
|
|
280
|
-
/** Add
|
|
289
|
+
/** Add repair's outside-scope count to its selected audit. */
|
|
290
|
+
function repairAuditOf(audit, outside) {
|
|
291
|
+
return {
|
|
292
|
+
...audit,
|
|
293
|
+
outside
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Add a materialization result to a repair audit. */
|
|
281
297
|
function auditToRepairResult(audit, result) {
|
|
282
298
|
return {
|
|
283
299
|
...audit,
|
|
@@ -304,9 +320,9 @@ function countPart(count, label) {
|
|
|
304
320
|
/** Render nonzero audit buckets or `clean`. */
|
|
305
321
|
function bucketText(counts) {
|
|
306
322
|
const parts = [];
|
|
307
|
-
if (counts.drifted > 0) parts.push(
|
|
308
|
-
if (counts.missing > 0) parts.push(
|
|
309
|
-
if (counts.foreign > 0) parts.push(
|
|
323
|
+
if (counts.drifted > 0) parts.push(`${counts.drifted} drifted`);
|
|
324
|
+
if (counts.missing > 0) parts.push(`${counts.missing} missing`);
|
|
325
|
+
if (counts.foreign > 0) parts.push(`${counts.foreign} unexpected`);
|
|
310
326
|
return parts.length > 0 ? parts.join(", ") : "clean";
|
|
311
327
|
}
|
|
312
328
|
/** Render audit's origin-aware verdict. */
|
|
@@ -314,7 +330,12 @@ function auditVerdict(audit, plan) {
|
|
|
314
330
|
const count = audit.findings.length;
|
|
315
331
|
if (audit.clean) return `audit: ${countPart(count, "artifact")} — clean`;
|
|
316
332
|
const split = partitionFindings(audit.findings, plan);
|
|
317
|
-
|
|
333
|
+
const owned = split.owned.drifted === 0 && split.owned.missing === 0;
|
|
334
|
+
const generated = bucketText(split.generated);
|
|
335
|
+
const foreign = bucketText(split.foreign);
|
|
336
|
+
if (owned && generated === "clean") return `audit: ${countPart(count, "artifact")} — host-owned clean; ${foreign}`;
|
|
337
|
+
const suffix = foreign === "clean" ? "" : `; unexpected: ${foreign}`;
|
|
338
|
+
return owned ? `audit: ${countPart(count, "artifact")} — host-owned clean; ${generated} (generated)${suffix}` : `audit: ${countPart(count, "artifact")} — host-owned: ${bucketText(split.owned)}; generated: ${generated}${suffix}`;
|
|
318
339
|
}
|
|
319
340
|
/** Render non-aligned audit findings as terminal table rows. */
|
|
320
341
|
function findingRows(findings, plan) {
|
|
@@ -341,6 +362,43 @@ function auditTable(audit, plan) {
|
|
|
341
362
|
};
|
|
342
363
|
}
|
|
343
364
|
/**
|
|
365
|
+
* Count the files one authorized write will create or overwrite.
|
|
366
|
+
*
|
|
367
|
+
* @param counts - The drift tallies of every target the write covers.
|
|
368
|
+
* @param replace - Whether stale byte replacement was explicitly authorized.
|
|
369
|
+
* @returns Every missing file, plus drifted files only under `replace`.
|
|
370
|
+
* @remarks
|
|
371
|
+
* The one figure every write confirmation asks about, so `repair` and `fleet`
|
|
372
|
+
* cannot drift apart on it. An unexpected file is never counted here: only
|
|
373
|
+
* `--prune` deletes one, and only after its own separate question.
|
|
374
|
+
*/
|
|
375
|
+
function countWrites(counts, replace) {
|
|
376
|
+
let total = 0;
|
|
377
|
+
for (const count of counts) total += count.missing + (replace ? count.drifted : 0);
|
|
378
|
+
return total;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Test whether a reported repository remains dirty inside or outside the selected scope.
|
|
382
|
+
*
|
|
383
|
+
* @param audit - The selected-scope audit.
|
|
384
|
+
* @param outside - Findings outside that scope.
|
|
385
|
+
* @returns Whether either source reports drift.
|
|
386
|
+
*/
|
|
387
|
+
function hasFindings(audit, outside) {
|
|
388
|
+
return !audit.clean || outside > 0;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Render one write verb's ownership boundary in that verb's own voice.
|
|
392
|
+
*
|
|
393
|
+
* @param verb - The command whose scope this is.
|
|
394
|
+
* @param generated - Whether generated canon was included in the scope.
|
|
395
|
+
* @param repos - The number of repositories the write covers, absent for a single target.
|
|
396
|
+
* @returns The scope line naming what the command restores, what it replaces only with `--replace`, and what it never touches.
|
|
397
|
+
*/
|
|
398
|
+
function scopeLine(verb, generated, repos) {
|
|
399
|
+
return `${verb} scope${repos === void 0 ? "" : ` across ${countPart(repos, "repo")}`}: ${generated ? REPAIR_GENERATED_SCOPE : REPAIR_SCOPE}`;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
344
402
|
* Render drift outside repair's selected ownership boundary.
|
|
345
403
|
*
|
|
346
404
|
* @param count - The number of findings outside the selected scope.
|
|
@@ -349,24 +407,84 @@ function auditTable(audit, plan) {
|
|
|
349
407
|
*/
|
|
350
408
|
function scopeNote(count, generated) {
|
|
351
409
|
if (count === 0) return void 0;
|
|
352
|
-
return generated ? `note: ${countPart(count, "finding")} outside host-owned and generated repair scope — run 'audit' for the list; starter files and package
|
|
410
|
+
return generated ? `note: ${countPart(count, "finding")} outside host-owned and generated repair scope — run 'audit' for the list; present starter files and package publication metadata remain protected` : `note: ${countPart(count, "finding")} outside host-owned repair scope — run 'audit' for the list`;
|
|
353
411
|
}
|
|
354
412
|
/**
|
|
355
413
|
* Render repair's dry-run verdict.
|
|
356
414
|
*
|
|
357
415
|
* @param audit - The audit over the selected repair plan.
|
|
358
|
-
* @param
|
|
416
|
+
* @param options - The selected scope and write authorizations.
|
|
359
417
|
* @returns The scope-aware clean or drifted verdict.
|
|
360
418
|
*/
|
|
361
|
-
function repairVerdict(audit,
|
|
362
|
-
const scope = generated ? "host-owned and generated" : "host-owned";
|
|
419
|
+
function repairVerdict(audit, options) {
|
|
420
|
+
const scope = options.generated ? "host-owned and generated" : "host-owned";
|
|
363
421
|
if (audit.clean) return `repair: ${countPart(audit.findings.length, `${scope} artifact`)} aligned — nothing to write`;
|
|
364
|
-
|
|
422
|
+
const head = `repair: ${scope}: ${bucketText(audit)}`;
|
|
423
|
+
if (audit.drifted === 0) return options.apply ? `${head} — missing files will be restored` : `${head} — pass --apply to write`;
|
|
424
|
+
if (audit.missing === 0) return options.replace ? `${head} — --apply overwrites drifted files, discarding local changes` : `${head} — drifted files change only with --replace, which discards local changes`;
|
|
425
|
+
return options.replace ? `${head} — --apply restores missing files and overwrites drifted ones, discarding local changes` : `${head} — --apply restores missing files; drifted files change only with --replace, which discards local changes`;
|
|
365
426
|
}
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Merge only generated service scripts into an existing manifest.
|
|
429
|
+
*
|
|
430
|
+
* @param current - The existing consumer manifest text.
|
|
431
|
+
* @param generated - The canonical manifest text for the derived service blueprint.
|
|
432
|
+
* @param services - The declared service vendor names.
|
|
433
|
+
* @returns Formatter-stable manifest text preserving publication metadata and unrelated scripts.
|
|
434
|
+
*/
|
|
435
|
+
function mergeServiceManifest(current, generated, services) {
|
|
436
|
+
const currentManifest = parseJSON(current);
|
|
437
|
+
const generatedManifest = parseJSON(generated);
|
|
438
|
+
if (!isRecord(currentManifest) || !isRecord(generatedManifest)) throw new ScaffoldError("INVALID", "Service adoption requires object package manifests");
|
|
439
|
+
const currentScriptsValue = ownDataValue(currentManifest, "scripts");
|
|
440
|
+
const generatedScriptsValue = ownDataValue(generatedManifest, "scripts");
|
|
441
|
+
if (!isRecord(currentScriptsValue) || !isRecord(generatedScriptsValue)) throw new ScaffoldError("INVALID", "Service adoption requires object package scripts");
|
|
442
|
+
const serviceKeys = ["test:service", ...services.map((service) => `test:service:${service}`)];
|
|
443
|
+
const serviceNames = new Set(serviceKeys);
|
|
444
|
+
const currentPublish = ownDataValue(currentScriptsValue, "prepublishOnly");
|
|
445
|
+
const generatedPublish = ownDataValue(generatedScriptsValue, "prepublishOnly");
|
|
446
|
+
if (typeof currentPublish !== "string" || typeof generatedPublish !== "string") throw new ScaffoldError("INVALID", "Service adoption requires a prepublishOnly script");
|
|
447
|
+
const suffix = " && npm run test:service";
|
|
448
|
+
const publish = currentPublish.endsWith(suffix) ? currentPublish : `${currentPublish}${suffix}`;
|
|
449
|
+
const scripts = {};
|
|
450
|
+
for (const name of Object.keys(generatedScriptsValue)) {
|
|
451
|
+
if (serviceNames.has(name)) {
|
|
452
|
+
const value = ownDataValue(generatedScriptsValue, name);
|
|
453
|
+
if (typeof value !== "string") throw new ScaffoldError("INVALID", `Generated service script is missing at ${name}`);
|
|
454
|
+
scripts[name] = value;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (name === "prepublishOnly") {
|
|
458
|
+
scripts[name] = publish;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const value = ownDataValue(currentScriptsValue, name);
|
|
462
|
+
if (value !== void 0) scripts[name] = value;
|
|
463
|
+
}
|
|
464
|
+
for (const name of Object.keys(currentScriptsValue)) {
|
|
465
|
+
if (Object.hasOwn(scripts, name)) continue;
|
|
466
|
+
if (name === "test:service" || name.startsWith("test:service:")) continue;
|
|
467
|
+
const value = ownDataValue(currentScriptsValue, name);
|
|
468
|
+
if (value !== void 0) scripts[name] = value;
|
|
469
|
+
}
|
|
470
|
+
return `${JSON.stringify({
|
|
471
|
+
...currentManifest,
|
|
472
|
+
scripts
|
|
473
|
+
}, void 0, " ")}\n`;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Render repair's closing tally.
|
|
477
|
+
*
|
|
478
|
+
* @param tally - The displayed counts of one repair run.
|
|
479
|
+
* @returns The tally line, in the same words the audit table above it used.
|
|
480
|
+
* @remarks
|
|
481
|
+
* A drifted file repair was not authorized to overwrite is counted on its own
|
|
482
|
+
* rather than folded in with the aligned files, because `unchanged` is already
|
|
483
|
+
* the audit table's word for a file that matches canon.
|
|
484
|
+
*/
|
|
485
|
+
function repairTally(tally) {
|
|
486
|
+
const left = tally.drifted === 0 ? "" : `, ${countPart(tally.drifted, `${DRIFT_LABEL.stale} file`)} left alone`;
|
|
487
|
+
return `${ACTION_LABEL.written} ${tally.written}, ${ACTION_LABEL.skipped} ${tally.unchanged}${left}, ${ACTION_LABEL.removed} ${tally.removed}`;
|
|
370
488
|
}
|
|
371
489
|
/** Render synchronization freshness as table rows. */
|
|
372
490
|
function syncRows(report) {
|
|
@@ -415,7 +533,7 @@ function fleetRepoLine(name, outcome) {
|
|
|
415
533
|
}
|
|
416
534
|
/** Render fleet's repository totals. */
|
|
417
535
|
function fleetTotals(drifted, failed) {
|
|
418
|
-
return `total: ${countPart(drifted, "
|
|
536
|
+
return `total: ${countPart(drifted, "dirty repo")}, ${failed} failed`;
|
|
419
537
|
}
|
|
420
538
|
/** Create catalog's terminal table. */
|
|
421
539
|
function catalogTable(entries) {
|
|
@@ -447,13 +565,25 @@ function newPlanTable(summary) {
|
|
|
447
565
|
]
|
|
448
566
|
};
|
|
449
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Render one contained write destination as the operator's own path to it.
|
|
570
|
+
*
|
|
571
|
+
* @param root - The invocation directory every write is confined beneath.
|
|
572
|
+
* @param destination - The contained physical destination.
|
|
573
|
+
* @returns The destination relative to the invocation directory, or the absolute path when it is not beneath it.
|
|
574
|
+
*/
|
|
575
|
+
function describeDestination(root, destination) {
|
|
576
|
+
const path = relative(root, destination).replaceAll("\\", "/");
|
|
577
|
+
if (path === "") return ".";
|
|
578
|
+
return path.startsWith("..") ? destination : `./${path}`;
|
|
579
|
+
}
|
|
450
580
|
/** Render `new`'s dry-run destination. */
|
|
451
|
-
function newPlanPreview(
|
|
452
|
-
return `will write into
|
|
581
|
+
function newPlanPreview(destination) {
|
|
582
|
+
return `will write into ${destination}`;
|
|
453
583
|
}
|
|
454
584
|
/** Render `new`'s write result. */
|
|
455
|
-
function newApplySuccess(count,
|
|
456
|
-
return `wrote ${countPart(count, "file")} into
|
|
585
|
+
function newApplySuccess(count, destination) {
|
|
586
|
+
return `wrote ${countPart(count, "file")} into ${destination}`;
|
|
457
587
|
}
|
|
458
588
|
/** Render catalog's write result. */
|
|
459
589
|
function catalogApplySuccess(path) {
|
|
@@ -468,12 +598,21 @@ function applyConfirmMessage(files, repos) {
|
|
|
468
598
|
function pruneConfirmMessage(count) {
|
|
469
599
|
return `Also delete ${countPart(count, "unexpected file")} under .claude/agents, .codex/agents, and scripts? `;
|
|
470
600
|
}
|
|
471
|
-
/**
|
|
472
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Render the interactive audit-to-repair handoff as the list of actions it authorizes.
|
|
603
|
+
*
|
|
604
|
+
* @param missing - The number of missing host-owned files the inherited repair would restore.
|
|
605
|
+
* @param drifted - The number of drifted host-owned files it would overwrite, zero unless `--replace` authorized replacement.
|
|
606
|
+
* @param foreign - The number of unexpected files found.
|
|
607
|
+
* @param prune - Whether `--prune` authorized deletion, without which no unexpected file is touched.
|
|
608
|
+
* @returns The confirmation question, naming each authorized action and the cost of overwriting.
|
|
609
|
+
*/
|
|
610
|
+
function repairHandoff(missing, drifted, foreign, prune) {
|
|
473
611
|
const parts = [];
|
|
474
|
-
if (
|
|
475
|
-
if (
|
|
476
|
-
|
|
612
|
+
if (missing > 0) parts.push(`restore ${countPart(missing, "missing host-owned file")}`);
|
|
613
|
+
if (drifted > 0) parts.push(`overwrite ${countPart(drifted, "drifted host-owned file")}, discarding local changes`);
|
|
614
|
+
if (prune && foreign > 0) parts.push(`delete ${countPart(foreign, "unexpected file")}`);
|
|
615
|
+
return `${parts.join("; ")} — run repair now? `;
|
|
477
616
|
}
|
|
478
617
|
/** Render one unresolved Orkestrel dependency token. */
|
|
479
618
|
function unknownOrkestrelToken(token, suggestion) {
|
|
@@ -567,9 +706,23 @@ function invalidName(name, pattern) {
|
|
|
567
706
|
function unresolvedVersion(names) {
|
|
568
707
|
return `could not resolve the latest version for ${names.map((name) => `"${name}"`).join(", ")} — check the name or pass name@range`;
|
|
569
708
|
}
|
|
570
|
-
/**
|
|
709
|
+
/**
|
|
710
|
+
* Render drift that belongs to generated artifacts.
|
|
711
|
+
*
|
|
712
|
+
* @param count - The number of findings on generated artifacts.
|
|
713
|
+
* @returns The guidance line, stating what each repair scope does rather than how the files differ.
|
|
714
|
+
*/
|
|
571
715
|
function generatedNote(count) {
|
|
572
|
-
return `${countPart(count, "finding")} in generated files —
|
|
716
|
+
return `${countPart(count, "finding")} in generated files — run 'scaffold repair --generated' to restore missing ones; add --replace to overwrite drifted ones, discarding local changes`;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Render the explicit destructive opt-in for stale host-owned files.
|
|
720
|
+
*
|
|
721
|
+
* @param count - The number of stale host-owned files.
|
|
722
|
+
* @returns The replacement guidance line, naming the safe default before its destructive opt-in.
|
|
723
|
+
*/
|
|
724
|
+
function replacementNote(count) {
|
|
725
|
+
return `${countPart(count, "drifted host-owned file")} — repair leaves drifted files alone; 'scaffold repair --replace' overwrites them, discarding local changes`;
|
|
573
726
|
}
|
|
574
727
|
/**
|
|
575
728
|
* Render honest repair guidance for computed and protected manifest drift.
|
|
@@ -589,16 +742,19 @@ function renderComputedNotes(findings, plan) {
|
|
|
589
742
|
}
|
|
590
743
|
const notes = [];
|
|
591
744
|
if (computed > 0) notes.push(generatedNote(computed));
|
|
592
|
-
if (manifest > 0) notes.push(`${countPart(manifest, "finding")} in package.json — repair
|
|
745
|
+
if (manifest > 0 && plan.blueprint.services.length > 0) notes.push(`${countPart(manifest, "finding")} in package.json — 'scaffold repair --generated' repairs generated service scripts; review any remaining publication metadata directly`);
|
|
746
|
+
else if (manifest > 0) notes.push(`${countPart(manifest, "finding")} in package.json — repair does not rewrite protected publication metadata; review and edit it directly`);
|
|
593
747
|
return notes;
|
|
594
748
|
}
|
|
595
749
|
/** Render live dependency freshness tallies. */
|
|
596
750
|
function auditLiveNote(current, behind, failed) {
|
|
597
|
-
return `live: ${
|
|
751
|
+
return `live: ${current} current, ${behind} behind, ${failed} failed`;
|
|
598
752
|
}
|
|
599
753
|
/** Render whether an audit compared content or presence. */
|
|
600
|
-
function comparisonLine(
|
|
601
|
-
|
|
754
|
+
function comparisonLine(compared, presence) {
|
|
755
|
+
if (presence === 0) return "comparing: file contents for host-owned files";
|
|
756
|
+
if (compared === 0) return `comparing: file presence for ${countPart(presence, "presence-owned file")}`;
|
|
757
|
+
return `comparing: file contents for ${countPart(compared, "host-owned file")}; presence for ${countPart(presence, "presence-owned file")}`;
|
|
602
758
|
}
|
|
603
759
|
/** Render catalog's final verdict. */
|
|
604
760
|
function catalogVerdict(clean) {
|
|
@@ -662,6 +818,10 @@ function parseArguments(argv) {
|
|
|
662
818
|
type: "boolean",
|
|
663
819
|
default: false
|
|
664
820
|
},
|
|
821
|
+
replace: {
|
|
822
|
+
type: "boolean",
|
|
823
|
+
default: false
|
|
824
|
+
},
|
|
665
825
|
strict: {
|
|
666
826
|
type: "boolean",
|
|
667
827
|
default: false
|
|
@@ -824,8 +984,8 @@ var CLI = class {
|
|
|
824
984
|
compiler.destroy();
|
|
825
985
|
}
|
|
826
986
|
}
|
|
827
|
-
#prunePaths(target, host,
|
|
828
|
-
const seam =
|
|
987
|
+
#prunePaths(target, host, services) {
|
|
988
|
+
const seam = services.length > 0 ? SERVICE_SCRIPT_PATH : void 0;
|
|
829
989
|
return pruneTargets(target, host).filter((path) => path !== seam);
|
|
830
990
|
}
|
|
831
991
|
/**
|
|
@@ -838,8 +998,8 @@ var CLI = class {
|
|
|
838
998
|
* unclean, so an "unexpected file" is honestly counted as drift (exit 1)
|
|
839
999
|
* instead of the structurally-always-zero `diffPlan.foreign`.
|
|
840
1000
|
*/
|
|
841
|
-
#scan(audit, target, host,
|
|
842
|
-
const paths = this.#prunePaths(target, host,
|
|
1001
|
+
#scan(audit, target, host, services) {
|
|
1002
|
+
const paths = this.#prunePaths(target, host, services);
|
|
843
1003
|
if (paths.length === 0) return audit;
|
|
844
1004
|
const findings = paths.map((path) => ({
|
|
845
1005
|
path,
|
|
@@ -858,8 +1018,8 @@ var CLI = class {
|
|
|
858
1018
|
* When fail-closed allowlist discovery raises a coded `TARGET` failure, retain
|
|
859
1019
|
* the existing findings and mark the audit incomplete instead of crashing.
|
|
860
1020
|
*/
|
|
861
|
-
#scanSafe(audit, target, host,
|
|
862
|
-
const scanned = attempt(() => this.#scan(audit, target, host,
|
|
1021
|
+
#scanSafe(audit, target, host, services) {
|
|
1022
|
+
const scanned = attempt(() => this.#scan(audit, target, host, services));
|
|
863
1023
|
if (scanned.success) return {
|
|
864
1024
|
audit: scanned.value,
|
|
865
1025
|
skipped: false
|
|
@@ -884,21 +1044,63 @@ var CLI = class {
|
|
|
884
1044
|
* caller compiles the full plan anyway. Diff it too so a clean scoped verdict can point at drift
|
|
885
1045
|
* outside the selected boundary. The count feeds the shared `scopeNote` renderer.
|
|
886
1046
|
*/
|
|
887
|
-
#outside(compiled, target) {
|
|
888
|
-
const full =
|
|
889
|
-
|
|
1047
|
+
#outside(compiled, selected, target, host) {
|
|
1048
|
+
const full = hydratePlan(compiled, host);
|
|
1049
|
+
const selectedPaths = new Set(selected.artifacts.map((artifact) => artifact.path));
|
|
1050
|
+
return diffPlan(full, readTarget(target, full.artifacts.map((artifact) => artifact.path))).findings.filter((finding) => finding.drift !== "aligned" && !selectedPaths.has(finding.path)).length;
|
|
890
1051
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1052
|
+
#emitRepair(audit, generated, json, result) {
|
|
1053
|
+
if (json) {
|
|
1054
|
+
this.#write(result === void 0 ? audit : auditToRepairResult(audit, result));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const note = scopeNote(audit.outside, generated);
|
|
1058
|
+
if (note !== void 0) this.#reporter.line(note);
|
|
1059
|
+
}
|
|
1060
|
+
/** Promote only absent service-owned starter seams into repairable missing artifacts. */
|
|
1061
|
+
#promoteServices(plan, target) {
|
|
1062
|
+
if (plan.blueprint.services.length === 0) return plan;
|
|
1063
|
+
const paths = [SERVICE_SCRIPT_PATH, "tests/config/services.test.ts"];
|
|
1064
|
+
const current = readTarget(target, paths);
|
|
1065
|
+
return {
|
|
1066
|
+
...plan,
|
|
1067
|
+
artifacts: plan.artifacts.map((artifact) => artifact.origin === "template" && paths.includes(artifact.path) && !Object.hasOwn(current, artifact.path) ? {
|
|
1068
|
+
...artifact,
|
|
1069
|
+
origin: "computed"
|
|
1070
|
+
} : artifact)
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/** Select repair ownership and merge only the manifest's generated service-script keys. */
|
|
1074
|
+
#scopeRepair(compiled, target, generated) {
|
|
1075
|
+
const promoted = this.#promoteServices(compiled, target);
|
|
1076
|
+
const currentManifest = generated && promoted.blueprint.services.length > 0 ? readManifest(target) : void 0;
|
|
1077
|
+
const artifacts = [];
|
|
1078
|
+
for (const artifact of promoted.artifacts) {
|
|
1079
|
+
if (artifact.origin === "host" || artifact.path === SERVICE_SCRIPT_PATH || artifact.path === "tests/config/services.test.ts" || generated && artifact.origin === "computed" && artifact.path !== "package.json") {
|
|
1080
|
+
artifacts.push(artifact);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (currentManifest !== void 0 && artifact.origin === "computed" && artifact.path === "package.json") {
|
|
1084
|
+
const content = mergeServiceManifest(currentManifest, artifact.content, promoted.blueprint.services);
|
|
1085
|
+
if (content !== currentManifest) artifacts.push({
|
|
1086
|
+
...artifact,
|
|
1087
|
+
content
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
...promoted,
|
|
896
1093
|
blueprint: {
|
|
897
|
-
...
|
|
1094
|
+
...promoted.blueprint,
|
|
898
1095
|
overrides: []
|
|
899
1096
|
},
|
|
900
|
-
artifacts
|
|
1097
|
+
artifacts
|
|
901
1098
|
};
|
|
1099
|
+
}
|
|
1100
|
+
/** Compile and audit the selected repair ownership boundary for one structural snapshot. */
|
|
1101
|
+
#prepareRepair(spec, target, host, generated, json) {
|
|
1102
|
+
const [compiled] = this.#compile(spec, json);
|
|
1103
|
+
const scoped = this.#scopeRepair(compiled, target, generated);
|
|
902
1104
|
let plan;
|
|
903
1105
|
try {
|
|
904
1106
|
plan = hydratePlan(scoped, host);
|
|
@@ -930,14 +1132,12 @@ var CLI = class {
|
|
|
930
1132
|
}
|
|
931
1133
|
/**
|
|
932
1134
|
* The shared write-confirmation gate every verb calls before it touches disk.
|
|
933
|
-
* `--apply`
|
|
934
|
-
*
|
|
935
|
-
* confirm with `default: false` (EOF on stdin resolves to the default).
|
|
1135
|
+
* `--apply` is the sole write authorization. JSON and non-terminal calls do
|
|
1136
|
+
* not prompt once authorized; `--yes` skips the terminal confirmation.
|
|
936
1137
|
*/
|
|
937
1138
|
async #apply(terminal, message, values, json) {
|
|
938
|
-
if (values.apply) return
|
|
939
|
-
if (json) return
|
|
940
|
-
if (values.yes) return true;
|
|
1139
|
+
if (!values.apply) return false;
|
|
1140
|
+
if (json || values.yes || !this.#tty) return true;
|
|
941
1141
|
return this.#guard(terminal.confirm({
|
|
942
1142
|
message,
|
|
943
1143
|
default: false
|
|
@@ -945,18 +1145,16 @@ var CLI = class {
|
|
|
945
1145
|
}
|
|
946
1146
|
/**
|
|
947
1147
|
* The SECOND, separate confirm for `--prune`-eligible deletions — never
|
|
948
|
-
* bundled into
|
|
949
|
-
*
|
|
1148
|
+
* bundled into the write question. Both `--prune` and `--apply` are required;
|
|
1149
|
+
* `--yes` only skips this confirmation and never authorizes deletion.
|
|
950
1150
|
*/
|
|
951
1151
|
async #prune(terminal, message, values, json) {
|
|
952
1152
|
if (!values.prune) return false;
|
|
953
|
-
if (values.apply)
|
|
954
|
-
|
|
955
|
-
if (values.yes) return true;
|
|
956
|
-
if (!this.#tty) {
|
|
957
|
-
this.#reporter.line(PRUNE_SKIPPED);
|
|
1153
|
+
if (!values.apply) {
|
|
1154
|
+
if (!json && !this.#tty) this.#reporter.line(PRUNE_SKIPPED);
|
|
958
1155
|
return false;
|
|
959
1156
|
}
|
|
1157
|
+
if (json || values.yes || !this.#tty) return true;
|
|
960
1158
|
return this.#guard(terminal.confirm({
|
|
961
1159
|
message,
|
|
962
1160
|
default: false
|
|
@@ -985,13 +1183,13 @@ var CLI = class {
|
|
|
985
1183
|
* Best-effort vendored `@orkestrel` catalog names, resolved via `host`
|
|
986
1184
|
* (`hostRoot()`, or the active `--from` override) through the host manifest
|
|
987
1185
|
* — `undefined` when the catalog cannot be established (a missing/unreadable
|
|
988
|
-
* manifest, no
|
|
1186
|
+
* manifest, no `CATALOG_AGENT_PATH` entry, or any other failure),
|
|
989
1187
|
* degrading the dependency prompt to shape-only validation instead of blocking on it.
|
|
990
1188
|
*/
|
|
991
1189
|
#names(host) {
|
|
992
1190
|
const names = attempt(() => {
|
|
993
1191
|
const manifest = readHostManifest(host);
|
|
994
|
-
const full = locateHostSource(manifest,
|
|
1192
|
+
const full = locateHostSource(manifest, CATALOG_AGENT_PATH, host);
|
|
995
1193
|
if (full === void 0 || !existsSync(full)) return void 0;
|
|
996
1194
|
const relative$1 = relative(host, full).replaceAll("\\", "/");
|
|
997
1195
|
return catalogNames(readFileText(host, relative$1, "TARGET", "host"));
|
|
@@ -1088,10 +1286,11 @@ var CLI = class {
|
|
|
1088
1286
|
dependencies: deps
|
|
1089
1287
|
}), json);
|
|
1090
1288
|
const summary = planToSummary(plan);
|
|
1289
|
+
const label = describeDestination(process.cwd(), destination);
|
|
1091
1290
|
if (!json) {
|
|
1092
1291
|
this.#reporter.section("Plan");
|
|
1093
1292
|
this.#reporter.table(newPlanTable(summary));
|
|
1094
|
-
this.#reporter.line(newPlanPreview(
|
|
1293
|
+
this.#reporter.line(newPlanPreview(label));
|
|
1095
1294
|
}
|
|
1096
1295
|
if (!await this.#apply(terminal, applyConfirmMessage(summary.host + summary.template + summary.computed), values, json)) {
|
|
1097
1296
|
if (json) this.#write(summaryToNewResult(summary, false));
|
|
@@ -1107,7 +1306,7 @@ var CLI = class {
|
|
|
1107
1306
|
const result = materializer.materialize(plan, destination);
|
|
1108
1307
|
const count = result.written.length + result.copied.length;
|
|
1109
1308
|
if (json) this.#write(summaryToNewResult(summary, true));
|
|
1110
|
-
else this.#succeed(spinner, json, newApplySuccess(count,
|
|
1309
|
+
else this.#succeed(spinner, json, newApplySuccess(count, label));
|
|
1111
1310
|
} catch (error) {
|
|
1112
1311
|
this.#reject(spinner, json, error);
|
|
1113
1312
|
} finally {
|
|
@@ -1175,7 +1374,7 @@ var CLI = class {
|
|
|
1175
1374
|
this.#reject(spinner, json, error);
|
|
1176
1375
|
}
|
|
1177
1376
|
} else if (json) this.#write(report);
|
|
1178
|
-
process.exitCode = report.
|
|
1377
|
+
process.exitCode = report.failed > 0 ? 1 : report.clean || proceed ? 0 : 1;
|
|
1179
1378
|
}
|
|
1180
1379
|
/** `scaffold audit` — whole-plan conformance report; offers a repair handoff on drift. */
|
|
1181
1380
|
async #audit(values, json) {
|
|
@@ -1202,7 +1401,7 @@ var CLI = class {
|
|
|
1202
1401
|
const host = values.from?.[0] ?? hostRoot();
|
|
1203
1402
|
let plan;
|
|
1204
1403
|
try {
|
|
1205
|
-
plan = hydratePlan(compiled, host);
|
|
1404
|
+
plan = hydratePlan(this.#promoteServices(compiled, target), host);
|
|
1206
1405
|
} catch (error) {
|
|
1207
1406
|
this.#error(error, json);
|
|
1208
1407
|
}
|
|
@@ -1211,7 +1410,7 @@ var CLI = class {
|
|
|
1211
1410
|
...diffPlan(plan, readTarget(target, artifactPaths)),
|
|
1212
1411
|
questions
|
|
1213
1412
|
};
|
|
1214
|
-
const scanned = this.#scanSafe(rawAudit, target, host, spec.
|
|
1413
|
+
const scanned = this.#scanSafe(rawAudit, target, host, spec.services);
|
|
1215
1414
|
const audit = scanned.audit;
|
|
1216
1415
|
let drifted = !audit.clean;
|
|
1217
1416
|
let live;
|
|
@@ -1245,19 +1444,22 @@ var CLI = class {
|
|
|
1245
1444
|
}
|
|
1246
1445
|
if (scanned.skipped) this.#reporter.line(SCAN_SKIPPED);
|
|
1247
1446
|
for (const question of audit.questions) if (!question.blocking) this.#reporter.line(`warning: ${question.text}`);
|
|
1248
|
-
|
|
1447
|
+
const hostArtifacts = plan.artifacts.filter((artifact) => artifact.origin === "host");
|
|
1448
|
+
const presenceOwned = hostArtifacts.filter((artifact) => artifact.hex === void 0).length;
|
|
1449
|
+
this.#reporter.line(comparisonLine(hostArtifacts.length - presenceOwned, presenceOwned));
|
|
1249
1450
|
this.#reporter.table(auditTable(audit, plan));
|
|
1250
1451
|
this.#reporter.line(auditVerdict(audit, plan));
|
|
1251
1452
|
if (live !== void 0) this.#reporter.line(auditLiveNote(live.current, live.behind, live.failed));
|
|
1252
1453
|
if (!audit.clean) {
|
|
1253
1454
|
const split = partitionFindings(audit.findings, plan);
|
|
1254
|
-
const
|
|
1455
|
+
const replace = values.replace === true;
|
|
1456
|
+
const ownedCount = split.owned.missing + (replace ? split.owned.drifted : 0);
|
|
1255
1457
|
const pruneRequested = values.prune === true;
|
|
1256
1458
|
const offerHandoff = this.#tty && (ownedCount > 0 || audit.foreign > 0 && pruneRequested);
|
|
1257
1459
|
let handoffAccepted = false;
|
|
1258
1460
|
if (offerHandoff) {
|
|
1259
1461
|
const terminal = createTerminal();
|
|
1260
|
-
const message = repairHandoff(
|
|
1462
|
+
const message = repairHandoff(split.owned.missing, replace ? split.owned.drifted : 0, audit.foreign, pruneRequested);
|
|
1261
1463
|
handoffAccepted = await this.#guard(terminal.confirm({
|
|
1262
1464
|
message,
|
|
1263
1465
|
default: false
|
|
@@ -1265,12 +1467,13 @@ var CLI = class {
|
|
|
1265
1467
|
if (handoffAccepted) {
|
|
1266
1468
|
await this.#repair(values, false);
|
|
1267
1469
|
const rawFinal = diffPlan(plan, readTarget(target, artifactPaths));
|
|
1268
|
-
const finalScanned = this.#scanSafe(rawFinal, target, host, spec.
|
|
1470
|
+
const finalScanned = this.#scanSafe(rawFinal, target, host, spec.services);
|
|
1269
1471
|
process.exitCode = finalScanned.audit.clean ? 0 : 1;
|
|
1270
1472
|
return;
|
|
1271
1473
|
}
|
|
1272
1474
|
}
|
|
1273
1475
|
if (!handoffAccepted) {
|
|
1476
|
+
if (split.owned.drifted > 0 && !replace) this.#reporter.line(replacementNote(split.owned.drifted));
|
|
1274
1477
|
if (audit.foreign > 0 && !pruneRequested) this.#reporter.line(FOREIGN_HINT);
|
|
1275
1478
|
for (const note of renderComputedNotes(audit.findings, plan)) this.#reporter.line(note);
|
|
1276
1479
|
}
|
|
@@ -1287,34 +1490,49 @@ var CLI = class {
|
|
|
1287
1490
|
this.#error(error, json);
|
|
1288
1491
|
}
|
|
1289
1492
|
const generated = values.generated === true;
|
|
1493
|
+
const replace = values.replace === true;
|
|
1290
1494
|
const host = values.from?.[0] ?? hostRoot();
|
|
1291
1495
|
const prepared = this.#prepareRepair(spec, target, host, generated, json);
|
|
1292
|
-
|
|
1496
|
+
let compiled = prepared[0];
|
|
1293
1497
|
let plan = prepared[1];
|
|
1294
1498
|
let audit = prepared[2];
|
|
1499
|
+
let outcome = repairAuditOf(this.#scanSafe(audit, target, host, spec.services).audit, this.#outside(compiled, plan, target, host));
|
|
1500
|
+
const verdict = {
|
|
1501
|
+
generated,
|
|
1502
|
+
replace,
|
|
1503
|
+
apply: values.apply === true
|
|
1504
|
+
};
|
|
1295
1505
|
if (!json) {
|
|
1296
|
-
this.#reporter.line(generated
|
|
1506
|
+
if (!outcome.clean) this.#reporter.line(scopeLine("repair", generated));
|
|
1297
1507
|
this.#reporter.section("Audit");
|
|
1298
|
-
this.#reporter.table(auditTable(
|
|
1508
|
+
this.#reporter.table(auditTable(outcome, plan));
|
|
1299
1509
|
}
|
|
1300
|
-
const prunePaths = values.prune ? this.#prunePaths(target, host, spec.
|
|
1510
|
+
const prunePaths = values.prune ? this.#prunePaths(target, host, spec.services) : [];
|
|
1301
1511
|
const pruneSnapshot = readTarget(target, prunePaths);
|
|
1302
|
-
if (
|
|
1303
|
-
if (json) this.#
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1512
|
+
if (outcome.clean && prunePaths.length === 0) {
|
|
1513
|
+
if (!json) this.#reporter.line(repairVerdict(outcome, verdict));
|
|
1514
|
+
this.#emitRepair(outcome, generated, json);
|
|
1515
|
+
process.exitCode = hasFindings(outcome, outcome.outside) ? 1 : 0;
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (!json) this.#reporter.line(repairVerdict(outcome, verdict));
|
|
1519
|
+
const repairable = countWrites([audit], replace);
|
|
1520
|
+
if (repairable === 0 && prunePaths.length === 0) {
|
|
1521
|
+
if (!json) this.#reporter.line(repairTally({
|
|
1522
|
+
written: 0,
|
|
1523
|
+
unchanged: audit.findings.length - audit.drifted - audit.missing - audit.foreign,
|
|
1524
|
+
drifted: audit.drifted,
|
|
1525
|
+
removed: 0
|
|
1526
|
+
}));
|
|
1527
|
+
this.#emitRepair(outcome, generated, json);
|
|
1528
|
+
process.exitCode = 1;
|
|
1310
1529
|
return;
|
|
1311
1530
|
}
|
|
1312
|
-
if (!json) this.#reporter.line(repairVerdict(audit, generated));
|
|
1313
1531
|
const terminal = createTerminal();
|
|
1314
1532
|
let proceed = true;
|
|
1315
|
-
if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(
|
|
1533
|
+
if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(repairable), values, json);
|
|
1316
1534
|
if (!proceed) {
|
|
1317
|
-
|
|
1535
|
+
this.#emitRepair(outcome, generated, json);
|
|
1318
1536
|
process.exitCode = 1;
|
|
1319
1537
|
return;
|
|
1320
1538
|
}
|
|
@@ -1335,31 +1553,41 @@ var CLI = class {
|
|
|
1335
1553
|
global: currentGlobal
|
|
1336
1554
|
};
|
|
1337
1555
|
const refreshed = this.#prepareRepair(spec, target, host, generated, json);
|
|
1556
|
+
compiled = refreshed[0];
|
|
1338
1557
|
plan = refreshed[1];
|
|
1339
1558
|
audit = refreshed[2];
|
|
1559
|
+
outcome = repairAuditOf(this.#scanSafe(audit, target, host, spec.services).audit, this.#outside(compiled, plan, target, host));
|
|
1340
1560
|
}
|
|
1341
1561
|
const spinner = this.#spinner("repairing", json);
|
|
1342
1562
|
spinner?.start();
|
|
1343
1563
|
const materializer = createMaterializer({ host });
|
|
1344
1564
|
try {
|
|
1345
|
-
const result = materializer.repair(plan, audit, target);
|
|
1565
|
+
const result = materializer.repair(plan, audit, target, replace);
|
|
1346
1566
|
const removed = doPrune ? materializer.prune(target, pruneSnapshot).removed : [];
|
|
1347
|
-
|
|
1567
|
+
const finalAudit = repairAuditOf(this.#scanSafe(diffPlan(plan, readTarget(target, plan.artifacts.map((artifact) => artifact.path))), target, host, spec.services).audit, outcome.outside);
|
|
1568
|
+
const left = replace ? 0 : audit.drifted;
|
|
1569
|
+
if (!json) this.#succeed(spinner, json, repairTally({
|
|
1570
|
+
written: result.written.length + result.copied.length,
|
|
1571
|
+
unchanged: result.skipped.length - left,
|
|
1572
|
+
drifted: left,
|
|
1573
|
+
removed: removed.length
|
|
1574
|
+
}));
|
|
1575
|
+
this.#emitRepair(finalAudit, generated, json, {
|
|
1348
1576
|
...result,
|
|
1349
1577
|
removed
|
|
1350
|
-
})
|
|
1351
|
-
|
|
1578
|
+
});
|
|
1579
|
+
process.exitCode = hasFindings(finalAudit, finalAudit.outside) ? 1 : 0;
|
|
1352
1580
|
} catch (error) {
|
|
1353
1581
|
this.#reject(spinner, json, error);
|
|
1354
1582
|
} finally {
|
|
1355
1583
|
materializer.destroy();
|
|
1356
1584
|
}
|
|
1357
|
-
process.exitCode = 0;
|
|
1358
1585
|
}
|
|
1359
1586
|
/** `scaffold fleet` — audit/repair every `@orkestrel` package beneath the current directory's immediate children. */
|
|
1360
1587
|
async #fleet(values, json) {
|
|
1361
1588
|
const root = this.#contain(".", json);
|
|
1362
1589
|
const generated = values.generated === true;
|
|
1590
|
+
const replace = values.replace === true;
|
|
1363
1591
|
const packages = discoverPackages(root);
|
|
1364
1592
|
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);
|
|
1365
1593
|
const host = values.from?.[0] ?? hostRoot();
|
|
@@ -1369,6 +1597,7 @@ var CLI = class {
|
|
|
1369
1597
|
const name = basename(directory);
|
|
1370
1598
|
try {
|
|
1371
1599
|
const compiler = createCompiler();
|
|
1600
|
+
let compiled;
|
|
1372
1601
|
let scoped;
|
|
1373
1602
|
let questions;
|
|
1374
1603
|
try {
|
|
@@ -1378,14 +1607,8 @@ var CLI = class {
|
|
|
1378
1607
|
const message = scaffolding.questions.map((question) => question.text).join("; ");
|
|
1379
1608
|
throw new ScaffoldError("INVALID", message);
|
|
1380
1609
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
blueprint: {
|
|
1384
|
-
...scaffolding.plan.blueprint,
|
|
1385
|
-
overrides: []
|
|
1386
|
-
},
|
|
1387
|
-
artifacts: scaffolding.plan.artifacts.filter((artifact) => artifact.origin === "host" || generated && artifact.origin === "computed" && artifact.path !== "package.json")
|
|
1388
|
-
};
|
|
1610
|
+
compiled = scaffolding.plan;
|
|
1611
|
+
scoped = this.#scopeRepair(compiled, directory, generated);
|
|
1389
1612
|
questions = scaffolding.questions;
|
|
1390
1613
|
} finally {
|
|
1391
1614
|
compiler.destroy();
|
|
@@ -1396,12 +1619,15 @@ var CLI = class {
|
|
|
1396
1619
|
...diffPlan(plan, readTarget(directory, paths)),
|
|
1397
1620
|
questions
|
|
1398
1621
|
};
|
|
1399
|
-
const audit = this.#scan(rawAudit, directory, host, plan.blueprint.
|
|
1622
|
+
const audit = this.#scan(rawAudit, directory, host, plan.blueprint.services);
|
|
1623
|
+
const outside = this.#outside(compiled, plan, directory, host);
|
|
1400
1624
|
repos.push({
|
|
1401
1625
|
name,
|
|
1402
1626
|
directory,
|
|
1403
1627
|
plan,
|
|
1404
|
-
|
|
1628
|
+
rawAudit,
|
|
1629
|
+
audit,
|
|
1630
|
+
outside
|
|
1405
1631
|
});
|
|
1406
1632
|
} catch (error) {
|
|
1407
1633
|
failures.push({
|
|
@@ -1419,6 +1645,8 @@ var CLI = class {
|
|
|
1419
1645
|
foreign: repo.audit.foreign
|
|
1420
1646
|
}));
|
|
1421
1647
|
for (const question of repo.audit.questions) if (!question.blocking) this.#reporter.line(`${repo.name}: warning: ${question.text}`);
|
|
1648
|
+
const note = scopeNote(repo.outside, generated);
|
|
1649
|
+
if (note !== void 0) this.#reporter.line(`${repo.name}: ${note}`);
|
|
1422
1650
|
}
|
|
1423
1651
|
for (const failure of failures) this.#reporter.line(fleetRepoLine(failure.name, {
|
|
1424
1652
|
state: "failed",
|
|
@@ -1426,17 +1654,19 @@ var CLI = class {
|
|
|
1426
1654
|
}));
|
|
1427
1655
|
}
|
|
1428
1656
|
const dirty = repos.filter((repo) => !repo.audit.clean);
|
|
1657
|
+
const reportedDirty = repos.filter((repo) => hasFindings(repo.audit, repo.outside));
|
|
1429
1658
|
if (dirty.length === 0) {
|
|
1430
|
-
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1431
|
-
else this.#reporter.line(fleetTotals(
|
|
1432
|
-
process.exitCode = failures.length > 0 ? 1 : 0;
|
|
1659
|
+
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false, repo.outside)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1660
|
+
else this.#reporter.line(fleetTotals(reportedDirty.length, failures.length));
|
|
1661
|
+
process.exitCode = reportedDirty.length > 0 || failures.length > 0 ? 1 : 0;
|
|
1433
1662
|
return;
|
|
1434
1663
|
}
|
|
1435
|
-
const fileCount = dirty.
|
|
1664
|
+
const fileCount = countWrites(dirty.map((repo) => repo.audit), replace);
|
|
1665
|
+
if (!json && values.apply === true) this.#reporter.line(scopeLine("fleet", generated, dirty.length));
|
|
1436
1666
|
const terminal = createTerminal();
|
|
1437
1667
|
const proceed = await this.#apply(terminal, applyConfirmMessage(fileCount, dirty.length), values, json);
|
|
1438
1668
|
const pruneSets = proceed && values.prune ? new Map(dirty.map((repo) => {
|
|
1439
|
-
const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.
|
|
1669
|
+
const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.services);
|
|
1440
1670
|
return [repo.name, readTarget(repo.directory, paths)];
|
|
1441
1671
|
})) : /* @__PURE__ */ new Map();
|
|
1442
1672
|
const prunePaths = dirty.flatMap((repo) => Object.keys(pruneSets.get(repo.name) ?? {}).map((path) => `${repo.name}/${path}`));
|
|
@@ -1446,27 +1676,27 @@ var CLI = class {
|
|
|
1446
1676
|
}
|
|
1447
1677
|
const doPrune = proceed && prunePaths.length > 0 && await this.#prune(terminal, pruneConfirmMessage(prunePaths.length), values, json);
|
|
1448
1678
|
if (!proceed) {
|
|
1449
|
-
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1450
|
-
else this.#reporter.line(fleetTotals(
|
|
1679
|
+
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false, repo.outside)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1680
|
+
else this.#reporter.line(fleetTotals(reportedDirty.length, failures.length));
|
|
1451
1681
|
process.exitCode = 1;
|
|
1452
1682
|
return;
|
|
1453
1683
|
}
|
|
1454
1684
|
const materializer = createMaterializer({ host });
|
|
1455
|
-
let drifted = 0;
|
|
1685
|
+
let drifted = repos.filter((repo) => repo.audit.clean && repo.outside > 0).length;
|
|
1456
1686
|
let failedCount = failures.length;
|
|
1457
|
-
const entries = repos.filter((repo) => repo.audit.clean).map((repo) => fleetEntryOf(repo.name, repo.audit, false));
|
|
1687
|
+
const entries = repos.filter((repo) => repo.audit.clean).map((repo) => fleetEntryOf(repo.name, repo.audit, false, repo.outside));
|
|
1458
1688
|
try {
|
|
1459
1689
|
for (const repo of dirty) try {
|
|
1460
|
-
materializer.repair(repo.plan, repo.
|
|
1690
|
+
materializer.repair(repo.plan, repo.rawAudit, repo.directory, replace);
|
|
1461
1691
|
if (doPrune) materializer.prune(repo.directory, pruneSets.get(repo.name) ?? {});
|
|
1462
1692
|
const paths = repo.plan.artifacts.map((artifact) => artifact.path);
|
|
1463
1693
|
const rawFinal = diffPlan(repo.plan, readTarget(repo.directory, paths));
|
|
1464
|
-
const finalAudit = this.#scan(rawFinal, repo.directory, host, repo.plan.blueprint.
|
|
1465
|
-
if (
|
|
1466
|
-
entries.push(fleetEntryOf(repo.name, finalAudit, false));
|
|
1694
|
+
const finalAudit = this.#scan(rawFinal, repo.directory, host, repo.plan.blueprint.services);
|
|
1695
|
+
if (hasFindings(finalAudit, repo.outside)) drifted += 1;
|
|
1696
|
+
entries.push(fleetEntryOf(repo.name, finalAudit, false, repo.outside));
|
|
1467
1697
|
if (!json) this.#reporter.line(fleetRepoLine(repo.name, {
|
|
1468
1698
|
state: "repaired",
|
|
1469
|
-
remaining: finalAudit.drifted + finalAudit.missing + finalAudit.foreign
|
|
1699
|
+
remaining: finalAudit.drifted + finalAudit.missing + finalAudit.foreign + repo.outside
|
|
1470
1700
|
}));
|
|
1471
1701
|
} catch (error) {
|
|
1472
1702
|
failedCount += 1;
|
|
@@ -1643,6 +1873,7 @@ var CLI = class {
|
|
|
1643
1873
|
if (!isVerb(command)) this.#usage(didYouMean(command), json);
|
|
1644
1874
|
if (command !== "catalog" && values.from !== void 0 && values.from.length > 1) this.#usage(`--from may be provided only once for '${command}'`, json);
|
|
1645
1875
|
if (command === "mirror" && values.deps !== void 0) this.#usage("--deps is not supported for 'mirror'; use 'pull --deps'", json);
|
|
1876
|
+
if (command === "fleet" && values.target !== void 0) this.#usage("--target is not supported for 'fleet'; change into the fleet root", json);
|
|
1646
1877
|
if (values.help) {
|
|
1647
1878
|
process.stdout.write(`${verbHelp(command)}\n`);
|
|
1648
1879
|
process.exitCode = 0;
|