grimoire-wizard 0.4.0 → 0.5.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
@@ -1549,6 +1549,18 @@ grimoire run examples/yaml/pipeline.yaml
1549
1549
 
1550
1550
  ---
1551
1551
 
1552
+ ## AI Agent Integration
1553
+
1554
+ Building wizard configs with an AI assistant? Give it this reference:
1555
+
1556
+ ```
1557
+ https://raw.githubusercontent.com/YosefHayim/grimoire/main/docs/GRIMOIRE_REFERENCE.md
1558
+ ```
1559
+
1560
+ This single file contains the complete grimoire-wizard schema, all step types, conditions, actions, handler contracts, and annotated examples — everything an AI agent needs to generate correct configs in one shot.
1561
+
1562
+ ---
1563
+
1552
1564
  ## License
1553
1565
 
1554
1566
  MIT. PRs welcome.
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(),
@@ -211,7 +218,8 @@ var wizardConfigSchema = z.object({
211
218
  }).optional(),
212
219
  extends: z.string().optional(),
213
220
  checks: z.array(preFlightCheckSchema).optional(),
214
- actions: z.array(actionConfigSchema).optional()
221
+ actions: z.array(actionConfigSchema).optional(),
222
+ onComplete: z.string().optional()
215
223
  }).superRefine((config, ctx) => {
216
224
  const stepIds = /* @__PURE__ */ new Set();
217
225
  for (const step of config.steps) {
@@ -486,6 +494,8 @@ async function loadWizardConfig(filePath) {
486
494
 
487
495
  // src/runner.ts
488
496
  import { execSync } from "child_process";
497
+ import { resolve as resolve2, dirname as dirname2 } from "path";
498
+ import { pathToFileURL } from "url";
489
499
 
490
500
  // src/conditions.ts
491
501
  function isRecord(value) {
@@ -825,6 +835,41 @@ var THEME_PRESETS = {
825
835
  };
826
836
  var PRESET_NAMES = Object.keys(THEME_PRESETS);
827
837
 
838
+ // src/spinners.ts
839
+ var spinners = {
840
+ dots: { interval: 80, frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] },
841
+ dots2: { interval: 80, frames: ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"] },
842
+ line: { interval: 130, frames: ["-", "\\", "|", "/"] },
843
+ arc: { interval: 100, frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"] },
844
+ circle: { interval: 80, frames: ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] },
845
+ circleHalves: { interval: 50, frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"] },
846
+ triangle: { interval: 50, frames: ["\u25E2", "\u25E3", "\u25E4", "\u25E5"] },
847
+ pipe: { interval: 100, frames: ["\u2524", "\u2518", "\u2534", "\u2514", "\u251C", "\u250C", "\u252C", "\u2510"] },
848
+ arrow: { interval: 100, frames: ["\u2190", "\u2196", "\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199"] },
849
+ 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"] },
850
+ bouncingBar: { interval: 80, frames: ["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"] },
851
+ bouncingBall: { interval: 80, frames: ["( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF)", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "(\u25CF )"] },
852
+ simpleDots: { interval: 400, frames: [". ", ".. ", "...", " "] },
853
+ 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"] },
854
+ star: { interval: 70, frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"] }
855
+ };
856
+ var DEFAULT_SPINNER = "circle";
857
+ function resolveSpinner(config) {
858
+ if (!config) {
859
+ return spinners[DEFAULT_SPINNER];
860
+ }
861
+ if (typeof config === "string") {
862
+ if (config in spinners) {
863
+ return spinners[config];
864
+ }
865
+ throw new Error(`Unknown spinner preset: "${config}". Available: ${Object.keys(spinners).join(", ")}`);
866
+ }
867
+ return {
868
+ frames: config.frames,
869
+ interval: config.interval ?? 80
870
+ };
871
+ }
872
+
828
873
  // src/theme.ts
829
874
  var DEFAULT_TOKENS = {
830
875
  primary: "#5B9BD5",
@@ -854,7 +899,8 @@ function resolveTheme(themeConfig) {
854
899
  muted: chalk.hex(tokens.muted),
855
900
  accent: chalk.hex(tokens.accent),
856
901
  bold: chalk.bold,
857
- icons
902
+ icons,
903
+ spinner: resolveSpinner(themeConfig?.spinner)
858
904
  };
859
905
  }
860
906
 
@@ -1078,6 +1124,17 @@ function resolveTemplate(template, answers) {
1078
1124
  return _match;
1079
1125
  });
1080
1126
  }
1127
+ function resolveTemplateStrict(template, answers) {
1128
+ return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
1129
+ const trimmedKey = key.trim();
1130
+ if (!(trimmedKey in answers)) {
1131
+ throw new Error(`Action references unknown step "${trimmedKey}"`);
1132
+ }
1133
+ const value = answers[trimmedKey];
1134
+ if (Array.isArray(value)) return value.join(", ");
1135
+ return String(value);
1136
+ });
1137
+ }
1081
1138
 
1082
1139
  // src/banner.ts
1083
1140
  import figlet from "figlet";
@@ -1569,8 +1626,13 @@ async function runWizard(config, options) {
1569
1626
  if (state.status === "done" && !quiet) {
1570
1627
  renderer.renderSummary(state.answers, config.steps, theme);
1571
1628
  }
1572
- if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1573
- await executeActions(config.actions, state.answers, theme, renderer);
1629
+ if (state.status === "done" && !isMock) {
1630
+ if (config.onComplete) {
1631
+ await executeOnComplete(config.onComplete, options?.configFilePath, state.answers, config, theme, renderer);
1632
+ }
1633
+ if (config.actions && config.actions.length > 0) {
1634
+ await executeActions(config.actions, state.answers, theme, renderer);
1635
+ }
1574
1636
  }
1575
1637
  emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: state.status === "cancelled" }, theme);
1576
1638
  if (state.status === "done" && cacheEnabled) {
@@ -1785,6 +1847,25 @@ function resolveStepTemplates(step, answers) {
1785
1847
  };
1786
1848
  }
1787
1849
  }
1850
+ async function executeOnComplete(handlerPath, configFilePath, answers, config, theme, renderer) {
1851
+ if (renderer) emitEvent(renderer, { type: "oncomplete:start" }, theme);
1852
+ const resolvedPath = configFilePath ? resolve2(dirname2(configFilePath), handlerPath) : resolve2(handlerPath);
1853
+ try {
1854
+ const mod = await import(pathToFileURL(resolvedPath).href);
1855
+ if (typeof mod.default !== "function") {
1856
+ throw new Error(`onComplete handler "${handlerPath}" must export a default function`);
1857
+ }
1858
+ await mod.default({ answers, config });
1859
+ if (renderer) emitEvent(renderer, { type: "oncomplete:pass" }, theme);
1860
+ } catch (error) {
1861
+ const message = error instanceof Error ? error.message : String(error);
1862
+ if (renderer) emitEvent(renderer, { type: "oncomplete:fail", error: message }, theme);
1863
+ console.log(`
1864
+ ${theme.error("\u2717")} onComplete handler failed: ${message}
1865
+ `);
1866
+ throw error;
1867
+ }
1868
+ }
1788
1869
  async function executeActions(actions, answers, theme, renderer) {
1789
1870
  if (renderer) emitEvent(renderer, { type: "actions:start" }, theme);
1790
1871
  console.log(`
@@ -1794,8 +1875,8 @@ async function executeActions(actions, answers, theme, renderer) {
1794
1875
  if (action.when && !evaluateCondition(action.when, answers)) {
1795
1876
  continue;
1796
1877
  }
1797
- const resolvedCommand = resolveTemplate(action.run, answers);
1798
- const resolvedName = action.name ? resolveTemplate(action.name, answers) : void 0;
1878
+ const resolvedCommand = resolveTemplateStrict(action.run, answers);
1879
+ const resolvedName = action.name ? resolveTemplateStrict(action.name, answers) : void 0;
1799
1880
  const label = resolvedName ?? resolvedCommand;
1800
1881
  try {
1801
1882
  execSync(resolvedCommand, { stdio: "pipe" });
@@ -2392,7 +2473,6 @@ var S_STEP_ERROR = u("\u25B2", "x");
2392
2473
  var S_CORNER_TR = u("\u256E", "+");
2393
2474
  var S_CORNER_BR = u("\u256F", "+");
2394
2475
  var S_BAR_H = u("\u2500", "-");
2395
- var S_SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"];
2396
2476
 
2397
2477
  // src/renderers/clack.ts
2398
2478
  var ClackRenderer = class extends InquirerRenderer {
@@ -2531,13 +2611,14 @@ var ClackRenderer = class extends InquirerRenderer {
2531
2611
  process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256E${bottomLine}`)}
2532
2612
  `);
2533
2613
  }
2534
- startSpinner(message, _theme) {
2614
+ startSpinner(message, theme) {
2535
2615
  this.spinnerFrameIndex = 0;
2616
+ const { frames, interval } = theme.spinner;
2536
2617
  this.spinnerInterval = setInterval(() => {
2537
- const frame = S_SPINNER_FRAMES[this.spinnerFrameIndex % S_SPINNER_FRAMES.length];
2618
+ const frame = frames[this.spinnerFrameIndex % frames.length];
2538
2619
  process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
2539
2620
  this.spinnerFrameIndex++;
2540
- }, 80);
2621
+ }, interval);
2541
2622
  }
2542
2623
  stopSpinner(message, theme) {
2543
2624
  if (this.spinnerInterval) {
@@ -2552,7 +2633,7 @@ var ClackRenderer = class extends InquirerRenderer {
2552
2633
 
2553
2634
  // src/cli.ts
2554
2635
  import { writeFileSync as writeFileSync6 } from "fs";
2555
- import { resolve as resolve2 } from "path";
2636
+ import { resolve as resolve3 } from "path";
2556
2637
  import { fileURLToPath } from "url";
2557
2638
  import { stringify as yamlStringify } from "yaml";
2558
2639
  var plainMode = false;
@@ -2568,7 +2649,7 @@ program.name("grimoire").description("Config-driven CLI wizard framework").versi
2568
2649
  });
2569
2650
  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
2651
  try {
2571
- const fullPath = resolve2(configPath);
2652
+ const fullPath = resolve3(configPath);
2572
2653
  const config = await loadWizardConfig(fullPath);
2573
2654
  if (opts.dryRun) {
2574
2655
  printDryRun(config);
@@ -2594,10 +2675,11 @@ program.command("run").description("Run a wizard from a config file").argument("
2594
2675
  mockAnswers,
2595
2676
  templateAnswers,
2596
2677
  cache: opts.cache,
2597
- resume: opts.resume
2678
+ resume: opts.resume,
2679
+ configFilePath: fullPath
2598
2680
  });
2599
2681
  const rawOutputPath = opts.output ?? config.output?.path;
2600
- const outputPath = rawOutputPath ? resolve2(resolveTemplate(rawOutputPath, answers)) : void 0;
2682
+ const outputPath = rawOutputPath ? resolve3(resolveTemplate(rawOutputPath, answers)) : void 0;
2601
2683
  if (isJsonOutput) {
2602
2684
  const stepsCompleted = Object.keys(answers).length;
2603
2685
  const result = {
@@ -2643,7 +2725,7 @@ program.command("run").description("Run a wizard from a config file").argument("
2643
2725
  });
2644
2726
  program.command("validate").description("Validate a wizard config file without running it").argument("<config>", "Path to wizard config file").action(async (configPath) => {
2645
2727
  try {
2646
- const fullPath = resolve2(configPath);
2728
+ const fullPath = resolve3(configPath);
2647
2729
  const config = await loadWizardConfig(fullPath);
2648
2730
  console.log(`
2649
2731
  \u2713 Valid wizard config: "${config.meta.name}"`);
@@ -2660,7 +2742,7 @@ program.command("validate").description("Validate a wizard config file without r
2660
2742
  });
2661
2743
  program.command("create").description("Interactively scaffold a new wizard config file").argument("[output]", "Output file path", "wizard.yaml").action(async (output) => {
2662
2744
  try {
2663
- const resolvedPath = resolve2(output);
2745
+ const resolvedPath = resolve3(output);
2664
2746
  await scaffoldWizard(resolvedPath);
2665
2747
  } catch (error) {
2666
2748
  if (error instanceof Error) {
@@ -2673,7 +2755,7 @@ program.command("create").description("Interactively scaffold a new wizard confi
2673
2755
  });
2674
2756
  program.command("demo").description("Run a demo wizard showcasing all step types").action(async () => {
2675
2757
  try {
2676
- const demoPath = resolve2(
2758
+ const demoPath = resolve3(
2677
2759
  fileURLToPath(import.meta.url),
2678
2760
  "..",
2679
2761
  "..",
@@ -2812,6 +2894,11 @@ function printDryRun(config) {
2812
2894
  console.log(` Step ${num} ${typeStr} ${idStr} ${msg}${suffix}`);
2813
2895
  }
2814
2896
  console.log();
2897
+ if (config.onComplete) {
2898
+ console.log(` ${theme.bold("onComplete handler:")}`);
2899
+ console.log(` ${theme.muted(config.onComplete)}`);
2900
+ console.log();
2901
+ }
2815
2902
  if (config.actions && config.actions.length > 0) {
2816
2903
  console.log(` ${theme.bold("Post-wizard actions:")}`);
2817
2904
  for (const action of config.actions) {