@biffo/cli 0.148.2 → 0.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +84 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,8 +25,19 @@ import { dirname, join, parse as parsePath } from "path";
|
|
|
25
25
|
import { fileURLToPath } from "url";
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
var SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
28
|
+
var DeclinedMigrationSchema = z.object({
|
|
29
|
+
/** The *template's* filename for the migration, e.g. `0010_add_organizations.py`. */
|
|
30
|
+
file: z.string({ required_error: "file is required \u2014 it must name a template migration" }).min(1, "file is required \u2014 it must name a template migration"),
|
|
31
|
+
// `required_error` as well as `min(1)`: zod reports a bare "Required" for an
|
|
32
|
+
// absent key, which would lose the guidance in the exact case most likely to
|
|
33
|
+
// occur — someone adding an entry and omitting the field.
|
|
34
|
+
reason: z.string({ required_error: "reason is required \u2014 a decline nobody can review is drift" }).min(1, "reason is required \u2014 a decline nobody can review is drift"),
|
|
35
|
+
/** Optional `owner/repo#123` recording where the decline is being resolved. */
|
|
36
|
+
upstream: z.string().optional()
|
|
37
|
+
});
|
|
28
38
|
var CoreManifestSchema = z.object({
|
|
29
|
-
version: z.string().regex(SEMVER, "must be a semver, e.g. 1.2.3")
|
|
39
|
+
version: z.string().regex(SEMVER, "must be a semver, e.g. 1.2.3"),
|
|
40
|
+
declinedMigrations: z.array(DeclinedMigrationSchema).optional()
|
|
30
41
|
});
|
|
31
42
|
var CORE_VERSION_FILE = "core.version";
|
|
32
43
|
var INSTANCE_CORE_FILE = "biffo.core.json";
|
|
@@ -112,6 +123,9 @@ function readInstanceCoreVersion(cwd) {
|
|
|
112
123
|
const inherited = join(cwd, CORE_VERSION_FILE);
|
|
113
124
|
return existsSync(inherited) ? readCoreVersionFile(inherited) : null;
|
|
114
125
|
}
|
|
126
|
+
return parseInstanceCoreManifest(path).validated.version;
|
|
127
|
+
}
|
|
128
|
+
function parseInstanceCoreManifest(path) {
|
|
115
129
|
let parsed;
|
|
116
130
|
try {
|
|
117
131
|
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -123,7 +137,15 @@ function readInstanceCoreVersion(cwd) {
|
|
|
123
137
|
const detail = result.error.issues[0]?.message ?? "unexpected shape";
|
|
124
138
|
throw new Error(`${INSTANCE_CORE_FILE} is invalid: ${detail}`);
|
|
125
139
|
}
|
|
126
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
validated: result.data,
|
|
142
|
+
raw: parsed ?? {}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function readDeclinedMigrations(cwd) {
|
|
146
|
+
const path = join(cwd, INSTANCE_CORE_FILE);
|
|
147
|
+
if (!existsSync(path)) return [];
|
|
148
|
+
return parseInstanceCoreManifest(path).validated.declinedMigrations ?? [];
|
|
127
149
|
}
|
|
128
150
|
function planCoreVersionCleanup(cwd) {
|
|
129
151
|
const path = join(cwd, CORE_VERSION_FILE);
|
|
@@ -150,13 +172,19 @@ function coreVersionsEqual(a, b) {
|
|
|
150
172
|
return false;
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
|
-
function serializeInstanceCoreVersion(version) {
|
|
175
|
+
function serializeInstanceCoreVersion(version, rest = {}) {
|
|
154
176
|
parseCoreVersion(version);
|
|
155
|
-
return `${JSON.stringify({ version }, null, 2)}
|
|
177
|
+
return `${JSON.stringify({ version, ...rest }, null, 2)}
|
|
156
178
|
`;
|
|
157
179
|
}
|
|
158
180
|
function writeInstanceCoreVersion(cwd, version) {
|
|
159
|
-
|
|
181
|
+
const path = join(cwd, INSTANCE_CORE_FILE);
|
|
182
|
+
let rest = {};
|
|
183
|
+
if (existsSync(path)) {
|
|
184
|
+
rest = { ...parseInstanceCoreManifest(path).raw };
|
|
185
|
+
delete rest.version;
|
|
186
|
+
}
|
|
187
|
+
writeFileSync(path, serializeInstanceCoreVersion(version, rest));
|
|
160
188
|
}
|
|
161
189
|
function latestCoreVersionFromTags(repo, git = defaultTagRunner, options = {}) {
|
|
162
190
|
if (options.fetch !== false) {
|
|
@@ -350,10 +378,11 @@ var coreDiffCommand = new Command("diff").description(
|
|
|
350
378
|
).option("--cwd <path>", "Instance repo root to inspect (defaults to the current directory)").option(
|
|
351
379
|
"--template <path>",
|
|
352
380
|
"Path to a biffo-template checkout to compare against (defaults to the template this CLI ships with)"
|
|
353
|
-
).action(async (options) => {
|
|
381
|
+
).option("--json", "Emit the classification as JSON on stdout instead of the human report").action(async (options) => {
|
|
354
382
|
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
355
383
|
const runOptions = { cwd };
|
|
356
384
|
if (options.template) runOptions.templateRoot = resolve(options.template);
|
|
385
|
+
if (options.json) runOptions.json = true;
|
|
357
386
|
try {
|
|
358
387
|
await runCoreDiff(runOptions);
|
|
359
388
|
} catch (err) {
|
|
@@ -368,6 +397,20 @@ async function runCoreDiff(options) {
|
|
|
368
397
|
const instanceVersion = readInstanceCoreVersion(options.cwd);
|
|
369
398
|
const diff = computeCoreDiff(templateRoot, options.cwd, manifest);
|
|
370
399
|
const total = diff.added.length + diff.removed.length + diff.modified.length;
|
|
400
|
+
if (options.json) {
|
|
401
|
+
const payload = {
|
|
402
|
+
schemaVersion: 1,
|
|
403
|
+
instanceCore: instanceVersion ?? null,
|
|
404
|
+
templateCore: templateVersion,
|
|
405
|
+
modified: diff.modified,
|
|
406
|
+
added: diff.added,
|
|
407
|
+
removed: diff.removed,
|
|
408
|
+
instanceOnly: diff.instanceOnly,
|
|
409
|
+
unchanged: diff.unchanged
|
|
410
|
+
};
|
|
411
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
371
414
|
console.log(chalk2.bold("\n Biffo core diff\n"));
|
|
372
415
|
console.log(` instance core: ${instanceVersion ?? chalk2.dim("(unrecorded)")}`);
|
|
373
416
|
console.log(` template core: ${templateVersion}
|
|
@@ -1521,8 +1564,19 @@ function planMigrationCarry(options) {
|
|
|
1521
1564
|
const entries = [];
|
|
1522
1565
|
const skipped = [];
|
|
1523
1566
|
const recognised = [];
|
|
1567
|
+
const declinedIndex = new Map((options.declined ?? []).map((d) => [d.file, d]));
|
|
1568
|
+
const declined = [];
|
|
1524
1569
|
let head = instanceHead;
|
|
1525
1570
|
for (const m of chainOrder(template)) {
|
|
1571
|
+
const decline = declinedIndex.get(m.file);
|
|
1572
|
+
if (decline) {
|
|
1573
|
+
declined.push({
|
|
1574
|
+
file: m.file,
|
|
1575
|
+
reason: decline.reason,
|
|
1576
|
+
...decline.upstream !== void 0 && { upstream: decline.upstream }
|
|
1577
|
+
});
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1526
1580
|
const already = alreadyCarried(m, identity, templateBodyCounts);
|
|
1527
1581
|
if (already) {
|
|
1528
1582
|
skipped.push(m.file);
|
|
@@ -1569,7 +1623,9 @@ function planMigrationCarry(options) {
|
|
|
1569
1623
|
],
|
|
1570
1624
|
`${MIGRATIONS_VERSIONS_DIR} (after carry)`
|
|
1571
1625
|
);
|
|
1572
|
-
|
|
1626
|
+
const templateFiles = new Set(template.map((m) => m.file));
|
|
1627
|
+
const staleDeclines = [...declinedIndex.keys()].filter((f) => !templateFiles.has(f));
|
|
1628
|
+
return { entries, instanceHead, skipped, recognised, declined, staleDeclines };
|
|
1573
1629
|
}
|
|
1574
1630
|
function indexInstanceMigrations(instance) {
|
|
1575
1631
|
const index = {
|
|
@@ -2437,7 +2493,11 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
|
|
|
2437
2493
|
console.log(` merge base: ${fromVersion}`);
|
|
2438
2494
|
console.log(` target: ${toVersion}
|
|
2439
2495
|
`);
|
|
2440
|
-
const migrations = planMigrationCarry({
|
|
2496
|
+
const migrations = planMigrationCarry({
|
|
2497
|
+
templateDir: theirsDir,
|
|
2498
|
+
instanceDir: options.cwd,
|
|
2499
|
+
declined: readDeclinedMigrations(options.cwd)
|
|
2500
|
+
});
|
|
2441
2501
|
const coreVersionCleanup = planCoreVersionCleanup(options.cwd);
|
|
2442
2502
|
if (plan.changes.length === 0 && migrations.entries.length === 0) {
|
|
2443
2503
|
log.success("Nothing to upgrade \u2014 the instance already matches the target for all core files.");
|
|
@@ -2674,6 +2734,22 @@ function printMigrationCarry(migrations) {
|
|
|
2674
2734
|
` ${chalk4.dim("already carried".padEnd(15))} ${r.file} ` + chalk4.dim(`\u2192 this instance calls it ${r.instanceFile} (matched by ${r.how})`)
|
|
2675
2735
|
);
|
|
2676
2736
|
}
|
|
2737
|
+
for (const d of migrations.declined) {
|
|
2738
|
+
const upstream = d.upstream ? chalk4.dim(` [${d.upstream}]`) : "";
|
|
2739
|
+
console.log(
|
|
2740
|
+
` ${chalk4.yellow("declined".padEnd(15))} ${d.file} ` + chalk4.dim(`\u2192 skipped per biffo.core.json: ${d.reason}`) + upstream
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
for (const f of migrations.staleDeclines) {
|
|
2744
|
+
console.log(
|
|
2745
|
+
` ${chalk4.yellow("stale decline".padEnd(15))} ${f} ` + chalk4.dim("\u2192 declined in biffo.core.json, but the target template has no such migration.")
|
|
2746
|
+
);
|
|
2747
|
+
console.log(
|
|
2748
|
+
chalk4.dim(
|
|
2749
|
+
" Either the filename is wrong (declining nothing) or the decline\n has outlived its cause and should be deleted."
|
|
2750
|
+
)
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2677
2753
|
for (const e of migrations.entries) {
|
|
2678
2754
|
const suffix = e.reissuedFrom ? chalk4.dim(
|
|
2679
2755
|
` (revision ${e.revision}, re-issued from ${e.reissuedFrom}; revises ${e.downRevision ?? "base"})`
|