@orkestrel/scaffold 0.0.22 → 0.0.24
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/README.md +84 -99
- package/dist/bin/main.js +1094 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/host/CLAUDE.md +3 -1
- package/dist/host/agents/orchestration.md +61 -4
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +9 -9
- package/dist/host/claude/rules/architecture.md +45 -3
- package/dist/host/claude/rules/quality.md +4 -0
- package/dist/host/claude/rules/tests.md +57 -1
- package/dist/host/claude/rules/workspace.md +50 -17
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/configs/helpers.ts +762 -0
- package/dist/host/dotfiles/oxlintrc.json +2 -1
- package/dist/host/guides/scaffold.md +862 -0
- package/dist/host/manifest.json +40 -33
- package/dist/host/tests/config.test.ts +544 -0
- package/dist/host/tests/policy.test.ts +46 -0
- package/dist/host/tests/setupPolicy.ts +557 -602
- package/dist/src/core/index.cjs +3569 -10510
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2361 -2789
- package/dist/src/core/index.d.ts +2361 -2789
- package/dist/src/core/index.js +3513 -10374
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2855 -3765
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1920 -1335
- package/dist/src/server/index.d.ts +1920 -1335
- package/dist/src/server/index.js +2812 -3680
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -23
- package/dist/bin/scaffold.js +0 -1896
- package/dist/bin/scaffold.js.map +0 -1
- package/dist/host/guides/src/scaffold.md +0 -2886
- /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
package/dist/bin/scaffold.js
DELETED
|
@@ -1,1896 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
4
|
-
import * as tls from "node:tls";
|
|
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
|
-
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
|
-
import { createReporter, createSpinner, createStyler } from "@orkestrel/console";
|
|
8
|
-
import { createServerSink } from "@orkestrel/console/server";
|
|
9
|
-
import { attempt, isRecord, parseJSON } from "@orkestrel/contract";
|
|
10
|
-
import { isTerminalError } from "@orkestrel/terminal";
|
|
11
|
-
import { createTerminal } from "@orkestrel/terminal/server";
|
|
12
|
-
import { parseArgs } from "node:util";
|
|
13
|
-
//#region src/bin/constants.ts
|
|
14
|
-
/** The command-line interface's closed command vocabulary. */
|
|
15
|
-
var KNOWN_VERBS = Object.freeze([
|
|
16
|
-
"new",
|
|
17
|
-
"pull",
|
|
18
|
-
"mirror",
|
|
19
|
-
"audit",
|
|
20
|
-
"repair",
|
|
21
|
-
"fleet",
|
|
22
|
-
"catalog"
|
|
23
|
-
]);
|
|
24
|
-
/** Internal artifact origins translated into user-facing labels. */
|
|
25
|
-
var ORIGIN_LABEL = Object.freeze({
|
|
26
|
-
host: "host-owned",
|
|
27
|
-
template: "starter",
|
|
28
|
-
computed: "generated"
|
|
29
|
-
});
|
|
30
|
-
/** Internal drift states translated into user-facing labels. */
|
|
31
|
-
var DRIFT_LABEL = Object.freeze({
|
|
32
|
-
aligned: "unchanged",
|
|
33
|
-
stale: "drifted",
|
|
34
|
-
missing: "missing",
|
|
35
|
-
foreign: "unexpected file"
|
|
36
|
-
});
|
|
37
|
-
/** Dependency freshness states translated into user-facing labels. */
|
|
38
|
-
var FRESHNESS_LABEL = Object.freeze({
|
|
39
|
-
current: "unchanged",
|
|
40
|
-
behind: "behind",
|
|
41
|
-
missing: "missing upstream",
|
|
42
|
-
failed: "fetch failed"
|
|
43
|
-
});
|
|
44
|
-
/** Materializer actions translated into user-facing labels. */
|
|
45
|
-
var ACTION_LABEL = Object.freeze({
|
|
46
|
-
written: "wrote",
|
|
47
|
-
copied: "wrote",
|
|
48
|
-
skipped: "unchanged",
|
|
49
|
-
removed: "removed"
|
|
50
|
-
});
|
|
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
|
-
/** The dry-run note for `new`. */
|
|
56
|
-
var NEW_DRY_RUN_NOTE = "dry run — pass --apply to write";
|
|
57
|
-
/** The fallback message for a malformed command line without an error message. */
|
|
58
|
-
var INVALID_ARGUMENTS_MESSAGE = "invalid arguments";
|
|
59
|
-
/** Shared prompt-cancellation message. */
|
|
60
|
-
var CANCELLED_MESSAGE = "cancelled — nothing written";
|
|
61
|
-
/** Terminal choices for a new workspace's src and app environments. */
|
|
62
|
-
var ENVIRONMENT_CHOICES = Object.freeze([
|
|
63
|
-
{
|
|
64
|
-
name: "core",
|
|
65
|
-
value: "core",
|
|
66
|
-
description: "the pure engine"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "browser",
|
|
70
|
-
value: "browser",
|
|
71
|
-
description: "DOM-facing environment"
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: "server",
|
|
75
|
-
value: "server",
|
|
76
|
-
description: "node-facing environment"
|
|
77
|
-
}
|
|
78
|
-
]);
|
|
79
|
-
/** The safety model included in full help. */
|
|
80
|
-
var SAFETY_BANNER = [
|
|
81
|
-
"safety: every verb is a dry run by default.",
|
|
82
|
-
"on a terminal, a write prompts for confirmation; in a script, pass --apply (and --yes to skip the confirm).",
|
|
83
|
-
"every write is confined to the current working directory — cd there first.",
|
|
84
|
-
"when Node exposes system-CA controls, TLS adds the OS certificate store; earlier supported Node 22 releases use default roots. NODE_EXTRA_CA_CERTS adds custom PEMs."
|
|
85
|
-
].join("\n");
|
|
86
|
-
/** Stable command exit-code meanings. */
|
|
87
|
-
var EXIT_CODES = Object.freeze([
|
|
88
|
-
["0", "clean / success"],
|
|
89
|
-
["1", "drift or failure"],
|
|
90
|
-
["2", "usage error"]
|
|
91
|
-
]);
|
|
92
|
-
/** One-line command summaries. */
|
|
93
|
-
var VERB_SUMMARY = Object.freeze({
|
|
94
|
-
new: "scaffold a workspace into ./<name>",
|
|
95
|
-
pull: "refresh vendored guides/versions, report drift",
|
|
96
|
-
mirror: "refresh every published Orkestrel package guide",
|
|
97
|
-
audit: "whole-plan conformance report",
|
|
98
|
-
repair: "restore missing host-owned files; replace drifted bytes only with --replace",
|
|
99
|
-
fleet: "audit/repair every workspace under the cwd's immediate children",
|
|
100
|
-
catalog: "regenerate the fleet package-catalog table"
|
|
101
|
-
});
|
|
102
|
-
/** Compact command flag references. */
|
|
103
|
-
var VERB_FLAGS = Object.freeze({
|
|
104
|
-
new: "--src a,b --app a,b --deps x,y --apply --yes --target <path> --from <path>",
|
|
105
|
-
pull: "--target . --deps x,y --apply --yes --strict",
|
|
106
|
-
mirror: "--target . --apply --yes --strict",
|
|
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
|
-
catalog: "--from <path> ... --target <repo> --offline --apply --yes"
|
|
111
|
-
});
|
|
112
|
-
/** Plain-language command flag descriptions. */
|
|
113
|
-
var VERB_FLAG_HELP = Object.freeze({
|
|
114
|
-
new: [
|
|
115
|
-
["--src a,b", "which src environments to include (core, browser, server)"],
|
|
116
|
-
["--app a,b", "which app environments to include (core, browser, server)"],
|
|
117
|
-
["--deps x,y", "@orkestrel/* dependencies to add (installed as dependencies)"],
|
|
118
|
-
["--apply", "write the files (default is a dry run)"],
|
|
119
|
-
["--yes", "skip the confirmation question"],
|
|
120
|
-
["--target <path>", "destination directory (default: ./<name>)"],
|
|
121
|
-
["--from <path>", "read the template from a local path instead of the bundled one"]
|
|
122
|
-
],
|
|
123
|
-
pull: [
|
|
124
|
-
["--target .", "directory to refresh (default: current directory)"],
|
|
125
|
-
["--deps x,y", "limit the refresh to these dependencies"],
|
|
126
|
-
["--apply", "write the refreshed files (default is a dry run)"],
|
|
127
|
-
["--yes", "skip the confirmation question"],
|
|
128
|
-
["--strict", "fail (exit 1) on any drift, even non-fatal"]
|
|
129
|
-
],
|
|
130
|
-
mirror: [
|
|
131
|
-
["--target .", "directory whose guide mirror is refreshed (default: current directory)"],
|
|
132
|
-
["--apply", "write the refreshed guides (default is a dry run)"],
|
|
133
|
-
["--yes", "skip the confirmation question"],
|
|
134
|
-
["--strict", "fail immediately when an upstream guide cannot be fetched"]
|
|
135
|
-
],
|
|
136
|
-
audit: [
|
|
137
|
-
["--target .", "directory to audit (default: current directory)"],
|
|
138
|
-
["--live", "also check upstream freshness over the network"],
|
|
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"],
|
|
141
|
-
["--from <path>", "read the template from a local path instead of the bundled one"],
|
|
142
|
-
["--groups a,b", "limit the audit to these artifact groups"]
|
|
143
|
-
],
|
|
144
|
-
repair: [
|
|
145
|
-
["--target .", "directory to repair (default: current directory)"],
|
|
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"],
|
|
148
|
-
["--apply", "write the fixes (default is a dry run)"],
|
|
149
|
-
["--yes", "skip the confirmation question"],
|
|
150
|
-
["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts"],
|
|
151
|
-
["--from <path>", "read the template from a local path instead of the bundled one"]
|
|
152
|
-
],
|
|
153
|
-
fleet: [
|
|
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"],
|
|
156
|
-
["--apply", "write fixes across every package (default is a dry run)"],
|
|
157
|
-
["--yes", "skip the confirmation question"],
|
|
158
|
-
["--prune", "also DELETE unexpected files under .claude/agents, .codex/agents, and scripts, per package"],
|
|
159
|
-
["--from <path>", "read the template from a local path instead of the bundled one"]
|
|
160
|
-
],
|
|
161
|
-
catalog: [
|
|
162
|
-
["--from <path> ...", "one or more local package paths to include"],
|
|
163
|
-
["--target <repo>", "the repository whose Orkestrel agent catalog table gets updated"],
|
|
164
|
-
["--offline", "skip network lookups (npm registry) for package descriptions"],
|
|
165
|
-
["--apply", "write the updated table (default is a dry run)"],
|
|
166
|
-
["--yes", "skip the confirmation question"]
|
|
167
|
-
]
|
|
168
|
-
});
|
|
169
|
-
/** Dry-run and confirmation notes per command. */
|
|
170
|
-
var VERB_DRY_RUN_NOTE = Object.freeze({
|
|
171
|
-
new: "dry run by default — add --apply to write the files, --yes to skip the question",
|
|
172
|
-
pull: "dry run by default — add --apply to write the refreshed files, --yes to skip the question",
|
|
173
|
-
mirror: "dry run by default — add --apply to write every published package guide, --yes to skip the question",
|
|
174
|
-
audit: "read-only — audit never writes; pass --live to also check upstream freshness",
|
|
175
|
-
repair: "dry run by default — add --apply to write, --yes to skip the question",
|
|
176
|
-
fleet: "dry run by default — add --apply to write across every package, --yes to skip the question",
|
|
177
|
-
catalog: "dry run by default — add --apply to write, --yes to skip the question"
|
|
178
|
-
});
|
|
179
|
-
/** One concrete invocation per command. */
|
|
180
|
-
var VERB_EXAMPLE = Object.freeze({
|
|
181
|
-
new: "example: scaffold new widget --src core,server --app core,browser --apply",
|
|
182
|
-
pull: "example: scaffold pull --apply",
|
|
183
|
-
mirror: "example: scaffold mirror --apply --yes",
|
|
184
|
-
audit: "example: scaffold audit --live",
|
|
185
|
-
repair: "example: scaffold repair --apply",
|
|
186
|
-
fleet: "example: scaffold fleet --apply --yes",
|
|
187
|
-
catalog: "example: scaffold catalog --apply"
|
|
188
|
-
});
|
|
189
|
-
/** Message used when a prune scan has no candidates. */
|
|
190
|
-
var PRUNE_EMPTY = "no unexpected files to delete";
|
|
191
|
-
/** Guidance for unexpected files outside a non-pruning repair handoff. */
|
|
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";
|
|
193
|
-
/** Interactive dependency prompt. */
|
|
194
|
-
var ORKESTREL_DEPS_PROMPT = "@orkestrel dependencies (comma-separated short names, e.g. contract, emitter — installed as dependencies)";
|
|
195
|
-
/** Catalog-degraded validation note. */
|
|
196
|
-
var CATALOG_UNRESOLVED_NOTE = "couldn't resolve the vendored @orkestrel catalog — validating names by shape only";
|
|
197
|
-
/** Opening marker for the generated package catalog block. */
|
|
198
|
-
var CATALOG_START_MARKER = "<!-- catalog:start -->";
|
|
199
|
-
/** Closing marker for the generated package catalog block. */
|
|
200
|
-
var CATALOG_END_MARKER = "<!-- catalog:end -->";
|
|
201
|
-
/** Non-terminal prune safety note. */
|
|
202
|
-
var PRUNE_SKIPPED = "prune skipped — pass --apply to authorize deletion; --yes only skips confirmation";
|
|
203
|
-
/** Degraded unexpected-file scan note. */
|
|
204
|
-
var SCAN_SKIPPED = "unexpected-file scanning skipped — couldn't establish the template source";
|
|
205
|
-
//#endregion
|
|
206
|
-
//#region src/bin/errors.ts
|
|
207
|
-
/** Internal sentinel that unwinds command dispatch without bypassing cleanup. */
|
|
208
|
-
var CLIExitError = class extends Error {
|
|
209
|
-
code;
|
|
210
|
-
constructor(code) {
|
|
211
|
-
super(`cli-exit:${String(code)}`);
|
|
212
|
-
this.code = code;
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
//#endregion
|
|
216
|
-
//#region src/bin/shapers.ts
|
|
217
|
-
/** Partition findings by whether repair owns their artifact origin. The `owned` and `generated` buckets carry no foreign findings by construction. */
|
|
218
|
-
function partitionFindings(findings, plan) {
|
|
219
|
-
const origins = new Map(plan.artifacts.map((artifact) => [artifact.path, artifact.origin]));
|
|
220
|
-
let ownedDrifted = 0;
|
|
221
|
-
let ownedMissing = 0;
|
|
222
|
-
let generatedDrifted = 0;
|
|
223
|
-
let generatedMissing = 0;
|
|
224
|
-
let foreign = 0;
|
|
225
|
-
for (const finding of findings) {
|
|
226
|
-
const origin = origins.get(finding.path);
|
|
227
|
-
const owned = origin === "host" || origin === "template";
|
|
228
|
-
if (finding.drift === "aligned") continue;
|
|
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
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
owned: {
|
|
243
|
-
drifted: ownedDrifted,
|
|
244
|
-
missing: ownedMissing,
|
|
245
|
-
foreign: 0
|
|
246
|
-
},
|
|
247
|
-
generated: {
|
|
248
|
-
drifted: generatedDrifted,
|
|
249
|
-
missing: generatedMissing,
|
|
250
|
-
foreign: 0
|
|
251
|
-
},
|
|
252
|
-
foreign: {
|
|
253
|
-
drifted: 0,
|
|
254
|
-
missing: 0,
|
|
255
|
-
foreign
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
/** Create one deterministic fleet result entry. */
|
|
260
|
-
function fleetEntryOf(name, counts, failed, outside = 0) {
|
|
261
|
-
return {
|
|
262
|
-
name,
|
|
263
|
-
drifted: counts?.drifted ?? 0,
|
|
264
|
-
missing: counts?.missing ?? 0,
|
|
265
|
-
foreign: counts?.foreign ?? 0,
|
|
266
|
-
failed,
|
|
267
|
-
outside
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
/** Create the command-line interface's machine-readable failure envelope. */
|
|
271
|
-
function errorEnvelopeOf(code, message) {
|
|
272
|
-
return { error: {
|
|
273
|
-
code,
|
|
274
|
-
message
|
|
275
|
-
} };
|
|
276
|
-
}
|
|
277
|
-
/** Project a plan summary to the stable machine-readable `new` result. */
|
|
278
|
-
function summaryToNewResult(summary, applied) {
|
|
279
|
-
return {
|
|
280
|
-
name: summary.name,
|
|
281
|
-
src: summary.src,
|
|
282
|
-
app: summary.app,
|
|
283
|
-
host: summary.host,
|
|
284
|
-
template: summary.template,
|
|
285
|
-
computed: summary.computed,
|
|
286
|
-
applied
|
|
287
|
-
};
|
|
288
|
-
}
|
|
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. */
|
|
297
|
-
function auditToRepairResult(audit, result) {
|
|
298
|
-
return {
|
|
299
|
-
...audit,
|
|
300
|
-
result
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
/** Create the stable machine-readable catalog result. */
|
|
304
|
-
function catalogResultOf(entries, drift, shrink) {
|
|
305
|
-
return shrink === void 0 ? {
|
|
306
|
-
entries,
|
|
307
|
-
drift
|
|
308
|
-
} : {
|
|
309
|
-
entries,
|
|
310
|
-
drift,
|
|
311
|
-
shrink
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
//#endregion
|
|
315
|
-
//#region src/bin/helpers.ts
|
|
316
|
-
/** Render one pluralized count. */
|
|
317
|
-
function countPart(count, label) {
|
|
318
|
-
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
319
|
-
}
|
|
320
|
-
/** Render nonzero audit buckets or `clean`. */
|
|
321
|
-
function bucketText(counts) {
|
|
322
|
-
const parts = [];
|
|
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`);
|
|
326
|
-
return parts.length > 0 ? parts.join(", ") : "clean";
|
|
327
|
-
}
|
|
328
|
-
/** Render audit's origin-aware verdict. */
|
|
329
|
-
function auditVerdict(audit, plan) {
|
|
330
|
-
const count = audit.findings.length;
|
|
331
|
-
if (audit.clean) return `audit: ${countPart(count, "artifact")} — clean`;
|
|
332
|
-
const split = partitionFindings(audit.findings, plan);
|
|
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}`;
|
|
339
|
-
}
|
|
340
|
-
/** Render non-aligned audit findings as terminal table rows. */
|
|
341
|
-
function findingRows(findings, plan) {
|
|
342
|
-
const origins = new Map(plan.artifacts.map((artifact) => [artifact.path, artifact.origin]));
|
|
343
|
-
return findings.filter((finding) => finding.drift !== "aligned").map((finding) => {
|
|
344
|
-
const origin = origins.get(finding.path);
|
|
345
|
-
const category = origin === void 0 ? "unexpected file" : ORIGIN_LABEL[origin];
|
|
346
|
-
return [
|
|
347
|
-
DRIFT_LABEL[finding.drift],
|
|
348
|
-
category,
|
|
349
|
-
finding.path
|
|
350
|
-
];
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
/** Create audit's terminal table. */
|
|
354
|
-
function auditTable(audit, plan) {
|
|
355
|
-
return {
|
|
356
|
-
columns: [
|
|
357
|
-
{ label: "Status" },
|
|
358
|
-
{ label: "Kind" },
|
|
359
|
-
{ label: "Path" }
|
|
360
|
-
],
|
|
361
|
-
rows: findingRows(audit.findings, plan)
|
|
362
|
-
};
|
|
363
|
-
}
|
|
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
|
-
/**
|
|
402
|
-
* Render drift outside repair's selected ownership boundary.
|
|
403
|
-
*
|
|
404
|
-
* @param count - The number of findings outside the selected scope.
|
|
405
|
-
* @param generated - Whether generated canon was included in the repair scope.
|
|
406
|
-
* @returns The scope guidance line, or `undefined` when no findings remain outside scope.
|
|
407
|
-
*/
|
|
408
|
-
function scopeNote(count, generated) {
|
|
409
|
-
if (count === 0) return void 0;
|
|
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`;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Render repair's dry-run verdict.
|
|
414
|
-
*
|
|
415
|
-
* @param audit - The audit over the selected repair plan.
|
|
416
|
-
* @param options - The selected scope and write authorizations.
|
|
417
|
-
* @returns The scope-aware clean or drifted verdict.
|
|
418
|
-
*/
|
|
419
|
-
function repairVerdict(audit, options) {
|
|
420
|
-
const scope = options.generated ? "host-owned and generated" : "host-owned";
|
|
421
|
-
if (audit.clean) return `repair: ${countPart(audit.findings.length, `${scope} artifact`)} aligned — nothing to write`;
|
|
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`;
|
|
426
|
-
}
|
|
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}`;
|
|
488
|
-
}
|
|
489
|
-
/** Render synchronization freshness as table rows. */
|
|
490
|
-
function syncRows(report) {
|
|
491
|
-
const guides = report.guides.map((guide) => [
|
|
492
|
-
guide.name,
|
|
493
|
-
"guide",
|
|
494
|
-
FRESHNESS_LABEL[guide.freshness] ?? guide.freshness
|
|
495
|
-
]);
|
|
496
|
-
const versions = report.versions.map((version) => [
|
|
497
|
-
version.name,
|
|
498
|
-
"version",
|
|
499
|
-
FRESHNESS_LABEL[version.freshness] ?? version.freshness
|
|
500
|
-
]);
|
|
501
|
-
return [...guides, ...versions];
|
|
502
|
-
}
|
|
503
|
-
/** Create a synchronization terminal table. */
|
|
504
|
-
function syncTable(report) {
|
|
505
|
-
return {
|
|
506
|
-
columns: [
|
|
507
|
-
{ label: "Name" },
|
|
508
|
-
{ label: "Kind" },
|
|
509
|
-
{ label: "Freshness" }
|
|
510
|
-
],
|
|
511
|
-
rows: syncRows(report)
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
/** Render cause notes from non-current synchronization entries. */
|
|
515
|
-
function syncCauseNotes(report) {
|
|
516
|
-
return [...report.guides, ...report.versions].filter((entry) => entry.note !== void 0).map((entry) => ` ${entry.name}: ${FRESHNESS_LABEL[entry.freshness] ?? entry.freshness} — ${entry.note}`);
|
|
517
|
-
}
|
|
518
|
-
/** Render a command-specific synchronization tally. */
|
|
519
|
-
function syncVerdict(report, action) {
|
|
520
|
-
const count = report.guides.length + report.versions.length;
|
|
521
|
-
return `${action}: ${String(count)} ${count === 1 ? "entry" : "entries"} — ${String(report.failed)} failed`;
|
|
522
|
-
}
|
|
523
|
-
/** Render a synchronization success tally. */
|
|
524
|
-
function syncSuccess(count) {
|
|
525
|
-
return `wrote ${countPart(count, "guide")}`;
|
|
526
|
-
}
|
|
527
|
-
/** Render one fleet repository outcome. */
|
|
528
|
-
function fleetRepoLine(name, outcome) {
|
|
529
|
-
if (outcome.state === "clean") return `${name}: clean`;
|
|
530
|
-
if (outcome.state === "drifted") return `${name}: ${bucketText(outcome)}`;
|
|
531
|
-
if (outcome.state === "repaired") return `${name}: repaired (${countPart(outcome.remaining, "finding")} remaining)`;
|
|
532
|
-
return `${name}: ${outcome.message}`;
|
|
533
|
-
}
|
|
534
|
-
/** Render fleet's repository totals. */
|
|
535
|
-
function fleetTotals(drifted, failed) {
|
|
536
|
-
return `total: ${countPart(drifted, "dirty repo")}, ${failed} failed`;
|
|
537
|
-
}
|
|
538
|
-
/** Create catalog's terminal table. */
|
|
539
|
-
function catalogTable(entries) {
|
|
540
|
-
return {
|
|
541
|
-
columns: [{ label: "Package" }, { label: "Version" }],
|
|
542
|
-
rows: entries.map((entry) => [entry.name, entry.version])
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
/** Render catalog shrink risk when present. */
|
|
546
|
-
function catalogShrinkWarning(oldRows, newRows) {
|
|
547
|
-
if (newRows >= oldRows) return void 0;
|
|
548
|
-
return `warning: catalog shrinks from ${countPart(oldRows, "row")} to ${newRows}`;
|
|
549
|
-
}
|
|
550
|
-
/** Render catalog source tallies. */
|
|
551
|
-
function catalogCounts(published, local) {
|
|
552
|
-
return `catalog: ${countPart(published, "published package")}, ${countPart(local, "local-only")}`;
|
|
553
|
-
}
|
|
554
|
-
/** Create `new`'s plan summary table. */
|
|
555
|
-
function newPlanTable(summary) {
|
|
556
|
-
return {
|
|
557
|
-
columns: [{ label: "Origin" }, {
|
|
558
|
-
label: "Count",
|
|
559
|
-
align: "right"
|
|
560
|
-
}],
|
|
561
|
-
rows: [
|
|
562
|
-
["host-owned", String(summary.host)],
|
|
563
|
-
["starter", String(summary.template)],
|
|
564
|
-
["generated", String(summary.computed)]
|
|
565
|
-
]
|
|
566
|
-
};
|
|
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
|
-
}
|
|
580
|
-
/** Render `new`'s dry-run destination. */
|
|
581
|
-
function newPlanPreview(destination) {
|
|
582
|
-
return `will write into ${destination}`;
|
|
583
|
-
}
|
|
584
|
-
/** Render `new`'s write result. */
|
|
585
|
-
function newApplySuccess(count, destination) {
|
|
586
|
-
return `wrote ${countPart(count, "file")} into ${destination}`;
|
|
587
|
-
}
|
|
588
|
-
/** Render catalog's write result. */
|
|
589
|
-
function catalogApplySuccess(path) {
|
|
590
|
-
return `wrote ${path}`;
|
|
591
|
-
}
|
|
592
|
-
/** Render the shared write confirmation. */
|
|
593
|
-
function applyConfirmMessage(files, repos) {
|
|
594
|
-
const scope = repos === void 0 ? "" : ` across ${countPart(repos, "repo")}`;
|
|
595
|
-
return `Apply — write ${countPart(files, "file")}${scope}? `;
|
|
596
|
-
}
|
|
597
|
-
/** Render the separate prune confirmation. */
|
|
598
|
-
function pruneConfirmMessage(count) {
|
|
599
|
-
return `Also delete ${countPart(count, "unexpected file")} under .claude/agents, .codex/agents, and scripts? `;
|
|
600
|
-
}
|
|
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) {
|
|
611
|
-
const parts = [];
|
|
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? `;
|
|
616
|
-
}
|
|
617
|
-
/** Render one unresolved Orkestrel dependency token. */
|
|
618
|
-
function unknownOrkestrelToken(token, suggestion) {
|
|
619
|
-
const message = `"${token}" is not a published @orkestrel package`;
|
|
620
|
-
return suggestion === void 0 ? `${message} — try again` : `${message} — did you mean "${suggestion}"? try again`;
|
|
621
|
-
}
|
|
622
|
-
/** Render compact command usage. */
|
|
623
|
-
function shortUsage() {
|
|
624
|
-
return [
|
|
625
|
-
"scaffold <verb> [options]",
|
|
626
|
-
"",
|
|
627
|
-
...KNOWN_VERBS.map((verb) => ` ${verb.padEnd(8)}${VERB_SUMMARY[verb]}`),
|
|
628
|
-
"",
|
|
629
|
-
"run 'scaffold <verb> --help' for a verb's full reference"
|
|
630
|
-
].join("\n");
|
|
631
|
-
}
|
|
632
|
-
/** Render the full command reference. */
|
|
633
|
-
function fullHelp() {
|
|
634
|
-
const verbs = KNOWN_VERBS.map((verb) => ` ${verb} ${VERB_FLAGS[verb]}\n ${VERB_SUMMARY[verb]}`);
|
|
635
|
-
const exits = EXIT_CODES.map(([code, meaning]) => ` ${code} ${meaning}`);
|
|
636
|
-
return [
|
|
637
|
-
"scaffold <verb> [options]",
|
|
638
|
-
"",
|
|
639
|
-
...verbs,
|
|
640
|
-
"",
|
|
641
|
-
SAFETY_BANNER,
|
|
642
|
-
"",
|
|
643
|
-
"exit codes:",
|
|
644
|
-
...exits
|
|
645
|
-
].join("\n");
|
|
646
|
-
}
|
|
647
|
-
/** Render one command's help reference. */
|
|
648
|
-
function verbHelp(verb) {
|
|
649
|
-
const flags = VERB_FLAG_HELP[verb].map(([flag, meaning]) => ` ${flag.padEnd(20)}${meaning}`);
|
|
650
|
-
return [
|
|
651
|
-
`scaffold ${verb} ${VERB_FLAGS[verb]}`,
|
|
652
|
-
"",
|
|
653
|
-
VERB_SUMMARY[verb],
|
|
654
|
-
VERB_DRY_RUN_NOTE[verb],
|
|
655
|
-
"",
|
|
656
|
-
...flags,
|
|
657
|
-
"",
|
|
658
|
-
VERB_EXAMPLE[verb]
|
|
659
|
-
].join("\n");
|
|
660
|
-
}
|
|
661
|
-
/** Compute Levenshtein edit distance. */
|
|
662
|
-
function editDistance(left, right) {
|
|
663
|
-
const rows = left.length + 1;
|
|
664
|
-
const columns = right.length + 1;
|
|
665
|
-
const table = new Uint32Array(rows * columns);
|
|
666
|
-
for (let row = 0; row < rows; row += 1) table[row * columns] = row;
|
|
667
|
-
for (let column = 0; column < columns; column += 1) table[column] = column;
|
|
668
|
-
for (let row = 1; row < rows; row += 1) for (let column = 1; column < columns; column += 1) {
|
|
669
|
-
const cost = left[row - 1] === right[column - 1] ? 0 : 1;
|
|
670
|
-
const index = row * columns + column;
|
|
671
|
-
table[index] = Math.min((table[index - columns] ?? 0) + 1, (table[index - 1] ?? 0) + 1, (table[index - columns - 1] ?? 0) + cost);
|
|
672
|
-
}
|
|
673
|
-
return table[rows * columns - 1] ?? 0;
|
|
674
|
-
}
|
|
675
|
-
/** Find the nearest string by edit distance. */
|
|
676
|
-
function nearest(input, set) {
|
|
677
|
-
let best;
|
|
678
|
-
let distance = Number.POSITIVE_INFINITY;
|
|
679
|
-
for (const candidate of set) {
|
|
680
|
-
const candidateDistance = editDistance(input, candidate);
|
|
681
|
-
if (candidateDistance < distance) {
|
|
682
|
-
distance = candidateDistance;
|
|
683
|
-
best = candidate;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return best;
|
|
687
|
-
}
|
|
688
|
-
/** Render an unknown command with a nearest-command hint. */
|
|
689
|
-
function didYouMean(command) {
|
|
690
|
-
const suggestion = nearest(command, KNOWN_VERBS);
|
|
691
|
-
return suggestion === void 0 ? `unknown command "${command}"` : `unknown command "${command}" — did you mean "${suggestion}"?`;
|
|
692
|
-
}
|
|
693
|
-
/** Render one prune preview line per exact path. */
|
|
694
|
-
function prunePreview(paths) {
|
|
695
|
-
return paths.map((path) => ` delete ${path}`);
|
|
696
|
-
}
|
|
697
|
-
/** Render non-terminal guidance for missing input. */
|
|
698
|
-
function missingInput(input, verb) {
|
|
699
|
-
return `missing ${input} — pass it as a flag/argument, or run 'scaffold ${verb}' on a terminal to be guided`;
|
|
700
|
-
}
|
|
701
|
-
/** Render an invalid package name. */
|
|
702
|
-
function invalidName(name, pattern) {
|
|
703
|
-
return `Package name "${name}" must match ${pattern}`;
|
|
704
|
-
}
|
|
705
|
-
/** Render dependencies whose latest versions could not be resolved. */
|
|
706
|
-
function unresolvedVersion(names) {
|
|
707
|
-
return `could not resolve the latest version for ${names.map((name) => `"${name}"`).join(", ")} — check the name or pass name@range`;
|
|
708
|
-
}
|
|
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
|
-
*/
|
|
715
|
-
function generatedNote(count) {
|
|
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`;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Render honest repair guidance for computed and protected manifest drift.
|
|
729
|
-
*
|
|
730
|
-
* @param findings - The audit findings to classify.
|
|
731
|
-
* @param plan - The plan that owns each computed artifact.
|
|
732
|
-
* @returns One line for repairable computed drift and one for protected manifest drift when present.
|
|
733
|
-
*/
|
|
734
|
-
function renderComputedNotes(findings, plan) {
|
|
735
|
-
const origins = new Map(plan.artifacts.map((artifact) => [artifact.path, artifact.origin]));
|
|
736
|
-
let computed = 0;
|
|
737
|
-
let manifest = 0;
|
|
738
|
-
for (const finding of findings) {
|
|
739
|
-
if (finding.drift === "aligned" || origins.get(finding.path) !== "computed") continue;
|
|
740
|
-
if (finding.path === "package.json") manifest += 1;
|
|
741
|
-
else computed += 1;
|
|
742
|
-
}
|
|
743
|
-
const notes = [];
|
|
744
|
-
if (computed > 0) notes.push(generatedNote(computed));
|
|
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`);
|
|
747
|
-
return notes;
|
|
748
|
-
}
|
|
749
|
-
/** Render live dependency freshness tallies. */
|
|
750
|
-
function auditLiveNote(current, behind, failed) {
|
|
751
|
-
return `live: ${current} current, ${behind} behind, ${failed} failed`;
|
|
752
|
-
}
|
|
753
|
-
/** Render whether an audit compared content or presence. */
|
|
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")}`;
|
|
758
|
-
}
|
|
759
|
-
/** Render catalog's final verdict. */
|
|
760
|
-
function catalogVerdict(clean) {
|
|
761
|
-
return clean ? "catalog: clean" : "catalog: drifted — pass --apply to write";
|
|
762
|
-
}
|
|
763
|
-
/** Render a caught error without leaking implementation detail. */
|
|
764
|
-
function describeError(error) {
|
|
765
|
-
if (isScaffoldError(error)) return `[${error.code}] ${error.message}`;
|
|
766
|
-
return error instanceof Error ? error.message : "unknown error";
|
|
767
|
-
}
|
|
768
|
-
/** Resolve and confine one write destination to its invocation root. */
|
|
769
|
-
function containDestination(root, candidate) {
|
|
770
|
-
if (!isFilesystemPath(root) || !isFilesystemPath(candidate)) throw new ScaffoldError("INVALID", "Target paths must not contain control characters");
|
|
771
|
-
const contained = attempt(() => resolvePhysicalPath(root, candidate, "INVALID", "working directory"));
|
|
772
|
-
if (contained.success) return contained.value;
|
|
773
|
-
if (isScaffoldError(contained.error) && contained.error.code === "INVALID") throw new ScaffoldError("INVALID", `Target "${candidate}" is outside or traverses a linked parent of the working directory — run scaffold from the physical directory you want to write beneath.`, { path: candidate });
|
|
774
|
-
throw contained.error;
|
|
775
|
-
}
|
|
776
|
-
/** Return one dependency-token issue, or absence when it is valid. */
|
|
777
|
-
function orkestrelTokenIssue(normalized, catalog) {
|
|
778
|
-
if (catalog === void 0) return DEPENDENCY_NAME_PATTERN.test(normalized) ? void 0 : unknownOrkestrelToken(normalized, void 0);
|
|
779
|
-
if (catalog.includes(normalized)) return void 0;
|
|
780
|
-
return unknownOrkestrelToken(normalized, nearest(normalized, catalog));
|
|
781
|
-
}
|
|
782
|
-
//#endregion
|
|
783
|
-
//#region src/bin/parsers.ts
|
|
784
|
-
/** Parse a strict command-line argument vector. */
|
|
785
|
-
function parseArguments(argv) {
|
|
786
|
-
if (argv.some((argument) => !isTerminalText(argument))) throw new ScaffoldError("INVALID", "Command arguments must not contain control characters");
|
|
787
|
-
const args = argv[0] === "--" ? argv.slice(1) : [...argv];
|
|
788
|
-
return parseArgs({
|
|
789
|
-
args,
|
|
790
|
-
allowPositionals: true,
|
|
791
|
-
options: {
|
|
792
|
-
src: { type: "string" },
|
|
793
|
-
app: { type: "string" },
|
|
794
|
-
deps: { type: "string" },
|
|
795
|
-
groups: { type: "string" },
|
|
796
|
-
target: { type: "string" },
|
|
797
|
-
from: {
|
|
798
|
-
type: "string",
|
|
799
|
-
multiple: true
|
|
800
|
-
},
|
|
801
|
-
apply: {
|
|
802
|
-
type: "boolean",
|
|
803
|
-
default: false
|
|
804
|
-
},
|
|
805
|
-
yes: {
|
|
806
|
-
type: "boolean",
|
|
807
|
-
default: false
|
|
808
|
-
},
|
|
809
|
-
json: {
|
|
810
|
-
type: "boolean",
|
|
811
|
-
default: false
|
|
812
|
-
},
|
|
813
|
-
prune: {
|
|
814
|
-
type: "boolean",
|
|
815
|
-
default: false
|
|
816
|
-
},
|
|
817
|
-
generated: {
|
|
818
|
-
type: "boolean",
|
|
819
|
-
default: false
|
|
820
|
-
},
|
|
821
|
-
replace: {
|
|
822
|
-
type: "boolean",
|
|
823
|
-
default: false
|
|
824
|
-
},
|
|
825
|
-
strict: {
|
|
826
|
-
type: "boolean",
|
|
827
|
-
default: false
|
|
828
|
-
},
|
|
829
|
-
live: {
|
|
830
|
-
type: "boolean",
|
|
831
|
-
default: false
|
|
832
|
-
},
|
|
833
|
-
offline: {
|
|
834
|
-
type: "boolean",
|
|
835
|
-
default: false
|
|
836
|
-
},
|
|
837
|
-
help: {
|
|
838
|
-
type: "boolean",
|
|
839
|
-
default: false,
|
|
840
|
-
short: "h"
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
/** Split a comma-separated token list, trimming and dropping empty entries. */
|
|
846
|
-
function splitTokens(raw) {
|
|
847
|
-
return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
848
|
-
}
|
|
849
|
-
/** Normalize an Orkestrel dependency token to its full package name. */
|
|
850
|
-
function normalizeOrkestrelToken(token) {
|
|
851
|
-
return token.startsWith("@orkestrel/") ? token : `@orkestrel/${token}`;
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Parse and resolve a pull dependency selection against the target manifest.
|
|
855
|
-
*
|
|
856
|
-
* @param raw - The comma-separated full package names, or `undefined` for all.
|
|
857
|
-
* @param declared - The target's declared Orkestrel dependencies.
|
|
858
|
-
* @returns `undefined` for all dependencies, otherwise the exact selected records.
|
|
859
|
-
* @throws `ScaffoldError('INVALID')` for empty, malformed, repeated, or undeclared names.
|
|
860
|
-
*/
|
|
861
|
-
function parsePullDependencies(raw, declared) {
|
|
862
|
-
if (raw === void 0) return void 0;
|
|
863
|
-
const names = raw.split(",").map((name) => name.trim());
|
|
864
|
-
if (names.length === 0 || names.some((name) => !DEPENDENCY_NAME_PATTERN.test(name)) || new Set(names).size !== names.length) throw new ScaffoldError("INVALID", "Pull dependencies must be unique, comma-separated @orkestrel/* package names");
|
|
865
|
-
const selected = declared.filter((dependency) => names.includes(dependency.name));
|
|
866
|
-
const found = new Set(selected.map((dependency) => dependency.name));
|
|
867
|
-
const missing = names.filter((name) => !found.has(name));
|
|
868
|
-
if (missing.length > 0) throw new ScaffoldError("INVALID", `Pull dependencies are not declared by the target: ${missing.join(", ")}`, { dependencies: missing });
|
|
869
|
-
return selected;
|
|
870
|
-
}
|
|
871
|
-
//#endregion
|
|
872
|
-
//#region src/bin/validators.ts
|
|
873
|
-
/** Narrow a positional command to the command-line interface's vocabulary. */
|
|
874
|
-
function isVerb(value) {
|
|
875
|
-
return KNOWN_VERBS.some((verb) => verb === value);
|
|
876
|
-
}
|
|
877
|
-
//#endregion
|
|
878
|
-
//#region src/bin/CLI.ts
|
|
879
|
-
/** Stateful command-line orchestration and its process boundary. */
|
|
880
|
-
var CLI = class {
|
|
881
|
-
#sync;
|
|
882
|
-
#sink = createServerSink();
|
|
883
|
-
#tty = process.stdout.isTTY === true;
|
|
884
|
-
#styler = createStyler({ enabled: process.env.NO_COLOR === void 0 && this.#tty });
|
|
885
|
-
#reporter = createReporter({
|
|
886
|
-
sink: this.#sink,
|
|
887
|
-
width: this.#sink.columns,
|
|
888
|
-
styler: this.#styler
|
|
889
|
-
});
|
|
890
|
-
#json = false;
|
|
891
|
-
/**
|
|
892
|
-
* Create command-line orchestration with optional upstream endpoint settings.
|
|
893
|
-
*
|
|
894
|
-
* @param sync - Sync settings used by every live dependency operation.
|
|
895
|
-
*/
|
|
896
|
-
constructor(sync) {
|
|
897
|
-
this.#sync = parseSyncOptions(sync);
|
|
898
|
-
}
|
|
899
|
-
/** Execute one command-line argument vector. */
|
|
900
|
-
async run(argv) {
|
|
901
|
-
this.#trust();
|
|
902
|
-
try {
|
|
903
|
-
await this.#dispatch(argv);
|
|
904
|
-
} catch (error) {
|
|
905
|
-
if (error instanceof CLIExitError) process.exitCode = error.code;
|
|
906
|
-
else if (this.#json) {
|
|
907
|
-
const code = isScaffoldError(error) ? error.code : "ERROR";
|
|
908
|
-
const message = isScaffoldError(error) ? error.message : describeError(error);
|
|
909
|
-
this.#write(errorEnvelopeOf(code, message));
|
|
910
|
-
process.exitCode = 1;
|
|
911
|
-
} else {
|
|
912
|
-
this.#reporter.status("error", describeError(error));
|
|
913
|
-
process.exitCode = 1;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Widen Node's default trusted-issuer set to include the OS certificate
|
|
919
|
-
* store, so `fetch` behind a corporate TLS-inspecting proxy behaves like npm
|
|
920
|
-
* (`cafile`) and browsers (OS trust store) instead of failing with
|
|
921
|
-
* `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` against Node's bundled CA list alone.
|
|
922
|
-
* Feature-detected (`tls.getCACertificates` / `tls.setDefaultCACertificates`
|
|
923
|
-
* ship on Node ≈22.16+/24.5+; this package's floor is `>=22.12.0`) and captured
|
|
924
|
-
* through `attempt` — any failure is a silent no-op, never a crash. This only ADDS
|
|
925
|
-
* trusted issuers; it never touches `rejectUnauthorized` or
|
|
926
|
-
* `NODE_TLS_REJECT_UNAUTHORIZED`, so certificate verification stays on.
|
|
927
|
-
*/
|
|
928
|
-
#trust() {
|
|
929
|
-
if (typeof tls.getCACertificates !== "function" || typeof tls.setDefaultCACertificates !== "function") return;
|
|
930
|
-
attempt(() => {
|
|
931
|
-
const merged = /* @__PURE__ */ new Set([...tls.getCACertificates("default"), ...tls.getCACertificates("system")]);
|
|
932
|
-
tls.setDefaultCACertificates([...merged]);
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
/** Write ONE machine-readable JSON value to stdout — the entire `--json` output contract. */
|
|
936
|
-
#write(value) {
|
|
937
|
-
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
938
|
-
}
|
|
939
|
-
/** Report a general operation failure with exit 1 as prose or one JSON error envelope. */
|
|
940
|
-
#fail(message, json) {
|
|
941
|
-
if (json) this.#write(errorEnvelopeOf("ERROR", message));
|
|
942
|
-
else this.#reporter.status("error", message);
|
|
943
|
-
throw new CLIExitError(1);
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* A general operation failure from a caught error — the real
|
|
947
|
-
* `ScaffoldError` code when available ('ERROR' last resort), prose (via
|
|
948
|
-
* `describe`, which still carries the bracketed code for a human reader) or
|
|
949
|
-
* the one JSON error envelope under `--json` (code and message kept
|
|
950
|
-
* SEPARATE — never double-encoding the code into the message text).
|
|
951
|
-
*/
|
|
952
|
-
#error(error, json) {
|
|
953
|
-
if (json) {
|
|
954
|
-
const code = isScaffoldError(error) ? error.code : "ERROR";
|
|
955
|
-
const message = isScaffoldError(error) ? error.message : describeError(error);
|
|
956
|
-
this.#write(errorEnvelopeOf(code, message));
|
|
957
|
-
} else this.#reporter.status("error", describeError(error));
|
|
958
|
-
throw new CLIExitError(1);
|
|
959
|
-
}
|
|
960
|
-
/** A usage error (bad flag value, unknown verb — exit 2) — stderr prose, or the one JSON error envelope under `--json`. */
|
|
961
|
-
#usage(message, json) {
|
|
962
|
-
if (json) this.#write(errorEnvelopeOf("USAGE", message));
|
|
963
|
-
else process.stderr.write(`${message}\n`);
|
|
964
|
-
throw new CLIExitError(2);
|
|
965
|
-
}
|
|
966
|
-
/** Contain a destination and report any coded escape through the shared CLI error path. */
|
|
967
|
-
#contain(candidate, json) {
|
|
968
|
-
const contained = attempt(() => containDestination(process.cwd(), candidate));
|
|
969
|
-
if (contained.success) return contained.value;
|
|
970
|
-
this.#error(contained.error, json);
|
|
971
|
-
}
|
|
972
|
-
/** Compile a spec and report unresolved blocking questions through the shared CLI error path. */
|
|
973
|
-
#compile(spec, json, groups) {
|
|
974
|
-
const compiler = createCompiler();
|
|
975
|
-
try {
|
|
976
|
-
const scaffolding = compiler.compile(spec, groups);
|
|
977
|
-
if (!scaffolding.plan) {
|
|
978
|
-
const blocking = scaffolding.questions.filter((question) => question.blocking);
|
|
979
|
-
const message = (blocking.length > 0 ? blocking : scaffolding.questions).map((question) => question.text).join("; ");
|
|
980
|
-
this.#fail(message, json);
|
|
981
|
-
}
|
|
982
|
-
return [scaffolding.plan, scaffolding.questions];
|
|
983
|
-
} finally {
|
|
984
|
-
compiler.destroy();
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
#prunePaths(target, host, services) {
|
|
988
|
-
const seam = services.length > 0 ? SERVICE_SCRIPT_PATH : void 0;
|
|
989
|
-
return pruneTargets(target, host).filter((path) => path !== seam);
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Merge the physical prune scan into `audit` as `foreign` findings — pure
|
|
993
|
-
* object-spread composition in the BIN only (`src/core`'s `diffPlan` is never
|
|
994
|
-
* modified/reimplemented). Every unexpected path except the declared service
|
|
995
|
-
* workspace's exact `SERVICE_SCRIPT_PATH` becomes one `'orchestration'`-group
|
|
996
|
-
* `foreign` finding (the `Group` every `PRUNE_DIRECTORIES` entry—`.claude/agents`,
|
|
997
|
-
* `.codex/agents`, `scripts`—belongs to); any scan hit makes the merged audit
|
|
998
|
-
* unclean, so an "unexpected file" is honestly counted as drift (exit 1)
|
|
999
|
-
* instead of the structurally-always-zero `diffPlan.foreign`.
|
|
1000
|
-
*/
|
|
1001
|
-
#scan(audit, target, host, services) {
|
|
1002
|
-
const paths = this.#prunePaths(target, host, services);
|
|
1003
|
-
if (paths.length === 0) return audit;
|
|
1004
|
-
const findings = paths.map((path) => ({
|
|
1005
|
-
path,
|
|
1006
|
-
group: "orchestration",
|
|
1007
|
-
drift: "foreign"
|
|
1008
|
-
}));
|
|
1009
|
-
return {
|
|
1010
|
-
...audit,
|
|
1011
|
-
clean: false,
|
|
1012
|
-
foreign: audit.foreign + paths.length,
|
|
1013
|
-
findings: [...audit.findings, ...findings]
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Add unexpected-file findings when the vendored allowlist can be established.
|
|
1018
|
-
* When fail-closed allowlist discovery raises a coded `TARGET` failure, retain
|
|
1019
|
-
* the existing findings and mark the audit incomplete instead of crashing.
|
|
1020
|
-
*/
|
|
1021
|
-
#scanSafe(audit, target, host, services) {
|
|
1022
|
-
const scanned = attempt(() => this.#scan(audit, target, host, services));
|
|
1023
|
-
if (scanned.success) return {
|
|
1024
|
-
audit: scanned.value,
|
|
1025
|
-
skipped: false
|
|
1026
|
-
};
|
|
1027
|
-
if (isScaffoldError(scanned.error) && scanned.error.code === "TARGET") return {
|
|
1028
|
-
audit: {
|
|
1029
|
-
...audit,
|
|
1030
|
-
clean: false,
|
|
1031
|
-
complete: false,
|
|
1032
|
-
questions: [...audit.questions, {
|
|
1033
|
-
field: "host",
|
|
1034
|
-
text: scanned.error.message,
|
|
1035
|
-
blocking: true
|
|
1036
|
-
}]
|
|
1037
|
-
},
|
|
1038
|
-
skipped: true
|
|
1039
|
-
};
|
|
1040
|
-
throw scanned.error;
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* `repair` scopes to host-owned artifacts by default and optionally generated canon, but the
|
|
1044
|
-
* caller compiles the full plan anyway. Diff it too so a clean scoped verdict can point at drift
|
|
1045
|
-
* outside the selected boundary. The count feeds the shared `scopeNote` renderer.
|
|
1046
|
-
*/
|
|
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;
|
|
1051
|
-
}
|
|
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,
|
|
1093
|
-
blueprint: {
|
|
1094
|
-
...promoted.blueprint,
|
|
1095
|
-
overrides: []
|
|
1096
|
-
},
|
|
1097
|
-
artifacts
|
|
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);
|
|
1104
|
-
let plan;
|
|
1105
|
-
try {
|
|
1106
|
-
plan = hydratePlan(scoped, host);
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
this.#error(error, json);
|
|
1109
|
-
}
|
|
1110
|
-
try {
|
|
1111
|
-
const audit = diffPlan(plan, readTarget(target, plan.artifacts.map((artifact) => artifact.path)));
|
|
1112
|
-
return [
|
|
1113
|
-
compiled,
|
|
1114
|
-
plan,
|
|
1115
|
-
audit
|
|
1116
|
-
];
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
this.#error(error, json);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
/** Reject a cancelled prompt (ctrl-c) with the shared `CANCELLED_MESSAGE` — exit 1, nothing written. */
|
|
1122
|
-
async #guard(promise) {
|
|
1123
|
-
try {
|
|
1124
|
-
return await promise;
|
|
1125
|
-
} catch (error) {
|
|
1126
|
-
if (isTerminalError(error) && error.code === "CANCEL") {
|
|
1127
|
-
this.#reporter.line(CANCELLED_MESSAGE);
|
|
1128
|
-
throw new CLIExitError(1);
|
|
1129
|
-
}
|
|
1130
|
-
throw error;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* The shared write-confirmation gate every verb calls before it touches disk.
|
|
1135
|
-
* `--apply` is the sole write authorization. JSON and non-terminal calls do
|
|
1136
|
-
* not prompt once authorized; `--yes` skips the terminal confirmation.
|
|
1137
|
-
*/
|
|
1138
|
-
async #apply(terminal, message, values, json) {
|
|
1139
|
-
if (!values.apply) return false;
|
|
1140
|
-
if (json || values.yes || !this.#tty) return true;
|
|
1141
|
-
return this.#guard(terminal.confirm({
|
|
1142
|
-
message,
|
|
1143
|
-
default: false
|
|
1144
|
-
}));
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* The SECOND, separate confirm for `--prune`-eligible deletions — never
|
|
1148
|
-
* bundled into the write question. Both `--prune` and `--apply` are required;
|
|
1149
|
-
* `--yes` only skips this confirmation and never authorizes deletion.
|
|
1150
|
-
*/
|
|
1151
|
-
async #prune(terminal, message, values, json) {
|
|
1152
|
-
if (!values.prune) return false;
|
|
1153
|
-
if (!values.apply) {
|
|
1154
|
-
if (!json && !this.#tty) this.#reporter.line(PRUNE_SKIPPED);
|
|
1155
|
-
return false;
|
|
1156
|
-
}
|
|
1157
|
-
if (json || values.yes || !this.#tty) return true;
|
|
1158
|
-
return this.#guard(terminal.confirm({
|
|
1159
|
-
message,
|
|
1160
|
-
default: false
|
|
1161
|
-
}));
|
|
1162
|
-
}
|
|
1163
|
-
/** A spinner for a long-running write step, absent under `--json` or off a TTY sink. */
|
|
1164
|
-
#spinner(message, json) {
|
|
1165
|
-
return json || !this.#tty ? void 0 : createSpinner({
|
|
1166
|
-
message,
|
|
1167
|
-
sink: this.#sink,
|
|
1168
|
-
styler: this.#styler
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
/** Announce a successful write — the spinner's own success line, or a plain `reporter.status` without one (never under `--json`). */
|
|
1172
|
-
#succeed(spinner, json, message) {
|
|
1173
|
-
if (spinner) spinner.success(message);
|
|
1174
|
-
else if (!json) this.#reporter.status("success", message);
|
|
1175
|
-
}
|
|
1176
|
-
/** Announce a failed write and halt(1) — the spinner's own failure line (if any), then the shared `fail`. */
|
|
1177
|
-
#reject(spinner, json, error) {
|
|
1178
|
-
const message = describeError(error);
|
|
1179
|
-
if (spinner) spinner.failure(message);
|
|
1180
|
-
this.#fail(message, json);
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Best-effort vendored `@orkestrel` catalog names, resolved via `host`
|
|
1184
|
-
* (`hostRoot()`, or the active `--from` override) through the host manifest
|
|
1185
|
-
* — `undefined` when the catalog cannot be established (a missing/unreadable
|
|
1186
|
-
* manifest, no `CATALOG_AGENT_PATH` entry, or any other failure),
|
|
1187
|
-
* degrading the dependency prompt to shape-only validation instead of blocking on it.
|
|
1188
|
-
*/
|
|
1189
|
-
#names(host) {
|
|
1190
|
-
const names = attempt(() => {
|
|
1191
|
-
const manifest = readHostManifest(host);
|
|
1192
|
-
const full = locateHostSource(manifest, CATALOG_AGENT_PATH, host);
|
|
1193
|
-
if (full === void 0 || !existsSync(full)) return void 0;
|
|
1194
|
-
const relative$1 = relative(host, full).replaceAll("\\", "/");
|
|
1195
|
-
return catalogNames(readFileText(host, relative$1, "TARGET", "host"));
|
|
1196
|
-
});
|
|
1197
|
-
return names.success ? names.value : void 0;
|
|
1198
|
-
}
|
|
1199
|
-
/** Prompt for `@orkestrel` short-name dependencies until every token resolves or input is empty. */
|
|
1200
|
-
async #prompt(terminal, catalog) {
|
|
1201
|
-
for (;;) {
|
|
1202
|
-
const tokens = splitTokens(await this.#guard(terminal.input({
|
|
1203
|
-
message: ORKESTREL_DEPS_PROMPT,
|
|
1204
|
-
default: ""
|
|
1205
|
-
})));
|
|
1206
|
-
if (tokens.length === 0) return [];
|
|
1207
|
-
const normalized = tokens.map(normalizeOrkestrelToken);
|
|
1208
|
-
const issue = normalized.map((token) => orkestrelTokenIssue(token, catalog)).find((message) => message !== void 0);
|
|
1209
|
-
if (issue === void 0) return normalized;
|
|
1210
|
-
this.#reporter.line(issue);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
/** `scaffold new` — scaffold a package into `./<name>` (or `--target`). */
|
|
1214
|
-
async #new(values, argument, json) {
|
|
1215
|
-
const terminal = createTerminal();
|
|
1216
|
-
let name;
|
|
1217
|
-
if (argument !== void 0) name = argument;
|
|
1218
|
-
else if (json) this.#usage("a package name is required with --json", json);
|
|
1219
|
-
else if (!this.#tty) this.#usage(missingInput("a package name", "new"), json);
|
|
1220
|
-
else name = await this.#guard(terminal.input({
|
|
1221
|
-
message: "Package name",
|
|
1222
|
-
validate: { pattern: NAME_PATTERN.source }
|
|
1223
|
-
}));
|
|
1224
|
-
if (!NAME_PATTERN.test(name)) this.#usage(invalidName(name, NAME_PATTERN.source), json);
|
|
1225
|
-
let srcInput;
|
|
1226
|
-
let appInput;
|
|
1227
|
-
if (values.src !== void 0) srcInput = values.src.split(",").map((candidate) => candidate.trim());
|
|
1228
|
-
else if (values.app !== void 0) srcInput = [];
|
|
1229
|
-
else if (json || !this.#tty) this.#usage("at least one of --src or --app is required", json);
|
|
1230
|
-
else srcInput = await this.#guard(terminal.checkbox({
|
|
1231
|
-
message: "Published src environments",
|
|
1232
|
-
choices: ENVIRONMENT_CHOICES,
|
|
1233
|
-
min: 0
|
|
1234
|
-
}));
|
|
1235
|
-
if (values.app !== void 0) appInput = values.app.split(",").map((candidate) => candidate.trim());
|
|
1236
|
-
else if (values.src !== void 0 || json || !this.#tty) appInput = [];
|
|
1237
|
-
else appInput = await this.#guard(terminal.checkbox({
|
|
1238
|
-
message: "Application environments",
|
|
1239
|
-
choices: ENVIRONMENT_CHOICES,
|
|
1240
|
-
min: 0
|
|
1241
|
-
}));
|
|
1242
|
-
const unrecognizedSrcEnvironment = srcInput.filter((candidate) => !ENVIRONMENTS.some((environment) => environment === candidate));
|
|
1243
|
-
if (unrecognizedSrcEnvironment.length > 0) this.#usage(`Environment "${unrecognizedSrcEnvironment.join("\", \"")}" is not recognized`, json);
|
|
1244
|
-
if (new Set(srcInput).size !== srcInput.length) this.#usage("Published src environments must not repeat", json);
|
|
1245
|
-
const src = ENVIRONMENTS.filter((environment) => srcInput.includes(environment));
|
|
1246
|
-
const unrecognizedAppEnvironment = appInput.filter((candidate) => !ENVIRONMENTS.some((environment) => environment === candidate));
|
|
1247
|
-
if (unrecognizedAppEnvironment.length > 0) this.#usage(`Application environment "${unrecognizedAppEnvironment.join("\", \"")}" is not recognized`, json);
|
|
1248
|
-
if (new Set(appInput).size !== appInput.length) this.#usage("Application environments must not repeat", json);
|
|
1249
|
-
const app = ENVIRONMENTS.filter((environment) => appInput.includes(environment));
|
|
1250
|
-
if (src.length === 0 && app.length === 0) this.#usage("at least one source or application environment is required", json);
|
|
1251
|
-
const destination = this.#contain(values.target ?? `./${name}`, json);
|
|
1252
|
-
if (!isVacant(destination)) this.#error(new ScaffoldError("TARGET", "new requires a vacant target", { target: destination }), json);
|
|
1253
|
-
const explicitHost = values.from?.[0];
|
|
1254
|
-
if (explicitHost !== void 0) {
|
|
1255
|
-
if (!isRealDirectory(explicitHost)) this.#error(new ScaffoldError("TARGET", `Host root is not a physical directory at ${explicitHost}`, { host: explicitHost }), json);
|
|
1256
|
-
readHostManifest(explicitHost);
|
|
1257
|
-
}
|
|
1258
|
-
let depNames;
|
|
1259
|
-
if (values.deps !== void 0) {
|
|
1260
|
-
depNames = values.deps.split(",").filter((depName) => depName.length > 0);
|
|
1261
|
-
const badDep = depNames.find((depName) => !DEPENDENCY_NAME_PATTERN.test(depName));
|
|
1262
|
-
if (badDep !== void 0) this.#usage(`Dependency name "${badDep}" must match ${DEPENDENCY_NAME_PATTERN.source}`, json);
|
|
1263
|
-
} else if (json || !this.#tty) depNames = [];
|
|
1264
|
-
else {
|
|
1265
|
-
const catalogHost = values.from?.[0] ?? hostRoot();
|
|
1266
|
-
const catalog = this.#names(catalogHost);
|
|
1267
|
-
if (catalog === void 0) this.#reporter.line(CATALOG_UNRESOLVED_NOTE);
|
|
1268
|
-
depNames = await this.#prompt(terminal, catalog);
|
|
1269
|
-
}
|
|
1270
|
-
const sync = createSync(this.#sync);
|
|
1271
|
-
let versions;
|
|
1272
|
-
try {
|
|
1273
|
-
versions = await sync.lookup(depNames);
|
|
1274
|
-
} finally {
|
|
1275
|
-
sync.destroy();
|
|
1276
|
-
}
|
|
1277
|
-
const unresolved = versions.filter((version) => version.freshness === "missing" || version.freshness === "failed").map((version) => version.name);
|
|
1278
|
-
if (unresolved.length > 0) this.#fail(unresolvedVersion(unresolved), json);
|
|
1279
|
-
const deps = versions.map((version) => {
|
|
1280
|
-
if (version.freshness !== "behind") this.#fail(unresolvedVersion([version.name]), json);
|
|
1281
|
-
return dependency(version.name, `^${version.latest}`);
|
|
1282
|
-
});
|
|
1283
|
-
const [plan] = this.#compile(blueprint(name, {
|
|
1284
|
-
src,
|
|
1285
|
-
app,
|
|
1286
|
-
dependencies: deps
|
|
1287
|
-
}), json);
|
|
1288
|
-
const summary = planToSummary(plan);
|
|
1289
|
-
const label = describeDestination(process.cwd(), destination);
|
|
1290
|
-
if (!json) {
|
|
1291
|
-
this.#reporter.section("Plan");
|
|
1292
|
-
this.#reporter.table(newPlanTable(summary));
|
|
1293
|
-
this.#reporter.line(newPlanPreview(label));
|
|
1294
|
-
}
|
|
1295
|
-
if (!await this.#apply(terminal, applyConfirmMessage(summary.host + summary.template + summary.computed), values, json)) {
|
|
1296
|
-
if (json) this.#write(summaryToNewResult(summary, false));
|
|
1297
|
-
else this.#reporter.line(NEW_DRY_RUN_NOTE);
|
|
1298
|
-
process.exitCode = 0;
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
const spinner = this.#spinner("materializing", json);
|
|
1302
|
-
spinner?.start();
|
|
1303
|
-
const host = values.from?.[0];
|
|
1304
|
-
const materializer = createMaterializer(host === void 0 ? void 0 : { host });
|
|
1305
|
-
try {
|
|
1306
|
-
const result = materializer.materialize(plan, destination);
|
|
1307
|
-
const count = result.written.length + result.copied.length;
|
|
1308
|
-
if (json) this.#write(summaryToNewResult(summary, true));
|
|
1309
|
-
else this.#succeed(spinner, json, newApplySuccess(count, label));
|
|
1310
|
-
} catch (error) {
|
|
1311
|
-
this.#reject(spinner, json, error);
|
|
1312
|
-
} finally {
|
|
1313
|
-
materializer.destroy();
|
|
1314
|
-
}
|
|
1315
|
-
process.exitCode = 0;
|
|
1316
|
-
}
|
|
1317
|
-
/** `scaffold pull` — refresh vendored dependency mirrors and report range drift. */
|
|
1318
|
-
async #pull(values, json) {
|
|
1319
|
-
const target = this.#contain(values.target ?? ".", json);
|
|
1320
|
-
const sync = createSync(values.strict === void 0 ? this.#sync : {
|
|
1321
|
-
...this.#sync,
|
|
1322
|
-
strict: values.strict
|
|
1323
|
-
});
|
|
1324
|
-
try {
|
|
1325
|
-
let report;
|
|
1326
|
-
try {
|
|
1327
|
-
const declared = manifestToDependencies(readManifest(target));
|
|
1328
|
-
const selected = parsePullDependencies(values.deps, declared);
|
|
1329
|
-
report = await sync.pull(target, selected);
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
this.#error(error, json);
|
|
1332
|
-
}
|
|
1333
|
-
await this.#refresh(sync, report, target, values, json, "pull", true);
|
|
1334
|
-
} finally {
|
|
1335
|
-
sync.destroy();
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
/** `scaffold mirror` — refresh every published Orkestrel package guide. */
|
|
1339
|
-
async #mirror(values, json) {
|
|
1340
|
-
const target = this.#contain(values.target ?? ".", json);
|
|
1341
|
-
const sync = createSync(values.strict === void 0 ? this.#sync : {
|
|
1342
|
-
...this.#sync,
|
|
1343
|
-
strict: values.strict
|
|
1344
|
-
});
|
|
1345
|
-
try {
|
|
1346
|
-
let report;
|
|
1347
|
-
try {
|
|
1348
|
-
report = await sync.mirror(target);
|
|
1349
|
-
} catch (error) {
|
|
1350
|
-
this.#error(error, json);
|
|
1351
|
-
}
|
|
1352
|
-
await this.#refresh(sync, report, target, values, json, "mirror", report.failed === 0);
|
|
1353
|
-
} finally {
|
|
1354
|
-
sync.destroy();
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
async #refresh(sync, report, target, values, json, action, writable) {
|
|
1358
|
-
if (!json) {
|
|
1359
|
-
this.#reporter.table(syncTable(report));
|
|
1360
|
-
for (const line of syncCauseNotes(report)) this.#reporter.line(line);
|
|
1361
|
-
this.#reporter.line(syncVerdict(report, action));
|
|
1362
|
-
}
|
|
1363
|
-
const toWrite = [...report.guides, ...report.versions].filter((entry) => entry.freshness !== "current").length;
|
|
1364
|
-
const terminal = createTerminal();
|
|
1365
|
-
const proceed = writable && toWrite > 0 ? await this.#apply(terminal, applyConfirmMessage(toWrite), values, json) : false;
|
|
1366
|
-
if (proceed) {
|
|
1367
|
-
const spinner = this.#spinner("writing mirrors", json);
|
|
1368
|
-
spinner?.start();
|
|
1369
|
-
try {
|
|
1370
|
-
const written = await sync.write(report, target);
|
|
1371
|
-
if (json) this.#write(report);
|
|
1372
|
-
else this.#succeed(spinner, json, syncSuccess(written.length));
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
this.#reject(spinner, json, error);
|
|
1375
|
-
}
|
|
1376
|
-
} else if (json) this.#write(report);
|
|
1377
|
-
process.exitCode = report.failed > 0 ? 1 : report.clean || proceed ? 0 : 1;
|
|
1378
|
-
}
|
|
1379
|
-
/** `scaffold audit` — whole-plan conformance report; offers a repair handoff on drift. */
|
|
1380
|
-
async #audit(values, json) {
|
|
1381
|
-
const target = this.#contain(values.target ?? ".", json);
|
|
1382
|
-
let spec;
|
|
1383
|
-
try {
|
|
1384
|
-
spec = deriveBlueprint(target);
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
this.#error(error, json);
|
|
1387
|
-
}
|
|
1388
|
-
const deps = [
|
|
1389
|
-
...spec.dependencies,
|
|
1390
|
-
...spec.peers,
|
|
1391
|
-
...spec.extras
|
|
1392
|
-
];
|
|
1393
|
-
const groupsInput = values.groups?.split(",");
|
|
1394
|
-
let groups;
|
|
1395
|
-
if (groupsInput !== void 0) {
|
|
1396
|
-
const unrecognized = groupsInput.filter((name) => !GROUPS.some((group) => group === name));
|
|
1397
|
-
if (unrecognized.length > 0) this.#usage(`Group "${unrecognized.join("\", \"")}" is not recognized`, json);
|
|
1398
|
-
groups = GROUPS.filter((group) => groupsInput.includes(group));
|
|
1399
|
-
}
|
|
1400
|
-
const [compiled, questions] = this.#compile(spec, json, groups);
|
|
1401
|
-
const host = values.from?.[0] ?? hostRoot();
|
|
1402
|
-
let plan;
|
|
1403
|
-
try {
|
|
1404
|
-
plan = hydratePlan(this.#promoteServices(compiled, target), host);
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
this.#error(error, json);
|
|
1407
|
-
}
|
|
1408
|
-
const artifactPaths = plan.artifacts.map((artifact) => artifact.path);
|
|
1409
|
-
const rawAudit = {
|
|
1410
|
-
...diffPlan(plan, readTarget(target, artifactPaths)),
|
|
1411
|
-
questions
|
|
1412
|
-
};
|
|
1413
|
-
const scanned = this.#scanSafe(rawAudit, target, host, spec.services);
|
|
1414
|
-
const audit = scanned.audit;
|
|
1415
|
-
let drifted = !audit.clean;
|
|
1416
|
-
let live;
|
|
1417
|
-
if (values.live) {
|
|
1418
|
-
const sync = createSync(this.#sync);
|
|
1419
|
-
try {
|
|
1420
|
-
const manifestName = manifestToName(readManifest(target));
|
|
1421
|
-
const guideDependencies = deps.filter((entry) => entry.name !== manifestName);
|
|
1422
|
-
const guides = await sync.guides(guideDependencies);
|
|
1423
|
-
const versions = await sync.versions(deps);
|
|
1424
|
-
const entries = [...guides, ...versions];
|
|
1425
|
-
drifted ||= entries.some((entry) => entry.freshness !== "current");
|
|
1426
|
-
const current = entries.filter((entry) => entry.freshness === "current").length;
|
|
1427
|
-
const behind = entries.filter((entry) => entry.freshness === "behind").length;
|
|
1428
|
-
live = {
|
|
1429
|
-
current,
|
|
1430
|
-
behind,
|
|
1431
|
-
failed: entries.length - current - behind
|
|
1432
|
-
};
|
|
1433
|
-
} finally {
|
|
1434
|
-
sync.destroy();
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
if (json) {
|
|
1438
|
-
this.#write(live === void 0 ? audit : {
|
|
1439
|
-
...audit,
|
|
1440
|
-
live
|
|
1441
|
-
});
|
|
1442
|
-
process.exitCode = drifted ? 1 : 0;
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (scanned.skipped) this.#reporter.line(SCAN_SKIPPED);
|
|
1446
|
-
for (const question of audit.questions) if (!question.blocking) this.#reporter.line(`warning: ${question.text}`);
|
|
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));
|
|
1450
|
-
this.#reporter.table(auditTable(audit, plan));
|
|
1451
|
-
this.#reporter.line(auditVerdict(audit, plan));
|
|
1452
|
-
if (live !== void 0) this.#reporter.line(auditLiveNote(live.current, live.behind, live.failed));
|
|
1453
|
-
if (!audit.clean) {
|
|
1454
|
-
const split = partitionFindings(audit.findings, plan);
|
|
1455
|
-
const replace = values.replace === true;
|
|
1456
|
-
const ownedCount = split.owned.missing + (replace ? split.owned.drifted : 0);
|
|
1457
|
-
const pruneRequested = values.prune === true;
|
|
1458
|
-
const offerHandoff = this.#tty && (ownedCount > 0 || audit.foreign > 0 && pruneRequested);
|
|
1459
|
-
let handoffAccepted = false;
|
|
1460
|
-
if (offerHandoff) {
|
|
1461
|
-
const terminal = createTerminal();
|
|
1462
|
-
const message = repairHandoff(split.owned.missing, replace ? split.owned.drifted : 0, audit.foreign, pruneRequested);
|
|
1463
|
-
handoffAccepted = await this.#guard(terminal.confirm({
|
|
1464
|
-
message,
|
|
1465
|
-
default: false
|
|
1466
|
-
}));
|
|
1467
|
-
if (handoffAccepted) {
|
|
1468
|
-
await this.#repair(values, false);
|
|
1469
|
-
const rawFinal = diffPlan(plan, readTarget(target, artifactPaths));
|
|
1470
|
-
const finalScanned = this.#scanSafe(rawFinal, target, host, spec.services);
|
|
1471
|
-
process.exitCode = finalScanned.audit.clean ? 0 : 1;
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
if (!handoffAccepted) {
|
|
1476
|
-
if (split.owned.drifted > 0 && !replace) this.#reporter.line(replacementNote(split.owned.drifted));
|
|
1477
|
-
if (audit.foreign > 0 && !pruneRequested) this.#reporter.line(FOREIGN_HINT);
|
|
1478
|
-
for (const note of renderComputedNotes(audit.findings, plan)) this.#reporter.line(note);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
process.exitCode = drifted ? 1 : 0;
|
|
1482
|
-
}
|
|
1483
|
-
/** `scaffold repair` — restore host-owned files and optionally generated canon for one target. */
|
|
1484
|
-
async #repair(values, json) {
|
|
1485
|
-
const target = this.#contain(values.target ?? ".", json);
|
|
1486
|
-
let spec;
|
|
1487
|
-
try {
|
|
1488
|
-
spec = deriveBlueprint(target);
|
|
1489
|
-
} catch (error) {
|
|
1490
|
-
this.#error(error, json);
|
|
1491
|
-
}
|
|
1492
|
-
const generated = values.generated === true;
|
|
1493
|
-
const replace = values.replace === true;
|
|
1494
|
-
const host = values.from?.[0] ?? hostRoot();
|
|
1495
|
-
const prepared = this.#prepareRepair(spec, target, host, generated, json);
|
|
1496
|
-
let compiled = prepared[0];
|
|
1497
|
-
let plan = prepared[1];
|
|
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
|
-
};
|
|
1505
|
-
if (!json) {
|
|
1506
|
-
if (!outcome.clean) this.#reporter.line(scopeLine("repair", generated));
|
|
1507
|
-
this.#reporter.section("Audit");
|
|
1508
|
-
this.#reporter.table(auditTable(outcome, plan));
|
|
1509
|
-
}
|
|
1510
|
-
const prunePaths = values.prune ? this.#prunePaths(target, host, spec.services) : [];
|
|
1511
|
-
const pruneSnapshot = readTarget(target, prunePaths);
|
|
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;
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
const terminal = createTerminal();
|
|
1532
|
-
let proceed = true;
|
|
1533
|
-
if (!audit.clean) proceed = await this.#apply(terminal, applyConfirmMessage(repairable), values, json);
|
|
1534
|
-
if (!proceed) {
|
|
1535
|
-
this.#emitRepair(outcome, generated, json);
|
|
1536
|
-
process.exitCode = 1;
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
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
|
-
}
|
|
1543
|
-
const doPrune = prunePaths.length > 0 && await this.#prune(terminal, pruneConfirmMessage(prunePaths.length), values, json);
|
|
1544
|
-
let currentGlobal;
|
|
1545
|
-
try {
|
|
1546
|
-
currentGlobal = deriveBlueprint(target).global;
|
|
1547
|
-
} catch (error) {
|
|
1548
|
-
this.#error(error, json);
|
|
1549
|
-
}
|
|
1550
|
-
if (currentGlobal !== spec.global) {
|
|
1551
|
-
spec = {
|
|
1552
|
-
...spec,
|
|
1553
|
-
global: currentGlobal
|
|
1554
|
-
};
|
|
1555
|
-
const refreshed = this.#prepareRepair(spec, target, host, generated, json);
|
|
1556
|
-
compiled = refreshed[0];
|
|
1557
|
-
plan = refreshed[1];
|
|
1558
|
-
audit = refreshed[2];
|
|
1559
|
-
outcome = repairAuditOf(this.#scanSafe(audit, target, host, spec.services).audit, this.#outside(compiled, plan, target, host));
|
|
1560
|
-
}
|
|
1561
|
-
const spinner = this.#spinner("repairing", json);
|
|
1562
|
-
spinner?.start();
|
|
1563
|
-
const materializer = createMaterializer({ host });
|
|
1564
|
-
try {
|
|
1565
|
-
const result = materializer.repair(plan, audit, target, replace);
|
|
1566
|
-
const removed = doPrune ? materializer.prune(target, pruneSnapshot).removed : [];
|
|
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, {
|
|
1576
|
-
...result,
|
|
1577
|
-
removed
|
|
1578
|
-
});
|
|
1579
|
-
process.exitCode = hasFindings(finalAudit, finalAudit.outside) ? 1 : 0;
|
|
1580
|
-
} catch (error) {
|
|
1581
|
-
this.#reject(spinner, json, error);
|
|
1582
|
-
} finally {
|
|
1583
|
-
materializer.destroy();
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
/** `scaffold fleet` — audit/repair every `@orkestrel` package beneath the current directory's immediate children. */
|
|
1587
|
-
async #fleet(values, json) {
|
|
1588
|
-
const root = this.#contain(".", json);
|
|
1589
|
-
const generated = values.generated === true;
|
|
1590
|
-
const replace = values.replace === true;
|
|
1591
|
-
const packages = discoverPackages(root);
|
|
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);
|
|
1593
|
-
const host = values.from?.[0] ?? hostRoot();
|
|
1594
|
-
const repos = [];
|
|
1595
|
-
const failures = [];
|
|
1596
|
-
for (const directory of packages) {
|
|
1597
|
-
const name = basename(directory);
|
|
1598
|
-
try {
|
|
1599
|
-
const compiler = createCompiler();
|
|
1600
|
-
let compiled;
|
|
1601
|
-
let scoped;
|
|
1602
|
-
let questions;
|
|
1603
|
-
try {
|
|
1604
|
-
const spec = deriveBlueprint(directory);
|
|
1605
|
-
const scaffolding = compiler.compile(spec);
|
|
1606
|
-
if (!scaffolding.plan) {
|
|
1607
|
-
const message = scaffolding.questions.map((question) => question.text).join("; ");
|
|
1608
|
-
throw new ScaffoldError("INVALID", message);
|
|
1609
|
-
}
|
|
1610
|
-
compiled = scaffolding.plan;
|
|
1611
|
-
scoped = this.#scopeRepair(compiled, directory, generated);
|
|
1612
|
-
questions = scaffolding.questions;
|
|
1613
|
-
} finally {
|
|
1614
|
-
compiler.destroy();
|
|
1615
|
-
}
|
|
1616
|
-
const plan = hydratePlan(scoped, host);
|
|
1617
|
-
const paths = plan.artifacts.map((artifact) => artifact.path);
|
|
1618
|
-
const rawAudit = {
|
|
1619
|
-
...diffPlan(plan, readTarget(directory, paths)),
|
|
1620
|
-
questions
|
|
1621
|
-
};
|
|
1622
|
-
const audit = this.#scan(rawAudit, directory, host, plan.blueprint.services);
|
|
1623
|
-
const outside = this.#outside(compiled, plan, directory, host);
|
|
1624
|
-
repos.push({
|
|
1625
|
-
name,
|
|
1626
|
-
directory,
|
|
1627
|
-
plan,
|
|
1628
|
-
rawAudit,
|
|
1629
|
-
audit,
|
|
1630
|
-
outside
|
|
1631
|
-
});
|
|
1632
|
-
} catch (error) {
|
|
1633
|
-
failures.push({
|
|
1634
|
-
name,
|
|
1635
|
-
message: describeError(error)
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
if (!json) {
|
|
1640
|
-
for (const repo of repos) {
|
|
1641
|
-
this.#reporter.line(fleetRepoLine(repo.name, repo.audit.clean ? { state: "clean" } : {
|
|
1642
|
-
state: "drifted",
|
|
1643
|
-
drifted: repo.audit.drifted,
|
|
1644
|
-
missing: repo.audit.missing,
|
|
1645
|
-
foreign: repo.audit.foreign
|
|
1646
|
-
}));
|
|
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}`);
|
|
1650
|
-
}
|
|
1651
|
-
for (const failure of failures) this.#reporter.line(fleetRepoLine(failure.name, {
|
|
1652
|
-
state: "failed",
|
|
1653
|
-
message: failure.message
|
|
1654
|
-
}));
|
|
1655
|
-
}
|
|
1656
|
-
const dirty = repos.filter((repo) => !repo.audit.clean);
|
|
1657
|
-
const reportedDirty = repos.filter((repo) => hasFindings(repo.audit, repo.outside));
|
|
1658
|
-
if (dirty.length === 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;
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
const fileCount = countWrites(dirty.map((repo) => repo.audit), replace);
|
|
1665
|
-
if (!json && values.apply === true) this.#reporter.line(scopeLine("fleet", generated, dirty.length));
|
|
1666
|
-
const terminal = createTerminal();
|
|
1667
|
-
const proceed = await this.#apply(terminal, applyConfirmMessage(fileCount, dirty.length), values, json);
|
|
1668
|
-
const pruneSets = proceed && values.prune ? new Map(dirty.map((repo) => {
|
|
1669
|
-
const paths = this.#prunePaths(repo.directory, host, repo.plan.blueprint.services);
|
|
1670
|
-
return [repo.name, readTarget(repo.directory, paths)];
|
|
1671
|
-
})) : /* @__PURE__ */ new Map();
|
|
1672
|
-
const prunePaths = dirty.flatMap((repo) => Object.keys(pruneSets.get(repo.name) ?? {}).map((path) => `${repo.name}/${path}`));
|
|
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
|
-
}
|
|
1677
|
-
const doPrune = proceed && prunePaths.length > 0 && await this.#prune(terminal, pruneConfirmMessage(prunePaths.length), values, json);
|
|
1678
|
-
if (!proceed) {
|
|
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));
|
|
1681
|
-
process.exitCode = 1;
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
const materializer = createMaterializer({ host });
|
|
1685
|
-
let drifted = repos.filter((repo) => repo.audit.clean && repo.outside > 0).length;
|
|
1686
|
-
let failedCount = failures.length;
|
|
1687
|
-
const entries = repos.filter((repo) => repo.audit.clean).map((repo) => fleetEntryOf(repo.name, repo.audit, false, repo.outside));
|
|
1688
|
-
try {
|
|
1689
|
-
for (const repo of dirty) try {
|
|
1690
|
-
materializer.repair(repo.plan, repo.rawAudit, repo.directory, replace);
|
|
1691
|
-
if (doPrune) materializer.prune(repo.directory, pruneSets.get(repo.name) ?? {});
|
|
1692
|
-
const paths = repo.plan.artifacts.map((artifact) => artifact.path);
|
|
1693
|
-
const rawFinal = diffPlan(repo.plan, readTarget(repo.directory, paths));
|
|
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));
|
|
1697
|
-
if (!json) this.#reporter.line(fleetRepoLine(repo.name, {
|
|
1698
|
-
state: "repaired",
|
|
1699
|
-
remaining: finalAudit.drifted + finalAudit.missing + finalAudit.foreign + repo.outside
|
|
1700
|
-
}));
|
|
1701
|
-
} catch (error) {
|
|
1702
|
-
failedCount += 1;
|
|
1703
|
-
entries.push(fleetEntryOf(repo.name, void 0, true));
|
|
1704
|
-
if (!json) this.#reporter.line(fleetRepoLine(repo.name, {
|
|
1705
|
-
state: "failed",
|
|
1706
|
-
message: describeError(error)
|
|
1707
|
-
}));
|
|
1708
|
-
}
|
|
1709
|
-
} finally {
|
|
1710
|
-
materializer.destroy();
|
|
1711
|
-
}
|
|
1712
|
-
if (json) this.#write([...entries, ...failures.map((failure) => fleetEntryOf(failure.name, void 0, true))]);
|
|
1713
|
-
else this.#reporter.line(fleetTotals(drifted, failedCount));
|
|
1714
|
-
process.exitCode = drifted > 0 || failedCount > 0 ? 1 : 0;
|
|
1715
|
-
}
|
|
1716
|
-
/** `scaffold catalog` — regenerate the fleet package catalog table embedded in `.claude/agents/orkestrel.md`. */
|
|
1717
|
-
async #catalog(values, json) {
|
|
1718
|
-
const target = this.#contain(values.target ?? ".", json);
|
|
1719
|
-
const explicitRoots = values.from;
|
|
1720
|
-
let entries;
|
|
1721
|
-
let published = 0;
|
|
1722
|
-
let localOnly = 0;
|
|
1723
|
-
const notes = /* @__PURE__ */ new Map();
|
|
1724
|
-
if (values.offline) {
|
|
1725
|
-
const roots = explicitRoots ?? [process.cwd()];
|
|
1726
|
-
try {
|
|
1727
|
-
entries = catalogPackages(roots);
|
|
1728
|
-
} catch (error) {
|
|
1729
|
-
this.#error(error, json);
|
|
1730
|
-
}
|
|
1731
|
-
} else {
|
|
1732
|
-
const sync = createSync(this.#sync);
|
|
1733
|
-
sync.emitter.on("package", (name, note) => {
|
|
1734
|
-
if (note !== "") notes.set(name, note);
|
|
1735
|
-
});
|
|
1736
|
-
let registryEntries;
|
|
1737
|
-
try {
|
|
1738
|
-
registryEntries = await sync.catalog();
|
|
1739
|
-
} catch (error) {
|
|
1740
|
-
this.#error(error, json);
|
|
1741
|
-
} finally {
|
|
1742
|
-
sync.destroy();
|
|
1743
|
-
}
|
|
1744
|
-
published = registryEntries.length;
|
|
1745
|
-
let localEntries = [];
|
|
1746
|
-
if (explicitRoots !== void 0) try {
|
|
1747
|
-
localEntries = catalogPackages(explicitRoots);
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
this.#error(error, json);
|
|
1750
|
-
}
|
|
1751
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1752
|
-
for (const entry of registryEntries) merged.set(entry.name, entry);
|
|
1753
|
-
for (const local of localEntries) {
|
|
1754
|
-
const existing = merged.get(local.name);
|
|
1755
|
-
if (existing === void 0) {
|
|
1756
|
-
merged.set(local.name, local);
|
|
1757
|
-
localOnly += 1;
|
|
1758
|
-
} else if (local.description.length > 0) merged.set(local.name, {
|
|
1759
|
-
...existing,
|
|
1760
|
-
description: local.description
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
entries = [...merged.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1764
|
-
}
|
|
1765
|
-
const block = catalogToBlock(entries);
|
|
1766
|
-
const agentPath = this.#contain(join(target, CATALOG_AGENT_PATH), json);
|
|
1767
|
-
let current;
|
|
1768
|
-
let baseline;
|
|
1769
|
-
try {
|
|
1770
|
-
current = readFileText(target, CATALOG_AGENT_PATH, "TARGET", "target");
|
|
1771
|
-
baseline = digestText(current);
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
this.#error(new ScaffoldError("TARGET", `Failed to read ${agentPath}`, {
|
|
1774
|
-
path: agentPath,
|
|
1775
|
-
error
|
|
1776
|
-
}), json);
|
|
1777
|
-
}
|
|
1778
|
-
const startIndex = current.indexOf(CATALOG_START_MARKER);
|
|
1779
|
-
const endIndex = current.indexOf(CATALOG_END_MARKER);
|
|
1780
|
-
const startParts = current.split(CATALOG_START_MARKER);
|
|
1781
|
-
const endParts = current.split(CATALOG_END_MARKER);
|
|
1782
|
-
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex || startParts.length !== 2 || endParts.length !== 2) this.#error(new ScaffoldError("TARGET", `Expected exactly one ordered "${CATALOG_START_MARKER}" / "${CATALOG_END_MARKER}" pair in ${agentPath}`, { path: agentPath }), json);
|
|
1783
|
-
const updated = `${current.slice(0, startIndex + CATALOG_START_MARKER.length)}\n\n${block}\n${current.slice(endIndex)}`;
|
|
1784
|
-
const oldBlock = current.slice(startIndex + CATALOG_START_MARKER.length, endIndex);
|
|
1785
|
-
const oldRows = catalogNames(oldBlock).length;
|
|
1786
|
-
const shrink = entries.length < oldRows ? oldRows - entries.length : void 0;
|
|
1787
|
-
if (updated === current) {
|
|
1788
|
-
if (json) this.#write(catalogResultOf(entries, false));
|
|
1789
|
-
else this.#reporter.line(catalogVerdict(true));
|
|
1790
|
-
process.exitCode = 0;
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
if (!json) {
|
|
1794
|
-
this.#reporter.table(catalogTable(entries));
|
|
1795
|
-
const warning = catalogShrinkWarning(oldRows, entries.length);
|
|
1796
|
-
if (warning !== void 0) this.#reporter.line(warning);
|
|
1797
|
-
if (values.offline) {
|
|
1798
|
-
const missingDescription = entries.filter((entry) => entry.description.length === 0).map((entry) => entry.name);
|
|
1799
|
-
if (missingDescription.length > 0) this.#reporter.line(`${missingDescription.length} without guide description: ${missingDescription.join(", ")}`);
|
|
1800
|
-
} else {
|
|
1801
|
-
this.#reporter.line(catalogCounts(published, localOnly));
|
|
1802
|
-
for (const [name, note] of notes) this.#reporter.line(` ${name}: ${note}`);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
const terminal = createTerminal();
|
|
1806
|
-
if (!await this.#apply(terminal, applyConfirmMessage(1), values, json)) {
|
|
1807
|
-
if (json) this.#write(catalogResultOf(entries, true, shrink));
|
|
1808
|
-
else this.#reporter.line(catalogVerdict(false));
|
|
1809
|
-
process.exitCode = 1;
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
if (Buffer.byteLength(updated, "utf8") > MAX_ARTIFACT_BYTES) this.#error(new ScaffoldError("WRITE", `Catalog exceeds the artifact limit at ${agentPath}`, {
|
|
1813
|
-
path: agentPath,
|
|
1814
|
-
limit: MAX_ARTIFACT_BYTES
|
|
1815
|
-
}), json);
|
|
1816
|
-
const transaction = WriteTransaction.create(target, [CATALOG_AGENT_PATH], [{
|
|
1817
|
-
path: CATALOG_AGENT_PATH,
|
|
1818
|
-
shape: "file",
|
|
1819
|
-
digest: baseline
|
|
1820
|
-
}]);
|
|
1821
|
-
const staged = attempt(() => {
|
|
1822
|
-
validateWriteDirectories(transaction);
|
|
1823
|
-
const destination = resolvePhysicalPath(transaction.stage, CATALOG_AGENT_PATH, "WRITE", "staging");
|
|
1824
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
1825
|
-
validateWriteDirectories(transaction);
|
|
1826
|
-
const contained = resolvePhysicalPath(transaction.stage, CATALOG_AGENT_PATH, "WRITE", "staging");
|
|
1827
|
-
writeFileSync(contained, updated, {
|
|
1828
|
-
encoding: "utf8",
|
|
1829
|
-
flag: "wx"
|
|
1830
|
-
});
|
|
1831
|
-
if (digestFile(contained) !== digestText(updated)) throw new ScaffoldError("WRITE", `Staged catalog changed at ${agentPath}`, { path: agentPath });
|
|
1832
|
-
validateWriteDirectories(transaction);
|
|
1833
|
-
});
|
|
1834
|
-
if (!staged.success) {
|
|
1835
|
-
const cleanup = attempt(() => discardWriteTransaction(transaction));
|
|
1836
|
-
this.#error(new ScaffoldError("WRITE", `Failed to stage ${agentPath}`, {
|
|
1837
|
-
path: agentPath,
|
|
1838
|
-
error: staged.error,
|
|
1839
|
-
cleanup: cleanup.success ? void 0 : cleanup.error
|
|
1840
|
-
}), json);
|
|
1841
|
-
}
|
|
1842
|
-
const committed = attempt(() => commitWriteTransaction(transaction, [CATALOG_AGENT_PATH]));
|
|
1843
|
-
if (!committed.success) this.#error(committed.error, json);
|
|
1844
|
-
if (json) this.#write(catalogResultOf(entries, true, shrink));
|
|
1845
|
-
else this.#reporter.status("success", catalogApplySuccess(agentPath));
|
|
1846
|
-
process.exitCode = 0;
|
|
1847
|
-
}
|
|
1848
|
-
/**
|
|
1849
|
-
* The whole command dispatch — a single top-level driver (no nested function
|
|
1850
|
-
* declarations, AGENTS §4). Every verb sets `process.exitCode` (never
|
|
1851
|
-
* `process.exit`) and returns, or `halt()`s through a `finally` that
|
|
1852
|
-
* tears its entities down first; the caller at the bottom of this file
|
|
1853
|
-
* catches exactly one sentinel (`CliExit`) and stops.
|
|
1854
|
-
*/
|
|
1855
|
-
async #dispatch(argv) {
|
|
1856
|
-
let parsed;
|
|
1857
|
-
try {
|
|
1858
|
-
parsed = parseArguments(argv);
|
|
1859
|
-
} catch (error) {
|
|
1860
|
-
process.stderr.write(`${error instanceof Error ? error.message : INVALID_ARGUMENTS_MESSAGE}\n`);
|
|
1861
|
-
process.exitCode = 2;
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
const { values, positionals } = parsed;
|
|
1865
|
-
const [command, argument] = positionals;
|
|
1866
|
-
const json = values.json === true;
|
|
1867
|
-
this.#json = json;
|
|
1868
|
-
if (command === void 0) {
|
|
1869
|
-
process.stdout.write(`${values.help ? fullHelp() : shortUsage()}\n`);
|
|
1870
|
-
process.exitCode = 0;
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
if (!isVerb(command)) this.#usage(didYouMean(command), json);
|
|
1874
|
-
if (command !== "catalog" && values.from !== void 0 && values.from.length > 1) this.#usage(`--from may be provided only once for '${command}'`, json);
|
|
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);
|
|
1877
|
-
if (values.help) {
|
|
1878
|
-
process.stdout.write(`${verbHelp(command)}\n`);
|
|
1879
|
-
process.exitCode = 0;
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
if (command === "new") return this.#new(values, argument, json);
|
|
1883
|
-
if (command === "pull") return this.#pull(values, json);
|
|
1884
|
-
if (command === "mirror") return this.#mirror(values, json);
|
|
1885
|
-
if (command === "audit") return this.#audit(values, json);
|
|
1886
|
-
if (command === "repair") return this.#repair(values, json);
|
|
1887
|
-
if (command === "fleet") return this.#fleet(values, json);
|
|
1888
|
-
return this.#catalog(values, json);
|
|
1889
|
-
}
|
|
1890
|
-
};
|
|
1891
|
-
//#endregion
|
|
1892
|
-
//#region src/bin/scaffold.ts
|
|
1893
|
-
await new CLI().run(process.argv.slice(2));
|
|
1894
|
-
//#endregion
|
|
1895
|
-
|
|
1896
|
-
//# sourceMappingURL=scaffold.js.map
|