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/index.d.ts CHANGED
@@ -164,6 +164,10 @@ interface ThemeConfig {
164
164
  stepPending?: string;
165
165
  pointer?: string;
166
166
  };
167
+ spinner?: string | {
168
+ frames: string[];
169
+ interval?: number;
170
+ };
167
171
  }
168
172
  interface PreFlightCheck {
169
173
  name: string;
@@ -175,12 +179,17 @@ interface ActionConfig {
175
179
  run: string;
176
180
  when?: Condition;
177
181
  }
182
+ type OnCompleteHandler = (context: {
183
+ answers: Record<string, unknown>;
184
+ config: WizardConfig;
185
+ }) => Promise<void> | void;
178
186
  interface WizardConfig {
179
187
  meta: {
180
188
  name: string;
181
189
  version?: string;
182
190
  description?: string;
183
191
  review?: boolean;
192
+ icon?: string;
184
193
  };
185
194
  theme?: ThemeConfig;
186
195
  steps: StepConfig[];
@@ -191,6 +200,7 @@ interface WizardConfig {
191
200
  extends?: string;
192
201
  checks?: PreFlightCheck[];
193
202
  actions?: ActionConfig[];
203
+ onComplete?: string;
194
204
  }
195
205
  interface WizardState {
196
206
  currentStepId: string;
@@ -268,6 +278,13 @@ type WizardEvent = {
268
278
  } | {
269
279
  type: 'action:fail';
270
280
  name: string;
281
+ } | {
282
+ type: 'oncomplete:start';
283
+ } | {
284
+ type: 'oncomplete:pass';
285
+ } | {
286
+ type: 'oncomplete:fail';
287
+ error: string;
271
288
  };
272
289
  interface ResolvedTheme {
273
290
  primary: (text: string) => string;
@@ -279,6 +296,10 @@ interface ResolvedTheme {
279
296
  accent: (text: string) => string;
280
297
  bold: (text: string) => string;
281
298
  icons: Required<NonNullable<ThemeConfig['icons']>>;
299
+ spinner: {
300
+ frames: string[];
301
+ interval: number;
302
+ };
282
303
  }
283
304
  interface WizardRenderer {
284
305
  renderText(step: TextStepConfig, state: WizardState, theme: ResolvedTheme): Promise<string>;
@@ -315,6 +336,79 @@ declare function wizardReducer(state: WizardState, transition: WizardTransition,
315
336
 
316
337
  declare function resolveTheme(themeConfig?: ThemeConfig): ResolvedTheme;
317
338
 
339
+ interface SpinnerConfig {
340
+ frames: string[];
341
+ interval: number;
342
+ }
343
+ declare const spinners: {
344
+ readonly dots: {
345
+ readonly interval: 80;
346
+ readonly frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
347
+ };
348
+ readonly dots2: {
349
+ readonly interval: 80;
350
+ readonly frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
351
+ };
352
+ readonly line: {
353
+ readonly interval: 130;
354
+ readonly frames: ["-", "\\", "|", "/"];
355
+ };
356
+ readonly arc: {
357
+ readonly interval: 100;
358
+ readonly frames: ["◜", "◠", "◝", "◞", "◡", "◟"];
359
+ };
360
+ readonly circle: {
361
+ readonly interval: 80;
362
+ readonly frames: ["◒", "◐", "◓", "◑"];
363
+ };
364
+ readonly circleHalves: {
365
+ readonly interval: 50;
366
+ readonly frames: ["◐", "◓", "◑", "◒"];
367
+ };
368
+ readonly triangle: {
369
+ readonly interval: 50;
370
+ readonly frames: ["◢", "◣", "◤", "◥"];
371
+ };
372
+ readonly pipe: {
373
+ readonly interval: 100;
374
+ readonly frames: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"];
375
+ };
376
+ readonly arrow: {
377
+ readonly interval: 100;
378
+ readonly frames: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"];
379
+ };
380
+ readonly arrow3: {
381
+ readonly interval: 120;
382
+ readonly frames: ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"];
383
+ };
384
+ readonly bouncingBar: {
385
+ readonly interval: 80;
386
+ readonly frames: ["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"];
387
+ };
388
+ readonly bouncingBall: {
389
+ readonly interval: 80;
390
+ readonly frames: ["( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )", "(● )"];
391
+ };
392
+ readonly simpleDots: {
393
+ readonly interval: 400;
394
+ readonly frames: [". ", ".. ", "...", " "];
395
+ };
396
+ readonly aesthetic: {
397
+ readonly interval: 80;
398
+ readonly frames: ["▰▱▱▱▱▱▱", "▰▰▱▱▱▱▱", "▰▰▰▱▱▱▱", "▰▰▰▰▱▱▱", "▰▰▰▰▰▱▱", "▰▰▰▰▰▰▱", "▰▰▰▰▰▰▰", "▰▱▱▱▱▱▱"];
399
+ };
400
+ readonly star: {
401
+ readonly interval: 70;
402
+ readonly frames: ["✶", "✸", "✹", "✺", "✹", "✷"];
403
+ };
404
+ };
405
+ type SpinnerName = keyof typeof spinners;
406
+ declare const DEFAULT_SPINNER: SpinnerName;
407
+ declare function resolveSpinner(config?: string | {
408
+ frames: string[];
409
+ interval?: number;
410
+ }): SpinnerConfig;
411
+
318
412
  declare function resolveEnvDefault(value: string | undefined): string | undefined;
319
413
 
320
414
  interface StepPlugin {
@@ -346,6 +440,8 @@ interface RunWizardOptions {
346
440
  };
347
441
  mru?: boolean;
348
442
  resume?: boolean;
443
+ configFilePath?: string;
444
+ optionsProvider?: (stepId: string, answers: Record<string, unknown>) => Promise<SelectOption[] | undefined>;
349
445
  }
350
446
  declare function runPreFlightChecks(checks: PreFlightCheck[], theme: ResolvedTheme, renderer?: WizardRenderer): void;
351
447
  declare function runWizard(config: WizardConfig, options?: RunWizardOptions): Promise<Record<string, unknown>>;
@@ -375,6 +471,12 @@ declare class InquirerRenderer implements WizardRenderer {
375
471
  * Array values are joined with ", ". Unresolved placeholders remain as-is.
376
472
  */
377
473
  declare function resolveTemplate(template: string, answers: Record<string, unknown>): string;
474
+ /**
475
+ * Resolve {{stepId}} placeholders strictly — throws if any placeholder
476
+ * references a step-id not found in the answers map.
477
+ * Used by actions where missing answers indicate a config error.
478
+ */
479
+ declare function resolveTemplateStrict(template: string, answers: Record<string, unknown>): string;
378
480
 
379
481
  /**
380
482
  * Render a figlet ASCII art banner for the wizard name.
@@ -382,6 +484,7 @@ declare function resolveTemplate(template: string, answers: Record<string, unkno
382
484
  */
383
485
  declare function renderBanner(name: string, theme: ResolvedTheme, options?: {
384
486
  plain?: boolean;
487
+ icon?: string;
385
488
  }): string;
386
489
 
387
490
  declare function slugify(name: string): string;
@@ -455,4 +558,4 @@ interface PipelineStep {
455
558
  }
456
559
  declare function runPipeline(steps: PipelineStep[], globalOptions?: Omit<RunWizardOptions, 'mockAnswers' | 'templateAnswers'>): Promise<Record<string, Record<string, unknown>>>;
457
560
 
458
- export { type ActionConfig, ClackRenderer, type Condition, type ConfirmStepConfig, type EditorStepConfig, type GrimoirePlugin, InkRenderer, InquirerRenderer, type MessageStepConfig, type MultiSelectStepConfig, type NoteStepConfig, type NumberStepConfig, type PasswordStepConfig, type PathStepConfig, type PipelineStep, type PreFlightCheck, type ResolvedTheme, type RunWizardOptions, type SavedProgress, type SearchStepConfig, type SelectChoice, type SelectOption, type SelectStepConfig, type SeparatorOption, type StepConfig, type StepPlugin, type TextStepConfig, type ThemeConfig, type ToggleStepConfig, type ValidationRule, type WizardConfig, type WizardEvent, type WizardRenderer, type WizardState, type WizardTransition, clearCache, clearMruData, clearPlugins, clearProgress, createWizardState, defineWizard, deleteTemplate, evaluateCondition, getCacheDir, getOrderedOptions, getPluginStep, getVisibleSteps, isStepVisible, listTemplates, loadCachedAnswers, loadProgress, loadTemplate, loadWizardConfig, parseWizardConfig, parseWizardYAML, recordSelection, registerPlugin, renderBanner, resolveEnvDefault, resolveNextStep, resolveTemplate, resolveTheme, runPipeline, runPreFlightChecks, runWizard, saveCachedAnswers, saveProgress, saveTemplate, slugify, validateStepAnswer, wizardReducer };
561
+ export { type ActionConfig, ClackRenderer, type Condition, type ConfirmStepConfig, DEFAULT_SPINNER, type EditorStepConfig, type GrimoirePlugin, InkRenderer, InquirerRenderer, type MessageStepConfig, type MultiSelectStepConfig, type NoteStepConfig, type NumberStepConfig, type OnCompleteHandler, type PasswordStepConfig, type PathStepConfig, type PipelineStep, type PreFlightCheck, type ResolvedTheme, type RunWizardOptions, type SavedProgress, type SearchStepConfig, type SelectChoice, type SelectOption, type SelectStepConfig, type SeparatorOption, type SpinnerConfig, type SpinnerName, type StepConfig, type StepPlugin, type TextStepConfig, type ThemeConfig, type ToggleStepConfig, type ValidationRule, type WizardConfig, type WizardEvent, type WizardRenderer, type WizardState, type WizardTransition, clearCache, clearMruData, clearPlugins, clearProgress, createWizardState, defineWizard, deleteTemplate, evaluateCondition, getCacheDir, getOrderedOptions, getPluginStep, getVisibleSteps, isStepVisible, listTemplates, loadCachedAnswers, loadProgress, loadTemplate, loadWizardConfig, parseWizardConfig, parseWizardYAML, recordSelection, registerPlugin, renderBanner, resolveEnvDefault, resolveNextStep, resolveSpinner, resolveTemplate, resolveTemplateStrict, resolveTheme, runPipeline, runPreFlightChecks, runWizard, saveCachedAnswers, saveProgress, saveTemplate, slugify, spinners, validateStepAnswer, wizardReducer };
package/dist/index.js CHANGED
@@ -218,7 +218,14 @@ var init_schema = __esm({
218
218
  stepDone: z.string().optional(),
219
219
  stepPending: z.string().optional(),
220
220
  pointer: z.string().optional()
221
- }).optional()
221
+ }).optional(),
222
+ spinner: z.union([
223
+ z.string(),
224
+ z.object({
225
+ frames: z.array(z.string()).min(1),
226
+ interval: z.number().positive().optional()
227
+ })
228
+ ]).optional()
222
229
  });
223
230
  preFlightCheckSchema = z.object({
224
231
  name: z.string(),
@@ -235,7 +242,8 @@ var init_schema = __esm({
235
242
  name: z.string(),
236
243
  version: z.string().optional(),
237
244
  description: z.string().optional(),
238
- review: z.boolean().optional()
245
+ review: z.boolean().optional(),
246
+ icon: z.string().optional()
239
247
  }),
240
248
  theme: themeConfigSchema.optional(),
241
249
  steps: z.array(stepConfigSchema).min(1),
@@ -245,7 +253,8 @@ var init_schema = __esm({
245
253
  }).optional(),
246
254
  extends: z.string().optional(),
247
255
  checks: z.array(preFlightCheckSchema).optional(),
248
- actions: z.array(actionConfigSchema).optional()
256
+ actions: z.array(actionConfigSchema).optional(),
257
+ onComplete: z.string().optional()
249
258
  }).superRefine((config, ctx) => {
250
259
  const stepIds = /* @__PURE__ */ new Set();
251
260
  for (const step of config.steps) {
@@ -869,6 +878,41 @@ var THEME_PRESETS = {
869
878
  };
870
879
  var PRESET_NAMES = Object.keys(THEME_PRESETS);
871
880
 
881
+ // src/spinners.ts
882
+ var spinners = {
883
+ dots: { interval: 80, frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] },
884
+ dots2: { interval: 80, frames: ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"] },
885
+ line: { interval: 130, frames: ["-", "\\", "|", "/"] },
886
+ arc: { interval: 100, frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"] },
887
+ circle: { interval: 80, frames: ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] },
888
+ circleHalves: { interval: 50, frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"] },
889
+ triangle: { interval: 50, frames: ["\u25E2", "\u25E3", "\u25E4", "\u25E5"] },
890
+ pipe: { interval: 100, frames: ["\u2524", "\u2518", "\u2534", "\u2514", "\u251C", "\u250C", "\u252C", "\u2510"] },
891
+ arrow: { interval: 100, frames: ["\u2190", "\u2196", "\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199"] },
892
+ 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"] },
893
+ bouncingBar: { interval: 80, frames: ["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"] },
894
+ bouncingBall: { interval: 80, frames: ["( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF)", "( \u25CF )", "( \u25CF )", "( \u25CF )", "( \u25CF )", "(\u25CF )"] },
895
+ simpleDots: { interval: 400, frames: [". ", ".. ", "...", " "] },
896
+ 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"] },
897
+ star: { interval: 70, frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"] }
898
+ };
899
+ var DEFAULT_SPINNER = "circle";
900
+ function resolveSpinner(config) {
901
+ if (!config) {
902
+ return spinners[DEFAULT_SPINNER];
903
+ }
904
+ if (typeof config === "string") {
905
+ if (config in spinners) {
906
+ return spinners[config];
907
+ }
908
+ throw new Error(`Unknown spinner preset: "${config}". Available: ${Object.keys(spinners).join(", ")}`);
909
+ }
910
+ return {
911
+ frames: config.frames,
912
+ interval: config.interval ?? 80
913
+ };
914
+ }
915
+
872
916
  // src/theme.ts
873
917
  var DEFAULT_TOKENS = {
874
918
  primary: "#5B9BD5",
@@ -898,7 +942,8 @@ function resolveTheme(themeConfig) {
898
942
  muted: chalk.hex(tokens.muted),
899
943
  accent: chalk.hex(tokens.accent),
900
944
  bold: chalk.bold,
901
- icons
945
+ icons,
946
+ spinner: resolveSpinner(themeConfig?.spinner)
902
947
  };
903
948
  }
904
949
 
@@ -925,6 +970,8 @@ function resolveEnvDefaultBoolean(value) {
925
970
 
926
971
  // src/runner.ts
927
972
  import { execSync } from "child_process";
973
+ import { resolve as resolve2, dirname as dirname2 } from "path";
974
+ import { pathToFileURL } from "url";
928
975
 
929
976
  // src/renderers/inquirer.ts
930
977
  import {
@@ -1125,14 +1172,27 @@ function resolveTemplate(template, answers) {
1125
1172
  return _match;
1126
1173
  });
1127
1174
  }
1175
+ function resolveTemplateStrict(template, answers) {
1176
+ return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
1177
+ const trimmedKey = key.trim();
1178
+ if (!(trimmedKey in answers)) {
1179
+ throw new Error(`Action references unknown step "${trimmedKey}"`);
1180
+ }
1181
+ const value = answers[trimmedKey];
1182
+ if (Array.isArray(value)) return value.join(", ");
1183
+ return String(value);
1184
+ });
1185
+ }
1128
1186
 
1129
1187
  // src/banner.ts
1130
1188
  import figlet from "figlet";
1131
1189
  import gradient from "gradient-string";
1132
1190
  var GRIMOIRE_GRADIENT = gradient(["#C084FC", "#5B9BD5", "#6BCB77"]);
1133
1191
  function renderBanner(name, theme, options) {
1192
+ const icon = options?.icon;
1193
+ const prefix = icon ? `${icon} ` : "";
1134
1194
  if (options?.plain) {
1135
- return ` ${theme.bold(name)}`;
1195
+ return ` ${prefix}${theme.bold(name)}`;
1136
1196
  }
1137
1197
  try {
1138
1198
  const art = figlet.textSync(name, {
@@ -1140,9 +1200,11 @@ function renderBanner(name, theme, options) {
1140
1200
  horizontalLayout: "default"
1141
1201
  });
1142
1202
  const lines = art.split("\n").map((line) => ` ${line}`).join("\n");
1143
- return GRIMOIRE_GRADIENT(lines);
1203
+ const banner = GRIMOIRE_GRADIENT(lines);
1204
+ return icon ? ` ${icon}
1205
+ ${banner}` : banner;
1144
1206
  } catch {
1145
- return ` ${theme.bold(name)}`;
1207
+ return ` ${prefix}${theme.bold(name)}`;
1146
1208
  }
1147
1209
  }
1148
1210
 
@@ -1384,11 +1446,14 @@ function emitEvent(renderer, event, theme) {
1384
1446
  function runPreFlightChecks(checks, theme, renderer) {
1385
1447
  if (renderer) emitEvent(renderer, { type: "checks:start", checks }, theme);
1386
1448
  for (const check of checks) {
1449
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: check.name }, theme);
1387
1450
  try {
1388
1451
  execSync(check.run, { stdio: "pipe" });
1452
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: `${check.name}` }, theme);
1389
1453
  console.log(` ${theme.success("\u2713")} ${check.name}`);
1390
1454
  if (renderer) emitEvent(renderer, { type: "check:pass", name: check.name }, theme);
1391
1455
  } catch {
1456
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1392
1457
  console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
1393
1458
  if (renderer) emitEvent(renderer, { type: "check:fail", name: check.name, message: check.message }, theme);
1394
1459
  throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
@@ -1508,8 +1573,17 @@ async function runWizard(config, options) {
1508
1573
  const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
1509
1574
  const templatedStep = resolveStepTemplates(withTemplate, state.answers);
1510
1575
  const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
1576
+ let finalStep = mruStep;
1577
+ if (!isMock && options?.optionsProvider && isSelectLikeStep(currentStep.type)) {
1578
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: resolvedMessage }, theme);
1579
+ const dynamicOptions = await options.optionsProvider(currentStep.id, state.answers);
1580
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: resolvedMessage }, theme);
1581
+ if (dynamicOptions) {
1582
+ finalStep = { ...mruStep, options: dynamicOptions };
1583
+ }
1584
+ }
1511
1585
  try {
1512
- const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
1586
+ const value = isMock ? getMockValue(finalStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(finalStep), state, theme) : await renderStep(renderer, finalStep, state, theme);
1513
1587
  if (pluginStep?.validate) {
1514
1588
  const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1515
1589
  if (pluginError) {
@@ -1625,8 +1699,13 @@ async function runWizard(config, options) {
1625
1699
  if (state.status === "done" && !quiet) {
1626
1700
  renderer.renderSummary(state.answers, config.steps, theme);
1627
1701
  }
1628
- if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1629
- await executeActions(config.actions, state.answers, theme, renderer);
1702
+ if (state.status === "done" && !isMock) {
1703
+ if (config.onComplete) {
1704
+ await executeOnComplete(config.onComplete, options?.configFilePath, state.answers, config, theme, renderer);
1705
+ }
1706
+ if (config.actions && config.actions.length > 0) {
1707
+ await executeActions(config.actions, state.answers, theme, renderer);
1708
+ }
1630
1709
  }
1631
1710
  emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: state.status === "cancelled" }, theme);
1632
1711
  if (state.status === "done" && cacheEnabled) {
@@ -1841,6 +1920,28 @@ function resolveStepTemplates(step, answers) {
1841
1920
  };
1842
1921
  }
1843
1922
  }
1923
+ async function executeOnComplete(handlerPath, configFilePath, answers, config, theme, renderer) {
1924
+ if (renderer) emitEvent(renderer, { type: "oncomplete:start" }, theme);
1925
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: `Running onComplete handler...` }, theme);
1926
+ const resolvedPath = configFilePath ? resolve2(dirname2(configFilePath), handlerPath) : resolve2(handlerPath);
1927
+ try {
1928
+ const mod = await import(pathToFileURL(resolvedPath).href);
1929
+ if (typeof mod.default !== "function") {
1930
+ throw new Error(`onComplete handler "${handlerPath}" must export a default function`);
1931
+ }
1932
+ await mod.default({ answers, config });
1933
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: "Handler complete" }, theme);
1934
+ if (renderer) emitEvent(renderer, { type: "oncomplete:pass" }, theme);
1935
+ } catch (error) {
1936
+ const message = error instanceof Error ? error.message : String(error);
1937
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1938
+ if (renderer) emitEvent(renderer, { type: "oncomplete:fail", error: message }, theme);
1939
+ console.log(`
1940
+ ${theme.error("\u2717")} onComplete handler failed: ${message}
1941
+ `);
1942
+ throw error;
1943
+ }
1944
+ }
1844
1945
  async function executeActions(actions, answers, theme, renderer) {
1845
1946
  if (renderer) emitEvent(renderer, { type: "actions:start" }, theme);
1846
1947
  console.log(`
@@ -1850,14 +1951,17 @@ async function executeActions(actions, answers, theme, renderer) {
1850
1951
  if (action.when && !evaluateCondition(action.when, answers)) {
1851
1952
  continue;
1852
1953
  }
1853
- const resolvedCommand = resolveTemplate(action.run, answers);
1854
- const resolvedName = action.name ? resolveTemplate(action.name, answers) : void 0;
1954
+ const resolvedCommand = resolveTemplateStrict(action.run, answers);
1955
+ const resolvedName = action.name ? resolveTemplateStrict(action.name, answers) : void 0;
1855
1956
  const label = resolvedName ?? resolvedCommand;
1957
+ if (renderer) emitEvent(renderer, { type: "spinner:start", message: label }, theme);
1856
1958
  try {
1857
1959
  execSync(resolvedCommand, { stdio: "pipe" });
1960
+ if (renderer) emitEvent(renderer, { type: "spinner:stop", message: label }, theme);
1858
1961
  console.log(` ${theme.success("\u2713")} ${label}`);
1859
1962
  if (renderer) emitEvent(renderer, { type: "action:pass", name: label }, theme);
1860
1963
  } catch {
1964
+ if (renderer) emitEvent(renderer, { type: "spinner:stop" }, theme);
1861
1965
  console.log(` ${theme.error("\u2717")} ${label}`);
1862
1966
  if (renderer) emitEvent(renderer, { type: "action:fail", name: label }, theme);
1863
1967
  throw new Error(`Action failed: ${label}`);
@@ -1867,7 +1971,7 @@ async function executeActions(actions, answers, theme, renderer) {
1867
1971
  }
1868
1972
  function printWizardHeader(config, theme, plain) {
1869
1973
  console.log();
1870
- console.log(renderBanner(config.meta.name, theme, { plain }));
1974
+ console.log(renderBanner(config.meta.name, theme, { plain, icon: config.meta.icon }));
1871
1975
  if (config.meta.description) {
1872
1976
  console.log(` ${theme.muted(config.meta.description)}`);
1873
1977
  }
@@ -2102,7 +2206,6 @@ var S_STEP_ERROR = u("\u25B2", "x");
2102
2206
  var S_CORNER_TR = u("\u256E", "+");
2103
2207
  var S_CORNER_BR = u("\u256F", "+");
2104
2208
  var S_BAR_H = u("\u2500", "-");
2105
- var S_SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"];
2106
2209
 
2107
2210
  // src/renderers/clack.ts
2108
2211
  var ClackRenderer = class extends InquirerRenderer {
@@ -2241,13 +2344,14 @@ var ClackRenderer = class extends InquirerRenderer {
2241
2344
  process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256E${bottomLine}`)}
2242
2345
  `);
2243
2346
  }
2244
- startSpinner(message, _theme) {
2347
+ startSpinner(message, theme) {
2245
2348
  this.spinnerFrameIndex = 0;
2349
+ const { frames, interval } = theme.spinner;
2246
2350
  this.spinnerInterval = setInterval(() => {
2247
- const frame = S_SPINNER_FRAMES[this.spinnerFrameIndex % S_SPINNER_FRAMES.length];
2351
+ const frame = frames[this.spinnerFrameIndex % frames.length];
2248
2352
  process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
2249
2353
  this.spinnerFrameIndex++;
2250
- }, 80);
2354
+ }, interval);
2251
2355
  }
2252
2356
  stopSpinner(message, theme) {
2253
2357
  if (this.spinnerInterval) {
@@ -2345,6 +2449,7 @@ async function runPipeline(steps, globalOptions) {
2345
2449
  }
2346
2450
  export {
2347
2451
  ClackRenderer,
2452
+ DEFAULT_SPINNER,
2348
2453
  InkRenderer,
2349
2454
  InquirerRenderer,
2350
2455
  clearCache,
@@ -2372,7 +2477,9 @@ export {
2372
2477
  renderBanner,
2373
2478
  resolveEnvDefault,
2374
2479
  resolveNextStep,
2480
+ resolveSpinner,
2375
2481
  resolveTemplate,
2482
+ resolveTemplateStrict,
2376
2483
  resolveTheme,
2377
2484
  runPipeline,
2378
2485
  runPreFlightChecks,
@@ -2381,6 +2488,7 @@ export {
2381
2488
  saveProgress,
2382
2489
  saveTemplate,
2383
2490
  slugify,
2491
+ spinners,
2384
2492
  validateStepAnswer,
2385
2493
  wizardReducer
2386
2494
  };