grimoire-wizard 0.4.0 → 0.5.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.js CHANGED
@@ -184,7 +184,14 @@ var themeConfigSchema = z.object({
184
184
  stepDone: z.string().optional(),
185
185
  stepPending: z.string().optional(),
186
186
  pointer: z.string().optional()
187
- }).optional()
187
+ }).optional(),
188
+ spinner: z.union([
189
+ z.string(),
190
+ z.object({
191
+ frames: z.array(z.string()).min(1),
192
+ interval: z.number().positive().optional()
193
+ })
194
+ ]).optional()
188
195
  });
189
196
  var preFlightCheckSchema = z.object({
190
197
  name: z.string(),
@@ -201,7 +208,8 @@ var wizardConfigSchema = z.object({
201
208
  name: z.string(),
202
209
  version: z.string().optional(),
203
210
  description: z.string().optional(),
204
- review: z.boolean().optional()
211
+ review: z.boolean().optional(),
212
+ icon: z.string().optional()
205
213
  }),
206
214
  theme: themeConfigSchema.optional(),
207
215
  steps: z.array(stepConfigSchema).min(1),
@@ -211,7 +219,8 @@ var wizardConfigSchema = z.object({
211
219
  }).optional(),
212
220
  extends: z.string().optional(),
213
221
  checks: z.array(preFlightCheckSchema).optional(),
214
- actions: z.array(actionConfigSchema).optional()
222
+ actions: z.array(actionConfigSchema).optional(),
223
+ onComplete: z.string().optional()
215
224
  }).superRefine((config, ctx) => {
216
225
  const stepIds = /* @__PURE__ */ new Set();
217
226
  for (const step of config.steps) {
@@ -486,6 +495,8 @@ async function loadWizardConfig(filePath) {
486
495
 
487
496
  // src/runner.ts
488
497
  import { execSync } from "child_process";
498
+ import { resolve as resolve2, dirname as dirname2 } from "path";
499
+ import { pathToFileURL } from "url";
489
500
 
490
501
  // src/conditions.ts
491
502
  function isRecord(value) {
@@ -825,6 +836,41 @@ var THEME_PRESETS = {
825
836
  };
826
837
  var PRESET_NAMES = Object.keys(THEME_PRESETS);
827
838
 
839
+ // src/spinners.ts
840
+ var spinners = {
841
+ dots: { interval: 80, frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] },
842
+ dots2: { interval: 80, frames: ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"] },
843
+ line: { interval: 130, frames: ["-", "\\", "|", "/"] },
844
+ arc: { interval: 100, frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"] },
845
+ circle: { interval: 80, frames: ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] },
846
+ circleHalves: { interval: 50, frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"] },
847
+ triangle: { interval: 50, frames: ["\u25E2", "\u25E3", "\u25E4", "\u25E5"] },
848
+ pipe: { interval: 100, frames: ["\u2524", "\u2518", "\u2534", "\u2514", "\u251C", "\u250C", "\u252C", "\u2510"] },
849
+ arrow: { interval: 100, frames: ["\u2190", "\u2196", "\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199"] },
850
+ arrow3: { interval: 120, frames: ["\u25B9\u25B9\u25B9\u25B9\u25B9", "\u25B8\u25B9\u25B9\u25B9\u25B9", "\u25B9\u25B8\u25B9\u25B9\u25B9", "\u25B9\u25B9\u25B8\u25B9\u25B9", "\u25B9\u25B9\u25B9\u25B8\u25B9", "\u25B9\u25B9\u25B9\u25B9\u25B8"] },
851
+ bouncingBar: { interval: 80, frames: ["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"] },
852
+ bouncingBall: { interval: 80, frames: ["( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF)", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "(\u25CF )"] },
853
+ simpleDots: { interval: 400, frames: [". ", ".. ", "...", " "] },
854
+ aesthetic: { interval: 80, frames: ["\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B1\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B0\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B0\u25B0\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B0\u25B0\u25B0", "\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1"] },
855
+ star: { interval: 70, frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"] }
856
+ };
857
+ var DEFAULT_SPINNER = "circle";
858
+ function resolveSpinner(config) {
859
+ if (!config) {
860
+ return spinners[DEFAULT_SPINNER];
861
+ }
862
+ if (typeof config === "string") {
863
+ if (config in spinners) {
864
+ return spinners[config];
865
+ }
866
+ throw new Error(`Unknown spinner preset: "${config}". Available: ${Object.keys(spinners).join(", ")}`);
867
+ }
868
+ return {
869
+ frames: config.frames,
870
+ interval: config.interval ?? 80
871
+ };
872
+ }
873
+
828
874
  // src/theme.ts
829
875
  var DEFAULT_TOKENS = {
830
876
  primary: "#5B9BD5",
@@ -854,7 +900,8 @@ function resolveTheme(themeConfig) {
854
900
  muted: chalk.hex(tokens.muted),
855
901
  accent: chalk.hex(tokens.accent),
856
902
  bold: chalk.bold,
857
- icons
903
+ icons,
904
+ spinner: resolveSpinner(themeConfig?.spinner)
858
905
  };
859
906
  }
860
907
 
@@ -1078,14 +1125,27 @@ function resolveTemplate(template, answers) {
1078
1125
  return _match;
1079
1126
  });
1080
1127
  }
1128
+ function resolveTemplateStrict(template, answers) {
1129
+ return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
1130
+ const trimmedKey = key.trim();
1131
+ if (!(trimmedKey in answers)) {
1132
+ throw new Error(`Action references unknown step "${trimmedKey}"`);
1133
+ }
1134
+ const value = answers[trimmedKey];
1135
+ if (Array.isArray(value)) return value.join(", ");
1136
+ return String(value);
1137
+ });
1138
+ }
1081
1139
 
1082
1140
  // src/banner.ts
1083
1141
  import figlet from "figlet";
1084
1142
  import gradient from "gradient-string";
1085
1143
  var GRIMOIRE_GRADIENT = gradient(["#C084FC", "#5B9BD5", "#6BCB77"]);
1086
1144
  function renderBanner(name, theme, options) {
1145
+ const icon = options?.icon;
1146
+ const prefix = icon ? `${icon} ` : "";
1087
1147
  if (options?.plain) {
1088
- return ` ${theme.bold(name)}`;
1148
+ return ` ${prefix}${theme.bold(name)}`;
1089
1149
  }
1090
1150
  try {
1091
1151
  const art = figlet.textSync(name, {
@@ -1093,9 +1153,11 @@ function renderBanner(name, theme, options) {
1093
1153
  horizontalLayout: "default"
1094
1154
  });
1095
1155
  const lines = art.split("\n").map((line) => ` ${line}`).join("\n");
1096
- return GRIMOIRE_GRADIENT(lines);
1156
+ const banner = GRIMOIRE_GRADIENT(lines);
1157
+ return icon ? ` ${icon}
1158
+ ${banner}` : banner;
1097
1159
  } catch {
1098
- return ` ${theme.bold(name)}`;
1160
+ return ` ${prefix}${theme.bold(name)}`;
1099
1161
  }
1100
1162
  }
1101
1163
 
@@ -1328,11 +1390,14 @@ function emitEvent(renderer, event, theme) {
1328
1390
  function runPreFlightChecks(checks, theme, renderer) {
1329
1391
  if (renderer) emitEvent(renderer, { type: "checks:start", checks }, theme);
1330
1392
  for (const check of checks) {
1393
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: check.name }, theme);
1331
1394
  try {
1332
1395
  execSync(check.run, { stdio: "pipe" });
1396
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: `${check.name}` }, theme);
1333
1397
  console.log(` ${theme.success("\u2713")} ${check.name}`);
1334
1398
  if (renderer) emitEvent(renderer, { type: "check:pass", name: check.name }, theme);
1335
1399
  } catch {
1400
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1336
1401
  console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
1337
1402
  if (renderer) emitEvent(renderer, { type: "check:fail", name: check.name, message: check.message }, theme);
1338
1403
  throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
@@ -1452,8 +1517,17 @@ async function runWizard(config, options) {
1452
1517
  const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
1453
1518
  const templatedStep = resolveStepTemplates(withTemplate, state.answers);
1454
1519
  const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
1520
+ let finalStep = mruStep;
1521
+ if (!isMock && options?.optionsProvider && isSelectLikeStep(currentStep.type)) {
1522
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: resolvedMessage }, theme);
1523
+ const dynamicOptions = await options.optionsProvider(currentStep.id, state.answers);
1524
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: resolvedMessage }, theme);
1525
+ if (dynamicOptions) {
1526
+ finalStep = { ...mruStep, options: dynamicOptions };
1527
+ }
1528
+ }
1455
1529
  try {
1456
- const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
1530
+ const value = isMock ? getMockValue(finalStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(finalStep), state, theme) : await renderStep(renderer, finalStep, state, theme);
1457
1531
  if (pluginStep?.validate) {
1458
1532
  const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1459
1533
  if (pluginError) {
@@ -1569,8 +1643,13 @@ async function runWizard(config, options) {
1569
1643
  if (state.status === "done" && !quiet) {
1570
1644
  renderer.renderSummary(state.answers, config.steps, theme);
1571
1645
  }
1572
- if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1573
- await executeActions(config.actions, state.answers, theme, renderer);
1646
+ if (state.status === "done" && !isMock) {
1647
+ if (config.onComplete) {
1648
+ await executeOnComplete(config.onComplete, options?.configFilePath, state.answers, config, theme, renderer);
1649
+ }
1650
+ if (config.actions && config.actions.length > 0) {
1651
+ await executeActions(config.actions, state.answers, theme, renderer);
1652
+ }
1574
1653
  }
1575
1654
  emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: state.status === "cancelled" }, theme);
1576
1655
  if (state.status === "done" && cacheEnabled) {
@@ -1785,6 +1864,28 @@ function resolveStepTemplates(step, answers) {
1785
1864
  };
1786
1865
  }
1787
1866
  }
1867
+ async function executeOnComplete(handlerPath, configFilePath, answers, config, theme, renderer) {
1868
+ if (renderer) emitEvent(renderer, { type: "oncomplete:start" }, theme);
1869
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: `Running onComplete handler...` }, theme);
1870
+ const resolvedPath = configFilePath ? resolve2(dirname2(configFilePath), handlerPath) : resolve2(handlerPath);
1871
+ try {
1872
+ const mod = await import(pathToFileURL(resolvedPath).href);
1873
+ if (typeof mod.default !== "function") {
1874
+ throw new Error(`onComplete handler "${handlerPath}" must export a default function`);
1875
+ }
1876
+ await mod.default({ answers, config });
1877
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: "Handler complete" }, theme);
1878
+ if (renderer) emitEvent(renderer, { type: "oncomplete:pass" }, theme);
1879
+ } catch (error) {
1880
+ const message = error instanceof Error ? error.message : String(error);
1881
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1882
+ if (renderer) emitEvent(renderer, { type: "oncomplete:fail", error: message }, theme);
1883
+ console.log(`
1884
+ ${theme.error("\u2717")} onComplete handler failed: ${message}
1885
+ `);
1886
+ throw error;
1887
+ }
1888
+ }
1788
1889
  async function executeActions(actions, answers, theme, renderer) {
1789
1890
  if (renderer) emitEvent(renderer, { type: "actions:start" }, theme);
1790
1891
  console.log(`
@@ -1794,14 +1895,17 @@ async function executeActions(actions, answers, theme, renderer) {
1794
1895
  if (action.when && !evaluateCondition(action.when, answers)) {
1795
1896
  continue;
1796
1897
  }
1797
- const resolvedCommand = resolveTemplate(action.run, answers);
1798
- const resolvedName = action.name ? resolveTemplate(action.name, answers) : void 0;
1898
+ const resolvedCommand = resolveTemplateStrict(action.run, answers);
1899
+ const resolvedName = action.name ? resolveTemplateStrict(action.name, answers) : void 0;
1799
1900
  const label = resolvedName ?? resolvedCommand;
1901
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: label }, theme);
1800
1902
  try {
1801
1903
  execSync(resolvedCommand, { stdio: "pipe" });
1904
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: label }, theme);
1802
1905
  console.log(` ${theme.success("\u2713")} ${label}`);
1803
1906
  if (renderer) emitEvent(renderer, { type: "action:pass", name: label }, theme);
1804
1907
  } catch {
1908
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1805
1909
  console.log(` ${theme.error("\u2717")} ${label}`);
1806
1910
  if (renderer) emitEvent(renderer, { type: "action:fail", name: label }, theme);
1807
1911
  throw new Error(`Action failed: ${label}`);
@@ -1811,7 +1915,7 @@ async function executeActions(actions, answers, theme, renderer) {
1811
1915
  }
1812
1916
  function printWizardHeader(config, theme, plain) {
1813
1917
  console.log();
1814
- console.log(renderBanner(config.meta.name, theme, { plain }));
1918
+ console.log(renderBanner(config.meta.name, theme, { plain, icon: config.meta.icon }));
1815
1919
  if (config.meta.description) {
1816
1920
  console.log(` ${theme.muted(config.meta.description)}`);
1817
1921
  }
@@ -2392,7 +2496,6 @@ var S_STEP_ERROR = u("\u25B2", "x");
2392
2496
  var S_CORNER_TR = u("\u256E", "+");
2393
2497
  var S_CORNER_BR = u("\u256F", "+");
2394
2498
  var S_BAR_H = u("\u2500", "-");
2395
- var S_SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"];
2396
2499
 
2397
2500
  // src/renderers/clack.ts
2398
2501
  var ClackRenderer = class extends InquirerRenderer {
@@ -2531,13 +2634,14 @@ var ClackRenderer = class extends InquirerRenderer {
2531
2634
  process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256E${bottomLine}`)}
