@biffo/cli 0.149.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +68 -7
  2. 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 result.data.version;
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
- writeFileSync(join(cwd, INSTANCE_CORE_FILE), serializeInstanceCoreVersion(version));
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) {
@@ -1536,8 +1564,19 @@ function planMigrationCarry(options) {
1536
1564
  const entries = [];
1537
1565
  const skipped = [];
1538
1566
  const recognised = [];
1567
+ const declinedIndex = new Map((options.declined ?? []).map((d) => [d.file, d]));
1568
+ const declined = [];
1539
1569
  let head = instanceHead;
1540
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
+ }
1541
1580
  const already = alreadyCarried(m, identity, templateBodyCounts);
1542
1581
  if (already) {
1543
1582
  skipped.push(m.file);
@@ -1584,7 +1623,9 @@ function planMigrationCarry(options) {
1584
1623
  ],
1585
1624
  `${MIGRATIONS_VERSIONS_DIR} (after carry)`
1586
1625
  );
1587
- return { entries, instanceHead, skipped, recognised };
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 };
1588
1629
  }
1589
1630
  function indexInstanceMigrations(instance) {
1590
1631
  const index = {
@@ -2452,7 +2493,11 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
2452
2493
  console.log(` merge base: ${fromVersion}`);
2453
2494
  console.log(` target: ${toVersion}
2454
2495
  `);
2455
- const migrations = planMigrationCarry({ templateDir: theirsDir, instanceDir: options.cwd });
2496
+ const migrations = planMigrationCarry({
2497
+ templateDir: theirsDir,
2498
+ instanceDir: options.cwd,
2499
+ declined: readDeclinedMigrations(options.cwd)
2500
+ });
2456
2501
  const coreVersionCleanup = planCoreVersionCleanup(options.cwd);
2457
2502
  if (plan.changes.length === 0 && migrations.entries.length === 0) {
2458
2503
  log.success("Nothing to upgrade \u2014 the instance already matches the target for all core files.");
@@ -2689,6 +2734,22 @@ function printMigrationCarry(migrations) {
2689
2734
  ` ${chalk4.dim("already carried".padEnd(15))} ${r.file} ` + chalk4.dim(`\u2192 this instance calls it ${r.instanceFile} (matched by ${r.how})`)
2690
2735
  );
2691
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
+ }
2692
2753
  for (const e of migrations.entries) {
2693
2754
  const suffix = e.reissuedFrom ? chalk4.dim(
2694
2755
  ` (revision ${e.revision}, re-issued from ${e.reissuedFrom}; revises ${e.downRevision ?? "base"})`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.149.0",
3
+ "version": "0.150.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",