grimoire-wizard 0.3.1 → 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.
Files changed (42) hide show
  1. package/README.md +185 -18
  2. package/dist/cli.js +602 -109
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +207 -3
  5. package/dist/index.js +921 -371
  6. package/dist/index.js.map +1 -1
  7. package/examples/handlers/setup-project.ts +9 -0
  8. package/examples/json/all-features.json +66 -0
  9. package/examples/json/appstore-screenshot-wizard.json +362 -0
  10. package/examples/json/appstore-upload.json +104 -0
  11. package/examples/json/basic.json +72 -0
  12. package/examples/json/batch-generate.json +186 -0
  13. package/examples/json/brief-builder.json +519 -0
  14. package/examples/json/conditional.json +155 -0
  15. package/examples/json/cost-analyzer.json +83 -0
  16. package/examples/json/demo.json +130 -0
  17. package/examples/json/scraper-selector.json +63 -0
  18. package/examples/json/themed-catppuccin.json +39 -0
  19. package/examples/json/themed.json +103 -0
  20. package/examples/json/with-actions.json +61 -0
  21. package/examples/json/with-checks.json +47 -0
  22. package/examples/json/with-oncomplete.json +45 -0
  23. package/examples/yaml/appstore-screenshot-wizard.yaml +321 -0
  24. package/examples/yaml/appstore-upload.yaml +84 -0
  25. package/examples/yaml/batch-generate.yaml +156 -0
  26. package/examples/yaml/brief-builder.yaml +429 -0
  27. package/examples/yaml/cost-analyzer.yaml +69 -0
  28. package/examples/yaml/pipeline.yaml +35 -0
  29. package/examples/yaml/scraper-selector.yaml +52 -0
  30. package/examples/yaml/themed-catppuccin.yaml +31 -0
  31. package/examples/yaml/with-actions.yaml +45 -0
  32. package/examples/yaml/with-oncomplete.yaml +35 -0
  33. package/package.json +1 -1
  34. /package/examples/{all-features.yaml → yaml/all-features.yaml} +0 -0
  35. /package/examples/{base.yaml → yaml/base.yaml} +0 -0
  36. /package/examples/{basic.yaml → yaml/basic.yaml} +0 -0
  37. /package/examples/{conditional.yaml → yaml/conditional.yaml} +0 -0
  38. /package/examples/{demo.yaml → yaml/demo.yaml} +0 -0
  39. /package/examples/{ebay-mcp-setup.yaml → yaml/ebay-mcp-setup.yaml} +0 -0
  40. /package/examples/{extended.yaml → yaml/extended.yaml} +0 -0
  41. /package/examples/{themed.yaml → yaml/themed.yaml} +0 -0
  42. /package/examples/{with-checks.yaml → yaml/with-checks.yaml} +0 -0
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import chalk2 from "chalk";
4
+ import chalk3 from "chalk";
5
5
  import { program } from "commander";
6
6
 
7
7
  // src/parser.ts
@@ -146,6 +146,10 @@ var messageStepSchema = z.object({
146
146
  ...baseStepFields,
147
147
  type: z.literal("message")
148
148
  });