2532
2635
  `);
2533
2636
  }
2534
- startSpinner(message, _theme) {
2637
+ startSpinner(message, theme) {
2535
2638
  this.spinnerFrameIndex = 0;
2639
+ const { frames, interval } = theme.spinner;
2536
2640
  this.spinnerInterval = setInterval(() => {
2537
- const frame = S_SPINNER_FRAMES[this.spinnerFrameIndex % S_SPINNER_FRAMES.length];
2641
+ const frame = frames[this.spinnerFrameIndex % frames.length];
2538
2642
  process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
2539
2643
  this.spinnerFrameIndex++;
2540
- }, 80);
2644
+ }, interval);
2541
2645
  }
2542
2646
  stopSpinner(message, theme) {
2543
2647
  if (this.spinnerInterval) {
@@ -2552,7 +2656,7 @@ var ClackRenderer = class extends InquirerRenderer {
2552
2656
 
2553
2657
  // src/cli.ts
2554
2658
  import { writeFileSync as writeFileSync6 } from "fs";
2555
- import { resolve as resolve2 } from "path";
2659
+ import { resolve as resolve3 } from "path";
2556
2660
  import { fileURLToPath } from "url";
2557
2661
  import { stringify as yamlStringify } from "yaml";
2558
2662
  var plainMode = false;
@@ -2568,7 +2672,7 @@ program.name("grimoire").description("Config-driven CLI wizard framework").versi
2568
2672
  });
2569
2673
  program.command("run").description("Run a wizard from a config file").argument("<config>", "Path to wizard config file (.yaml, .json, .js, .ts)").option("-o, --output <path>", "Write answers to file").option("-f, --format <format>", "Output format: json, env, yaml", "json").option("-q, --quiet", "Suppress header and summary output").option("--dry-run", "Show step plan without running the wizard").option("--mock <json>", "Run wizard with preset answers (JSON string)").option("--json", "Output structured JSON result to stdout").option("--no-cache", "Disable answer caching for this run").option("--no-resume", "Disable progress resume for this run").option("--renderer <type>", "Renderer to use: inquirer (default), ink, or clack", "inquirer").option("--template <name>", "Load a saved template as defaults").action(async (configPath, opts) => {
2570
2674
  try {
2571
- const fullPath = resolve2(configPath);
2675
+ const fullPath = resolve3(configPath);
2572
2676
  const config = await loadWizardConfig(fullPath);
2573
2677
  if (opts.dryRun) {
2574
2678
  printDryRun(config);
@@ -2594,10 +2698,11 @@ program.command("run").description("Run a wizard from a config file").argument("
2594
2698
  mockAnswers,
2595
2699
  templateAnswers,
2596
2700
  cache: opts.cache,
2597
- resume: opts.resume
2701
+ resume: opts.resume,
2702
+ configFilePath: fullPath
2598
2703
  });
2599
2704
  const rawOutputPath = opts.output ?? config.output?.path;
2600
- const outputPath = rawOutputPath ? resolve2(resolveTemplate(rawOutputPath, answers)) : void 0;
2705
+ const outputPath = rawOutputPath ? resolve3(resolveTemplate(rawOutputPath, answers)) : void 0;
2601
2706
  if (isJsonOutput) {
2602
2707
  const stepsCompleted = Object.keys(answers).length;
2603
2708
  const result = {
@@ -2643,7 +2748,7 @@ program.command("run").description("Run a wizard from a config file").argument("
2643
2748
  });
2644
2749
  program.command("validate").description("Validate a wizard config file without running it").argument("<config>", "Path to wizard config file").action(async (configPath) => {
2645
2750
  try {
2646
- const fullPath = resolve2(configPath);
2751
+ const fullPath = resolve3(configPath);
2647
2752
  const config = await loadWizardConfig(fullPath);
2648
2753
  console.log(`
