grimoire-wizard 0.3.1 → 0.4.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 (37) hide show
  1. package/README.md +173 -18
  2. package/dist/cli.js +503 -97
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +107 -3
  5. package/dist/index.js +832 -367
  6. package/dist/index.js.map +1 -1
  7. package/examples/json/all-features.json +66 -0
  8. package/examples/json/appstore-screenshot-wizard.json +362 -0
  9. package/examples/json/appstore-upload.json +104 -0
  10. package/examples/json/basic.json +72 -0
  11. package/examples/json/batch-generate.json +186 -0
  12. package/examples/json/brief-builder.json +519 -0
  13. package/examples/json/conditional.json +155 -0
  14. package/examples/json/cost-analyzer.json +83 -0
  15. package/examples/json/demo.json +130 -0
  16. package/examples/json/scraper-selector.json +63 -0
  17. package/examples/json/themed-catppuccin.json +39 -0
  18. package/examples/json/themed.json +103 -0
  19. package/examples/json/with-checks.json +47 -0
  20. package/examples/yaml/appstore-screenshot-wizard.yaml +321 -0
  21. package/examples/yaml/appstore-upload.yaml +84 -0
  22. package/examples/yaml/batch-generate.yaml +156 -0
  23. package/examples/yaml/brief-builder.yaml +429 -0
  24. package/examples/yaml/cost-analyzer.yaml +69 -0
  25. package/examples/yaml/pipeline.yaml +35 -0
  26. package/examples/yaml/scraper-selector.yaml +52 -0
  27. package/examples/yaml/themed-catppuccin.yaml +31 -0
  28. package/package.json +1 -1
  29. /package/examples/{all-features.yaml → yaml/all-features.yaml} +0 -0
  30. /package/examples/{base.yaml → yaml/base.yaml} +0 -0
  31. /package/examples/{basic.yaml → yaml/basic.yaml} +0 -0
  32. /package/examples/{conditional.yaml → yaml/conditional.yaml} +0 -0
  33. /package/examples/{demo.yaml → yaml/demo.yaml} +0 -0
  34. /package/examples/{ebay-mcp-setup.yaml → yaml/ebay-mcp-setup.yaml} +0 -0
  35. /package/examples/{extended.yaml → yaml/extended.yaml} +0 -0
  36. /package/examples/{themed.yaml → yaml/themed.yaml} +0 -0
  37. /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(),
