@theholocron/cli 2.0.0-alpha.65 → 2.0.0-alpha.67

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/cli.mjs +126 -75
  2. package/package.json +3 -1
package/dist/cli.mjs CHANGED
@@ -7,6 +7,8 @@ import { createInterface } from "node:readline";
7
7
  import { stdin, stdout } from "node:process";
8
8
  import { ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
9
9
  import { Entry, findCredentials } from "@napi-rs/keyring";
10
+ import ora from "ora";
11
+ import chalk from "chalk";
10
12
  import { createHash } from "node:crypto";
11
13
  import { createGitHubClient } from "@theholocron/github-client";
12
14
  import { execFile, execFileSync, spawnSync } from "node:child_process";
@@ -221,6 +223,35 @@ function listStoredProviders() {
221
223
  }
222
224
  }
223
225
  //#endregion
226
+ //#region src/ui/progress.ts
227
+ /**
228
+ * Runs `fn`, showing an ora spinner for its duration in TTY environments.
229
+ * In non-TTY environments (CI, pipes, tests) the spinner is skipped entirely.
230
+ */
231
+ async function withSpinner(label, fn) {
232
+ if (!process.stdout.isTTY) return fn();
233
+ const spinner = ora(label).start();
234
+ try {
235
+ const result = await fn();
236
+ spinner.succeed();
237
+ return result;
238
+ } catch (err) {
239
+ spinner.fail();
240
+ throw err;
241
+ }
242
+ }
243
+ //#endregion
244
+ //#region src/ui/style.ts
245
+ const style = {
246
+ success: (msg) => `${chalk.green("✓")} ${msg}`,
247
+ warn: (msg) => `${chalk.yellow("⚠")} ${msg}`,
248
+ fail: (msg) => `${chalk.red("✗")} ${msg}`,
249
+ step: (msg) => `${chalk.cyan("→")} ${msg}`,
250
+ hint: (msg) => chalk.dim(msg),
251
+ dim: (msg) => chalk.dim(msg),
252
+ header: (msg) => chalk.bold(msg)
253
+ };
254
+ //#endregion
224
255
  //#region src/commands/auth.ts
