@theholocron/cli 4.15.0 → 4.16.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/README.md CHANGED
@@ -275,6 +275,20 @@ export default acmeConfig;
275
275
 
276
276
  ```
277
277
 
278
+ ## Upgrading
279
+
280
+ - **`holocron upgrade node <to>`** — patches every Node.js version pin
281
+ (`engines.node`, `.nvmrc`, `.node-version`, `.github/workflows/*`,
282
+ `Dockerfile`, `.tool-versions`) to a new major.
283
+ - **`holocron upgrade deps`** — bumps every `@theholocron/*` pin in
284
+ `pnpm-workspace.yaml` (`catalog:` and every named `catalogs.*`) to its latest
285
+ published version, then migrates `holocron.config.ts` from the 7.x preset
286
+ shape (`const { repo, workflows } = node()` + `repo.requiredChecks` +
287
+ `workflows:`) to the current composed-preset API (`const preset = node()` +
288
+ `...preset` + `tasks:` + `extraRequiredChecks:`). Mechanical transforms only —
289
+ review the result, then `pnpm install && holocron ci`. `--pins-only` skips
290
+ the config migration; `--dry-run` prints without writing.
291
+
278
292
  ## Auth — fine-grained tokens
279
293
 
280
294
  Each GitHub capability resolves its own fine-grained PAT so a compromised
@@ -385,7 +399,7 @@ holds the orchestration + the Holocron-specific credential resolution
385
399
  never hand-edit its `.github/workflows/*`).
386
400
  - `src/commands/` — `setup`, `sync`, `doctor`, `deploy`, `secret set`,
387
401
  `secrets sync`, `npm publish-initial`, `sync-github`, `upgrade node`,
388
- `plugin create`, `auth`
402
+ `upgrade deps`, `plugin create`, `auth`
389
403
 
390
404
  ## Status
391
405
 
package/dist/cli.mjs CHANGED
@@ -6234,6 +6234,224 @@ async function runSyncGithub(input) {
6234
6234
  });
6235
6235
  }
6236
6236
  //#endregion
6237
+ //#region src/commands/upgrade-deps.ts
6238
+ /**
6239
+ * `holocron upgrade deps` — bring a repo's `@theholocron/*` dependency pins and
6240
+ * its `holocron.config.ts` up to the current major.
6241
+ *
6242
+ * Two concerns:
6243
+ *
6244
+ * 1. **Catalog pins.** Every `@theholocron/*` entry in `pnpm-workspace.yaml`
6245
+ * (`catalog:` and every named `catalogs.*`) is bumped to the latest published
6246
+ * version, preserving the range prefix (`^` / `~`). Never downgrades.
6247
+ * 2. **`holocron.config.ts` migration.** `@theholocron/holocron-config@8`
6248
+ * dropped the 7.x preset shape (`const { repo, workflows, providers } =
6249
+ * node()` + `repo.requiredChecks` + `workflows:`) for a composed-preset
6250
+ * (`const preset = node()` + `...preset` + `tasks:` + `extraRequiredChecks:`).
6251
+ * This applies the mechanical transforms and flags anything that needs a
6252
+ * human eye.
6253
+ *
6254
+ * After running, `pnpm install && holocron ci`.
6255
+ */
6256
+ const FETCH_TIMEOUT_MS$1 = 4e3;
6257
+ /** Resolve a package's `latest` dist-tag from the npm registry. */
6258
+ const fetchLatestFromNpm = async (pkg) => {
6259
+ try {
6260
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, {
6261
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$1),
6262
+ headers: { accept: "application/vnd.npm.install-v1+json" }
6263
+ });
6264
+ if (!res.ok) return null;
6265
+ return (await res.json())["dist-tags"]?.["latest"] ?? null;
6266
+ } catch {
6267
+ return null;
6268
+ }
6269
+ };
6270
+ /** `a > b` for `x.y.z` release versions (prerelease tags are ignored). */
6271
+ function gt(a, b) {
6272
+ const pa = a.split(".").map((n) => parseInt(n, 10));
6273
+ const pb = b.split(".").map((n) => parseInt(n, 10));
6274
+ for (let i = 0; i < 3; i++) {
6275
+ const x = pa[i] ?? 0;
6276
+ const y = pb[i] ?? 0;
6277
+ if (x !== y) return x > y;
6278
+ }
6279
+ return false;
6280
+ }
6281
+ const CATALOG_LINE = /^(\s*['"]?)(@theholocron\/[a-z0-9-]+)(['"]?\s*:\s*['"]?)([~^]?)(\d+\.\d+\.\d+[\w.-]*)(['"]?\s*)$/;
6282
+ /**
6283
+ * Rewrite every `@theholocron/*` pin in a `pnpm-workspace.yaml` to `latest`,
6284
+ * keeping the range prefix. `latest` is looked up once per package.
6285
+ */
6286
+ async function bumpCatalogs(content, fetchLatest) {
6287
+ const lines = content.split("\n");
6288
+ const wanted = /* @__PURE__ */ new Set();
6289
+ for (const line of lines) {
6290
+ const m = CATALOG_LINE.exec(line);
6291
+ if (m) wanted.add(m[2]);
6292
+ }
6293
+ const latest = /* @__PURE__ */ new Map();
6294
+ await Promise.all([...wanted].map(async (pkg) => {
6295
+ latest.set(pkg, await fetchLatest(pkg));
6296
+ }));
6297
+ const bumps = [];
6298
+ return {
6299
+ content: lines.map((line) => {
6300
+ const m = CATALOG_LINE.exec(line);
6301
+ if (!m) return line;
6302
+ const [, pre, pkg, mid, prefix, current, post] = m;
6303
+ const next = latest.get(pkg);
6304
+ if (!next || next === current || !gt(next, current)) return line;
6305
+ bumps.push({
6306
+ pkg,
6307
+ from: current,
6308
+ to: next
6309
+ });
6310
+ return `${pre}${pkg}${mid}${prefix}${next}${post}`;
6311
+ }).join("\n"),
6312
+ bumps
6313
+ };
6314
+ }
6315
+ /** True if the file still uses the 7.x preset shape. */
6316
+ function isLegacyConfig(content) {
6317
+ return /const\s*\{[^}]*\b(repo|workflows|providers)\b[^}]*\}\s*=\s*(compose\(|[a-zA-Z]+\(\))/.test(content) || /\.\.\.repo\.requiredChecks/.test(content) || /^\s*workflows\s*:/m.test(content);
6318
+ }
6319
+ /**
6320
+ * Apply the mechanical 7.x → 8.x transforms to a `holocron.config.ts`. Best
6321
+ * effort — the result should always be reviewed and run through `holocron ci`.
6322
+ */
6323
+ function migrateConfig(content) {
6324
+ const transforms = [];
6325
+ const warnings = [];
6326
+ let s = content;
6327
+ if (!isLegacyConfig(s)) return {
6328
+ content,
6329
+ changed: false,
6330
+ transforms,
6331
+ warnings
6332
+ };
6333
+ s = s.replace(/const\s*\{[^}]*\}\s*=\s*((?:compose\([\s\S]*?\)|[a-zA-Z]+\([\s\S]*?\)))\s*;/, (_m, expr) => {
6334
+ transforms.push("destructured preset → `const preset = …`");
6335
+ return `const preset = ${expr.trim()};`;
6336
+ });
6337
+ if (/const preset =/.test(s) && !/\.\.\.preset\b/.test(s)) {
6338
+ s = s.replace(/(defineConfig\(\{\s*\n)/, `$1\t...preset,\n`);
6339
+ transforms.push("spread `...preset` into defineConfig()");
6340
+ }
6341
+ s = s.replace(/^\torg,\n/m, "");
6342
+ s = s.replace(/^\tdomain,\n/m, "");
6343
+ s = s.replace(/^\tdocs,\n/m, "");
6344
+ if (/\.\.\.repo\.properties/.test(s)) {
6345
+ s = s.replace(/\.\.\.repo\.properties/g, "...preset.repo?.properties");
6346
+ transforms.push("`...repo.properties` → `...preset.repo?.properties`");
6347
+ }
6348
+ if (/([{[,]\s*)\.\.\.repo\b(?!\.)/.test(s)) {
6349
+ s = s.replace(/([{[,]\s*)\.\.\.repo\b(?!\.)/g, "$1...preset.repo");
6350
+ transforms.push("`...repo` → `...preset.repo`");
6351
+ }
6352
+ if (/\.\.\.providers\b/.test(s)) {
6353
+ s = s.replace(/\.\.\.providers\b/g, "...preset.providers");
6354
+ transforms.push("`...providers` → `...preset.providers`");
6355
+ }
6356
+ const rc = /(\n\s*)requiredChecks\s*:\s*\[\s*\n?\s*\.\.\.repo\.requiredChecks\s*,?\s*([\s\S]*?)\n\s*\]\s*,/.exec(s);
6357
+ if (rc) {
6358
+ const custom = rc[2].trim().replace(/,\s*$/, "");
6359
+ s = s.replace(rc[0], "");
6360
+ if (custom) {
6361
+ const block = `\textraRequiredChecks: [\n\t\t${custom.split("\n").map((l) => l.trim()).filter(Boolean).join("\n ")}\n\t],\n`;
6362
+ s = s.replace(/(defineConfig\(\{\s*\n(?:\t\.\.\.preset,\n)?)/, `$1${block}`);
6363
+ transforms.push("`repo.requiredChecks` custom entries → top-level `extraRequiredChecks`");
6364
+ warnings.push("check the moved `extraRequiredChecks` — the preset already contributes lint/test/typecheck");
6365
+ } else {
6366
+ s = s.replace(/,(\s*)\.\.\.repo\.requiredChecks/g, "$1");
6367
+ transforms.push("dropped `...repo.requiredChecks`");
6368
+ }
6369
+ }
6370
+ if (/^\s*workflows\s*:/m.test(s)) {
6371
+ s = s.replace(/(\n\s*)workflows(\s*:\s*\[)/, "$1tasks$2");
6372
+ s = s.replace(/\.\.\.workflows\b/g, "...preset.tasks");
6373
+ transforms.push("`workflows:` → `tasks:` (`...workflows` → `...preset.tasks`)");
6374
+ warnings.push("review the `tasks` array — a bare workflow name that should gate merges now needs `{ name, required: true }`");
6375
+ }
6376
+ return {
6377
+ content: s,
6378
+ changed: s !== content,
6379
+ transforms,
6380
+ warnings
6381
+ };
6382
+ }
6383
+ async function runUpgradeDeps(input = {}) {
6384
+ const print = input.print ?? ((line) => console.log(line));
6385
+ const logger = input.logger ?? getLogger();
6386
+ const cwd = input.cwd ?? process.cwd();
6387
+ const dryRun = input.dryRun ?? false;
6388
+ const pinsOnly = input.pinsOnly ?? false;
6389
+ const _readFile = input.readFile ?? ((p) => readFileSync(p, "utf8"));
6390
+ const _writeFile = input.writeFile ?? ((p, c) => writeFileSync(p, c));
6391
+ const fetchLatest = input.fetchLatest ?? fetchLatestFromNpm;
6392
+ logger.info({
6393
+ dryRun: dryRun || void 0,
6394
+ pinsOnly: pinsOnly || void 0
6395
+ }, "upgrade deps: start");
6396
+ print(`Upgrading @theholocron/* dependencies${dryRun ? " (dry-run)" : ""}…`);
6397
+ const wsPath = join(cwd, "pnpm-workspace.yaml");
6398
+ let bumps = [];
6399
+ try {
6400
+ const result = await bumpCatalogs(_readFile(wsPath), fetchLatest);
6401
+ bumps = result.bumps;
6402
+ if (bumps.length > 0) {
6403
+ if (!dryRun) _writeFile(wsPath, result.content);
6404
+ print("");
6405
+ print(` pnpm-workspace.yaml — ${bumps.length} pin${bumps.length === 1 ? "" : "s"}`);
6406
+ for (const b of bumps) print(` ${dryRun ? "~" : "✓"} ${b.pkg} ${b.from} → ${b.to}`);
6407
+ } else print(" · pnpm-workspace.yaml — @theholocron/* pins already current");
6408
+ } catch {
6409
+ print(" · no pnpm-workspace.yaml — skipping catalog pins");
6410
+ }
6411
+ let configMigrated = false;
6412
+ let configWarnings = [];
6413
+ if (!pinsOnly) for (const name of ["holocron.config.ts", "holocron.config.js"]) {
6414
+ let cfg;
6415
+ try {
6416
+ cfg = _readFile(join(cwd, name));
6417
+ } catch {
6418
+ continue;
6419
+ }
6420
+ const m = migrateConfig(cfg);
6421
+ if (!m.changed) {
6422
+ print(` · ${name} — already on the current preset API`);
6423
+ break;
6424
+ }
6425
+ if (!dryRun) _writeFile(join(cwd, name), m.content);
6426
+ configMigrated = true;
6427
+ configWarnings = m.warnings;
6428
+ print("");
6429
+ print(` ${name} — ${dryRun ? "would migrate" : "migrated"} to the 8.x preset API`);
6430
+ for (const t of m.transforms) print(` ${dryRun ? "~" : "✓"} ${t}`);
6431
+ for (const w of m.warnings) print(` ⚠ ${w}`);
6432
+ break;
6433
+ }
6434
+ print("");
6435
+ if (bumps.length === 0 && !configMigrated) print("Nothing to do — already up to date.");
6436
+ else {
6437
+ print(`Next: ${dryRun ? "run without --dry-run, then " : ""}\`pnpm install && holocron ci\``);
6438
+ if (configMigrated) print(" review holocron.config.ts before committing.");
6439
+ }
6440
+ const status = dryRun ? "dry-run" : "ok";
6441
+ logger.info({
6442
+ bumps: bumps.length,
6443
+ configMigrated,
6444
+ warnings: configWarnings.length,
6445
+ status
6446
+ }, "upgrade deps: done");
6447
+ return {
6448
+ status,
6449
+ bumps,
6450
+ configMigrated,
6451
+ configWarnings
6452
+ };
6453
+ }
6454
+ //#endregion
6237
6455
  //#region src/commands/upgrade-node.ts
6238
6456
  const SKIP_DIRS = /* @__PURE__ */ new Set([
6239
6457
  "node_modules",
@@ -7433,6 +7651,20 @@ try {
7433
7651
  if (report.message) getLogger().error(`upgrade node: ${report.message}`);
7434
7652
  process.exitCode = 1;
7435
7653
  }
7654
+ }).command("deps", "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API", (yy) => yy.option("pins-only", {
7655
+ type: "boolean",
7656
+ default: false,
7657
+ describe: "Only bump the catalog pins — skip the holocron.config.ts migration"
7658
+ }), async (argv) => {
7659
+ const report = await runUpgradeDeps({
7660
+ cwd: argv.cwd,
7661
+ dryRun: argv.dryRun,
7662
+ pinsOnly: argv.pinsOnly
7663
+ });
7664
+ if (report.status === "fail") {
7665
+ if (report.message) getLogger().error(`upgrade deps: ${report.message}`);
7666
+ process.exitCode = 1;
7667
+ }
7436
7668
  }).demandCommand(1, "Run `holocron upgrade --help` to see available upgrade subcommands."), () => {}).command("auth <subcommand>", "Manage bootstrap credentials in the OS keyring", (y) => y.command("set <provider> [value]", "Verify + store a bootstrap token for a provider", (yy) => yy.positional("provider", {
7437
7669
  type: "string",
7438
7670
  demandOption: true