@orkestrel/scaffold 0.0.20 → 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 +385 -148
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +61 -42
- package/dist/host/CLAUDE.md +39 -387
- 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 +4 -4
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +12 -0
- 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 -87
- 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 +3 -3
- 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 +17 -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 +71 -14
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +18 -2
- 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 +15 -2
- 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 +4 -2
- package/dist/host/codex/agents/scout.toml +2 -2
- package/dist/host/codex/config.toml +23 -64
- package/dist/host/cursor/rules/orchestration.mdc +33 -0
- package/dist/host/guides/src/scaffold.md +419 -150
- package/dist/host/manifest.json +47 -1
- package/dist/host/tests/setupPolicy.ts +130 -12
- package/dist/src/core/index.cjs +2059 -913
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -47
- package/dist/src/core/index.d.ts +202 -47
- package/dist/src/core/index.js +2050 -914
- 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 +7 -5
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,47 +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 === "
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
if (finding.drift === "foreign") {
|
|
230
|
+
foreign += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (finding.drift === "stale") {
|
|
234
|
+
if (owned) ownedDrifted += 1;
|
|
235
|
+
else generatedDrifted += 1;
|
|
236
|
+
} else if (finding.drift === "missing") {
|
|
237
|
+
if (owned) ownedMissing += 1;
|
|
238
|
+
else generatedMissing += 1;
|
|
239
|
+
}
|
|
235
240
|
}
|
|
236
241
|
return {
|
|
237
242
|
owned: {
|
|
238
243
|
drifted: ownedDrifted,
|
|
239
244
|
missing: ownedMissing,
|
|
240
|
-
foreign:
|
|
245
|
+
foreign: 0
|
|
241
246
|
},
|
|
242
247
|
generated: {
|
|
243
248
|
drifted: generatedDrifted,
|
|
244
249
|
missing: generatedMissing,
|
|
245
|
-
foreign:
|
|
250
|
+
foreign: 0
|
|
251
|
+
},
|
|
252
|
+
foreign: {
|
|
253
|
+
drifted: 0,
|
|
254
|
+
missing: 0,
|
|
255
|
+
foreign
|
|
246
256
|
}
|
|
247
257
|
};
|
|
248
258
|
}
|
|
249
259
|
/** Create one deterministic fleet result entry. */
|
|
250
|
-
function fleetEntryOf(name, counts, failed) {
|
|
260
|
+
function fleetEntryOf(name, counts, failed, outside = 0) {
|
|
251
261
|
return {
|
|
252
262
|
name,
|
|
253
263
|
drifted: counts?.drifted ?? 0,
|
|
254
264
|
missing: counts?.missing ?? 0,
|
|
255
265
|
foreign: counts?.foreign ?? 0,
|
|
256
|
-
failed
|
|
266
|
+
failed,
|
|
267
|
+
outside
|
|
257
268
|
};
|
|
258
269
|
}
|
|
259
270
|
/** Create the command-line interface's machine-readable failure envelope. */
|
|
@@ -275,7 +286,14 @@ function summaryToNewResult(summary, applied) {
|
|
|
275
286
|
applied
|
|
276
287
|
};
|
|
277
288
|
}
|
|
278
|
-
/** 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. */
|
|
279
297
|
function auditToRepairResult(audit, result) {
|
|
280
298
|
return {
|
|
281
299
|
...audit,
|
|
@@ -302,9 +320,9 @@ function countPart(count, label) {
|
|
|
302
320
|
/** Render nonzero audit buckets or `clean`. */
|
|
303
321
|
function bucketText(counts) {
|
|
304
322
|
const parts = [];
|
|
305
|
-
if (counts.drifted > 0) parts.push(
|
|
306
|
-
if (counts.missing > 0) parts.push(
|
|
307
|
-
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`);
|
|
308
326
|
return parts.length > 0 ? parts.join(", ") : "clean";
|
|
309
327
|
}
|
|
310
328
|
/** Render audit's origin-aware verdict. */
|
|
@@ -312,7 +330,12 @@ function auditVerdict(audit, plan) {
|
|
|
312
330
|
const count = audit.findings.length;
|
|
313
331
|
if (audit.clean) return `audit: ${countPart(count, "artifact")} — clean`;
|
|
314
332
|
const split = partitionFindings(audit.findings, plan);
|
|
315
|
-
|
|
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}`;
|
|
316
339
|
}
|
|
317
340
|
/** Render non-aligned audit findings as terminal table rows. */
|
|
318
341
|
function findingRows(findings, plan) {
|
|
@@ -339,6 +362,43 @@ function auditTable(audit, plan) {
|
|
|
339
362
|
};
|
|
340
363
|
}
|
|
341
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
|
+
/**
|
|
342
402
|
* Render drift outside repair's selected ownership boundary.
|
|
343
403
|
*
|
|
344
404
|
* @param count - The number of findings outside the selected scope.
|
|
@@ -347,24 +407,84 @@ function auditTable(audit, plan) {
|
|
|
347
407
|
*/
|
|
348
408
|
function scopeNote(count, generated) {
|
|
349
409
|
if (count === 0) return void 0;
|
|
350
|
-
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`;
|
|
351
411
|
}
|
|
352
412
|
/**
|
|
353
413
|
* Render repair's dry-run verdict.
|
|
354
414
|
*
|
|
355
415
|
* @param audit - The audit over the selected repair plan.
|
|
356
|
-
* @param
|
|
416
|
+
* @param options - The selected scope and write authorizations.
|
|
357
417
|
* @returns The scope-aware clean or drifted verdict.
|
|
358
418
|
*/
|
|
359
|
-
function repairVerdict(audit,
|
|
360
|
-
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";
|
|
361
421
|
if (audit.clean) return `repair: ${countPart(audit.findings.length, `${scope} artifact`)} aligned — nothing to write`;
|
|
362
|
-
|
|
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`;
|
|
363
426
|
}
|
|
364
|
-
/**
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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}`;
|
|
368
488
|
}
|
|
369
489
|
/** Render synchronization freshness as table rows. */
|
|
370
490
|
function syncRows(report) {
|
|
@@ -413,7 +533,7 @@ function fleetRepoLine(name, outcome) {
|
|
|
413
533
|
}
|
|
414
534
|
/** Render fleet's repository totals. */
|
|
415
535
|
function fleetTotals(drifted, failed) {
|
|
416
|
-
return `total: ${countPart(drifted, "
|
|
536
|
+
return `total: ${countPart(drifted, "dirty repo")}, ${failed} failed`;
|
|
417
537
|
}
|
|
418
538
|
/** Create catalog's terminal table. */
|
|
419
539
|
function catalogTable(entries) {
|
|
@@ -445,13 +565,25 @@ function newPlanTable(summary) {
|
|
|
445
565
|
]
|
|
446
566
|
};
|
|
447
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
|
+
}
|
|
448
580
|
/** Render `new`'s dry-run destination. */
|
|
449
|
-
function newPlanPreview(
|
|
450
|
-
return `will write into
|
|
581
|
+
function newPlanPreview(destination) {
|
|
582
|
+
return `will write into ${destination}`;
|
|
451
583
|
}
|
|
452
584
|
/** Render `new`'s write result. */
|
|
453
|
-
function newApplySuccess(count,
|
|
454
|
-
return `wrote ${countPart(count, "file")} into
|
|
585
|
+
function newApplySuccess(count, destination) {
|
|
586
|
+
return `wrote ${countPart(count, "file")} into ${destination}`;
|
|
455
587
|
}
|
|
456
588
|
/** Render catalog's write result. */
|
|
457
589
|
function catalogApplySuccess(path) {
|
|
@@ -466,12 +598,21 @@ function applyConfirmMessage(files, repos) {
|
|
|
466
598
|
function pruneConfirmMessage(count) {
|
|
467
599
|
return `Also delete ${countPart(count, "unexpected file")} under .claude/agents, .codex/agents, and scripts? `;
|
|
468
600
|
}
|
|
469
|
-
/**
|
|
470
|
-
|
|
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) {
|
|
471
611
|
const parts = [];
|
|
472
|
-
if (
|
|
473
|
-
if (
|
|
474
|
-
|
|
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? `;
|
|
475
616
|
}
|
|
476
617
|
/** Render one unresolved Orkestrel dependency token. */
|
|
477
618
|
function unknownOrkestrelToken(token, suggestion) {
|
|
@@ -565,9 +706,23 @@ function invalidName(name, pattern) {
|
|
|
565
706
|
function unresolvedVersion(names) {
|
|
566
707
|
return `could not resolve the latest version for ${names.map((name) => `"${name}"`).join(", ")} — check the name or pass name@range`;
|
|
567
708
|
}
|
|
568
|
-
/**
|
|
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
|
+
*/
|
|
569
715
|
function generatedNote(count) {
|
|
570
|
-
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`;
|
|
571
726
|
}
|
|
572
727
|
/**
|
|
573
728
|
* Render honest repair guidance for computed and protected manifest drift.
|
|
@@ -587,16 +742,19 @@ function renderComputedNotes(findings, plan) {
|
|
|
587
742
|
}
|
|
588
743
|
const notes = [];
|
|
589
744
|
if (computed > 0) notes.push(generatedNote(computed));
|
|
590
|
-
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`);
|
|
591
747
|
return notes;
|
|
592
748
|
}
|
|
593
749
|
/** Render live dependency freshness tallies. */
|
|
594
750
|
function auditLiveNote(current, behind, failed) {
|
|
595
|
-
return `live: ${
|
|
751
|
+
return `live: ${current} current, ${behind} behind, ${failed} failed`;
|
|
596
752
|
}
|
|
597
753
|
/** Render whether an audit compared content or presence. */
|
|
598
|
-
function comparisonLine(
|
|
599
|
-
|
|
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")}`;
|
|
600
758
|
}
|
|
601
759
|
/** Render catalog's final verdict. */
|
|
602
760
|
function catalogVerdict(clean) {
|
|
@@ -660,6 +818,10 @@ function parseArguments(argv) {
|
|
|
660
818
|
type: "boolean",
|
|
661
819
|
default: false
|
|
662
820
|
},
|
|
821
|
+
replace: {
|
|
822
|
+
type: "boolean",
|
|
823
|
+
default: false
|
|
824
|
+
},
|
|
663
825
|
strict: {
|
|
664
826
|
type: "boolean",
|
|
665
827
|
default: false
|
|
@@ -822,8 +984,8 @@ var CLI = class {
|
|
|
822
984
|
compiler.destroy();
|
|
823
985
|
}
|
|
824
986
|
}
|
|
825
|
-
#prunePaths(target, host,
|
|
826
|
-
const seam =
|
|
987
|
+
#prunePaths(target, host, services) {
|
|
988
|
+
const seam = services.length > 0 ? SERVICE_SCRIPT_PATH : void 0;
|
|
827
989
|
return pruneTargets(target, host).filter((path) => path !== seam);
|
|
828
990
|
}
|
|
829
991
|
/**
|
|
@@ -836,8 +998,8 @@ var CLI = class {
|
|
|
836
998
|
* unclean, so an "unexpected file" is honestly counted as drift (exit 1)
|
|
837
999
|
* instead of the structurally-always-zero `diffPlan.foreign`.
|
|
838
1000
|
*/
|
|
839
|
-
#scan(audit, target, host,
|
|
840
|
-
const paths = this.#prunePaths(target, host,
|
|
1001
|
+
#scan(audit, target, host, services) {
|
|
1002
|
+
const paths = this.#prunePaths(target, host, services);
|
|
841
1003
|
if (paths.length === 0) return audit;
|
|
842
1004
|
const findings = paths.map((path) => ({
|
|
843
1005
|
path,
|
|
@@ -856,8 +1018,8 @@ var CLI = class {
|
|
|
856
1018
|
* When fail-closed allowlist discovery raises a coded `TARGET` failure, retain
|
|
857
1019
|
* the existing findings and mark the audit incomplete instead of crashing.
|
|
858
1020
|
*/
|
|
859
|
-
#scanSafe(audit, target, host,
|
|
860
|
-
const scanned = attempt(() => this.#scan(audit, target, host,
|
|
1021
|
+
#scanSafe(audit, target, host, services) {
|
|
1022
|
+
const scanned = attempt(() => this.#scan(audit, target, host, services));
|
|
861
1023
|
if (scanned.success) return {
|
|
862
1024
|
audit: scanned.value,
|
|
863
1025
|
skipped: false
|
|
@@ -882,21 +1044,63 @@ var CLI = class {
|
|
|
882
1044
|
* caller compiles the full plan anyway. Diff it too so a clean scoped verdict can point at drift
|
|
883
1045
|
* outside the selected boundary. The count feeds the shared `scopeNote` renderer.
|
|
884
1046
|
*/
|
|
885
|
-
#outside(compiled, target) {
|
|
886
|
-
const full =
|
|
887
|
-
|
|
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;
|
|
888
1051
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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,
|
|
894
1093
|
blueprint: {
|
|
895
|
-
...
|
|
1094
|
+
...promoted.blueprint,
|
|
896
1095
|
overrides: []
|
|
897
1096
|
},
|
|
898
|
-
artifacts
|
|
1097
|
+
artifacts
|
|
899
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);
|
|
900
1104
|
let plan;
|
|
901
1105
|
try {
|
|
902
1106
|
plan = hydratePlan(scoped, host);
|
|
@@ -928,14 +1132,12 @@ var CLI = class {
|
|
|
928
1132
|
}
|
|
929
1133
|
/**
|
|
930
1134
|
* The shared write-confirmation gate every verb calls before it touches disk.
|
|
931
|
-
* `--apply`
|
|
932
|
-
*
|
|
933
|
-
* 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.
|
|
934
1137
|
*/
|
|
935
1138
|
async #apply(terminal, message, values, json) {
|
|
936
|
-
if (values.apply) return
|
|
937
|
-
if (json) return
|
|
938
|
-
if (values.yes) return true;
|
|
1139
|
+
if (!values.apply) return false;
|
|
1140
|
+
if (json || values.yes || !this.#tty) return true;
|
|
939
1141
|
return this.#guard(terminal.confirm({
|
|
940
1142
|
message,
|
|
941
1143
|
default: false
|
|
@@ -943,18 +1145,16 @@ var CLI = class {
|
|
|
943
1145
|
}
|
|
944
1146
|
/**
|
|
945
1147
|
* The SECOND, separate confirm for `--prune`-eligible deletions — never
|
|
946
|
-
* bundled into
|
|
947
|
-
*
|
|
1148
|
+
* bundled into the write question. Both `--prune` and `--apply` are required;
|
|
1149
|
+
* `--yes` only skips this confirmation and never authorizes deletion.
|
|
948
1150
|
*/
|
|
949
1151
|
async #prune(terminal, message, values, json) {
|
|
950
1152
|
if (!values.prune) return false;
|
|
951
|
-
if (values.apply)
|
|
952
|
-
|
|
953
|
-
if (values.yes) return true;
|
|
954
|
-
if (!this.#tty) {
|
|
955
|
-
this.#reporter.line(PRUNE_SKIPPED);
|
|
1153
|
+
if (!values.apply) {
|
|
1154
|
+
if (!json && !this.#tty) this.#reporter.line(PRUNE_SKIPPED);
|
|
956
1155
|
return false;
|
|
957
1156
|
}
|
|
1157
|
+
if (json || values.yes || !this.#tty) return true;
|
|
958
1158
|
return this.#guard(terminal.confirm({
|
|
959
1159
|
message,
|
|
960
1160
|
default: false
|
|
@@ -983,13 +1183,13 @@ var CLI = class {
|
|
|
983
1183
|
* Best-effort vendored `@orkestrel` catalog names, resolved via `host`
|
|
984
1184
|
* (`hostRoot()`, or the active `--from` override) through the host manifest
|
|
985
1185
|
* — `undefined` when the catalog cannot be established (a missing/unreadable
|
|
986
|
-
* manifest, no
|
|
1186
|
+
* manifest, no `CATALOG_AGENT_PATH` entry, or any other failure),
|
|
987
1187
|
* degrading the dependency prompt to shape-only validation instead of blocking on it.
|
|
988
1188
|
*/
|
|
989
1189
|
#names(host) {
|
|
990
1190
|
const names = attempt(() => {
|
|
991
1191
|
const manifest = readHostManifest(host);
|
|
992
|
-
const full = locateHostSource(manifest,
|
|
1192
|
+
const full = locateHostSource(manifest, CATALOG_AGENT_PATH, host);
|
|
993
1193
|
if (full === void 0 || !existsSync(full)) return void 0;
|
|
994
1194
|
const relative$1 = relative(host, full).replaceAll("\\", "/");
|
|
995
1195
|
return catalogNames(readFileText(host, relative$1, "TARGET", "host"));
|
|
@@ -1086,10 +1286,11 @@ var CLI = class {
|
|
|
1086
1286
|
dependencies: deps
|
|
1087
1287
|
}), json);
|
|
1088
1288
|
const summary = planToSummary(plan);
|
|
1289
|
+
const label = describeDestination(process.cwd(), destination);
|
|
1089
1290
|
if (!json) {
|
|
1090
1291
|
this.#reporter.section("Plan");
|
|
1091
1292
|
this.#reporter.table(newPlanTable(summary));
|
|
1092
|
-
this.#reporter.line(newPlanPreview(
|
|
1293
|
+
this.#reporter.line(newPlanPreview(label));
|
|
1093
1294
|
}
|
|
1094
1295
|
if (!await this.#apply(terminal, applyConfirmMessage(summary.host + summary.template + summary.computed), values, json)) {
|
|
1095
1296
|
if (json) this.#write(summaryToNewResult(summary, false));
|
|
@@ -1105,7 +1306,7 @@ var CLI = class {
|
|
|
1105
1306
|
const result = materializer.materialize(plan, destination);
|
|
1106
1307
|
const count = result.written.length + result.copied.length;
|
|
1107
1308
|
if (json) this.#write(summaryToNewResult(summary, true));
|
|
1108
|
-
else this.#succeed(spinner, json, newApplySuccess(count,
|
|
1309
|
+
else this.#succeed(spinner, json, newApplySuccess(count, label));
|
|
1109
1310
|
} catch (error) {
|
|
1110
1311
|
this.#reject(spinner, json, error);
|
|
1111
1312
|
} finally {
|
|
@@ -1173,7 +1374,7 @@ var CLI = class {
|
|
|
1173
1374
|
this.#reject(spinner, json, error);
|
|
1174
1375
|
}
|
|
1175
1376
|
} else if (json) this.#write(report);
|
|
1176
|
-
process.exitCode = report.
|
|
1377
|
+
process.exitCode = report.failed > 0 ? 1 : report.clean || proceed ? 0 : 1;
|
|
1177
1378
|
}
|
|
1178
1379
|
/** `scaffold audit` — whole-plan conformance report; offers a repair handoff on drift. */
|
|
1179
1380
|
async #audit(values, json) {
|
|
@@ -1200,7 +1401,7 @@ var CLI = class {
|
|
|
1200
1401
|
const host = values.from?.[0] ?? hostRoot();
|
|
1201
1402
|
let plan;
|
|
1202
1403
|
try {
|
|
1203
|
-
plan = hydratePlan(compiled, host);
|
|
1404
|
+
plan = hydratePlan(this.#promoteServices(compiled, target), host);
|
|
1204
1405
|
} catch (error) {
|
|
1205
1406
|
this.#error(error, json);
|
|
1206
1407
|
}
|
|
@@ -1209,7 +1410,7 @@ var CLI = class {
|
|
|
1209
1410
|
...diffPlan(plan, readTarget(target, artifactPaths)),
|
|
1210
1411
|
questions
|
|
1211
1412
|
};
|
|
1212
|
-
const scanned = this.#scanSafe(rawAudit, target, host, spec.
|
|
1413
|
+
const scanned = this.#scanSafe(rawAudit, target, host, spec.services);
|
|
1213
1414
|
const audit = scanned.audit;
|
|
1214
1415
|
let drifted = !audit.clean;
|
|
1215
1416
|
let live;
|
|
@@ -1243,19 +1444,22 @@ var CLI = class {
|
|
|
1243
1444
|
}
|
|
1244
1445
|
if (scanned.skipped) this.#reporter.line(SCAN_SKIPPED);
|
|
1245
1446
|
for (const question of audit.questions) if (!question.blocking) this.#reporter.line(`warning: ${question.text}`);
|
|
1246
|
-
|
|
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));
|
|
1247
1450
|
this.#reporter.table(auditTable(audit, plan));
|
|
1248
1451
|
this.#reporter.line(auditVerdict(audit, plan));
|
|
1249
1452
|
if (live !== void 0) this.#reporter.line(auditLiveNote(live.current, live.behind, live.failed));
|
|
1250
1453
|
if (!audit.clean) {
|
|
1251
1454
|
const split = partitionFindings(audit.findings, plan);
|
|
1252
|
-
const
|
|
1455
|
+
const replace = values.replace === true;
|
|
1456
|
+
const ownedCount = split.owned.missing + (replace ? split.owned.drifted : 0);
|
|
1253
1457
|
const pruneRequested = values.prune === true;
|
|
1254
1458
|
const offerHandoff = this.#tty && (ownedCount > 0 || audit.foreign > 0 && pruneRequested);
|
|
1255
1459
|
let handoffAccepted = false;
|
|
1256
1460
|
if (offerHandoff) {
|
|
1257
1461
|
const terminal = createTerminal();
|
|
1258
|
-
const message = repairHandoff(
|
|
1462
|
+
const message = repairHandoff(split.owned.missing, replace ? split.owned.drifted : 0, audit.foreign, pruneRequested);
|
|
1259
1463
|
handoffAccepted = await this.#guard(terminal.confirm({
|
|
1260
1464
|
message,
|
|
1261
1465
|
default: false
|
|
@@ -1263,12 +1467,13 @@ var CLI = class {
|
|
|
1263
1467
|
if (handoffAccepted) {
|
|
1264
1468
|
await this.#repair(values, false);
|
|
1265
1469
|
const rawFinal = diffPlan(plan, readTarget(target, artifactPaths));
|
|
1266
|
-
const finalScanned = this.#scanSafe(rawFinal, target, host, spec.
|
|
1470
|
+
const finalScanned = this.#scanSafe(rawFinal, target, host, spec.services);
|
|
1267
1471
|
process.exitCode = finalScanned.audit.clean ? 0 : 1;
|
|
1268
1472
|
return;
|
|
1269
1473
|
}
|
|
1270
1474
|
}
|
|
1271
1475
|
if (!handoffAccepted) {
|
|
1476
|
+
if (split.owned.drifted > 0 && !replace) this.#reporter.line(replacementNote(split.owned.drifted));
|
|
1272
1477
|
if (audit.foreign > 0 && !pruneRequested) this.#reporter.line(FOREIGN_HINT);
|
|
1273
1478
|
for (const note of renderComputedNotes(audit.findings, plan)) this.#reporter.line(note);
|
|
1274
1479
|
}
|
|
@@ -1285,39 +1490,56 @@ var CLI = class {
|
|
|
1285
1490
|
this.#error(error, json);
|
|
1286
1491
|
}
|
|
1287
1492
|
const generated = values.generated === true;
|
|
1493
|
+
const replace = values.replace === true;
|
|
1288
1494
|
const host = values.from?.[0] ?? hostRoot();
|
|
1289
1495
|
const prepared = this.#prepareRepair(spec, target, host, generated, json);
|
|
1290
|
-
|
|
1496
|
+
let compiled = prepared[0];
|
|
1291
1497
|
let plan = prepared[1];
|
|
1292
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
|
+
};
|
|
1293
1505
|
if (!json) {
|
|
1294
|
-
this.#reporter.line(generated
|
|
1506
|
+
if (!outcome.clean) this.#reporter.line(scopeLine("repair", generated));
|
|
1295
1507
|
this.#reporter.section("Audit");
|
|
1296
|
-
this.#reporter.table(auditTable(
|
|
1508
|
+
this.#reporter.table(auditTable(outcome, plan));
|
|
1297
1509
|
}
|
|
1298
|
-
const prunePaths = values.prune ? this.#prunePaths(target, host, spec.
|
|
1510
|
+
const prunePaths = values.prune ? this.#prunePaths(target, host, spec.services) : [];
|
|
1299
1511
|
const pruneSnapshot = readTarget(target, prunePaths);
|
|
1300
|
-
if (
|
|
1301
|
-
if (json) this.#
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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;
|
|
1308
1529
|
return;
|
|
1309
1530
|
}
|
|
1310
|
-
if (!json) this.#reporter.line(repairVerdict(audit, generated));
|
|
1311
1531
|
const terminal = createTerminal();
|
|
1312
1532
|
let proceed = true;
|
|
1313
|
-
if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(
|
|
1533
|
+
if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(repairable), values, json);
|
|
1314
1534
|
if (!proceed) {
|
|
1315
|
-
|
|
1535
|
+
this.#emitRepair(outcome, generated, json);
|
|
1316
1536
|
process.exitCode = 1;
|
|
1317
1537
|
return;
|
|
1318
1538
|
}
|
|
1319
|
-
if (values.prune && !json)
|
|
1320
|
-
|
|
1539
|
+
if (values.prune && !json) {
|
|
1540
|
+
if (prunePaths.length === 0) this.#reporter.line(PRUNE_EMPTY);
|
|
1541
|
+
else for (const line of prunePreview(prunePaths)) this.#reporter.line(line);
|
|
1542
|
+
}
|
|
1321
1543
|
const doPrune = prunePaths.length > 0 && await this.#prune(terminal, pruneConfirmMessage(prunePaths.length), values, json);
|
|
1322
1544
|
let currentGlobal;
|
|
1323
1545
|
try {
|
|
@@ -1331,31 +1553,41 @@ var CLI = class {
|
|
|
1331
1553
|
global: currentGlobal
|
|
1332
1554
|
};
|
|
1333
1555
|
const refreshed = this.#prepareRepair(spec, target, host, generated, json);
|
|
1556
|
+
compiled = refreshed[0];
|
|
1334
1557
|
plan = refreshed[1];
|
|
1335
1558
|
audit = refreshed[2];
|
|
1559
|
+
outcome = repairAuditOf(this.#scanSafe(audit, target, host, spec.services).audit, this.#outside(compiled, plan, target, host));
|
|
1336
1560
|
}
|
|
1337
1561
|
const spinner = this.#spinner("repairing", json);
|
|
1338
1562
|
spinner?.start();
|
|
1339
1563
|
const materializer = createMaterializer({ host });
|
|
1340
1564
|
try {
|
|
1341
|
-
const result = materializer.repair(plan, audit, target);
|
|
1565
|
+
const result = materializer.repair(plan, audit, target, replace);
|
|
1342
1566
|
const removed = doPrune ? materializer.prune(target, pruneSnapshot).removed : [];
|
|
1343
|
-
|
|
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, {
|
|
1344
1576
|
...result,
|
|
1345
1577
|
removed
|
|
1346
|
-
})
|
|
1347
|
-
|
|
1578
|
+
});
|
|
1579
|
+
process.exitCode = hasFindings(finalAudit, finalAudit.outside) ? 1 : 0;
|
|
1348
1580
|
} catch (error) {
|
|
1349
1581
|
this.#reject(spinner, json, error);
|
|
1350
1582
|
} finally {
|
|
1351
1583
|
materializer.destroy();
|
|
1352
1584
|
}
|
|
1353
|
-
process.exitCode = 0;
|
|
1354
1585
|
}
|
|
1355
1586
|
/** `scaffold fleet` — audit/repair every `@orkestrel` package beneath the current directory's immediate children. */
|
|
1356
1587
|
async #fleet(values, json) {
|
|
1357
1588
|
const root = this.#contain(".", json);
|
|
1358
1589
|
const generated = values.generated === true;
|
|
1590
|
+
const replace = values.replace === true;
|
|
1359
1591
|
const packages = discoverPackages(root);
|
|
1360
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);
|
|
1361
1593
|
const host = values.from?.[0] ?? hostRoot();
|
|
@@ -1365,6 +1597,7 @@ var CLI = class {
|
|
|
1365
1597
|
const name = basename(directory);
|
|
1366
1598
|
try {
|
|
1367
1599
|
const compiler = createCompiler();
|
|
1600
|
+
let compiled;
|
|
1368
1601
|
let scoped;
|
|
1369
1602
|
let questions;
|
|
1370
1603
|
try {
|
|
@@ -1374,14 +1607,8 @@ var CLI = class {
|
|
|
1374
1607
|
const message = scaffolding.questions.map((question) => question.text).join("; ");
|
|
1375
1608
|
throw new ScaffoldError("INVALID", message);
|
|
1376
1609
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
blueprint: {
|
|
1380
|
-
...scaffolding.plan.blueprint,
|
|
1381
|
-
overrides: []
|
|
1382
|
-
},
|
|
1383
|
-
artifacts: scaffolding.plan.artifacts.filter((artifact) => artifact.origin === "host" || generated && artifact.origin === "computed" && artifact.path !== "package.json")
|
|
1384
|
-
};
|
|
1610
|
+
compiled = scaffolding.plan;
|
|
1611
|
+
scoped = this.#scopeRepair(compiled, directory, generated);
|
|
1385
1612
|
questions = scaffolding.questions;
|
|
1386
1613
|
} finally {
|
|
1387
1614
|
compiler.destroy();
|
|
@@ -1392,12 +1619,15 @@ var CLI = class {
|
|
|
1392
1619
|
...diffPlan(plan, readTarget(directory, paths)),
|
|
1393
1620
|
questions
|
|
1394
1621
|
};
|
|
1395
|
-
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);
|
|
1396
1624
|
repos.push({
|
|
1397
1625
|
name,
|
|
1398
1626
|
directory,
|
|
1399
1627
|
plan,
|
|
1400
|
-
|
|
1628
|
+
rawAudit,
|
|
1629
|
+
audit,
|
|
1630
|
+
outside
|
|
1401
1631
|
});
|
|
1402
1632
|
} catch (error) {
|
|
1403
1633
|
failures.push({
|
|
@@ -1415,6 +1645,8 @@ var CLI = class {
|
|
|
1415
1645
|
foreign: repo.audit.foreign
|
|
1416
1646
|
}));
|
|
1417
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}`);
|
|
1418
1650
|
}
|
|
1419
1651
|
for (const failure of failures) this.#reporter.line(fleetRepoLine(failure.name, {
|
|
1420
1652
|
state: "failed",
|
|
@@ -1422,45 +1654,49 @@ var CLI = class {
|
|
|
1422
1654
|
}));
|
|
1423
1655
|
}
|
|
1424
1656
|
const dirty = repos.filter((repo) => !repo.audit.clean);
|
|
1657
|
+
const reportedDirty = repos.filter((repo) => hasFindings(repo.audit, repo.outside));
|
|
1425
1658
|
if (dirty.length === 0) {
|
|
1426
|
-
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1427
|
-
else this.#reporter.line(fleetTotals(
|
|
1428
|
-
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;
|
|
1429
1662
|
return;
|
|
1430
1663
|
}
|
|
1431
|
-
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));
|
|
1432
1666
|
const terminal = createTerminal();
|
|
1433
1667
|
const proceed = await this.#apply(terminal, applyConfirmMessage(fileCount, dirty.length), values, json);
|
|
1434
1668
|
const pruneSets = proceed && values.prune ? new Map(dirty.map((repo) => {
|
|
1435
|
-
const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.
|
|
1669
|
+
const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.services);
|
|
1436
1670
|
return [repo.name, readTarget(repo.directory, paths)];
|
|
1437
1671
|
})) : /* @__PURE__ */ new Map();
|
|
1438
1672
|
const prunePaths = dirty.flatMap((repo) => Object.keys(pruneSets.get(repo.name) ?? {}).map((path) => `${repo.name}/${path}`));
|
|
1439
|
-
if (proceed && values.prune && !json)
|
|
1440
|
-
|
|
1673
|
+
if (proceed && values.prune && !json) {
|
|
1674
|
+
if (prunePaths.length === 0) this.#reporter.line(PRUNE_EMPTY);
|
|
1675
|
+
else for (const line of prunePreview(prunePaths)) this.#reporter.line(line);
|
|
1676
|
+
}
|
|
1441
1677
|
const doPrune = proceed && prunePaths.length > 0 && await this.#prune(terminal, pruneConfirmMessage(prunePaths.length), values, json);
|
|
1442
1678
|
if (!proceed) {
|
|
1443
|
-
if (json) this.#write([...repos.map((repo) => fleetEntryOf(repo.name, repo.audit, false)), ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1444
|
-
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));
|
|
1445
1681
|
process.exitCode = 1;
|
|
1446
1682
|
return;
|
|
1447
1683
|
}
|
|
1448
1684
|
const materializer = createMaterializer({ host });
|
|
1449
|
-
let drifted = 0;
|
|
1685
|
+
let drifted = repos.filter((repo) => repo.audit.clean && repo.outside > 0).length;
|
|
1450
1686
|
let failedCount = failures.length;
|
|
1451
|
-
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));
|
|
1452
1688
|
try {
|
|
1453
1689
|
for (const repo of dirty) try {
|
|
1454
|
-
materializer.repair(repo.plan, repo.
|
|
1690
|
+
materializer.repair(repo.plan, repo.rawAudit, repo.directory, replace);
|
|
1455
1691
|
if (doPrune) materializer.prune(repo.directory, pruneSets.get(repo.name) ?? {});
|
|
1456
1692
|
const paths = repo.plan.artifacts.map((artifact) => artifact.path);
|
|
1457
1693
|
const rawFinal = diffPlan(repo.plan, readTarget(repo.directory, paths));
|
|
1458
|
-
const finalAudit = this.#scan(rawFinal, repo.directory, host, repo.plan.blueprint.
|
|
1459
|
-
if (
|
|
1460
|
-
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));
|
|
1461
1697
|
if (!json) this.#reporter.line(fleetRepoLine(repo.name, {
|
|
1462
1698
|
state: "repaired",
|
|
1463
|
-
remaining: finalAudit.drifted + finalAudit.missing + finalAudit.foreign
|
|
1699
|
+
remaining: finalAudit.drifted + finalAudit.missing + finalAudit.foreign + repo.outside
|
|
1464
1700
|
}));
|
|
1465
1701
|
} catch (error) {
|
|
1466
1702
|
failedCount += 1;
|
|
@@ -1637,6 +1873,7 @@ var CLI = class {
|
|
|
1637
1873
|
if (!isVerb(command)) this.#usage(didYouMean(command), json);
|
|
1638
1874
|
if (command !== "catalog" && values.from !== void 0 && values.from.length > 1) this.#usage(`--from may be provided only once for '${command}'`, json);
|
|
1639
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);
|
|
1640
1877
|
if (values.help) {
|
|
1641
1878
|
process.stdout.write(`${verbHelp(command)}\n`);
|
|
1642
1879
|
process.exitCode = 0;
|