225
256
  /**
226
257
  * `holocron auth <subcommand>` — manage bootstrap credentials in the
@@ -263,11 +294,11 @@ async function runAuthSet(input) {
263
294
  });
264
295
  const packageName = resolvePluginPackage(provider);
265
296
  if (!token) {
266
- print(`no token supplied for \`${provider}\`.`);
267
- print(` pass as positional arg: holocron auth set ${provider} <token>`);
268
- print(` or via env: HOLOCRON_${provider.toUpperCase()}_TOKEN / ${provider.toUpperCase()}_TOKEN`);
297
+ print(style.fail(`no token supplied for \`${provider}\`.`));
298
+ print(style.hint(` pass as positional arg: holocron auth set ${provider} <token>`));
299
+ print(style.hint(` or via env: HOLOCRON_${provider.toUpperCase()}_TOKEN / ${provider.toUpperCase()}_TOKEN`));
269
300
  const hint = await tryLoadHint(importer, packageName);
270
- if (hint) print(` hint: ${hint}`);
301
+ if (hint) print(style.hint(` hint: ${hint}`));
271
302
  return {
272
303
  status: "fail",
273
304
  message: "no token supplied"
@@ -279,27 +310,28 @@ async function runAuthSet(input) {
279
310
  if (typeof module.verifyToken === "function") {
280
311
  const verified = await module.verifyToken(token);
281
312
  if (!verified.ok) {
282
- print(`token rejected by ${provider}: ${verified.message}`);
283
- if (module.AUTH_HINT) print(` hint: ${module.AUTH_HINT}`);
313
+ print(style.fail(`token rejected by ${provider}: ${verified.message}`));
314
+ if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
284
315
  return {
285
316
  status: "fail",
286
317
  message: verified.message
287
318
  };
288
319
  }
289
320
  subject = verified.subject;
290
- } else print(`(${provider} plugin has no verifyToken; storing without verification)`);
321
+ } else print(style.warn(`${provider} plugin has no verifyToken; storing without verification`));
291
322
  } catch (err) {
292
- print(`cannot verify token — failed to load ${packageName}: ${err instanceof Error ? err.message : String(err)}`);
293
- print(` storing token anyway; run 'holocron auth check ${provider}' once the plugin is installed`);
323
+ const msg = err instanceof Error ? err.message : String(err);
324
+ print(style.warn(`cannot verify token failed to load ${packageName}: ${msg}`));
325
+ print(style.hint(` storing token anyway; run 'holocron auth check ${provider}' once the plugin is installed`));
294
326
  }
295
327
  if (!setToken(provider, token)) {
296
- print(`keyring unavailable — token not stored. Use env vars instead.`);
328
+ print(style.fail(`keyring unavailable — token not stored. Use env vars instead.`));
297
329
  return {
298
330
  status: "fail",
299
331
  message: "keyring unavailable"
300
332
  };
301
333
  }
302
- print(`stored ${provider} token${subject ? ` (${subject})` : ""}`);
334
+ print(style.success(`stored ${provider} token${subject ? ` (${subject})` : ""}`));
303
335
  return {
304
336
  status: "ok",
305
337
  ...subject ? { message: subject } : {}
@@ -308,10 +340,10 @@ async function runAuthSet(input) {
308
340
  function runAuthUnset(input) {
309
341
  const print = input.print ?? ((l) => console.log(l));
310
342
  if (deleteToken(input.provider)) {
311
- print(`removed ${input.provider} token`);
343
+ print(style.success(`removed ${input.provider} token`));
312
344
  return { status: "ok" };
313
345
  }
314
- print(`no stored token for ${input.provider}`);
346
+ print(style.dim(`no stored token for ${input.provider}`));
315
347
  return {
316
348
  status: "skip",
317
349
  message: "nothing to remove"
@@ -323,7 +355,7 @@ async function runAuthCheck(input) {
323
355
  const { provider } = input;
324
356
  const token = getToken(provider);
325
357
  if (!token) {
326
- print(`no stored token for ${provider}`);
358
+ print(style.dim(`no stored token for ${provider}`));
327
359
  return {
328
360
  status: "skip",
329
361
  message: "no stored token"
@@ -333,29 +365,30 @@ async function runAuthCheck(input) {
333
365
  try {
334
366
  const module = await importer(packageName);
335
367
  if (typeof module.verifyToken !== "function") {
336
- print(`${provider}: token stored (plugin has no verifyToken; can't confirm validity)`);
368
+ print(style.warn(`${provider}: token stored (plugin has no verifyToken; can't confirm validity)`));
337
369
  return {
338
370
  status: "ok",
339
371
  message: "stored, unverified"
340
372
  };
341
373
  }
342
- const verified = await module.verifyToken(token);
374
+ const verify = () => module.verifyToken(token);
375
+ const verified = input.showSpinner !== false ? await withSpinner(`Verifying ${provider} token…`, verify) : await verify();
343
376
  if (verified.ok) {
344
- print(`${provider}: ok — ${verified.subject}`);
377
+ print(style.success(`${provider}: ok — ${verified.subject}`));
345
378
  return {
346
379
  status: "ok",
347
380
  message: verified.subject
348
381
  };
349
382
  }
350
- print(`${provider}: rejected — ${verified.message}`);
351
- if (module.AUTH_HINT) print(` hint: ${module.AUTH_HINT}`);
383
+ print(style.fail(`${provider}: rejected — ${verified.message}`));
384
+ if (module.AUTH_HINT) print(style.hint(` hint: ${module.AUTH_HINT}`));
352
385
  return {
353
386
  status: "fail",
354
387
  message: verified.message
355
388
  };
356
389
  } catch (err) {
357
390
  const msg = err instanceof Error ? err.message : String(err);
358
- print(`${provider}: cannot verify — ${msg}`);
391
+ print(style.fail(`${provider}: cannot verify — ${msg}`));
359
392
  return {
360
393
  status: "fail",
361
394
  message: msg
@@ -367,8 +400,8 @@ async function runAuthList(input = {}) {
367
400
  const importer = input.importer ?? defaultImporter$1;
368
401
  const providers = listStoredProviders();
369
402
  if (providers.length === 0) {
370
- print("no stored tokens.");
371
- print("run: holocron auth set <provider> <token>");
403
+ print(style.dim("no stored tokens."));
404
+ print(style.hint("run: holocron auth set <provider> <token>"));
372
405
  return {
373
406
  status: "ok",
374
407
  message: "none"
@@ -378,9 +411,13 @@ async function runAuthList(input = {}) {
378
411
  const check = await runAuthCheck({
379
412
  provider,
380
413
  importer,
381
- print: () => {}
414
+ print: () => {},
415
+ showSpinner: false
382
416
  });
383
- print(` ${check.status === "ok" ? "✓" : check.status === "fail" ? "✗" : "·"} ${provider}${check.message ? ` — ${check.message}` : ""}`);
417
+ const label = `${provider}${check.message ? ` — ${check.message}` : ""}`;
418
+ if (check.status === "ok") print(` ${style.success(label)}`);
419
+ else if (check.status === "fail") print(` ${style.fail(label)}`);
420
+ else print(` ${style.dim(`· ${label}`)}`);
384
421
  }
385
422
  return { status: "ok" };
386
423
  }
@@ -492,12 +529,12 @@ async function runDeploy(input) {
492
529
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
493
530
  await loader.load();
494
531
  const dryRun = input.context.dryRun ?? false;
495
- print(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`);
532
+ print(style.header(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
496
533
  if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
497
534
  const deploy = loader.get("deployment");
498
535
  if (dryRun) {
499
536
  const message = `would: ${deploy.providerName}.triggerDeployment(projectId=${input.projectId}, branch=${input.branch}${input.target ? `, target=${input.target}` : ""})`;
500
- print(` ${message}`);
537
+ print(` ${style.dim(`… ${message}`)}`);
501
538
  return {
502
539
  deployment: null,
503
540
  status: "dry-run",
@@ -505,19 +542,19 @@ async function runDeploy(input) {
505
542
  };
506
543
  }
507
544
  try {
508
- const record = await deploy.triggerDeployment({
545
+ const record = await withSpinner(`Deploying ${input.branch}${input.target ? ` → ${input.target}` : ""}…`, () => deploy.triggerDeployment({
509
546
  projectId: input.projectId,
510
547
  branch: input.branch,
511
548
  ...input.target ? { target: input.target } : {}
512
- });
513
- print(` ${record.status} — ${record.url}`);
549
+ }));
550
+ print(` ${style.success(`${record.status} — ${record.url}`)}`);
514
551
  return {
515
552
  deployment: record,
516
553
  status: "ok"
517
554
  };
518
555
  } catch (err) {
519
556
  const message = err instanceof Error ? err.message : String(err);
520
- print(` ${message}`);
557
+ print(` ${style.fail(message)}`);
521
558
  return {
522
559
  deployment: null,
523
560
  status: "fail",
@@ -530,11 +567,11 @@ async function runDeploy(input) {
530
567
  async function runDoctor(input) {
531
568
  const print = input.print ?? ((line) => console.log(line));
532
569
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
533
- await loader.load();
570
+ await withSpinner("Loading plugins…", () => loader.load());
534
571
  const rows = [];
535
572
  const config = input.loaded.resolved;
536
- print(`Holocron doctor — ${config.name}`);
537
- print(` config: ${input.loaded.filepath}`);
573
+ print(style.header(`Holocron doctor — ${config.name}`));
574
+ print(style.dim(` config: ${input.loaded.filepath}`));
538
575
  print("");
539
576
  for (const key of loader.loadedKeys()) {
540
577
  const cardinality = CARDINALITY[key];
@@ -554,7 +591,12 @@ async function runDoctor(input) {
554
591
  rows.push(row);
555
592
  }
556
593
  }
557
- for (const row of rows) print(` ${row.status === "ok" ? "✓" : row.status === "fail" ? "✗" : "·"} ${pad(row.capability, 14)} via ${pad(row.provider, 14)} ${row.message}`);
594
+ for (const row of rows) {
595
+ const label = `${pad(row.capability, 14)} via ${pad(row.provider, 14)} ${row.message}`;
596
+ if (row.status === "ok") print(` ${style.success(label)}`);
597
+ else if (row.status === "fail") print(` ${style.fail(label)}`);
598
+ else print(` ${style.dim(`· ${label}`)}`);
599
+ }
558
600
  const summary = rows.reduce((acc, r) => {
559
601
  acc[r.status] += 1;
560
602
  return acc;
@@ -563,8 +605,9 @@ async function runDoctor(input) {
563
605
  fail: 0,
564
606
  skip: 0
565
607
  });
608
+ const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped`;
566
609
  print("");
567
- print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped`);
610
+ print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
568
611
  return {
569
612
  rows,
570
613
  summary
@@ -849,7 +892,7 @@ var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.j
849
892
  var audit_default$1 = "name: Audit\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n build-script:\n description: Script to build and upload bundle stats to Codecov\n type: string\n required: false\n default: pnpm build\n secrets:\n CODECOV_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Audit the bundle size\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n concurrency:\n group: audit-${{ github.ref }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: eval \"$BUILD_SCRIPT\"\n name: Build and upload bundle stats\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n";
850
893
  //#endregion
851
894
  //#region src/templates/workflows/bookkeeping.yml
852
- var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n concurrency:\n group: bookkeeping-${{ github.event.pull_request.number || github.event.issue.number }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
895
+ var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n concurrency:\n group: bookkeeping-${{ github.event.pull_request.number || github.event.issue.number }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
853
896
  //#endregion
854
897
  //#region src/templates/workflows/codeql.yml
855
898
  var codeql_default$1 = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n concurrency:\n group: codeql-${{ github.ref }}\n cancel-in-progress: false\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
@@ -929,7 +972,7 @@ var dependabot_default = "# AUTO-GENERATED by holocron — run `holocron setup`
929
972
  var audit_default = "name: Audit\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n audit:\n uses: theholocron/.github/.github/workflows/audit.yml@main\n secrets: inherit\n";
930
973
  //#endregion
931
974
  //#region src/commands/workflows/bookkeeping.yml
932
- var bookkeeping_default = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n issues:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n";
975
+ var bookkeeping_default = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n";
933
976
  //#endregion
934
977
  //#region src/commands/workflows/codeql.yml
935
978
  var codeql_default = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n codeql:\n uses: theholocron/.github/.github/workflows/codeql.yml@main\n secrets: inherit\n";
@@ -2612,22 +2655,22 @@ function describeScope(scope) {
2612
2655
  async function runSecretsSync(input) {
2613
2656
  const print = input.print ?? ((line) => console.log(line));
2614
2657
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
2615
- await loader.load();
2658
+ await withSpinner("Loading plugins…", () => loader.load());
2616
2659
  const dryRun = input.context.dryRun ?? false;
2617
2660
  const targets = input.targets ?? ["production", "preview"];
2618
- print(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`);
2619
- print(` vault: ${vaultProviderName(loader)}`);
2661
+ print(style.header(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`));
2662
+ print(style.dim(` vault: ${vaultProviderName(loader)}`));
2620
2663
  print("");
2621
2664
  const vault = loader.get("vault");
2622
2665
  if (!vault.readEnvironment) throw new Error(`vault provider (${vault.providerName}) does not implement readEnvironment — sync needs bulk env reads`);
2623
- const envVars = await vault.readEnvironment(input.environmentId);
2666
+ const envVars = await withSpinner(`Reading vault environment ${input.environmentId}…`, () => vault.readEnvironment(input.environmentId));
2624
2667
  const keys = Object.keys(envVars).sort();
2625
- print(`read ${keys.length} keys from vault`);
2668
+ print(style.step(`read ${keys.length} keys from vault`));
2626
2669
  print("");
2627
2670
  const rows = [];
2628
2671
  if (loader.has("secrets")) {
2629
2672
  const secrets = loader.get("secrets");
2630
- print("secrets (repo scope)");
2673
+ print(style.step("secrets (repo scope)"));
2631
2674
  for (const key of keys) {
2632
2675
  rows.push(await runRow(`secrets:${secrets.providerName}`, "scope=repo", key, dryRun, async () => {
2633
2676
  await secrets.setSecret({ kind: "repo" }, key, envVars[key]);
@@ -2638,7 +2681,7 @@ async function runSecretsSync(input) {
2638
2681
  if (loader.has("deployment")) {
2639
2682
  const deploy = loader.get("deployment");
2640
2683
  if (!input.projectId) {
2641
- print("deployment");
2684
+ print(style.step("deployment"));
2642
2685
  const row = {
2643
2686
  destination: `deployment:${deploy.providerName}`,
2644
2687
  scope: "no projectId",
@@ -2649,7 +2692,7 @@ async function runSecretsSync(input) {
2649
2692
  rows.push(row);
2650
2693
  print(formatRow(row));
2651
2694
  } else for (const target of targets) {
2652
- print(`deployment (target=${target})`);
2695
+ print(style.step(`deployment (target=${target})`));
2653
2696
  for (const key of keys) {
2654
2697
  rows.push(await runRow(`deployment:${deploy.providerName}`, `target=${target}`, key, dryRun, async () => {
2655
2698
  await deploy.setEnvVar(input.projectId, target, key, envVars[key]);
@@ -2671,7 +2714,8 @@ async function runSecretsSync(input) {
2671
2714
  dryRun: 0
2672
2715
  });
2673
2716
  print("");
2674
- print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`);
2717
+ const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
2718
+ print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
2675
2719
  return {
2676
2720
  rows,
2677
2721
  summary
@@ -2703,9 +2747,12 @@ async function runRow(destination, scope, key, dryRun, body) {
2703
2747
  }
2704
2748
  }
2705
2749
  function formatRow(row) {
2706
- const icon = row.status === "ok" ? "✓" : row.status === "fail" ? "✗" : row.status === "dry-run" ? "…" : "·";
2707
- const detail = row.message ? ` (${row.message})` : "";
2708
- return ` ${icon} ${row.key}${detail}`;
2750
+ const detail = row.message ? style.dim(` (${row.message})`) : "";
2751
+ const label = `${row.key}${detail}`;
2752
+ if (row.status === "ok") return ` ${style.success(label)}`;
2753
+ if (row.status === "fail") return ` ${style.fail(label)}`;
2754
+ if (row.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
2755
+ return ` ${style.dim(`· ${label}`)}`;
2709
2756
  }
2710
2757
  function vaultProviderName(loader) {
2711
2758
  if (!loader.has("vault")) return "<missing>";
@@ -3122,18 +3169,18 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
3122
3169
  async function runSetup(input) {
3123
3170
  const print = input.print ?? ((line) => console.log(line));
3124
3171
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
3125
- await loader.load();
3172
+ await withSpinner("Loading plugins…", () => loader.load());
3126
3173
  const config = input.loaded.resolved;
3127
3174
  const dryRun = input.context.dryRun ?? false;
3128
3175
  const steps = [];
3129
3176
  const repo = config.repo;
3130
3177
  const effectivePreset = repo?.protection;
3131
- print(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`);
3132
- print(` config: ${input.loaded.filepath}`);
3178
+ print(style.header(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`));
3179
+ print(style.dim(` config: ${input.loaded.filepath}`));
3133
3180
  print("");
3134
3181
  if (loader.has("source")) {
3135
3182
  const source = loader.get("source");
3136
- print("source");
3183
+ print(style.step("source"));
3137
3184
  for (const method of [
3138
3185
  "enableVulnerabilityAlerts",
3139
3186
  "enableAutomatedSecurityFixes",
@@ -3173,7 +3220,7 @@ async function runSetup(input) {
3173
3220
  const workflows = config.workflows;
3174
3221
  if (loader.has("source") && workflows && workflows.length > 0) {
3175
3222
  const source = loader.get("source");
3176
- print("workflows");
3223
+ print(style.step("workflows"));
3177
3224
  for (const entry of workflows) {
3178
3225
  const name = typeof entry === "string" ? entry : entry.name;
3179
3226
  const withOverrides = typeof entry === "object" ? entry.with : void 0;
@@ -3254,7 +3301,7 @@ async function runSetup(input) {
3254
3301
  }
3255
3302
  if (loader.has("environments")) {
3256
3303
  const envs = loader.get("environments");
3257
- print("environments");
3304
+ print(style.step("environments"));
3258
3305
  for (const envName of ["staging", "production"]) {
3259
3306
  steps.push(await runStep("environments", `upsert ${envName}`, dryRun, async () => {
3260
3307
  await envs.upsertEnvironment({ name: envName });
@@ -3264,7 +3311,7 @@ async function runSetup(input) {
3264
3311
  }
3265
3312
  if (loader.has("deployment")) {
3266
3313
  const deploy = loader.get("deployment");
3267
- print("deployment");
3314
+ print(style.step("deployment"));
3268
3315
  steps.push(await runStep("deployment", `ensureProject ${config.name}`, dryRun, async () => {
3269
3316
  await deploy.ensureProject({ name: config.name });
3270
3317
  }));
@@ -3272,7 +3319,7 @@ async function runSetup(input) {
3272
3319
  }
3273
3320
  if (loader.has("auth")) {
3274
3321
  const auth = loader.get("auth");
3275
- print("auth");
3322
+ print(style.step("auth"));
3276
3323
  if (auth.ensureWebhookApp) {
3277
3324
  steps.push(await runStep("auth", "ensureWebhookApp", dryRun, async () => {
3278
3325
  return `webhook ${(await auth.ensureWebhookApp()).alreadyExists ? "exists" : "created"}`;
@@ -3290,7 +3337,7 @@ async function runSetup(input) {
3290
3337
  }
3291
3338
  if (loader.has("vault")) {
3292
3339
  const vault = loader.get("vault");
3293
- print("vault");
3340
+ print(style.step("vault"));
3294
3341
  if (vault.ensureProject) {
3295
3342
  steps.push(await runStep("vault", `ensureProject ${config.name}`, dryRun, async () => {
3296
3343
  return `project ${(await vault.ensureProject(config.name)).alreadyExists ? "exists" : "created"}`;
@@ -3327,7 +3374,7 @@ async function runSetup(input) {
3327
3374
  }
3328
3375
  if (loader.has("tooling")) {
3329
3376
  const tools = loader.get("tooling");
3330
- print("tooling");
3377
+ print(style.step("tooling"));
3331
3378
  for (const tool of tools) {
3332
3379
  steps.push(await runStep("tooling", `${tool.providerName}.sync`, dryRun, async () => {
3333
3380
  await tool.sync();
@@ -3348,23 +3395,24 @@ async function runSetup(input) {
3348
3395
  dryRun: 0
3349
3396
  });
3350
3397
  print("");
3351
- print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`);
3398
+ const summaryLine = ` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
3399
+ print(summary.fail > 0 ? style.fail(summaryLine.trim()) : style.success(summaryLine.trim()));
3352
3400
  if (steps.some((s) => s.reason === "permissions")) {
3353
3401
  print("");
3354
- print("Some steps failed with 403 (insufficient token permissions).");
3355
- print(" These operations require a temporary fine-grained PAT with the");
3356
- print(" following repository permissions:");
3402
+ print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
3403
+ print(style.hint(" These operations require a temporary fine-grained PAT with the"));
3404
+ print(style.hint(" following repository permissions:"));
3357
3405
  print("");
3358
- print(" · Administration — read and write");
3359
- print(" · Code scanning alerts — read and write");
3360
- print(" · Contents — read and write");
3361
- print(" · Secret scanning alerts — read and write");
3362
- print(" · Workflows — read and write");
3363
- print(" · Metadata — read (added automatically)");
3406
+ print(style.hint(" · Administration — read and write"));
3407
+ print(style.hint(" · Code scanning alerts — read and write"));
3408
+ print(style.hint(" · Contents — read and write"));
3409
+ print(style.hint(" · Secret scanning alerts — read and write"));
3410
+ print(style.hint(" · Workflows — read and write"));
3411
+ print(style.hint(" · Metadata — read (added automatically)"));
3364
3412
  print("");
3365
- print(" Create one at: https://github.com/settings/personal-access-tokens/new");
3366
- print(" Then re-run: holocron setup --token <your-temp-pat>");
3367
- print(" You can revoke it immediately after setup completes.");
3413
+ print(style.hint(" Create one at: https://github.com/settings/personal-access-tokens/new"));
3414
+ print(style.hint(" Then re-run: holocron setup --token <your-temp-pat>"));
3415
+ print(style.hint(" You can revoke it immediately after setup completes."));
3368
3416
  }
3369
3417
  return {
3370
3418
  steps,
@@ -3412,10 +3460,13 @@ function classify403(err) {
3412
3460
  return "permissions";
3413
3461
  }
3414
3462
  function formatStep(step) {
3415
- const icon = step.status === "ok" ? "✓" : step.status === "fail" ? "✗" : step.status === "dry-run" ? "…" : "·";
3416
3463
  const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
3417
- const detail = step.message ? ` (${step.message})` : "";
3418
- return ` ${icon} ${step.step}${tag}${detail}`;
3464
+ const detail = step.message ? style.dim(` (${step.message})`) : "";
3465
+ const label = `${step.step}${tag}${detail}`;
3466
+ if (step.status === "ok") return ` ${style.success(label)}`;
3467
+ if (step.status === "fail") return ` ${style.fail(label)}`;
3468
+ if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
3469
+ return ` ${style.dim(`· ${label}`)}`;
3419
3470
  }
3420
3471
  //#endregion
3421
3472
  //#region src/commands/sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.65",
3
+ "version": "2.0.0-alpha.67",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -36,6 +36,8 @@
36
36
  "@napi-rs/keyring": "^1.3.0",
37
37
  "@theholocron/github-client": "^0.11.3",
38
38
  "@theholocron/http-client": "^0.11.3",
39
+ "chalk": "^5.4.1",
40
+ "ora": "^8.2.0",
39
41
  "tsx": "^4.22.4",
40
42
  "yargs": "^18.0.0"
41
43
  },