@theholocron/cli 3.61.0 → 3.62.1

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/cli.mjs CHANGED
@@ -9,9 +9,9 @@ import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1, cr
9
9
  import { createEnvLookup } from "@theholocron/env-utils";
10
10
  import { Entry, findCredentials } from "@napi-rs/keyring";
11
11
  import { pathToFileURL } from "node:url";
12
+ import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/logger";
12
13
  import ora from "ora";
13
14
  import chalk from "chalk";
14
- import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/logger";
15
15
  import { execFile, execFileSync, spawnSync } from "node:child_process";
16
16
  import { homedir } from "node:os";
17
17
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
@@ -288,6 +288,94 @@ function resolveConfig(raw) {
288
288
  };
289
289
  }
290
290
  //#endregion
291
+ //#region src/logger.ts
292
+ /**
293
+ * CLI-side wiring for `@theholocron/logger`.
294
+ *
295
+ * `logger` is the operational-output channel — internal state, debug
296
+ * traces, errors, structured context that routes to Axiom. It runs in
297
+ * parallel to `print` (user-facing UX output) and does not replace it.
298
+ */
299
+ /**
300
+ * Resolve the explicit level to hand to `createLogger`, in priority order:
301
+ *
302
+ * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
303
+ * 3. `HOLOCRON_LOG_LEVEL` env var
304
+ * 4. `holocron.config` `log.level` (`configLevel`)
305
+ *
306
+ * Returns `undefined` when nothing applies — `createLogger` then defaults
307
+ * to `"info"`. Resolving the full chain here (rather than passing
308
+ * `configLevel` straight through) keeps config below the env var.
309
+ */
310
+ function resolveLogLevel(argv, configLevel) {
311
+ if (argv.verbose) return "debug";
312
+ if (argv.quiet) return "error";
313
+ return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
314
+ }
315
+ let root;
316
+ let rootLevel;
317
+ let rootCommand;
318
+ let rootAxiomKey;
319
+ /**
320
+ * Resolve Axiom credentials for the CLI. Env vars win — same contract as
321
+ * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
322
+ * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
323
+ * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
324
+ * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
325
+ * token and a dataset are found.
326
+ */
327
+ function resolveCliAxiom(opts) {
328
+ const fromEnv = resolveAxiomFromEnv();
329
+ if (fromEnv) return fromEnv;
330
+ const dataset = env.get("HOLOCRON_AXIOM_DATASET") || env.get("AXIOM_DATASET") || opts.configAxiomDataset;
331
+ if (!dataset) return void 0;
332
+ const org = opts.org ?? env.get("HOLOCRON_ORG");
333
+ const token = (org ? getToken(`axiom.${org}`) : null) ?? getToken("axiom");
334
+ return token ? {
335
+ dataset,
336
+ token
337
+ } : void 0;
338
+ }
339
+ /**
340
+ * The process-wide root logger. Built once (from `cli.ts`'s middleware,
341
+ * with the command name + flags + env). Rebuilt at most once more when a
342
+ * command's handler supplies `holocron.config` context the flag/env-only
343
+ * first pass could not have known — `log.level` (unless `--verbose` /
344
+ * `--quiet` already fixed it) or `log.axiom.dataset` + the resolved org
345
+ * for a keyring-backed Axiom transport. That rebuild generates a fresh
346
+ * `runId`, which is harmless: nothing logs between the middleware and the
347
+ * handler.
348
+ */
349
+ function buildCliLogger(argv, opts = {}) {
350
+ const { command, configLevel } = opts;
351
+ if (command) rootCommand = command;
352
+ const level = resolveLogLevel(argv, configLevel);
353
+ const axiom = resolveCliAxiom(opts);
354
+ const axiomKey = axiom?.dataset;
355
+ const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
356
+ if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
357
+ const built = createLogger({
358
+ ...level ? { level } : {},
359
+ ...axiom ? { axiom } : {}
360
+ });
361
+ root = {
362
+ logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
363
+ runId: built.runId
364
+ };
365
+ rootLevel = level;
366
+ rootAxiomKey = axiomKey;
367
+ }
368
+ return root;
369
+ }
370
+ /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
371
+ function getLogger() {
372
+ return (root ??= createLogger()).logger;
373
+ }
374
+ /** The current root logger's correlation id, if a root has been built. */
375
+ function getRunId() {
376
+ return root?.runId;
377
+ }
378
+ //#endregion
291
379
  //#region src/ui/progress.ts
