@theholocron/cli 3.53.0 → 3.55.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
@@ -168,6 +168,31 @@ holocron auth unset github.read # remove a stored token
168
168
  holocron auth list # show all stored providers
169
169
  ```
170
170
 
171
+ ## Logging
172
+
173
+ Operational output goes through [`@theholocron/logger`](../logger) — separate from
174
+ the user-facing `print` surface. Global flags:
175
+
176
+ ```sh
177
+ holocron doctor --verbose # log level → debug (full structured output)
178
+ holocron doctor --quiet # log level → error (suppress info + warn)
179
+ holocron doctor --debug # print "Run ID: <uuid>" at command end for Axiom lookup
180
+ ```
181
+
182
+ Level resolution (highest priority first): `--verbose` / `--quiet` →
183
+ `HOLOCRON_LOG_LEVEL` → `log.level` in `holocron.config` → `"info"`.
184
+
185
+ ```ts
186
+ export default defineConfig({
187
+ log: { level: "warn" },
188
+ });
189
+ ```
190
+
191
+ Axiom credentials (`HOLOCRON_AXIOM_TOKEN` / `AXIOM_TOKEN`,
192
+ `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET`) come from env vars only.
193
+ `HOLOCRON_TELEMETRY=false` disables the Axiom transport (and Sentry). See the
194
+ [logging guide](https://docs.theholocron.dev/holocron/logging/).
195
+
171
196
  ## What's in here
172
197
 
173
198
  - `src/capabilities/` — the 14 capability interfaces that providers
@@ -176,6 +201,8 @@ holocron auth list # show all stored providers
176
201
  `CapabilityConfigPackage`
177
202
  - `src/load-config.ts` — `loadConfig` — reads JSON/JS/TS config files
178
203
  - `src/define-config.ts` — `defineConfig` typed pass-through
204
+ - `src/logger.ts` — CLI-side `@theholocron/logger` wiring (`buildCliLogger`,
205
+ `resolveLogLevel`)
179
206
  - `src/loader.ts` — `PluginLoader` — dynamic-imports plugins, resolves
180
207
  capability config packages, builds the capability registry
181
208
  - `src/cli.ts` — yargs entry, dispatches subcommands
package/dist/cli.mjs CHANGED
@@ -14,6 +14,7 @@ import chalk from "chalk";
14
14
  import { execFile, execFileSync, spawnSync } from "node:child_process";
15
15
  import { homedir } from "node:os";
16
16
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
17
+ import { createLogger, parseLogLevel } from "@theholocron/logger";
17
18
  import { createHash } from "node:crypto";
18
19
  import { generateReadme } from "@theholocron/components-doc/markdown";
19
20
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
@@ -172,7 +173,8 @@ const CARDINALITY = {
172
173
  tooling: "many",
173
174
  notifications: "many",
174
175
  analytics: "many",
175
- observability: "many",
176
+ errors: "single",
177
+ logs: "single",
176
178
  wiki: "single",
177
179
  workers: "single"
178
180
  };
@@ -184,31 +186,6 @@ const CARDINALITY = {
184
186
  const REQUIRED_CAPABILITIES = [];
185
187
  //#endregion
186
188
  //#region src/config/config.ts
187
- /**
188
- * `holocron.config.json` schema, parser, and provider resolution.
189
- *
190
- * ESLint-style entry forms:
191
- *
192
- * "source": "github" ← single, short
193
- * "deployment": ["vercel", { team: "rando" }] ← single, with options
194
- * "notifications": ["slack", "discord"] ← multi, short
195
- * "notifications": [
196
- * ["slack", { channel: "#ops" }],
197
- * ["discord", { webhook: "env:HOOK" }]
198
- * ] ← multi, with options
199
- *
200
- * Discriminator: an array entry is a `[provider, options]` tuple when
201
- * the length is 2 AND element[1] is a non-array, non-null object.
202
- * Otherwise it's a multi-provider list (string[] or tuple[]).
203
- *
204
- * Validation rules:
205
- * - `vault` is REQUIRED (every project has secrets somewhere)
206
- * - Entries for `'many'` capabilities are normalized to an array of
207
- * normalized tuples; entries for `'single'` capabilities are
208
- * normalized to one tuple
209
- * - Tokens / secret values never appear in config — providers read
210
- * them from env (or pull from `vault` at runtime)
211
- */
212
189
  var ConfigError = class extends Error {
213
190
  name = "ConfigError";
214
191
  };
@@ -306,7 +283,8 @@ function resolveConfig(raw) {
306
283
  agent: raw.agent,
307
284
  skills: raw.skills,
308
285
  docs,
309
- env: raw.env
286
+ env: raw.env,
287
+ log: raw.log
310
288
  };
311
289
  }
312
290
  //#endregion
@@ -986,6 +964,19 @@ async function smokeCheck(key, provider, impl) {
986
964
  const desc = await impl.describe();
987
965
  return mk(key, provider, "ok", `provider: ${desc.provider}; env keys: ${desc.envKeys.join(", ")}`);
988
966
  }
967
+ case "errors": {
968
+ const desc = await impl.describe();
969
+ return mk(key, provider, "ok", `provider: ${desc.provider}; env keys: ${desc.envKeys.join(", ")}`);
970
+ }
971
+ case "logs": {
972
+ const logs = impl;
973
+ const desc = await logs.describe();
974
+ if (logs.whoami) {
975
+ const who = await logs.whoami();
976
+ return mk(key, provider, who.ok ? "ok" : "fail", `dataset: ${who.dataset}`);
977
+ }
978
+ return mk(key, provider, "ok", `provider: ${desc.provider}; env keys: ${desc.envKeys.join(", ")}`);
979
+ }
989
980
  default: return mk(key, provider, "skip", "loaded (no smoke check defined for this capability)");
990
981
  }
991
982
  } catch (err) {
@@ -3121,6 +3112,59 @@ var dependabot_default = "version: 2\nupdates:\n - package-ecosystem: npm\n
3121
3112
  //#region src/templates/labeler.yml
3122
3113
  var labeler_default = "bug:\n - '^fix'\n\nchore:\n - '^chore(?!\\(deps)'\n\nci:\n - '^ci'\n\ndependencies:\n - '^chore\\(deps'\n\ndocumentation:\n - '^docs'\n\nenhancement:\n - '^feat'\n\nperformance:\n - '^perf'\n\nrefactor:\n - '^refactor'\n\ntest:\n - '^test'\n";
3123
3114
  //#endregion
3115
+ //#region src/logger.ts
3116
+ /**
3117
+ * CLI-side wiring for `@theholocron/logger`.
3118
+ *
3119
+ * `logger` is the operational-output channel — internal state, debug
3120
+ * traces, errors, structured context that routes to Axiom. It runs in
3121
+ * parallel to `print` (user-facing UX output) and does not replace it.
3122
+ */
3123
+ /**
3124
+ * Resolve the explicit level to hand to `createLogger`, in priority order:
3125
+ *
3126
+ * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
3127
+ * 3. `HOLOCRON_LOG_LEVEL` env var
3128
+ * 4. `holocron.config` `log.level` (`configLevel`)
3129
+ *
3130
+ * Returns `undefined` when nothing applies — `createLogger` then defaults
3131
+ * to `"info"`. Resolving the full chain here (rather than passing
3132
+ * `configLevel` straight through) keeps config below the env var.
3133
+ */
3134
+ function resolveLogLevel(argv, configLevel) {
3135
+ if (argv.verbose) return "debug";
3136
+ if (argv.quiet) return "error";
3137
+ return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
3138
+ }
3139
+ let root;
3140
+ let rootLevel;
3141
+ /**
3142
+ * The process-wide root logger. Built once (from `cli.ts`'s middleware,
3143
+ * with flags + env only). Rebuilt at most once more when a command's
3144
+ * handler supplies its `holocron.config` `log.level` — a case the
3145
+ * flag/env-only first pass could not have known — as long as no
3146
+ * higher-priority `--verbose` / `--quiet` already fixed the level. That
3147
+ * rebuild generates a fresh `runId`, which is harmless: nothing logs
3148
+ * between the middleware and the handler.
3149
+ */
3150
+ function buildCliLogger(argv, configLevel) {
3151
+ const level = resolveLogLevel(argv, configLevel);
3152
+ const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
3153
+ if (!root || rebuildForConfig) {
3154
+ root = createLogger(level ? { level } : {});
3155
+ rootLevel = level;
3156
+ }
3157
+ return root;
3158
+ }
3159
+ /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
3160
+ function getLogger() {
3161
+ return (root ??= createLogger()).logger;
3162
+ }
3163
+ /** The current root logger's correlation id, if a root has been built. */
3164
+ function getRunId() {
3165
+ return root?.runId;
3166
+ }
3167
+ //#endregion
3124
3168
  //#region src/commands/setup-workflows/index.ts
3125
3169
  /**
3126
3170
  * Thin workflow wrapper templates for `holocron setup`.
@@ -3210,7 +3254,7 @@ function generateThinCallerContent(name, withOverrides, additionalPaths) {
3210
3254
  }
3211
3255
  const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
3212
3256
  const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
3213
- if (injected === result) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
3257
+ if (injected === result) getLogger().warn({ template: name }, "generateThinCallerContent: could not inject `with:` overrides");
3214
3258
  return injected;
3215
3259
  }
3216
3260
  /**
@@ -4585,6 +4629,17 @@ async function runSetup(input) {
4585
4629
  }
4586
4630
  print(formatStep(steps[steps.length - 1]));
4587
4631
  }
4632
+ if (loader.has("logs")) {
4633
+ const logs = loader.get("logs");
4634
+ print(style.step("logs"));
4635
+ if (logs.ensureDataset) for (const dataset of ["holocron-ci", "holocron-local"]) {
4636
+ steps.push(await runStep("logs", `ensureDataset ${dataset}`, dryRun, async () => {
4637
+ const result = await logs.ensureDataset(dataset);
4638
+ return `${dataset} ${result.alreadyExists ? "exists" : "created"}`;
4639
+ }));
4640
+ print(formatStep(steps[steps.length - 1]));
4641
+ }
4642
+ }
4588
4643
  if (loader.has("tooling")) {
4589
4644
  const tools = loader.get("tooling");
4590
4645
  print(style.step("tooling"));
@@ -4724,13 +4779,14 @@ async function runSkillsInstall(input) {
4724
4779
  function runSkillsRemove(input) {
4725
4780
  const { dryRun, repoRoot } = input.context;
4726
4781
  const exec = input.exec ?? defaultExec;
4782
+ const logger = input.logger ?? getLogger();
4727
4783
  const args = [
4728
4784
  "skills",
4729
4785
  "remove",
4730
4786
  ...input.names ?? []
4731
4787
  ];
4732
4788
  if (dryRun) {
4733
- console.log(`Would run: npx ${args.join(" ")}`);
4789
+ logger.info({ argv: args }, `Would run: npx ${args.join(" ")}`);
4734
4790
  return { status: "dry-run" };
4735
4791
  }
4736
4792
  const { exitCode } = exec("npx", args, { cwd: repoRoot });
@@ -4739,13 +4795,14 @@ function runSkillsRemove(input) {
4739
4795
  function runSkillsUpdate(input) {
4740
4796
  const { dryRun, repoRoot } = input.context;
4741
4797
  const exec = input.exec ?? defaultExec;
4798
+ const logger = input.logger ?? getLogger();
4742
4799
  const args = [
4743
4800
  "skills",
4744
4801
  "update",
4745
4802
  ...input.name ? [input.name] : []
4746
4803
  ];
4747
4804
  if (dryRun) {
4748
- console.log(`Would run: npx ${args.join(" ")}`);
4805
+ logger.info({ argv: args }, `Would run: npx ${args.join(" ")}`);
4749
4806
  return { status: "dry-run" };
4750
4807
  }
4751
4808
  const { exitCode } = exec("npx", args, { cwd: repoRoot });
@@ -6109,7 +6166,7 @@ async function fileExists(path) {
6109
6166
  //#region src/telemetry.ts
6110
6167
  const DSN = "https://95cbb72ad5636c94e119a5405ee8f55f@o4508238154104832.ingest.us.sentry.io/4511810950791168";
6111
6168
  function isEnabled() {
6112
- return !env.get("NO_HOLOCRON_TELEMETRY") && true;
6169
+ return !(env.get("HOLOCRON_TELEMETRY") === "false" || Boolean(env.get("NO_HOLOCRON_TELEMETRY"))) && true;
6113
6170
  }
6114
6171
  function init(version) {
6115
6172
  if (!isEnabled()) return;
@@ -6280,6 +6337,8 @@ const resolveSyncToken = createFeatureResolver({
6280
6337
  keyringKey: "github.sync"
6281
6338
  });
6282
6339
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
6340
+ /** Whether to print the correlation id at command end (`--debug` / `--verbose`). */
6341
+ let printRunId = false;
6283
6342
  /**
6284
6343
  * Resolves the active org in priority order:
6285
6344
  * 1. `--org` CLI flag
@@ -6296,7 +6355,7 @@ function tokenContext(rawTokens) {
6296
6355
  return parseTokenArgs(rawTokens);
6297
6356
  } catch (err) {
6298
6357
  if (err instanceof TokenParseError) {
6299
- console.error(`--token: ${err.message}`);
6358
+ getLogger().error(`--token: ${err.message}`);
6300
6359
  process.exitCode = 1;
6301
6360
  return null;
6302
6361
  }
@@ -6307,9 +6366,7 @@ init(CLI_VERSION);
6307
6366
  const updateCheckPromise = checkForUpdates(CLI_VERSION);
6308
6367
  let finishCommand = () => {};
6309
6368
  try {
6310
- await yargs(hideBin(process.argv)).middleware((argv) => {
6311
- finishCommand = startCommand(argv._.slice(0, 2).join(" ") || "unknown");
6312
- }).scriptName("").usage("holocron <command> [options]").option("dry-run", {
6369
+ await yargs(hideBin(process.argv)).scriptName("").usage("holocron <command> [options]").option("dry-run", {
6313
6370
  type: "boolean",
6314
6371
  default: false,
6315
6372
  describe: "Print what would be mutated without calling capability mutators. Commands branch on this; read-only commands ignore it."
@@ -6324,6 +6381,22 @@ try {
6324
6381
  type: "string",
6325
6382
  default: process.cwd(),
6326
6383
  describe: "Directory to search for holocron.config.json"
6384
+ }).option("verbose", {
6385
+ type: "boolean",
6386
+ default: false,
6387
+ describe: "Set the log level to debug — full structured operational output."
6388
+ }).option("debug", {
6389
+ type: "boolean",
6390
+ default: false,
6391
+ describe: "Print the run ID at command end for Axiom lookup. Does not change the log level."
6392
+ }).option("quiet", {
6393
+ type: "boolean",
6394
+ default: false,
6395
+ describe: "Set the log level to error — suppress info and warn."
6396
+ }).middleware((argv) => {
6397
+ finishCommand = startCommand(argv._.slice(0, 2).join(" ") || "unknown");
6398
+ printRunId = Boolean(argv.debug || argv.verbose);
6399
+ buildCliLogger(argv);
6327
6400
  }).command("version", "Print the CLI version", () => {}, () => {
6328
6401
  console.log(`holocron ${CLI_VERSION}`);
6329
6402
  }).command("clone", "Clone all repos in a GitHub org as siblings under a single directory", (y) => y.option("org", {
@@ -6340,7 +6413,7 @@ try {
6340
6413
  try {
6341
6414
  token = resolveCloneToken({ cliToken: tokens.cliTokens?.["github"] ?? tokens.cliToken });
6342
6415
  } catch (err) {
6343
- console.error(`clone: ${err instanceof AuthError ? err.message : String(err)}`);
6416
+ getLogger().error(`clone: ${err instanceof AuthError ? err.message : String(err)}`);
6344
6417
  process.exitCode = 1;
6345
6418
  return;
6346
6419
  }
@@ -6357,6 +6430,7 @@ try {
6357
6430
  const tokens = tokenContext(argv.token);
6358
6431
  if (!tokens) return;
6359
6432
  const loaded = await loadConfig(argv.cwd);
6433
+ buildCliLogger(argv, loaded.resolved.log?.level);
6360
6434
  if ((await runDoctor({
6361
6435
  loaded,
6362
6436
  context: {
@@ -6374,6 +6448,7 @@ try {
6374
6448
  const tokens = tokenContext(argv.token);
6375
6449
  if (!tokens) return;
6376
6450
  const loaded = await loadConfig(argv.cwd);
6451
+ buildCliLogger(argv, loaded.resolved.log?.level);
6377
6452
  if ((await runSetup({
6378
6453
  loaded,
6379
6454
  context: {
@@ -6439,6 +6514,7 @@ try {
6439
6514
  const scopeArg = argv.scope;
6440
6515
  const scope = parseScope(scopeArg);
6441
6516
  const loaded = await loadConfig(argv.cwd);
6517
+ buildCliLogger(argv, loaded.resolved.log?.level);
6442
6518
  if ((await runSecretSet({
6443
6519
  loaded,
6444
6520
  context: {
@@ -6468,6 +6544,7 @@ try {
6468
6544
  const tokens = tokenContext(argv.token);
6469
6545
  if (!tokens) return;
6470
6546
  const loaded = await loadConfig(argv.cwd);
6547
+ buildCliLogger(argv, loaded.resolved.log?.level);
6471
6548
  if ((await runSecretsSync({
6472
6549
  loaded,
6473
6550
  context: {
@@ -6496,6 +6573,7 @@ try {
6496
6573
  const tokens = tokenContext(argv.token);
6497
6574
  if (!tokens) return;
6498
6575
  const loaded = await loadConfig(argv.cwd);
6576
+ buildCliLogger(argv, loaded.resolved.log?.level);
6499
6577
  if ((await runDeploy({
6500
6578
  loaded,
6501
6579
  context: {
@@ -6523,6 +6601,7 @@ try {
6523
6601
  const tokens = tokenContext(argv.token);
6524
6602
  if (!tokens) return;
6525
6603
  const loaded = await loadConfig(argv.cwd);
6604
+ buildCliLogger(argv, loaded.resolved.log?.level);
6526
6605
  if ((await runCleanupPreview({
6527
6606
  loaded,
6528
6607
  context: {
@@ -6570,6 +6649,7 @@ try {
6570
6649
  const tokens = tokenContext(argv.token);
6571
6650
  if (!tokens) return;
6572
6651
  const loaded = await loadConfig(argv.cwd);
6652
+ buildCliLogger(argv, loaded.resolved.log?.level);
6573
6653
  if ((await runSync({
6574
6654
  loaded,
6575
6655
  context: {
@@ -6607,7 +6687,7 @@ try {
6607
6687
  else try {
6608
6688
  token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
6609
6689
  } catch (err) {
6610
- console.error(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
6690
+ getLogger().error(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
6611
6691
  process.exitCode = 1;
6612
6692
  return;
6613
6693
  }
@@ -6625,8 +6705,10 @@ try {
6625
6705
  describe: "Print what would change without writing",
6626
6706
  default: false
6627
6707
  }), async (argv) => {
6708
+ const loaded = await loadConfig(argv.cwd);
6709
+ buildCliLogger(argv, loaded.resolved.log?.level);
6628
6710
  if ((await runSyncReadme({
6629
- loaded: await loadConfig(argv.cwd),
6711
+ loaded,
6630
6712
  context: {
6631
6713
  repoRoot: argv.cwd,
6632
6714
  dryRun: argv.dryRun
@@ -6860,12 +6942,12 @@ try {
6860
6942
  }) === "yes";
6861
6943
  if (skills.length === 0) skills = parseTopics(await input({ message: "Agent skills (comma-separated, optional):" }));
6862
6944
  if (!type) {
6863
- console.error("new: template type is required");
6945
+ getLogger().error("new: template type is required");
6864
6946
  process.exitCode = 1;
6865
6947
  return;
6866
6948
  }
6867
6949
  if (!name) {
6868
- console.error("new: repo name is required");
6950
+ getLogger().error("new: repo name is required");
6869
6951
  process.exitCode = 1;
6870
6952
  return;
6871
6953
  }
@@ -6894,7 +6976,7 @@ try {
6894
6976
  })).status === "fail") process.exitCode = 1;
6895
6977
  } catch (err) {
6896
6978
  if (err instanceof NewError) {
6897
- console.error(`new: ${err.message}`);
6979
+ getLogger().error(`new: ${err.message}`);
6898
6980
  process.exitCode = 1;
6899
6981
  return;
6900
6982
  }
@@ -6910,7 +6992,7 @@ try {
6910
6992
  describe: "Vendor display name (PascalCase)"
6911
6993
  }).option("capability", {
6912
6994
  type: "string",
6913
- describe: "Capability key: source|ci|secrets|environments|issues|deployment|storage|auth|vault|dns|tooling|notifications|analytics|observability"
6995
+ describe: "Capability key: source|ci|secrets|environments|issues|deployment|storage|auth|vault|dns|tooling|notifications|analytics|errors|logs|wiki|workers"
6914
6996
  }).option("token-env", {
6915
6997
  type: "string",
6916
6998
  describe: "Holocron env var name (defaults to HOLOCRON_<VENDOR>_TOKEN)"
@@ -6955,7 +7037,7 @@ try {
6955
7037
  }).status === "fail") process.exitCode = 1;
6956
7038
  } catch (err) {
6957
7039
  if (err instanceof PluginCreateError) {
6958
- console.error(`plugin create: ${err.message}`);
7040
+ getLogger().error(`plugin create: ${err.message}`);
6959
7041
  process.exitCode = 1;
6960
7042
  return;
6961
7043
  }
@@ -6983,7 +7065,7 @@ try {
6983
7065
  extra
6984
7066
  });
6985
7067
  if (report.status === "fail") {
6986
- if (report.message) console.error(`upgrade node: ${report.message}`);
7068
+ if (report.message) getLogger().error(`upgrade node: ${report.message}`);
6987
7069
  process.exitCode = 1;
6988
7070
  }
6989
7071
  }).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", {
@@ -7017,6 +7099,8 @@ try {
7017
7099
  }
7018
7100
  finishCommand(!process.exitCode);
7019
7101
  endSession();
7102
+ const rid = getRunId();
7103
+ if (printRunId && rid) console.log(`Run ID: ${rid}`);
7020
7104
  (await updateCheckPromise)?.();
7021
7105
  await flush();
7022
7106
  /**