@@ -194,7 +200,8 @@ var wizardConfigSchema = z.object({
194
200
  meta: z.object({
195
201
  name: z.string(),
196
202
  version: z.string().optional(),
197
- description: z.string().optional()
203
+ description: z.string().optional(),
204
+ review: z.boolean().optional()
198
205
  }),
199
206
  theme: themeConfigSchema.optional(),
200
207
  steps: z.array(stepConfigSchema).min(1),
@@ -758,6 +765,67 @@ function applyValidationRule(rule, value) {
758
765
 
759
766
  // src/theme.ts
760
767
  import chalk from "chalk";
768
+
769
+ // src/themes/presets.ts
770
+ var THEME_PRESETS = {
771
+ default: {
772
+ primary: "#7C3AED",
773
+ success: "#10B981",
774
+ error: "#EF4444",
775
+ warning: "#F59E0B",
776
+ info: "#3B82F6",
777
+ muted: "#6B7280",
778
+ accent: "#8B5CF6"
779
+ },
780
+ catppuccin: {
781
+ primary: "#cba6f7",
782
+ success: "#a6e3a1",
783
+ error: "#f38ba8",
784
+ warning: "#fab387",
785
+ info: "#74c7ec",
786
+ muted: "#6c7086",
787
+ accent: "#f5c2e7"
788
+ },
789
+ dracula: {
790
+ primary: "#bd93f9",
791
+ success: "#50fa7b",
792
+ error: "#ff5555",
793
+ warning: "#ffb86c",
794
+ info: "#8be9fd",
795
+ muted: "#6272a4",
796
+ accent: "#ff79c6"
797
+ },
798
+ nord: {
799
+ primary: "#88c0d0",
800
+ success: "#a3be8c",
801
+ error: "#bf616a",
802
+ warning: "#ebcb8b",
803
+ info: "#81a1c1",
804
+ muted: "#4c566a",
805
+ accent: "#b48ead"
806
+ },
807
+ tokyonight: {
808
+ primary: "#7aa2f7",
809
+ success: "#9ece6a",
810
+ error: "#f7768e",
811
+ warning: "#e0af68",
812
+ info: "#7dcfff",
813
+ muted: "#565f89",
814
+ accent: "#bb9af7"
815
+ },
816
+ monokai: {
817
+ primary: "#ab9df2",
818
+ success: "#a9dc76",
819
+ error: "#ff6188",
820
+ warning: "#ffd866",
821
+ info: "#78dce8",
822
+ muted: "#727072",
823
+ accent: "#fc9867"
824
+ }
825
+ };
826
+ var PRESET_NAMES = Object.keys(THEME_PRESETS);
827
+
828
+ // src/theme.ts
761
829
  var DEFAULT_TOKENS = {
762
830
  primary: "#5B9BD5",
763
831
  success: "#6BCB77",
@@ -774,7 +842,8 @@ var DEFAULT_ICONS = {
774
842
  pointer: "\u203A"
775
843
  };
776
844
  function resolveTheme(themeConfig) {
777
- const tokens = { ...DEFAULT_TOKENS, ...themeConfig?.tokens };
845
+ const presetTokens = themeConfig?.preset ? THEME_PRESETS[themeConfig.preset] : void 0;
846
+ const tokens = { ...DEFAULT_TOKENS, ...presetTokens, ...themeConfig?.tokens };
778
847
  const icons = { ...DEFAULT_ICONS, ...themeConfig?.icons };
779
848
  return {
780
849
  primary: chalk.hex(tokens.primary),
@@ -1116,14 +1185,66 @@ function clearCache(wizardName, customDir) {
1116
1185
  }
1117
1186
  }
1118
1187
 
1119
- // src/mru.ts
1120
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
1188
+ // src/progress.ts
1189
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1121
1190
  import { join as join2 } from "path";
1122
1191
  import { homedir as homedir2 } from "os";
1123
- var MRU_DIR = join2(homedir2(), ".config", "grimoire", "mru");
1192
+ var DEFAULT_PROGRESS_DIR = join2(homedir2(), ".config", "grimoire", "progress");
1193
+ function getProgressFilePath(wizardName, customDir) {
1194
+ const dir = customDir ?? DEFAULT_PROGRESS_DIR;
1195
+ return join2(dir, `${slugify(wizardName)}.json`);
1196
+ }
1197
+ function saveProgress(wizardName, state, customDir, excludeStepIds) {
1198
+ try {
1199
+ const dir = customDir ?? DEFAULT_PROGRESS_DIR;
1200
+ mkdirSync2(dir, { recursive: true });
1201
+ const excludeSet = new Set(excludeStepIds ?? []);
1202
+ const filteredAnswers = {};
1203
+ for (const [key, value] of Object.entries(state.answers)) {
1204
+ if (!excludeSet.has(key)) {
1205
+ filteredAnswers[key] = value;
1206
+ }
1207
+ }
1208
+ const progress = {
1209
+ currentStepId: state.currentStepId,
1210
+ answers: filteredAnswers,
1211
+ history: state.history,
1212
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
1213
+ };
1214
+ const filePath = getProgressFilePath(wizardName, customDir);
1215
+ writeFileSync2(filePath, JSON.stringify(progress, null, 2) + "\n", "utf-8");
1216
+ } catch {
1217
+ }
1218
+ }
1219
+ function loadProgress(wizardName, customDir) {
1220
+ try {
1221
+ const filePath = getProgressFilePath(wizardName, customDir);
1222
+ const raw = readFileSync3(filePath, "utf-8");
1223
+ const parsed = JSON.parse(raw);
1224
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && "currentStepId" in parsed && "answers" in parsed && "history" in parsed && "savedAt" in parsed) {
1225
+ return parsed;
1226
+ }
1227
+ return null;
1228
+ } catch {
1229
+ return null;
1230
+ }
1231
+ }
1232
+ function clearProgress(wizardName, customDir) {
1233
+ try {
1234
+ const filePath = getProgressFilePath(wizardName, customDir);
1235
+ unlinkSync2(filePath);
1236
+ } catch {
1237
+ }
1238
+ }
1239
+
1240
+ // src/mru.ts
1241
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync } from "fs";
1242
+ import { join as join3 } from "path";
1243
+ import { homedir as homedir3 } from "os";
1244
+ var MRU_DIR = join3(homedir3(), ".config", "grimoire", "mru");
1124
1245
  function getMruFilePath(wizardName) {
1125
1246
  const safeName = wizardName.replace(/[^a-zA-Z0-9_-]/g, "_");
1126
- return join2(MRU_DIR, `${safeName}.json`);
1247
+ return join3(MRU_DIR, `${safeName}.json`);
1127
1248
  }
1128
1249
  function loadMruData(wizardName) {
1129
1250
  const filePath = getMruFilePath(wizardName);
@@ -1131,7 +1252,7 @@ function loadMruData(wizardName) {
1131
1252
  if (!existsSync(filePath)) {
1132
1253
  return {};
1133
1254
  }
1134
- const raw = readFileSync3(filePath, "utf-8");
1255
+ const raw = readFileSync4(filePath, "utf-8");
1135
1256
  const parsed = JSON.parse(raw);
1136
1257
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1137
1258
  return {};
@@ -1144,8 +1265,8 @@ function loadMruData(wizardName) {
1144
1265
  function saveMruData(wizardName, data) {
1145
1266
  const filePath = getMruFilePath(wizardName);
1146
1267
  try {
1147
- mkdirSync2(MRU_DIR, { recursive: true });
1148
- writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1268
+ mkdirSync3(MRU_DIR, { recursive: true });
1269
+ writeFileSync3(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1149
1270
  } catch {
1150
1271
  }
1151
1272
  }
@@ -1199,13 +1320,21 @@ function getOrderedOptions(wizardName, stepId, options) {
1199
1320
  }
1200
1321
 
1201
1322
  // src/runner.ts
1202
- function runPreFlightChecks(checks, theme) {
1323
+ function emitEvent(renderer, event, theme) {
1324
+ if (renderer.onEvent) {
1325
+ renderer.onEvent(event, theme);
1326
+ }
1327
+ }
1328
+ function runPreFlightChecks(checks, theme, renderer) {
1329
+ if (renderer) emitEvent(renderer, { type: "checks:start", checks }, theme);
1203
1330
  for (const check of checks) {
1204
1331
  try {
1205
1332
  execSync(check.run, { stdio: "pipe" });
1206
1333
  console.log(` ${theme.success("\u2713")} ${check.name}`);
1334
+ if (renderer) emitEvent(renderer, { type: "check:pass", name: check.name }, theme);
1207
1335
  } catch {
1208
1336
  console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
1337
+ if (renderer) emitEvent(renderer, { type: "check:fail", name: check.name, message: check.message }, theme);
1209
1338
  throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
1210
1339
  }
1211
1340
  }
@@ -1215,7 +1344,7 @@ function getMockValue(step, mockAnswers) {
1215
1344
  if (step.id in mockAnswers) {
1216
1345
  return mockAnswers[step.id];
1217
1346
  }
1218
- if (step.type === "message") {
1347
+ if (step.type === "message" || step.type === "note") {
1219
1348
  return true;
1220
1349
  }
1221
1350
  const defaultValue = getStepDefault(step);
@@ -1243,6 +1372,7 @@ function getStepDefault(step) {
1243
1372
  return step.default;
1244
1373
  case "password":
1245
1374
  case "message":
1375
+ case "note":
1246
1376
  return void 0;
1247
1377
  }
1248
1378
  }
@@ -1256,6 +1386,21 @@ async function runWizard(config, options) {
1256
1386
  const cacheDir = typeof options?.cache === "object" ? options.cache.dir : void 0;
1257
1387
  const mruEnabled = !isMock && options?.mru !== false;
1258
1388
  let state = createWizardState(config);
1389
+ const resumeEnabled = !isMock && options?.resume !== false;
1390
+ if (resumeEnabled) {
1391
+ const saved = loadProgress(config.meta.name);
1392
+ if (saved) {
1393
+ const stepExists = config.steps.some((s) => s.id === saved.currentStepId);
1394
+ if (stepExists) {
1395
+ state = {
1396
+ ...state,
1397
+ currentStepId: saved.currentStepId,
1398
+ answers: { ...state.answers, ...saved.answers },
1399
+ history: saved.history
1400
+ };
1401
+ }
1402
+ }
1403
+ }
1259
1404
  const cachedAnswers = cacheEnabled ? loadCachedAnswers(config.meta.name, cacheDir) : void 0;
1260
1405
  const userPlugins = options?.plugins;
1261
1406
  if (userPlugins) {
@@ -1265,103 +1410,169 @@ async function runWizard(config, options) {
1265
1410
  }
1266
1411
  try {
1267
1412
  if (!isMock && config.checks && config.checks.length > 0) {
1268
- runPreFlightChecks(config.checks, theme);
1413
+ runPreFlightChecks(config.checks, theme, renderer);
1269
1414
  }
1270
1415
  if (!quiet) {
1271
1416
  printWizardHeader(config, theme, options?.plain);
1272
1417
  }
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) {
1418
+ const visibleStepsForCount = getVisibleSteps(config, state.answers);
1419
+ emitEvent(renderer, { type: "session:start", wizard: config.meta.name, description: config.meta.description, totalSteps: visibleStepsForCount.length }, theme);
1420
+ let needsReview = true;
1421
+ while (needsReview) {
1422
+ let previousGroup;
1423
+ while (state.status === "running") {
1424
+ const visibleSteps = getVisibleSteps(config, state.answers);
1425
+ const currentStep = config.steps.find((s) => s.id === state.currentStepId);
1426
+ if (!currentStep) {
1427
+ throw new Error(`Current step not found: "${state.currentStepId}"`);
1428
+ }
1281
1429
  if (currentStep.group !== void 0 && currentStep.group !== previousGroup) {
1282
1430
  const resolvedGroup = resolveTemplate(currentStep.group, state.answers);
1283
- renderer.renderGroupHeader(resolvedGroup, theme);
1431
+ if (!isMock) {
1432
+ renderer.renderGroupHeader(resolvedGroup, theme);
1433
+ }
1434
+ emitEvent(renderer, { type: "group:start", group: resolvedGroup }, theme);
1284
1435
  }
1285
1436
  previousGroup = currentStep.group;
1286
1437
  const stepIndex = visibleSteps.findIndex((s) => s.id === state.currentStepId);
1287
1438
  const resolvedMessage = resolveTemplate(currentStep.message, state.answers);
1288
1439
  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) {
1440
+ if (!isMock) {
1441
+ renderer.renderStepHeader(stepIndex, visibleSteps.length, resolvedMessage, theme, resolvedDescription);
1442
+ }
1443
+ emitEvent(renderer, { type: "step:start", stepId: currentStep.id, stepIndex, totalVisible: visibleSteps.length, step: currentStep }, theme);
1444
+ if (currentStep.type === "note") {
1445
+ emitEvent(renderer, { type: "note", title: resolvedMessage, body: resolvedDescription ?? "" }, theme);
1446
+ }
1447
+ if (options?.onBeforeStep) {
1448
+ await options.onBeforeStep(currentStep.id, currentStep, state);
1449
+ }
1450
+ const pluginStep = getPluginStep(currentStep.type);
1451
+ const resolvedStep = pluginStep ? currentStep : resolveStepDefaults(currentStep, cachedAnswers);
1452
+ const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
1453
+ const templatedStep = resolveStepTemplates(withTemplate, state.answers);
1454
+ const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
1455
+ try {
1456
+ const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
1457
+ if (pluginStep?.validate) {
1458
+ const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1459
+ if (pluginError) {
1460
+ if (isMock) {
1461
+ throw new Error(
1462
+ `Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
1463
+ );
1464
+ }
1465
+ console.log(theme.error(`
1466
+ ${pluginError}
1467
+ `));
1468
+ continue;
1469
+ }
1470
+ }
1471
+ const nextState = wizardReducer(state, { type: "NEXT", value }, config);
1472
+ if (nextState.errors[currentStep.id]) {
1473
+ const errorMsg = resolveTemplate(nextState.errors[currentStep.id] ?? "", state.answers);
1474
+ emitEvent(renderer, { type: "step:error", stepId: currentStep.id, error: errorMsg }, theme);
1304
1475
  if (isMock) {
1305
1476
  throw new Error(
1306
- `Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
1477
+ `Mock mode: validation failed for step "${currentStep.id}": ${errorMsg}`
1307
1478
  );
1308
1479
  }
1309
1480
  console.log(theme.error(`
1310
- ${pluginError}
1481
+ ${errorMsg}
1311
1482
  `));
1483
+ state = { ...nextState, errors: {} };
1312
1484
  continue;
1313
1485
  }
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(`
1486
+ if (!isMock && options?.asyncValidate) {
1487
+ const asyncError = await options.asyncValidate(currentStep.id, value, nextState.answers);
1488
+ if (asyncError !== null) {
1489
+ console.log(theme.error(`
1333
1490
  ${asyncError}
1334
1491
  `));
1335
- state = { ...nextState, errors: {} };
1336
- continue;
1492
+ state = { ...nextState, errors: {} };
1493
+ continue;
1494
+ }
1495
+ }
1496
+ if (options?.onAfterStep) {
1497
+ await options.onAfterStep(currentStep.id, value, nextState);
1498
+ }
1499
+ state = nextState;
1500
+ emitEvent(renderer, { type: "step:complete", stepId: currentStep.id, value, step: currentStep }, theme);
1501
+ if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1502
+ recordSelection(config.meta.name, currentStep.id, value);
1337
1503
  }
1504
+ options?.onStepComplete?.(currentStep.id, value, state);
1505
+ } catch (error) {
1506
+ if (!isMock && isUserCancel(error)) {
1507
+ state = wizardReducer(state, { type: "CANCEL" }, config);
1508
+ options?.onCancel?.(state);
1509
+ const passwordStepIds = config.steps.filter((s) => s.type === "password").map((s) => s.id);
1510
+ saveProgress(config.meta.name, {
1511
+ currentStepId: state.currentStepId,
1512
+ answers: state.answers,
1513
+ history: state.history
1514
+ }, void 0, passwordStepIds);
1515
+ emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: true }, theme);
1516
+ if (!quiet) {
1517
+ console.log(theme.warning("\n Wizard cancelled.\n"));
1518
+ }
1519
+ return state.answers;
1520
+ }
1521
+ throw error;
1338
1522
  }
1339
- if (options?.onAfterStep) {
1340
- await options.onAfterStep(currentStep.id, value, nextState);
1523
+ }
1524
+ if (config.meta.review && !isMock && state.status === "done") {
1525
+ const reviewLines = [];
1526
+ for (const step of config.steps) {
1527
+ const answer = state.answers[step.id];
1528
+ if (answer === void 0) continue;
1529
+ const display = step.type === "password" ? "****" : Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
1530
+ reviewLines.push(`${step.id}: ${display}`);
1341
1531
  }
1342
- state = nextState;
1343
- if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1344
- recordSelection(config.meta.name, currentStep.id, value);
1532
+ emitEvent(renderer, { type: "note", title: "Review your answers", body: reviewLines.join("\n") }, theme);
1533
+ console.log(`
1534
+ ${theme.bold("Review your answers:")}
1535
+ `);
1536
+ for (const line of reviewLines) {
1537
+ console.log(` ${line}`);
1345
1538
  }
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;
1539
+ console.log();
1540
+ const { confirm: confirmPrompt } = await import("@inquirer/prompts");
1541
+ const ok = await confirmPrompt({
1542
+ message: "Everything look right?",
1543
+ default: true
1544
+ });
1545
+ if (ok) {
1546
+ needsReview = false;
1547
+ } else {
1548
+ const { select: selectPrompt } = await import("@inquirer/prompts");
1549
+ const stepsWithAnswers = config.steps.filter(
1550
+ (s) => state.answers[s.id] !== void 0 && s.type !== "note" && s.type !== "message"
1551
+ );
1552
+ const stepToRevisit = await selectPrompt({
1553
+ message: "Which step would you like to change?",
1554
+ choices: stepsWithAnswers.map((s) => ({
1555
+ name: `${s.id}: ${s.type === "password" ? "****" : String(state.answers[s.id] ?? "")}`,
1556
+ value: s.id
1557
+ }))
1558
+ });
1559
+ state = {
1560
+ ...state,
1561
+ currentStepId: stepToRevisit,
1562
+ status: "running"
1563
+ };
1355
1564
  }
1356
- throw error;
1565
+ } else {
1566
+ needsReview = false;
1357
1567
  }
1358
1568
  }
1359
1569
  if (state.status === "done" && !quiet) {
1360
1570
  renderer.renderSummary(state.answers, config.steps, theme);
1361
1571
  }
1362
1572
  if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1363
- await executeActions(config.actions, state.answers, theme);
1573
+ await executeActions(config.actions, state.answers, theme, renderer);
1364
1574
  }
1575
+ emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: state.status === "cancelled" }, theme);
1365
1576
  if (state.status === "done" && cacheEnabled) {
1366
1577
  const passwordStepIds = new Set(
1367
1578
  config.steps.filter((s) => s.type === "password").map((s) => s.id)
@@ -1374,6 +1585,9 @@ async function runWizard(config, options) {
1374
1585
  }
1375
1586
  saveCachedAnswers(config.meta.name, answersToCache, cacheDir);
1376
1587
  }
1588
+ if (state.status === "done") {
1589
+ clearProgress(config.meta.name);
1590
+ }
1377
1591
  return state.answers;
1378
1592
  } finally {
1379
1593
  if (userPlugins) {
@@ -1413,6 +1627,8 @@ function renderStep(renderer, step, state, theme) {
1413
1627
  case "message":
1414
1628
  renderer.renderMessage(step, state, theme);
1415
1629
  return Promise.resolve(true);
1630
+ case "note":
1631
+ return Promise.resolve(true);
1416
1632
  }
1417
1633
  }
1418
1634
  function resolveStepDefaults(step, cachedAnswers) {
@@ -1463,6 +1679,7 @@ function resolveStepDefaults(step, cachedAnswers) {
1463
1679
  }
1464
1680
  case "password":
1465
1681
  case "message":
1682
+ case "note":
1466
1683
  return step;
1467
1684
  }
1468
1685
  }
@@ -1472,7 +1689,7 @@ function getCachedDefault(stepId, cachedAnswers) {
1472
1689
  }
1473
1690
  function applyTemplateDefaults(step, templateAnswers) {
1474
1691
  if (!(step.id in templateAnswers)) return step;
1475
- if (step.type === "password" || step.type === "message") return step;
1692
+ if (step.type === "password" || step.type === "message" || step.type === "note") return step;
1476
1693
  const value = templateAnswers[step.id];
1477
1694
  switch (step.type) {
1478
1695
  case "text":
@@ -1561,13 +1778,15 @@ function resolveStepTemplates(step, answers) {
1561
1778
  case "confirm":
1562
1779
  case "toggle":
1563
1780
  case "message":
1781
+ case "note":
1564
1782
  return {
1565
1783
  ...step,
1566
1784
  description: step.description ? resolveTemplate(step.description, answers) : void 0
1567
1785
  };
1568
1786
  }
1569
1787
  }
1570
- async function executeActions(actions, answers, theme) {
1788
+ async function executeActions(actions, answers, theme, renderer) {
1789
+ if (renderer) emitEvent(renderer, { type: "actions:start" }, theme);
1571
1790
  console.log(`
1572
1791
  ${theme.bold("Running actions...")}
1573
1792
  `);
@@ -1581,8 +1800,10 @@ async function executeActions(actions, answers, theme) {
1581
1800
  try {
1582
1801
  execSync(resolvedCommand, { stdio: "pipe" });
1583
1802
  console.log(` ${theme.success("\u2713")} ${label}`);
1803
+ if (renderer) emitEvent(renderer, { type: "action:pass", name: label }, theme);
1584
1804
  } catch {
1585
1805
  console.log(` ${theme.error("\u2717")} ${label}`);
1806
+ if (renderer) emitEvent(renderer, { type: "action:fail", name: label }, theme);
1586
1807
  throw new Error(`Action failed: ${label}`);
1587
1808
  }
1588
1809
  }
@@ -1606,7 +1827,7 @@ function isUserCancel(error) {
1606
1827
  // src/scaffolder.ts
1607
1828
  import { input as input2, select as select2, confirm as confirm2, number as number2 } from "@inquirer/prompts";
1608
1829
  import { stringify } from "yaml";
1609
- import { writeFileSync as writeFileSync3 } from "fs";
1830
+ import { writeFileSync as writeFileSync4 } from "fs";
1610
1831
  import { relative } from "path";
1611
1832
  var STEP_TYPE_CHOICES = [
1612
1833
  { name: "text", value: "text" },
@@ -1723,7 +1944,7 @@ async function scaffoldWizard(outputPath) {
1723
1944
  config.theme = { tokens: { primary: primaryColor } };
1724
1945
  }
1725
1946
  const yamlContent = stringify(config);
1726
- writeFileSync3(outputPath, yamlContent, "utf-8");
1947
+ writeFileSync4(outputPath, yamlContent, "utf-8");
1727
1948
  const displayPath = relative(process.cwd(), outputPath);
1728
1949
  console.log(`
1729
1950
  \u2713 Created wizard config: ${outputPath}`);
@@ -1914,20 +2135,20 @@ complete -c grimoire -n "__fish_seen_subcommand_from completion" -f -a "fish" -d
1914
2135
  }
1915
2136
 
1916
2137
  // 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");
2138
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
2139
+ import { join as join4 } from "path";
2140
+ import { homedir as homedir4 } from "os";
2141
+ var TEMPLATES_DIR = join4(homedir4(), ".config", "grimoire", "templates");
1921
2142
  function getWizardTemplateDir(wizardName) {
1922
- return join3(TEMPLATES_DIR, slugify(wizardName));
2143
+ return join4(TEMPLATES_DIR, slugify(wizardName));
1923
2144
  }
1924
2145
  function getTemplateFilePath(wizardName, templateName) {
1925
- return join3(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
2146
+ return join4(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
1926
2147
  }
1927
2148
  function loadTemplate(wizardName, templateName) {
1928
2149
  try {
1929
2150
  const filePath = getTemplateFilePath(wizardName, templateName);
1930
- const raw = readFileSync4(filePath, "utf-8");
2151
+ const raw = readFileSync5(filePath, "utf-8");
1931
2152
  const parsed = JSON.parse(raw);
1932
2153
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1933
2154
  return parsed;
@@ -1949,7 +2170,7 @@ function listTemplates(wizardName) {
1949
2170
  function deleteTemplate(wizardName, templateName) {
1950
2171
  try {
1951
2172
  const filePath = getTemplateFilePath(wizardName, templateName);
1952
- unlinkSync2(filePath);
2173
+ unlinkSync3(filePath);
1953
2174
  } catch {
1954
2175
  }
1955
2176
  }
@@ -2149,23 +2370,203 @@ var InkRenderer = class {
2149
2370
  }
2150
2371
  };
2151
2372
 
2373
+ // src/renderers/clack.ts
2374
+ import chalk2 from "chalk";
2375
+
2376
+ // src/renderers/symbols.ts
2377
+ function isUnicodeSupported() {
2378
+ if (process.platform === "win32") {
2379
+ return Boolean(process.env["WT_SESSION"]) || process.env["TERM_PROGRAM"] === "vscode";
2380
+ }
2381
+ return process.env["TERM"] !== "linux";
2382
+ }
2383
+ var unicode = isUnicodeSupported();
2384
+ var u = (unicodeChar, fallback) => unicode ? unicodeChar : fallback;
2385
+ var S_BAR_START = u("\u250C", "T");
2386
+ var S_BAR = u("\u2502", "|");
2387
+ var S_BAR_END = u("\u2514", "\u2014");
2388
+ var S_STEP_ACTIVE = u("\u25C6", "*");
2389
+ var S_STEP_SUBMIT = u("\u25C7", "o");
2390
+ var S_STEP_CANCEL = u("\u25A0", "x");
2391
+ var S_STEP_ERROR = u("\u25B2", "x");
2392
+ var S_CORNER_TR = u("\u256E", "+");
2393
+ var S_CORNER_BR = u("\u256F", "+");
2394
+ var S_BAR_H = u("\u2500", "-");
2395
+ var S_SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"];
2396
+
2397
+ // src/renderers/clack.ts
2398
+ var ClackRenderer = class extends InquirerRenderer {
2399
+ spinnerInterval;
2400
+ spinnerFrameIndex = 0;
2401
+ renderStepHeader() {
2402
+ }
2403
+ renderGroupHeader() {
2404
+ }
2405
+ renderSummary(answers, steps, theme) {
2406
+ const entries = [];
2407
+ for (const step of steps) {
2408
+ const answer = answers[step.id];
2409
+ if (answer === void 0) continue;
2410
+ const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
2411
+ entries.push({ id: step.id, display });
2412
+ }
2413
+ if (entries.length === 0) return;
2414
+ const title = "Summary";
2415
+ const lines = entries.map((e) => `${e.id}: ${e.display}`);
2416
+ this.writeNoteBox(title, lines, theme);
2417
+ }
2418
+ onEvent(event, theme) {
2419
+ switch (event.type) {
2420
+ case "session:start":
2421
+ this.handleSessionStart(event, theme);
2422
+ break;
2423
+ case "session:end":
2424
+ this.handleSessionEnd(event, theme);
2425
+ break;
2426
+ case "step:start":
2427
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2428
+ `);
2429
+ break;
2430
+ case "step:complete":
2431
+ this.handleStepComplete(event, theme);
2432
+ break;
2433
+ case "step:error":
2434
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(`${S_STEP_ERROR} ${event.error}`)}
2435
+ `);
2436
+ break;
2437
+ case "step:back":
2438
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.muted("\u21A9 Back")}
2439
+ `);
2440
+ break;
2441
+ case "group:start":
2442
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2443
+ `);
2444
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.accent(event.group)}
2445
+ `);
2446
+ break;
2447
+ case "note":
2448
+ this.writeNoteBox(event.title, event.body.split("\n"), theme);
2449
+ break;
2450
+ case "spinner:start":
2451
+ this.startSpinner(event.message, theme);
2452
+ break;
2453
+ case "spinner:stop":
2454
+ this.stopSpinner(event.message, theme);
2455
+ break;
2456
+ case "checks:start":
2457
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2458
+ `);
2459
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running checks...")}
2460
+ `);
2461
+ break;
2462
+ case "check:pass":
2463
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2464
+ `);
2465
+ break;
2466
+ case "check:fail":
2467
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}: ${event.message}
2468
+ `);
2469
+ break;
2470
+ case "actions:start":
2471
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2472
+ `);
2473
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running actions...")}
2474
+ `);
2475
+ break;
2476
+ case "action:pass":
2477
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2478
+ `);
2479
+ break;
2480
+ case "action:fail":
2481
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}
2482
+ `);
2483
+ break;
2484
+ }
2485
+ }
2486
+ handleSessionStart(event, theme) {
2487
+ process.stdout.write(`${chalk2.gray(S_BAR_START)} ${theme.bold(event.wizard)}
2488
+ `);
2489
+ if (event.description) {
2490
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.muted(event.description)}
2491
+ `);
2492
+ }
2493
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2494
+ `);
2495
+ }
2496
+ handleSessionEnd(event, theme) {
2497
+ if (event.cancelled) {
2498
+ process.stdout.write(`${theme.warning(S_STEP_CANCEL)} Cancelled
2499
+ `);
2500
+ } else {
2501
+ process.stdout.write(`${chalk2.gray(S_BAR_END)} ${theme.success("You're all set!")}
2502
+ `);
2503
+ }
2504
+ }
2505
+ handleStepComplete(event, theme) {
2506
+ const displayValue = event.step.type === "password" ? "****" : this.formatValue(event.value);
2507
+ process.stdout.write(
2508
+ `${chalk2.gray(S_STEP_SUBMIT)} ${event.step.message} ${chalk2.gray("\xB7")} ${theme.muted(displayValue)}
2509
+ `
2510
+ );
2511
+ }
2512
+ formatValue(value) {
2513
+ if (Array.isArray(value)) return value.map(String).join(", ");
2514
+ if (typeof value === "boolean") return value ? "Yes" : "No";
2515
+ return String(value);
2516
+ }
2517
+ writeNoteBox(title, lines, _theme) {
2518
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length));
2519
+ const padded = maxLen + 2;
2520
+ const topLine = `${S_BAR_H.repeat(padded - title.length - 1)}${S_CORNER_TR}`;
2521
+ const bottomLine = `${S_BAR_H.repeat(padded)}${S_CORNER_BR}`;
2522
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2523
+ `);
2524
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256D${S_BAR_H} ${title} ${topLine}`)}
2525
+ `);
2526
+ for (const line of lines) {
2527
+ const pad = " ".repeat(maxLen - line.length);
2528
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(S_BAR)} ${line}${pad} ${chalk2.gray(S_BAR)}
2529
+ `);
2530
+ }
2531
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${chalk2.gray(`\u256E${bottomLine}`)}
2532
+ `);
2533
+ }
2534
+ startSpinner(message, _theme) {
2535
+ this.spinnerFrameIndex = 0;
2536
+ this.spinnerInterval = setInterval(() => {
2537
+ const frame = S_SPINNER_FRAMES[this.spinnerFrameIndex % S_SPINNER_FRAMES.length];
2538
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
2539
+ this.spinnerFrameIndex++;
2540
+ }, 80);
2541
+ }
2542
+ stopSpinner(message, theme) {
2543
+ if (this.spinnerInterval) {
2544
+ clearInterval(this.spinnerInterval);
2545
+ this.spinnerInterval = void 0;
2546
+ }
2547
+ const finalMessage = message ?? "Done";
2548
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${finalMessage}
2549
+ `);
2550
+ }
2551
+ };
2552
+
2152
2553
  // src/cli.ts