292
380
  /**
293
381
  * Runs `fn`, showing an ora spinner for its duration in TTY environments.
@@ -356,6 +444,7 @@ function resolveAuthSetToken(input) {
356
444
  }
357
445
  async function runAuthSet(input) {
358
446
  const print = input.print ?? ((l) => console.log(l));
447
+ const logger = input.logger ?? getLogger();
359
448
  const importer = input.importer ?? defaultImporter$1;
360
449
  const { provider } = input;
361
450
  const keyringKey = input.org ? `${provider}.${input.org}` : provider;
@@ -374,6 +463,11 @@ async function runAuthSet(input) {
374
463
  const hint = await tryLoadHint(importer, packageName);
375
464
  if (hint) print(style.hint(` hint: ${hint}`));
376
465
  }
466
+ logger.warn({
467
+ provider,
468
+ keyringKey,
469
+ reason: "no token supplied"
470
+ }, `auth set: ${keyringKey}`);
377
471
  return {
378
472
  status: "fail",
379
473
  message: "no token supplied"
@@ -387,6 +481,12 @@ async function runAuthSet(input) {
387
481
  if (!verified.ok) {
388
482
  print(style.fail(`token rejected by ${provider}: ${verified.message}`));
389
483
  if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
484
+ logger.warn({
485
+ provider,
486
+ keyringKey,
487
+ verified: false,
488
+ reason: verified.message
489
+ }, `auth set: ${keyringKey}`);
390
490
  return {
391
491
  status: "fail",
392
492
  message: verified.message
@@ -401,12 +501,24 @@ async function runAuthSet(input) {
401
501
  }
402
502
  if (!setToken(keyringKey, token)) {
403
503
  print(style.fail(`keyring unavailable — token not stored. Use env vars instead.`));
504
+ logger.warn({
505
+ provider,
506
+ keyringKey,
507
+ reason: "keyring unavailable"
508
+ }, `auth set: ${keyringKey}`);
404
509
  return {
405
510
  status: "fail",
406
511
  message: "keyring unavailable"
407
512
  };
408
513
  }
409
514
  print(style.success(`stored ${keyringKey} token${subject ? ` (${subject})` : ""}`));
515
+ logger.info({
516
+ provider,
517
+ keyringKey,
518
+ verified: subject !== void 0,
519
+ ...subject ? { subject } : {},
520
+ status: "ok"
521
+ }, `auth set: ${keyringKey}`);
410
522
  return {
411
523
  status: "ok",
412
524
  ...subject ? { message: subject } : {}
@@ -414,11 +526,20 @@ async function runAuthSet(input) {
414
526
  }
415
527
  function runAuthUnset(input) {
416
528
  const print = input.print ?? ((l) => console.log(l));
529
+ const logger = input.logger ?? getLogger();
417
530
  if (deleteToken(input.provider)) {
418
531
  print(style.success(`removed ${input.provider} token`));
532
+ logger.info({
533
+ provider: input.provider,
534
+ status: "ok"
535
+ }, `auth unset: ${input.provider}`);
419
536
  return { status: "ok" };
420
537
  }
421
538
  print(style.dim(`no stored token for ${input.provider}`));
539
+ logger.info({
540
+ provider: input.provider,
541
+ status: "skip"
542
+ }, `auth unset: ${input.provider}`);
422
543
  return {
423
544
  status: "skip",
424
545
  message: "nothing to remove"
@@ -426,12 +547,19 @@ function runAuthUnset(input) {
426
547
  }
427
548
  async function runAuthCheck(input) {
428
549
  const print = input.print ?? ((l) => console.log(l));
550
+ const logger = input.logger ?? getLogger();
429
551
  const importer = input.importer ?? defaultImporter$1;
430
552
  const { provider } = input;
431
553
  const keyringKey = input.org ? `${provider}.${input.org}` : provider;
432
554
  const token = getToken(keyringKey);
433
555
  if (!token) {
434
556
  print(style.dim(`no stored token for ${keyringKey}`));
557
+ logger.info({
558
+ provider,
559
+ keyringKey,
560
+ status: "skip",
561
+ reason: "no stored token"
562
+ }, `auth check: ${keyringKey}`);
435
563
  return {
436
564
  status: "skip",
437
565
  message: "no stored token"
@@ -458,6 +586,13 @@ async function runAuthCheck(input) {
458
586
  const verified = input.showSpinner !== false ? await withSpinner(`Verifying ${keyringKey} token…`, verify) : await verify();
459
587
  if (verified.ok) {
460
588
  print(style.success(`${keyringKey}: ok — ${verified.subject}`));
589
+ logger.info({
590
+ provider,
591
+ keyringKey,
592
+ verified: true,
593
+ subject: verified.subject,
594
+ status: "ok"
595
+ }, `auth check: ${keyringKey}`);
461
596
  return {
462
597
  status: "ok",
463
598
  message: verified.subject
@@ -465,6 +600,13 @@ async function runAuthCheck(input) {
465
600
  }
466
601
  print(style.fail(`${keyringKey}: rejected — ${verified.message}`));
467
602
  if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
603
+ logger.warn({
604
+ provider,
605
+ keyringKey,
606
+ verified: false,
607
+ reason: verified.message,
608
+ status: "fail"
609
+ }, `auth check: ${keyringKey}`);
468
610
  return {
469
611
  status: "fail",
470
612
  message: verified.message
@@ -472,6 +614,12 @@ async function runAuthCheck(input) {
472
614
  } catch (err) {
473
615
  const msg = err instanceof Error ? err.message : String(err);
474
616
  print(style.fail(`${keyringKey}: cannot verify — ${msg}`));
617
+ logger.warn({
618
+ provider,
619
+ keyringKey,
620
+ reason: msg,
621
+ status: "fail"
622
+ }, `auth check: ${keyringKey}`);
475
623
  return {
476
624
  status: "fail",
477
625
  message: msg
@@ -480,28 +628,42 @@ async function runAuthCheck(input) {
480
628
  }
481
629
  async function runAuthList(input = {}) {
482
630
  const print = input.print ?? ((l) => console.log(l));
631
+ const logger = input.logger ?? getLogger();
483
632
  const importer = input.importer ?? defaultImporter$1;
484
633
  const providers = listStoredProviders();
485
634
  if (providers.length === 0) {
486
635
  print(style.dim("no stored tokens."));
487
636
  print(style.hint("run: holocron auth set <provider> <token>"));
637
+ logger.info({ providers: 0 }, "auth list: done");
488
638
  return {
489
639
  status: "ok",
490
640
  message: "none"
491
641
  };
492
642
  }
643
+ let ok = 0;
644
+ let fail = 0;
493
645
  for (const provider of providers.sort()) {
494
646
  const check = await runAuthCheck({
495
647
  provider,
496
648
  importer,
497
649
  print: () => {},
650
+ logger,
498
651
  showSpinner: false
499
652
  });
500
653
  const label = `${provider}${check.message ? ` — ${check.message}` : ""}`;
501
- if (check.status === "ok") print(` ${style.success(label)}`);
502
- else if (check.status === "fail") print(` ${style.fail(label)}`);
503
- else print(` ${style.dim(`· ${label}`)}`);
654
+ if (check.status === "ok") {
655
+ ok += 1;
656
+ print(` ${style.success(label)}`);
657
+ } else if (check.status === "fail") {
658
+ fail += 1;
659
+ print(` ${style.fail(label)}`);
660
+ } else print(` ${style.dim(`· ${label}`)}`);
504
661
  }
662
+ logger.info({
663
+ providers: providers.length,
664
+ ok,
665
+ fail
666
+ }, "auth list: done");
505
667
  return { status: "ok" };
506
668
  }
507
669
  async function tryLoadHint(importer, packageName) {
@@ -512,94 +674,6 @@ async function tryLoadHint(importer, packageName) {
512
674
  }
513
675
  }
514
676
  //#endregion
515
- //#region src/logger.ts
516
- /**
517
- * CLI-side wiring for `@theholocron/logger`.
518
- *
519
- * `logger` is the operational-output channel — internal state, debug
520
- * traces, errors, structured context that routes to Axiom. It runs in
521
- * parallel to `print` (user-facing UX output) and does not replace it.
522
- */
523
- /**
524
- * Resolve the explicit level to hand to `createLogger`, in priority order:
525
- *
526
- * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
527
- * 3. `HOLOCRON_LOG_LEVEL` env var
528
- * 4. `holocron.config` `log.level` (`configLevel`)
529
- *
530
- * Returns `undefined` when nothing applies — `createLogger` then defaults
531
- * to `"info"`. Resolving the full chain here (rather than passing
532
- * `configLevel` straight through) keeps config below the env var.
533
- */
534
- function resolveLogLevel(argv, configLevel) {
535
- if (argv.verbose) return "debug";
536
- if (argv.quiet) return "error";
537
- return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
538
- }
539
- let root;
540
- let rootLevel;
541
- let rootCommand;
542
- let rootAxiomKey;
543
- /**
544
- * Resolve Axiom credentials for the CLI. Env vars win — same contract as
545
- * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
546
- * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
547
- * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
548
- * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
549
- * token and a dataset are found.
550
- */
551
- function resolveCliAxiom(opts) {
552
- const fromEnv = resolveAxiomFromEnv();
553
- if (fromEnv) return fromEnv;
554
- const dataset = env.get("HOLOCRON_AXIOM_DATASET") || env.get("AXIOM_DATASET") || opts.configAxiomDataset;
555
- if (!dataset) return void 0;
556
- const org = opts.org ?? env.get("HOLOCRON_ORG");
557
- const token = (org ? getToken(`axiom.${org}`) : null) ?? getToken("axiom");
558
- return token ? {
559
- dataset,
560
- token
561
- } : void 0;
562
- }
563
- /**
564
- * The process-wide root logger. Built once (from `cli.ts`'s middleware,
565
- * with the command name + flags + env). Rebuilt at most once more when a
566
- * command's handler supplies `holocron.config` context the flag/env-only
567
- * first pass could not have known — `log.level` (unless `--verbose` /
568
- * `--quiet` already fixed it) or `log.axiom.dataset` + the resolved org
569
- * for a keyring-backed Axiom transport. That rebuild generates a fresh
570
- * `runId`, which is harmless: nothing logs between the middleware and the
571
- * handler.
572
- */
573
- function buildCliLogger(argv, opts = {}) {
574
- const { command, configLevel } = opts;
575
- if (command) rootCommand = command;
576
- const level = resolveLogLevel(argv, configLevel);
577
- const axiom = resolveCliAxiom(opts);
578
- const axiomKey = axiom?.dataset;
579
- const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
580
- if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
581
- const built = createLogger({
582
- ...level ? { level } : {},
583
- ...axiom ? { axiom } : {}
584
- });
585
- root = {
586
- logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
587
- runId: built.runId
588
- };
589
- rootLevel = level;
590
- rootAxiomKey = axiomKey;
591
- }
592
- return root;
593
- }
594
- /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
595
- function getLogger() {
596
- return (root ??= createLogger()).logger;
597
- }
598
- /** The current root logger's correlation id, if a root has been built. */
599
- function getRunId() {
600
- return root?.runId;
601
- }
602
- //#endregion
603
677
  //#region src/plugin/loader.ts
604
678
  /**
605
679
  * `PluginLoader` — loads provider plugins per the resolved config and