149
+ var noteStepSchema = z.object({
150
+ ...baseStepFields,
151
+ type: z.literal("note")
152
+ });
149
153
  var stepConfigSchema = z.discriminatedUnion("type", [
150
154
  textStepSchema,
151
155
  selectStepSchema,
@@ -157,13 +161,15 @@ var stepConfigSchema = z.discriminatedUnion("type", [
157
161
  editorStepSchema,
158
162
  pathStepSchema,
159
163
  toggleStepSchema,
160
- messageStepSchema
164
+ messageStepSchema,
165
+ noteStepSchema
161
166
  ]);
162
167
  var hexColorSchema = z.string().regex(
163
168
  /^#[0-9a-fA-F]{6}$/,
164
169
  "Must be a 6-digit hex color (e.g., #FF0000)"
165
170
  );
166
171
  var themeConfigSchema = z.object({
172
+ preset: z.enum(["default", "catppuccin", "dracula", "nord", "tokyonight", "monokai"]).optional(),
167
173
  tokens: z.object({
168
174
  primary: hexColorSchema.optional(),
169
175
  success: hexColorSchema.optional(),
@@ -178,7 +184,14 @@ var themeConfigSchema = z.object({
178
184
  stepDone: z.string().optional(),
179
185
  stepPending: z.string().optional(),
180
186
  pointer: z.string().optional()
181
- }).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()
182
195
  });
183
196
  var preFlightCheckSchema = z.object({
184
197
  name: z.string(),
@@ -194,7 +207,8 @@ var wizardConfigSchema = z.object({
194
207
  meta: z.object({
195
208
  name: z.string(),
196
209
  version: z.string().optional(),
197
- description: z.string().optional()
210
+ description: z.string().optional(),
211
+ review: z.boolean().optional()
198
212
  }),
199
213
  theme: themeConfigSchema.optional(),
200
214
  steps: z.array(stepConfigSchema).min(1),
@@ -204,7 +218,8 @@ var wizardConfigSchema = z.object({
204
218
  }).optional(),
205
219
  extends: z.string().optional(),
206
220
  checks: z.array(preFlightCheckSchema).optional(),
207
- actions: z.array(actionConfigSchema).optional()
221
+ actions: z.array(actionConfigSchema).optional(),
222
+ onComplete: z.string().optional()
208
223
  }).superRefine((config, ctx) => {
209
224
  const stepIds = /* @__PURE__ */ new Set();
210
225
  for (const step of config.steps) {
@@ -479,6 +494,8 @@ async function loadWizardConfig(filePath) {
479
494
 
480
495
  // src/runner.ts
481
496
  import { execSync } from "child_process";
497
+ import { resolve as resolve2, dirname as dirname2 } from "path";
498
+ import { pathToFileURL } from "url";
482
499
 
483
500
  // src/conditions.ts
484
501
  function isRecord(value) {
@@ -758,6 +775,102 @@ function applyValidationRule(rule, value) {
758
775
 
759
776
  // src/theme.ts
760
777
  import chalk from "chalk";
778
+
779
+ // src/themes/presets.ts
780
+ var THEME_PRESETS = {
781
+ default: {
782
+ primary: "#7C3AED",
783
+ success: "#10B981",
784
+ error: "#EF4444",
785
+ warning: "#F59E0B",
786
+ info: "#3B82F6",
787
+ muted: "#6B7280",
788
+ accent: "#8B5CF6"
789
+ },
790
+ catppuccin: {
791
+ primary: "#cba6f7",
792
+ success: "#a6e3a1",
793
+ error: "#f38ba8",
794
+ warning: "#fab387",
795
+ info: "#74c7ec",
796
+ muted: "#6c7086",
797
+ accent: "#f5c2e7"
798
+ },
799
+ dracula: {
800
+ primary: "#bd93f9",
801
+ success: "#50fa7b",
802
+ error: "#ff5555",
803
+ warning: "#ffb86c",
804
+ info: "#8be9fd",
805
+ muted: "#6272a4",
806
+ accent: "#ff79c6"
807
+ },
808
+ nord: {
809
+ primary: "#88c0d0",
810
+ success: "#a3be8c",
811
+ error: "#bf616a",
812
+ warning: "#ebcb8b",
813
+ info: "#81a1c1",
814
+ muted: "#4c566a",
815
+ accent: "#b48ead"
816
+ },
817
+ tokyonight: {
818
+ primary: "#7aa2f7",
819
+ success: "#9ece6a",
820
+ error: "#f7768e",
821
+ warning: "#e0af68",
822
+ info: "#7dcfff",
823
+ muted: "#565f89",
824
+ accent: "#bb9af7"
825
+ },
826
+ monokai: {
827
+ primary: "#ab9df2",
828
+ success: "#a9dc76",
829
+ error: "#ff6188",
830
+ warning: "#ffd866",
831
+ info: "#78dce8",
832
+ muted: "#727072",
833
+ accent: "#fc9867"
834
+ }
835
+ };
836
+ var PRESET_NAMES = Object.keys(THEME_PRESETS);
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
+
873
+ // src/theme.ts
761
874
  var DEFAULT_TOKENS = {
762
875
  primary: "#5B9BD5",
763
876
  success: "#6BCB77",
@@ -774,7 +887,8 @@ var DEFAULT_ICONS = {
774
887
  pointer: "\u203A"
775
888
  };
776
889
  function resolveTheme(themeConfig) {
777
- const tokens = { ...DEFAULT_TOKENS, ...themeConfig?.tokens };
890
+ const presetTokens = themeConfig?.preset ? THEME_PRESETS[themeConfig.preset] : void 0;
891
+ const tokens = { ...DEFAULT_TOKENS, ...presetTokens, ...themeConfig?.tokens };
778
892
  const icons = { ...DEFAULT_ICONS, ...themeConfig?.icons };
779
893
  return {
780
894
  primary: chalk.hex(tokens.primary),
@@ -785,7 +899,8 @@ function resolveTheme(themeConfig) {
785
899
  muted: chalk.hex(tokens.muted),
786
900
  accent: chalk.hex(tokens.accent),
787
901
  bold: chalk.bold,
788
- icons
902
+ icons,
903
+ spinner: resolveSpinner(themeConfig?.spinner)
789
904
  };
790
905
  }
791
906
 
@@ -1009,6 +1124,17 @@ function resolveTemplate(template, answers) {
1009
1124
  return _match;
1010
1125
  });
1011
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
+ }
1012
1138
 
1013
1139
  // src/banner.ts
1014
1140
  import figlet from "figlet";
@@ -1116,14 +1242,66 @@ function clearCache(wizardName, customDir) {
1116
1242
  }
1117
1243
  }
1118
1244
 
1119
- // src/mru.ts
1120
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
1245
+ // src/progress.ts
1246
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1121
1247
  import { join as join2 } from "path";
1122
1248
  import { homedir as homedir2 } from "os";
1123
- var MRU_DIR = join2(homedir2(), ".config", "grimoire", "mru");
1249
+ var DEFAULT_PROGRESS_DIR = join2(homedir2(), ".config", "grimoire", "progress");
1250
+ function getProgressFilePath(wizardName, customDir) {
1251
+ const dir = customDir ?? DEFAULT_PROGRESS_DIR;
1252
+ return join2(dir, `${slugify(wizardName)}.json`);
1253
+ }
1254
+ function saveProgress(wizardName, state, customDir, excludeStepIds) {
1255
+ try {
1256
+ const dir = customDir ?? DEFAULT_PROGRESS_DIR;
1257
+ mkdirSync2(dir, { recursive: true });
1258
+ const excludeSet = new Set(excludeStepIds ?? []);
1259
+ const filteredAnswers = {};
1260
+ for (const [key, value] of Object.entries(state.answers)) {
1261
+ if (!excludeSet.has(key)) {
1262
+ filteredAnswers[key] = value;
1263
+ }
1264
+ }
1265
+ const progress = {
1266
+ currentStepId: state.currentStepId,
1267
+ answers: filteredAnswers,
1268
+ history: state.history,
1269
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
1270
+ };
1271
+ const filePath = getProgressFilePath(wizardName, customDir);
1272
+ writeFileSync2(filePath, JSON.stringify(progress, null, 2) + "\n", "utf-8");
1273
+ } catch {
1274
+ }
1275
+ }
1276
+ function loadProgress(wizardName, customDir) {
1277
+ try {
1278
+ const filePath = getProgressFilePath(wizardName, customDir);
1279
+ const raw = readFileSync3(filePath, "utf-8");
1280
+ const parsed = JSON.parse(raw);
1281
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && "currentStepId" in parsed && "answers" in parsed && "history" in parsed && "savedAt" in parsed) {
1282
+ return parsed;
1283
+ }
1284
+ return null;
1285
+ } catch {
1286
+ return null;
1287
+ }
1288
+ }
1289
+ function clearProgress(wizardName, customDir) {
1290
+ try {
1291
+ const filePath = getProgressFilePath(wizardName, customDir);
1292
+ unlinkSync2(filePath);
1293
+ } catch {
1294
+ }
1295
+ }
1296
+
1297
+ // src/mru.ts
1298
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync } from "fs";
1299
+ import { join as join3 } from "path";
1300
+ import { homedir as homedir3 } from "os";
1301
+ var MRU_DIR = join3(homedir3(), ".config", "grimoire", "mru");
1124
1302
  function getMruFilePath(wizardName) {
1125
1303
  const safeName = wizardName.replace(/[^a-zA-Z0-9_-]/g, "_");
1126
- return join2(MRU_DIR, `${safeName}.json`);
1304
+ return join3(MRU_DIR, `${safeName}.json`);
1127
1305
  }
1128
1306
  function loadMruData(wizardName) {
1129
1307
  const filePath = getMruFilePath(wizardName);
@@ -1131,7 +1309,7 @@ function loadMruData(wizardName) {
1131
1309
  if (!existsSync(filePath)) {
1132
1310
  return {};
1133
1311
  }
1134
- const raw = readFileSync3(filePath, "utf-8");
1312
+ const raw = readFileSync4(filePath, "utf-8");
1135
1313
  const parsed = JSON.parse(raw);
1136
1314
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1137
1315
  return {};
@@ -1144,8 +1322,8 @@ function loadMruData(wizardName) {
1144
1322
  function saveMruData(wizardName, data) {
1145
1323
  const filePath = getMruFilePath(wizardName);
1146
1324
  try {
1147
- mkdirSync2(MRU_DIR, { recursive: true });
1148
- writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1325
+ mkdirSync3(MRU_DIR, { recursive: true });
1326
+ writeFileSync3(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1149
1327
  } catch {
1150
1328
  }
1151
1329
  }
@@ -1199,13 +1377,21 @@ function getOrderedOptions(wizardName, stepId, options) {
1199
1377
  }
1200
1378
 
1201
1379
  // src/runner.ts
1202
- function runPreFlightChecks(checks, theme) {
1380
+ function emitEvent(renderer, event, theme) {
1381
+ if (renderer.onEvent) {
1382
+ renderer.onEvent(event, theme);
1383
+ }
1384
+ }
1385
+ function runPreFlightChecks(checks, theme, renderer) {
1386
+ if (renderer) emitEvent(renderer, { type: "checks:start", checks }, theme);
1203
1387
  for (const check of checks) {
1204
1388
  try {
1205
1389
  execSync(check.run, { stdio: "pipe" });
1206
1390
  console.log(` ${theme.success("\u2713")} ${check.name}`);
1391
+ if (renderer) emitEvent(renderer, { type: "check:pass", name: check.name }, theme);
1207
1392
  } catch {
1208
1393
  console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
1394
+ if (renderer) emitEvent(renderer, { type: "check:fail", name: check.name, message: check.message }, theme);
1209
1395
  throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
1210
1396
  }
1211
1397
  }
@@ -1215,7 +1401,7 @@ function getMockValue(step, mockAnswers) {
1215
1401
  if (step.id in mockAnswers) {
1216
1402
  return mockAnswers[step.id];
1217
1403
  }
1218
- if (step.type === "message") {
1404
+ if (step.type === "message" || step.type === "note") {
1219
1405
  return true;
1220
1406
  }
1221
1407
  const defaultValue = getStepDefault(step);
@@ -1243,6 +1429,7 @@ function getStepDefault(step) {
1243
1429
  return step.default;
1244
1430
  case "password":
1245
1431
  case "message":
1432
+ case "note":
1246
1433
  return void 0;
1247
1434
  }
1248
1435
  }
@@ -1256,6 +1443,21 @@ async function runWizard(config, options) {
1256
1443
  const cacheDir = typeof options?.cache === "object" ? options.cache.dir : void 0;
1257
1444
  const mruEnabled = !isMock && options?.mru !== false;
1258
1445
  let state = createWizardState(config);
1446
+ const resumeEnabled = !isMock && options?.resume !== false;
1447
+ if (resumeEnabled) {
1448
+ const saved = loadProgress(config.meta.name);
1449
+ if (saved) {
1450
+ const stepExists = config.steps.some((s) => s.id === saved.currentStepId);
1451
+ if (stepExists) {
1452
+ state = {
1453
+ ...state,
1454
+ currentStepId: saved.currentStepId,
1455
+ answers: { ...state.answers, ...saved.answers },
1456
+ history: saved.history
1457
+ };
1458
+ }
1459
+ }
1460
+ }
1259
1461
  const cachedAnswers = cacheEnabled ? loadCachedAnswers(config.meta.name, cacheDir) : void 0;
1260
1462
  const userPlugins = options?.plugins;
1261
1463
  if (userPlugins) {
@@ -1265,103 +1467,174 @@ async function runWizard(config, options) {
1265
1467
  }
1266
1468
  try {
1267
1469
  if (!isMock && config.checks && config.checks.length > 0) {
1268
- runPreFlightChecks(config.checks, theme);
1470
+ runPreFlightChecks(config.checks, theme, renderer);
1269
1471
  }
1270
1472
  if (!quiet) {
1271
1473
  printWizardHeader(config, theme, options?.plain);
1272
1474
  }
1273
- let previousGroup;
1274
- while (state.status === "running") {
1275
- const visibleSteps = getVisibleSteps(config, state.answers);
1276
- const currentStep = config.steps.find((s) => s.id === state.currentStepId);
1277
- if (!currentStep) {
1278
- throw new Error(`Current step not found: "${state.currentStepId}"`);
1279
- }
1280
- if (!isMock) {
1475
+ const visibleStepsForCount = getVisibleSteps(config, state.answers);
1476
+ emitEvent(renderer, { type: "session:start", wizard: config.meta.name, description: config.meta.description, totalSteps: visibleStepsForCount.length }, theme);
1477
+ let needsReview = true;
1478
+ while (needsReview) {
1479
+ let previousGroup;
1480
+ while (state.status === "running") {
1481
+ const visibleSteps = getVisibleSteps(config, state.answers);
1482
+ const currentStep = config.steps.find((s) => s.id === state.currentStepId);
1483
+ if (!currentStep) {
1484
+ throw new Error(`Current step not found: "${state.currentStepId}"`);
1485
+ }
1281
1486
  if (currentStep.group !== void 0 && currentStep.group !== previousGroup) {
1282
1487
  const resolvedGroup = resolveTemplate(currentStep.group, state.answers);
1283
- renderer.renderGroupHeader(resolvedGroup, theme);
1488
+ if (!isMock) {
1489
+ renderer.renderGroupHeader(resolvedGroup, theme);
1490
+ }
1491
+ emitEvent(renderer, { type: "group:start", group: resolvedGroup }, theme);
1284
1492
  }
1285
1493
  previousGroup = currentStep.group;
1286
1494
  const stepIndex = visibleSteps.findIndex((s) => s.id === state.currentStepId);
1287
1495
  const resolvedMessage = resolveTemplate(currentStep.message, state.answers);
1288
1496
  const resolvedDescription = currentStep.description ? resolveTemplate(currentStep.description, state.answers) : void 0;
1289
- renderer.renderStepHeader(stepIndex, visibleSteps.length, resolvedMessage, theme, resolvedDescription);
1290
- }
1291
- if (options?.onBeforeStep) {
1292
- await options.onBeforeStep(currentStep.id, currentStep, state);
1293
- }
1294
- const pluginStep = getPluginStep(currentStep.type);
1295
- const resolvedStep = pluginStep ? currentStep : resolveStepDefaults(currentStep, cachedAnswers);
1296
- const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
1297
- const templatedStep = resolveStepTemplates(withTemplate, state.answers);
1298
- const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
1299
- try {
1300
- const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
1301
- if (pluginStep?.validate) {
1302
- const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1303
- if (pluginError) {
1497
+ if (!isMock) {
1498
+ renderer.renderStepHeader(stepIndex, visibleSteps.length, resolvedMessage, theme, resolvedDescription);
1499
+ }
1500
+ emitEvent(renderer, { type: "step:start", stepId: currentStep.id, stepIndex, totalVisible: visibleSteps.length, step: currentStep }, theme);
1501
+ if (currentStep.type === "note") {
1502
+ emitEvent(renderer, { type: "note", title: resolvedMessage, body: resolvedDescription ?? "" }, theme);
1503
+ }
1504
+ if (options?.onBeforeStep) {
1505
+ await options.onBeforeStep(currentStep.id, currentStep, state);
1506
+ }
1507
+ const pluginStep = getPluginStep(currentStep.type);
1508
+ const resolvedStep = pluginStep ? currentStep : resolveStepDefaults(currentStep, cachedAnswers);
1509
+ const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
1510
+ const templatedStep = resolveStepTemplates(withTemplate, state.answers);
1511
+ const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
1512
+ try {
1513
+ const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
1514
+ if (pluginStep?.validate) {
1515
+ const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1516
+ if (pluginError) {
1517
+ if (isMock) {
1518
+ throw new Error(
1519
+ `Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
1520
+ );
1521
+ }
1522
+ console.log(theme.error(`
1523
+ ${pluginError}
1524
+ `));
1525
+ continue;
1526
+ }
1527
+ }
1528
+ const nextState = wizardReducer(state, { type: "NEXT", value }, config);
1529
+ if (nextState.errors[currentStep.id]) {
1530
+ const errorMsg = resolveTemplate(nextState.errors[currentStep.id] ?? "", state.answers);
1531
+ emitEvent(renderer, { type: "step:error", stepId: currentStep.id, error: errorMsg }, theme);
1304
1532
  if (isMock) {
1305
1533
  throw new Error(
1306
- `Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
1534
+ `Mock mode: validation failed for step "${currentStep.id}": ${errorMsg}`
1307
1535
  );
1308
1536
  }
1309
1537
  console.log(theme.error(`
1310
- ${pluginError}
1538
+ ${errorMsg}
1311
1539
  `));
1540
+ state = { ...nextState, errors: {} };
1312
1541
  continue;
1313
1542
  }
1314
- }
1315
- const nextState = wizardReducer(state, { type: "NEXT", value }, config);
1316
- if (nextState.errors[currentStep.id]) {
1317
- const errorMsg = resolveTemplate(nextState.errors[currentStep.id] ?? "", state.answers);
1318
- if (isMock) {
1319
- throw new Error(
1320
- `Mock mode: validation failed for step "${currentStep.id}": ${errorMsg}`
1321
- );
1322
- }
1323
- console.log(theme.error(`
1324
- ${errorMsg}
1325
- `));
1326
- state = { ...nextState, errors: {} };
1327
- continue;
1328
- }
1329
- if (!isMock && options?.asyncValidate) {
1330
- const asyncError = await options.asyncValidate(currentStep.id, value, nextState.answers);
1331
- if (asyncError !== null) {
1332
- console.log(theme.error(`
1543
+ if (!isMock && options?.asyncValidate) {
1544
+ const asyncError = await options.asyncValidate(currentStep.id, value, nextState.answers);
1545
+ if (asyncError !== null) {
1546
+ console.log(theme.error(`
1333
1547
  ${asyncError}
1334
1548
  `));
1335
- state = { ...nextState, errors: {} };
1336
- continue;
1549
+ state = { ...nextState, errors: {} };
1550
+ continue;
1551
+ }
1552
+ }
1553
+ if (options?.onAfterStep) {
1554
+ await options.onAfterStep(currentStep.id, value, nextState);
1555
+ }
1556
+ state = nextState;
1557
+ emitEvent(renderer, { type: "step:complete", stepId: currentStep.id, value, step: currentStep }, theme);
1558
+ if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1559
+ recordSelection(config.meta.name, currentStep.id, value);
1337
1560
  }
1561
+ options?.onStepComplete?.(currentStep.id, value, state);
1562
+ } catch (error) {
1563
+ if (!isMock && isUserCancel(error)) {
1564
+ state = wizardReducer(state, { type: "CANCEL" }, config);
1565
+ options?.onCancel?.(state);
1566
+ const passwordStepIds = config.steps.filter((s) => s.type === "password").map((s) => s.id);
1567
+ saveProgress(config.meta.name, {
1568
+ currentStepId: state.currentStepId,
1569
+ answers: state.answers,
1570
+ history: state.history
1571
+ }, void 0, passwordStepIds);
1572
+ emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: true }, theme);
1573
+ if (!quiet) {
1574
+ console.log(theme.warning("\n Wizard cancelled.\n"));
1575
+ }
1576
+ return state.answers;
1577
+ }
1578
+ throw error;
1338
1579
  }
1339
- if (options?.onAfterStep) {
1340
- await options.onAfterStep(currentStep.id, value, nextState);
1580
+ }
1581
+ if (config.meta.review && !isMock && state.status === "done") {
1582
+ const reviewLines = [];
1583
+ for (const step of config.steps) {
1584
+ const answer = state.answers[step.id];
1585
+ if (answer === void 0) continue;
1586
+ const display = step.type === "password" ? "****" : Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
1587
+ reviewLines.push(`${step.id}: ${display}`);
1341
1588
  }
1342
- state = nextState;
1343
- if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1344
- recordSelection(config.meta.name, currentStep.id, value);
1589
+ emitEvent(renderer, { type: "note", title: "Review your answers", body: reviewLines.join("\n") }, theme);
1590
+ console.log(`
1591
+ ${theme.bold("Review your answers:")}
1592
+ `);
1593
+ for (const line of reviewLines) {
1594
+ console.log(` ${line}`);
1345
1595
  }
1346
- options?.onStepComplete?.(currentStep.id, value, state);
1347
- } catch (error) {
1348
- if (!isMock && isUserCancel(error)) {
1349
- state = wizardReducer(state, { type: "CANCEL" }, config);
1350
- options?.onCancel?.(state);
1351
- if (!quiet) {
1352
- console.log(theme.warning("\n Wizard cancelled.\n"));
1353
- }
1354
- return state.answers;
1596
+ console.log();
1597
+ const { confirm: confirmPrompt } = await import("@inquirer/prompts");
1598
+ const ok = await confirmPrompt({
1599
+ message: "Everything look right?",
1600
+ default: true
1601
+ });
1602
+ if (ok) {
1603
+ needsReview = false;
1604
+ } else {
1605
+ const { select: selectPrompt } = await import("@inquirer/prompts");
1606
+ const stepsWithAnswers = config.steps.filter(
1607
+ (s) => state.answers[s.id] !== void 0 && s.type !== "note" && s.type !== "message"
1608
+ );
1609
+ const stepToRevisit = await selectPrompt({
1610
+ message: "Which step would you like to change?",
1611
+ choices: stepsWithAnswers.map((s) => ({
1612
+ name: `${s.id}: ${s.type === "password" ? "****" : String(state.answers[s.id] ?? "")}`,
1613
+ value: s.id
1614
+ }))
1615
+ });
1616
+ state = {
1617
+ ...state,
1618
+ currentStepId: stepToRevisit,
1619
+ status: "running"
1620
+ };
1355
1621
  }
1356
- throw error;
1622
+ } else {
1623
+ needsReview = false;
1357
1624
  }
1358
1625
  }
1359
1626
  if (state.status === "done" && !quiet) {
1360
1627
  renderer.renderSummary(state.answers, config.steps, theme);
1361
1628
  }
1362
- if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1363
- await executeActions(config.actions, state.answers, theme);
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
+ }
1364
1636
  }
1637
+ emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: state.status === "cancelled" }, theme);
1365
1638
  if (state.status === "done" && cacheEnabled) {
1366
1639
  const passwordStepIds = new Set(
1367
1640
  config.steps.filter((s) => s.type === "password").map((s) => s.id)
@@ -1374,6 +1647,9 @@ async function runWizard(config, options) {
1374
1647
  }
1375
1648
  saveCachedAnswers(config.meta.name, answersToCache, cacheDir);
1376
1649
  }
1650
+ if (state.status === "done") {
1651
+ clearProgress(config.meta.name);
1652
+ }
1377
1653
  return state.answers;
1378
1654
  } finally {
1379
1655
  if (userPlugins) {
@@ -1413,6 +1689,8 @@ function renderStep(renderer, step, state, theme) {
1413
1689
  case "message":
1414
1690
  renderer.renderMessage(step, state, theme);
1415
1691
  return Promise.resolve(true);
1692
+ case "note":
1693
+ return Promise.resolve(true);
1416
1694
  }
1417
1695
  }
1418
1696
  function resolveStepDefaults(step, cachedAnswers) {
@@ -1463,6 +1741,7 @@ function resolveStepDefaults(step, cachedAnswers) {
1463
1741
  }
1464
1742
  case "password":
1465
1743
  case "message":
1744
+ case "note":
1466
1745
  return step;
1467
1746
  }
1468
1747
  }
@@ -1472,7 +1751,7 @@ function getCachedDefault(stepId, cachedAnswers) {
1472
1751
  }
1473
1752
  function applyTemplateDefaults(step, templateAnswers) {
1474
1753
  if (!(step.id in templateAnswers)) return step;
1475
- if (step.type === "password" || step.type === "message") return step;
1754
+ if (step.type === "password" || step.type === "message" || step.type === "note") return step;
1476
1755
  const value = templateAnswers[step.id];
1477
1756
  switch (step.type) {
1478
1757
  case "text":
@@ -1561,13 +1840,34 @@ function resolveStepTemplates(step, answers) {
1561
1840
  case "confirm":
1562
1841
  case "toggle":
1563
1842
  case "message":
1843
+ case "note":
1564
1844
  return {
1565
1845
  ...step,
1566
1846
  description: step.description ? resolveTemplate(step.description, answers) : void 0
1567
1847
  };
1568
1848
  }
1569
1849
  }
1570
- async function executeActions(actions, answers, theme) {
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
+ }
1869
+ async function executeActions(actions, answers, theme, renderer) {
1870
+ if (renderer) emitEvent(renderer, { type: "actions:start" }, theme);
1571
1871
  console.log(`
1572
1872
  ${theme.bold("Running actions...")}
1573
1873
  `);
@@ -1575,14 +1875,16 @@ async function executeActions(actions, answers, theme) {
1575
1875
  if (action.when && !evaluateCondition(action.when, answers)) {
1576
1876
  continue;
1577
1877
  }
1578
- const resolvedCommand = resolveTemplate(action.run, answers);
1579
- 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;
1580
1880
  const label = resolvedName ?? resolvedCommand;
1581
1881
  try {
1582
1882
  execSync(resolvedCommand, { stdio: "pipe" });
1583
1883
  console.log(` ${theme.success("\u2713")} ${label}`);
1884
+ if (renderer) emitEvent(renderer, { type: "action:pass", name: label }, theme);
1584
1885
  } catch {
1585
1886
  console.log(` ${theme.error("\u2717")} ${label}`);
1887
+ if (renderer) emitEvent(renderer, { type: "action:fail", name: label }, theme);
1586
1888
  throw new Error(`Action failed: ${label}`);
1587
1889
  }
1588
1890
  }
@@ -1606,7 +1908,7 @@ function isUserCancel(error) {
1606
1908
  // src/scaffolder.ts
1607
1909
  import { input as input2, select as select2, confirm as confirm2, number as number2 } from "@inquirer/prompts";
1608
1910
  import { stringify } from "yaml";
1609
- import { writeFileSync as writeFileSync3 } from "fs";
1911
+ import { writeFileSync as writeFileSync4 } from "fs";
1610
1912
  import { relative } from "path";
1611
1913
  var STEP_TYPE_CHOICES = [
1612
1914
  { name: "text", value: "text" },
@@ -1723,7 +2025,7 @@ async function scaffoldWizard(outputPath) {
1723
2025
  config.theme = { tokens: { primary: primaryColor } };
1724
2026
  }
1725
2027
  const yamlContent = stringify(config);
1726
- writeFileSync3(outputPath, yamlContent, "utf-8");
2028
+ writeFileSync4(outputPath, yamlContent, "utf-8");
1727
2029
  const displayPath = relative(process.cwd(), outputPath);
1728
2030
  console.log(`
1729
2031
  \u2713 Created wizard config: ${outputPath}`);
@@ -1914,20 +2216,20 @@ complete -c grimoire -n "__fish_seen_subcommand_from completion" -f -a "fish" -d
1914
2216
  }
1915
2217
 
1916
2218
  // src/templates.ts
1917
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
1918
- import { join as join3 } from "path";
1919
- import { homedir as homedir3 } from "os";
1920
- var TEMPLATES_DIR = join3(homedir3(), ".config", "grimoire", "templates");
2219
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
2220
+ import { join as join4 } from "path";
2221
+ import { homedir as homedir4 } from "os";
2222
+ var TEMPLATES_DIR = join4(homedir4(), ".config", "grimoire", "templates");
1921
2223
  function getWizardTemplateDir(wizardName) {
1922
- return join3(TEMPLATES_DIR, slugify(wizardName));
2224
+ return join4(TEMPLATES_DIR, slugify(wizardName));
1923
2225
  }
1924
2226
  function getTemplateFilePath(wizardName, templateName) {
1925
- return join3(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
2227
+ return join4(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
1926
2228
  }
1927
2229
  function loadTemplate(wizardName, templateName) {
1928
2230
  try {
1929
2231
  const filePath = getTemplateFilePath(wizardName, templateName);
1930
- const raw = readFileSync4(filePath, "utf-8");
2232
+ const raw = readFileSync5(filePath, "utf-8");
1931
2233
  const parsed = JSON.parse(raw);
1932
2234
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1933
2235
  return parsed;
@@ -1949,7 +2251,7 @@ function listTemplates(wizardName) {
1949
2251
  function deleteTemplate(wizardName, templateName) {
1950
2252
  try {
1951
2253
  const filePath = getTemplateFilePath(wizardName, templateName);
1952
- unlinkSync2(filePath);
2254
+ unlinkSync3(filePath);
1953
2255
  } catch {
1954
2256
  }
1955
2257
  }
@@ -2149,25 +2451,205 @@ var InkRenderer = class {
2149
2451
  }
2150
2452
  };
2151
2453
 
2454
+ // src/renderers/clack.ts
2455
+ import chalk2 from "chalk";
2456
+
2457
+ // src/renderers/symbols.ts
2458
+ function isUnicodeSupported() {
2459
+ if (process.platform === "win32") {
2460
+ return Boolean(process.env["WT_SESSION"]) || process.env["TERM_PROGRAM"] === "vscode";
2461
+ }
2462
+ return process.env["TERM"] !== "linux";
2463
+ }
2464
+ var unicode = isUnicodeSupported();
2465
+ var u = (unicodeChar, fallback) => unicode ? unicodeChar : fallback;
2466
+ var S_BAR_START = u("\u250C", "T");
2467
+ var S_BAR = u("\u2502", "|");
2468
+ var S_BAR_END = u("\u2514", "\u2014");
2469
+ var S_STEP_ACTIVE = u("\u25C6", "*");
2470
+ var S_STEP_SUBMIT = u("\u25C7", "o");
2471
+ var S_STEP_CANCEL = u("\u25A0", "x");
2472
+ var S_STEP_ERROR = u("\u25B2", "x");
2473
+ var S_CORNER_TR = u("\u256E", "+");
2474
+ var S_CORNER_BR = u("\u256F", "+");
2475
+ var S_BAR_H = u("\u2500", "-");
2476
+
2477
+ // src/renderers/clack.ts
2478
+ var ClackRenderer = class extends InquirerRenderer {
2479
+ spinnerInterval;
2480
+ spinnerFrameIndex = 0;
2481
+ renderStepHeader() {
2482
+ }
2483
+ renderGroupHeader() {
2484
+ }
2485
+ renderSummary(answers, steps, theme) {
2486
+ const entries = [];
2487
+ for (const step of steps) {
2488
+ const answer = answers[step.id];
2489
+ if (answer === void 0) continue;
2490
+ const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
2491
+ entries.push({ id: step.id, display });
2492
+ }
2493
+ if (entries.length === 0) return;
2494
+ const title = "Summary";
2495
+ const lines = entries.map((e) => `${e.id}: ${e.display}`);
2496
+ this.writeNoteBox(title, lines, theme);
2497
+ }
2498
+ onEvent(event, theme) {
2499
+ switch (event.type) {
2500
+ case "session:start":
2501
+ this.handleSessionStart(event, theme);
2502
+ break;
2503
+ case "session:end":
2504
+ this.handleSessionEnd(event, theme);
2505
+ break;
2506
+ case "step:start":
2507
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2508
+ `);
2509
+ break;
2510
+ case "step:complete":
2511
+ this.handleStepComplete(event, theme);
2512
+ break;
2513
+ case "step:error":
2514
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(`${S_STEP_ERROR} ${event.error}`)}
2515
+ `);
2516
+ break;
2517
+ case "step:back":
2518
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.muted("\u21A9 Back")}
2519
+ `);
2520
+ break;
2521
+ case "group:start":
2522
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2523
+ `);
2524
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.accent(event.group)}
2525
+ `);
2526
+ break;
2527
+ case "note":
2528
+ this.writeNoteBox(event.title, event.body.split("\n"), theme);
2529
+ break;
2530
+ case "spinner:start":
2531
+ this.startSpinner(event.message, theme);
2532
+ break;
2533
+ case "spinner:stop":
2534
+ this.stopSpinner(event.message, theme);
2535
+ break;
2536
+ case "checks:start":
2537
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2538
+ `);
2539
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running checks...")}
2540
+ `);
2541
+ break;
2542
+ case "check:pass":
2543
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2544
+ `);
2545
+ break;
2546
+ case "check:fail":
2547
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}: ${event.message}
2548
+ `);
2549
+ break;
2550
+ case "actions:start":
2551
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2552
+ `);
2553
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running actions...")}
2554
+ `);
2555
+ break;
2556
+ case "action:pass":
2557
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2558
+ `);
2559
+ break;
2560
+ case "action:fail":
2561
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}
2562
+ `);
2563
+ break;
2564
+ }
2565
+ }
2566
+ handleSessionStart(event, theme) {
2567
+ process.stdout.write(`${chalk2.gray(S_BAR_START)} ${theme.bold(event.wizard)}
2568
+ `);
2569
+ if (event.description) {
2570
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.muted(event.description)}
2571
+ `);
2572
+ }
2573
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2574
+ `);
2575
+ }
2576
+ handleSessionEnd(event, theme) {
2577
+ if (event.cancelled) {
2578
+ process.stdout.write(`${theme.warning(S_STEP_CANCEL)} Cancelled
2579
+ `);
2580
+ } else {
2581
+ process.stdout.write(`${chalk2.gray(S_BAR_END)} ${theme.success("You're all set!")}
2582
+ `);
2583
+ }
2584
+ }
2585
+ handleStepComplete(event, theme) {
2586
+ const displayValue = event.step.type === "password" ? "****" : this.formatValue(event.value);
2587
+ process.stdout.write(
2588
+ `${chalk2.gray(S_STEP_SUBMIT)} ${event.step.message} ${chalk2.gray("\xB7")} ${theme.muted(displayValue)}
2589
+ `
2590
+ );
2591
+ }
2592
+ formatValue(value) {
2593
+ if (Array.isArray(value)) return value.map(String).join(", ");
2594
+ if (typeof value === "boolean") return value ? "Yes" : "No";
2595
+ return String(value);
2596
+ }
2597
+ writeNoteBox(title, lines, _theme) {
2598
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length));
2599
+ const padded = maxLen + 2;
2600
+ const topLine = `${S_BAR_H.repeat(padded - title.length - 1)}${S_CORNER_TR}`;
2601
+ const bottomLine = `${S_BAR_H.repeat(padded)}${S_CORNER_BR}`;
2602
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2603
+ `);
2604
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256D${S_BAR_H} ${title} ${topLine}`)}
2605
+ `);
2606
+ for (const line of lines) {
2607
+ const pad = " ".repeat(maxLen - line.length);
2608
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(S_BAR)} ${line}${pad} ${chalk2.gray(S_BAR)}
2609
+ `);
2610
+ }
2611
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256E${bottomLine}`)}
2612
+ `);
2613
+ }
2614
+ startSpinner(message, theme) {
2615
+ this.spinnerFrameIndex = 0;
2616
+ const { frames, interval } = theme.spinner;
2617
+ this.spinnerInterval = setInterval(() => {
2618
+ const frame = frames[this.spinnerFrameIndex % frames.length];
2619
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
2620
+ this.spinnerFrameIndex++;
2621
+ }, interval);
2622
+ }
2623
+ stopSpinner(message, theme) {
2624
+ if (this.spinnerInterval) {
2625
+ clearInterval(this.spinnerInterval);
2626
+ this.spinnerInterval = void 0;
2627
+ }
2628
+ const finalMessage = message ?? "Done";
2629
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${finalMessage}
2630
+ `);
2631
+ }
2632
+ };
2633
+
2152
2634
  // src/cli.ts
2153
- import { writeFileSync as writeFileSync5 } from "fs";
2154
- import { resolve as resolve2 } from "path";
2635
+ import { writeFileSync as writeFileSync6 } from "fs";
2636
+ import { resolve as resolve3 } from "path";
2155
2637
  import { fileURLToPath } from "url";
2156
2638
  import { stringify as yamlStringify } from "yaml";
2157
2639
  var plainMode = false;
2158
2640
  function applyColorMode(opts) {
2159
2641
  if (opts.plain || opts.color === false || process.env["NO_COLOR"] !== void 0) {
2160
- chalk2.level = 0;
2642
+ chalk3.level = 0;
2161
2643
  plainMode = true;
2162
2644
  }
2163
2645
  }
2164
- program.name("grimoire").description("Config-driven CLI wizard framework").version("0.3.1").option("--no-color", "Disable colored output").option("--plain", "Plain output mode (no colors, no banner)").hook("preAction", () => {
2646
+ program.name("grimoire").description("Config-driven CLI wizard framework").version("0.4.0").option("--no-color", "Disable colored output").option("--plain", "Plain output mode (no colors, no banner)").hook("preAction", () => {
2165
2647
  const globalOpts = program.opts();
2166
2648
  applyColorMode(globalOpts);
2167
2649
  });
2168
- 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("--renderer <type>", "Renderer to use: inquirer (default) or ink", "inquirer").option("--template <name>", "Load a saved template as defaults").action(async (configPath, opts) => {
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) => {
2169
2651
  try {
2170
- const fullPath = resolve2(configPath);
2652
+ const fullPath = resolve3(configPath);
2171
2653
  const config = await loadWizardConfig(fullPath);
2172
2654
  if (opts.dryRun) {
2173
2655
  printDryRun(config);
@@ -2192,10 +2674,12 @@ program.command("run").description("Run a wizard from a config file").argument("
2192
2674
  plain: plainMode,
2193
2675
  mockAnswers,
2194
2676
  templateAnswers,
2195
- cache: opts.cache
2677
+ cache: opts.cache,
2678
+ resume: opts.resume,
2679
+ configFilePath: fullPath
2196
2680
  });
2197
2681
  const rawOutputPath = opts.output ?? config.output?.path;
2198
- const outputPath = rawOutputPath ? resolve2(resolveTemplate(rawOutputPath, answers)) : void 0;
2682
+ const outputPath = rawOutputPath ? resolve3(resolveTemplate(rawOutputPath, answers)) : void 0;
2199
2683
  if (isJsonOutput) {
2200
2684
  const stepsCompleted = Object.keys(answers).length;
2201
2685
  const result = {
@@ -2208,14 +2692,14 @@ program.command("run").description("Run a wizard from a config file").argument("
2208
2692
  const jsonStr = JSON.stringify(result, null, 2);
2209
2693
  console.log(jsonStr);
2210
2694
  if (outputPath) {
2211
- writeFileSync5(outputPath, jsonStr + "\n", "utf-8");
2695
+ writeFileSync6(outputPath, jsonStr + "\n", "utf-8");
2212
2696
  }
2213
2697
  return;
2214
2698
  }
2215
2699
  if (outputPath) {
2216
2700
  const format = opts.format ?? config.output?.format ?? "json";
2217
2701
  const content = formatOutput(answers, format);
2218
- writeFileSync5(outputPath, content, "utf-8");
2702
+ writeFileSync6(outputPath, content, "utf-8");
2219
2703
  if (!opts.quiet) {
2220
2704
  console.log(`
2221
2705
  Answers written to: ${outputPath}
@@ -2241,7 +2725,7 @@ program.command("run").description("Run a wizard from a config file").argument("
2241
2725
  });
2242
2726
  program.command("validate").description("Validate a wizard config file without running it").argument("<config>", "Path to wizard config file").action(async (configPath) => {
2243
2727
  try {
2244
- const fullPath = resolve2(configPath);
2728
+ const fullPath = resolve3(configPath);
2245
2729
  const config = await loadWizardConfig(fullPath);
2246
2730
  console.log(`
2247
2731
  \u2713 Valid wizard config: "${config.meta.name}"`);
@@ -2258,7 +2742,7 @@ program.command("validate").description("Validate a wizard config file without r
2258
2742
  });
2259
2743
  program.command("create").description("Interactively scaffold a new wizard config file").argument("[output]", "Output file path", "wizard.yaml").action(async (output) => {
2260
2744
  try {
2261
- const resolvedPath = resolve2(output);
2745
+ const resolvedPath = resolve3(output);
2262
2746
  await scaffoldWizard(resolvedPath);
2263
2747
  } catch (error) {
2264
2748
  if (error instanceof Error) {
@@ -2271,11 +2755,12 @@ program.command("create").description("Interactively scaffold a new wizard confi
2271
2755
  });
2272
2756
  program.command("demo").description("Run a demo wizard showcasing all step types").action(async () => {
2273
2757
  try {
2274
- const demoPath = resolve2(
2758
+ const demoPath = resolve3(
2275
2759
  fileURLToPath(import.meta.url),
2276
2760
  "..",
2277
2761
  "..",
2278
2762
  "examples",
2763
+ "yaml",
2279
2764
  "demo.yaml"
2280
2765
  );
2281
2766
  const config = await loadWizardConfig(demoPath);
@@ -2409,6 +2894,11 @@ function printDryRun(config) {
2409
2894
  console.log(` Step ${num} ${typeStr} ${idStr} ${msg}${suffix}`);
2410
2895
  }
2411
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
+ }
2412
2902
  if (config.actions && config.actions.length > 0) {
2413
2903
  console.log(` ${theme.bold("Post-wizard actions:")}`);
2414
2904
  for (const action of config.actions) {
@@ -2462,7 +2952,10 @@ function resolveRenderer(rendererName) {
2462
2952
  if (rendererName === "ink") {
2463
2953
  return new InkRenderer();
2464
2954
  }
2465
- throw new Error(`Unknown renderer: "${rendererName}". Supported: inquirer, ink`);
2955
+ if (rendererName === "clack") {
2956
+ return new ClackRenderer();
2957
+ }
2958
+ throw new Error(`Unknown renderer: "${rendererName}". Supported: inquirer, ink, clack`);
2466
2959
  }
2467
2960
  function toEnvKey(key) {
2468
2961
  return key.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();