2153
- import { writeFileSync as writeFileSync5 } from "fs";
2554
+ import { writeFileSync as writeFileSync6 } from "fs";
2154
2555
  import { resolve as resolve2 } from "path";
2155
2556
  import { fileURLToPath } from "url";
2156
2557
  import { stringify as yamlStringify } from "yaml";
2157
2558
  var plainMode = false;
2158
2559
  function applyColorMode(opts) {
2159
2560
  if (opts.plain || opts.color === false || process.env["NO_COLOR"] !== void 0) {
2160
- chalk2.level = 0;
2561
+ chalk3.level = 0;
2161
2562
  plainMode = true;
2162
2563
  }
2163
2564
  }
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", () => {
2565
+ 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
2566
  const globalOpts = program.opts();
2166
2567
  applyColorMode(globalOpts);
2167
2568
  });
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) => {
2569
+ 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
2570
  try {
2170
2571
  const fullPath = resolve2(configPath);
2171
2572
  const config = await loadWizardConfig(fullPath);
@@ -2192,7 +2593,8 @@ program.command("run").description("Run a wizard from a config file").argument("
2192
2593
  plain: plainMode,
2193
2594
  mockAnswers,
2194
2595
  templateAnswers,
2195
- cache: opts.cache
2596
+ cache: opts.cache,
2597
+ resume: opts.resume
2196
2598
  });
2197
2599
  const rawOutputPath = opts.output ?? config.output?.path;
2198
2600
  const outputPath = rawOutputPath ? resolve2(resolveTemplate(rawOutputPath, answers)) : void 0;
@@ -2208,14 +2610,14 @@ program.command("run").description("Run a wizard from a config file").argument("
2208
2610
  const jsonStr = JSON.stringify(result, null, 2);
2209
2611
  console.log(jsonStr);
2210
2612
  if (outputPath) {
2211
- writeFileSync5(outputPath, jsonStr + "\n", "utf-8");
2613
+ writeFileSync6(outputPath, jsonStr + "\n", "utf-8");
2212
2614
  }
2213
2615
  return;
2214
2616
  }
2215
2617
  if (outputPath) {
2216
2618
  const format = opts.format ?? config.output?.format ?? "json";
2217
2619
  const content = formatOutput(answers, format);
2218
- writeFileSync5(outputPath, content, "utf-8");
2620
+ writeFileSync6(outputPath, content, "utf-8");
2219
2621
  if (!opts.quiet) {
2220
2622
  console.log(`
2221
2623
  Answers written to: ${outputPath}
@@ -2276,6 +2678,7 @@ program.command("demo").description("Run a demo wizard showcasing all step types
2276
2678
  "..",
2277
2679
  "..",
2278
2680
  "examples",
2681
+ "yaml",
2279
2682
  "demo.yaml"
2280
2683
  );
2281
2684
  const config = await loadWizardConfig(demoPath);
@@ -2462,7 +2865,10 @@ function resolveRenderer(rendererName) {
2462
2865
  if (rendererName === "ink") {
2463
2866
  return new InkRenderer();
2464
2867
  }
2465
- throw new Error(`Unknown renderer: "${rendererName}". Supported: inquirer, ink`);
2868
+ if (rendererName === "clack") {
2869
+ return new ClackRenderer();
2870
+ }
2871
+ throw new Error(`Unknown renderer: "${rendererName}". Supported: inquirer, ink, clack`);
2466
2872
  }
2467
2873
  function toEnvKey(key) {
2468
2874
  return key.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();