@theholocron/cli 4.14.1 → 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
@@ -307,8 +321,8 @@ holocron auth list # show all stored providers
307
321
 
308
322
  ## Logging
309
323
 
310
- Operational output goes through [`@theholocron/logger`](../logger) — separate from
311
- the user-facing `print` surface. Global flags:
324
+ Operational output goes through `@theholocron/observability/logger` — separate
325
+ from the user-facing `print` surface. Global flags:
312
326
 
313
327
  ```sh
314
328
  holocron doctor --verbose # log level → debug (full structured output)
@@ -356,9 +370,11 @@ export default defineConfig({
356
370
  });
357
371
  ```
358
372
 
359
- Each SDK sits behind a Holocron-owned interface — `ErrorSink` (Sentry) /
360
- `AnalyticsSink` (PostHog) in `src/telemetry/sinks.ts`; `@sentry/node` and
361
- `posthog-node` are imported only from their adapter modules. See the
373
+ Each SDK sits behind an interface from `@theholocron/observability` —
374
+ `ErrorSink` (Sentry) / `AnalyticsSink` (PostHog); the adapters, and the only
375
+ `@sentry/node` / `posthog-node` imports, live in that package. `telemetry.ts`
376
+ holds the orchestration + the Holocron-specific credential resolution
377
+ (`telemetry/resolve.ts`). See the
362
378
  [telemetry guide](https://docs.theholocron.dev/holocron/telemetry/).
363
379
 
364
380
  ## What's in here
@@ -370,8 +386,8 @@ Each SDK sits behind a Holocron-owned interface — `ErrorSink` (Sentry) /
370
386
  - `src/config/load-config.ts` — `loadConfig` — reads `holocron.config.*`
371
387
  (file discovery via [`@theholocron/datapad`](../datapad))
372
388
  - `src/define-config.ts` — `defineConfig` typed pass-through
373
- - `src/logger.ts` — CLI-side `@theholocron/logger` wiring (`buildCliLogger`,
374
- `resolveLogLevel`)
389
+ - `src/logger.ts` — CLI-side `@theholocron/observability/logger` wiring
390
+ (`buildCliLogger`, `resolveLogLevel`)
375
391
  - `src/loader.ts` — `PluginLoader` — dynamic-imports plugins, resolves
376
392
  capability config packages, builds the capability registry
377
393
  - `src/cli.ts` — yargs entry, dispatches subcommands. `holocron run` / `ci` are
@@ -383,7 +399,7 @@ Each SDK sits behind a Holocron-owned interface — `ErrorSink` (Sentry) /
383
399
  never hand-edit its `.github/workflows/*`).
384
400
  - `src/commands/` — `setup`, `sync`, `doctor`, `deploy`, `secret set`,
385
401
  `secrets sync`, `npm publish-initial`, `sync-github`, `upgrade node`,
386
- `plugin create`, `auth`
402
+ `upgrade deps`, `plugin create`, `auth`
387
403
 
388
404
  ## Status
389
405
 
package/dist/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1, cr
11
11
  import { createEnvLookup } from "@theholocron/env-utils";
12
12
  import { Entry, findCredentials } from "@napi-rs/keyring";
13
13
  import { pathToFileURL } from "node:url";
14
- import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/logger";
14
+ import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/observability/logger";
15
15
  import ora from "ora";
16
16
  import chalk from "chalk";
17
17
  import { execFile, execFileSync, spawnSync } from "node:child_process";
@@ -21,8 +21,9 @@ import { createHash } from "node:crypto";
21
21
  import { generateReadme } from "@theholocron/components-doc/markdown";
22
22
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
23
23
  import { createGitHubClient } from "@theholocron/github-client";