2649
2754
  \u2713 Valid wizard config: "${config.meta.name}"`);
@@ -2660,7 +2765,7 @@ program.command("validate").description("Validate a wizard config file without r
2660
2765
  });
2661
2766
  program.command("create").description("Interactively scaffold a new wizard config file").argument("[output]", "Output file path", "wizard.yaml").action(async (output) => {
2662
2767
  try {
2663
- const resolvedPath = resolve2(output);
2768
+ const resolvedPath = resolve3(output);
2664
2769
  await scaffoldWizard(resolvedPath);
2665
2770
  } catch (error) {
2666
2771
  if (error instanceof Error) {
@@ -2673,7 +2778,7 @@ program.command("create").description("Interactively scaffold a new wizard confi
2673
2778
  });
2674
2779
  program.command("demo").description("Run a demo wizard showcasing all step types").action(async () => {
2675
2780
  try {
2676
- const demoPath = resolve2(
2781
+ const demoPath = resolve3(
2677
2782
  fileURLToPath(import.meta.url),
2678
2783
  "..",
2679
2784
  "..",
@@ -2812,6 +2917,11 @@ function printDryRun(config) {
2812
2917
  console.log(` Step ${num} ${typeStr} ${idStr} ${msg}${suffix}`);
2813
2918
  }
2814
2919
  console.log();
2920
+ if (config.onComplete) {
2921
+ console.log(` ${theme.bold("onComplete handler:")}`);
2922
+ console.log(` ${theme.muted(config.onComplete)}`);
2923
+ console.log();
2924
+ }
2815
2925
  if (config.actions && config.actions.length > 0) {
2816
2926
  console.log(` ${theme.bold("Post-wizard actions:")}`);
2817
2927
  for (const action of config.actions) {