24
- import { PostHog } from "posthog-node";
25
- import * as Sentry from "@sentry/node";
24
+ import { PostHogSink } from "@theholocron/observability/analytics";
25
+ import { NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/observability/core";
26
+ import { SentrySink } from "@theholocron/observability/errors";
26
27
  import { promisify } from "node:util";
27
28
  import { ConfigFileError, loadConfigFile } from "@theholocron/datapad";
28
29
  //#region src/env.ts
@@ -299,13 +300,6 @@ function resolveConfig(raw) {
299
300
  //#endregion
300
301
  //#region src/logger.ts
301
302
  /**
302
- * CLI-side wiring for `@theholocron/logger`.
303
- *
304
- * `logger` is the operational-output channel — internal state, debug
305
- * traces, errors, structured context that routes to Axiom. It runs in
306
- * parallel to `print` (user-facing UX output) and does not replace it.
307
- */
308
- /**
309
303
  * Resolve the explicit level to hand to `createLogger`, in priority order:
310
304
  *
311
305
  * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
@@ -327,7 +321,7 @@ let rootCommand;
327
321
  let rootAxiomKey;
328
322
  /**
329
323
  * Resolve Axiom credentials for the CLI. Env vars win — same contract as
330
- * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
324
+ * `@theholocron/observability`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
331
325
  * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
332
326
  * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
333
327
  * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
@@ -5821,58 +5815,6 @@ async function writeWorkflowFile(repoRoot, filename, content) {
5821
5815
  await writeFile(join(dir, filename), content, "utf8");
5822
5816
  }
5823
5817
  //#endregion
5824
- //#region src/telemetry/posthog-sink.ts
5825
- /**
5826
- * `PostHogSink` — the **only** `posthog-node` call site. A portable
5827
- * `AnalyticsSink` adapter: usage, adoption, retention. It reads no environment
5828
- * and ships no credentials — the constructor takes a resolved `key` + `host`;
5829
- * the caller (the CLI's `telemetry.ts`, via `telemetry/resolve.ts`) owns the
5830
- * env chain and the fallback. An empty `key` is the caller's signal to install
5831
- * a `NoopAnalyticsSink` instead — it is never passed here.
5832
- *
5833
- * Destined for `@theholocron/observability/analytics` (#635).
5834
- */
5835
- var PostHogSink = class {
5836
- #client;
5837
- constructor(config) {
5838
- this.#client = new PostHog(config.key, { host: config.host });
5839
- }
5840
- identify(distinctId, props) {
5841
- this.#client.identify({
5842
- distinctId,
5843
- properties: props
5844
- });
5845
- }
5846
- capture(distinctId, event, props) {
5847
- this.#client.capture({
5848
- distinctId,
5849
- event,
5850
- properties: props
5851
- });
5852
- }
5853
- async shutdown() {
5854
- await this.#client.shutdown();
5855
- }
5856
- };
5857
- //#endregion
5858
- //#region src/telemetry/redact.ts
5859
- /**
5860
- * Token-shape scrubbing for telemetry payloads — a raw token never leaves the
5861
- * process, even when a value is controlled. Regex-on-the-serialised-string
5862
- * (matches a token shape *anywhere*), distinct from `@theholocron/logger`'s
5863
- * field-path redaction. Shared by {@link scrubError} (Sentry `beforeSend`) and
5864
- * the PostHog `event()` path.
5865
- */
5866
- const TOKEN_RE = /\b(ghp_|ghs_|glpat-|xoxb-|xoxp-|npm_|sk-|[A-Z][A-Z0-9_]{2,}_TOKEN[=\s])[^\s"]*/g;
5867
- /** Replace token-shaped substrings in a string with `[REDACTED]`. */
5868
- function redact(raw) {
5869
- return raw.replace(TOKEN_RE, "[REDACTED]");
5870
- }
5871
- /** Deep-scrub every string in an object by round-tripping through {@link redact}. */
5872
- function redactObject(value) {
5873
- return JSON.parse(redact(JSON.stringify(value)));
5874
- }
5875
- //#endregion
5876
5818
  //#region src/telemetry/resolve.ts
5877
5819
  /**
5878
5820
  * Holocron-specific credential resolution for the telemetry sinks — the env
@@ -5908,76 +5850,16 @@ function resolvePostHogHost() {
5908
5850
  return env.get("HOLOCRON_POSTHOG_HOST") ?? env.get("POSTHOG_HOST") ?? DEFAULT_POSTHOG_HOST;
5909
5851
  }
5910
5852
  //#endregion
5911
- //#region src/telemetry/sentry-sink.ts
5912
- /** `beforeSend` — token-shaped strings never leave the process. */
5913
- function scrubError(event, _hint) {
5914
- return redactObject(event);
5915
- }
5916
- var SentrySink = class {
5917
- init(ctx) {
5918
- Sentry.init({
5919
- dsn: ctx.dsn,
5920
- release: ctx.release,
5921
- environment: ctx.environment,
5922
- tracesSampleRate: 1,
5923
- beforeSend: scrubError
5924
- });
5925
- Sentry.startSession();
5926
- for (const [key, value] of Object.entries(ctx.tags)) Sentry.setTag(key, value);
5927
- }
5928
- startSpan(name) {
5929
- Sentry.setTag("command", name);
5930
- const span = Sentry.startInactiveSpan({
5931
- name,
5932
- op: "holocron.command",
5933
- forceTransaction: true
5934
- });
5935
- return {
5936
- setStatus: (ok) => span.setStatus({ code: ok ? 1 : 2 }),
5937
- end: () => span.end()
5938
- };
5939
- }
5940
- captureException(err) {
5941
- Sentry.captureException(err);
5942
- }
5943
- endSession() {
5944
- Sentry.endSession();
5945
- }
5946
- async flush() {
5947
- await Sentry.close(2e3);
5948
- }
5949
- };
5950
- //#endregion
5951
- //#region src/telemetry/sinks.ts
5952
- /**
5953
- * The disabled path made explicit — one shared no-op instead of a scatter of
5954
- * `if (!enabled) return` guards. Used when telemetry is opted out
5955
- * (`HOLOCRON_TELEMETRY=false`, `config.telemetry.enabled: false`) or no
5956
- * credentials resolve.
5957
- */
5958
- var NoopErrorSink = class {
5959
- init() {}
5960
- startSpan() {}
5961
- captureException() {}
5962
- endSession() {}
5963
- async flush() {}
5964
- };
5965
- /** @see NoopErrorSink */
5966
- var NoopAnalyticsSink = class {
5967
- identify() {}
5968
- capture() {}
5969
- async shutdown() {}
5970
- };
5971
- //#endregion
5972
5853
  //#region src/telemetry.ts
5973
5854
  /**
5974
5855
  * CLI self-telemetry orchestration — errors (Sentry), usage analytics (PostHog).
5975
5856
  *
5976
- * This module holds *no* vendor SDK import. Each concern sits behind a
5977
- * Holocron-owned interface ({@link ErrorSink} / {@link AnalyticsSink}); the
5978
- * concrete `SentrySink` / `PostHogSink` are the only `@sentry/node` /
5979
- * `posthog-node` call sites, and a `Noop*Sink` is installed when telemetry is
5980
- * off. Same seam `@theholocron/logger` uses for Pino.
5857
+ * This module holds *no* vendor SDK import. Each concern sits behind an
5858
+ * interface from `@theholocron/observability` ({@link ErrorSink} /
5859
+ * {@link AnalyticsSink}); the `SentrySink` / `PostHogSink` adapters there are
5860
+ * the only `@sentry/node` / `posthog-node` call sites, and a `Noop*Sink` is
5861
+ * installed when telemetry is off. Same seam the package's `Logger` uses for
5862
+ * Pino.
5981
5863
  *
5982
5864
  * Activation is env-var + shipped-fallback, resolved here (`telemetry/resolve.ts`)
5983
5865
  * and passed to the sink — the adapters read no environment. The single kill
@@ -6352,6 +6234,224 @@ async function runSyncGithub(input) {
6352
6234
  });
6353
6235
  }
6354
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
6355
6455
  //#region src/commands/upgrade-node.ts
6356
6456
  const SKIP_DIRS = /* @__PURE__ */ new Set([
6357
6457
  "node_modules",
@@ -7551,6 +7651,20 @@ try {
7551
7651
  if (report.message) getLogger().error(`upgrade node: ${report.message}`);
7552
7652
  process.exitCode = 1;
7553
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
+ }
7554
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", {
7555
7669
  type: "string",
7556
7670
  demandOption: true