grimoire-wizard 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -56,6 +56,11 @@ var separatorOptionSchema = z.object({
56
56
  separator: z.string()
57
57
  });
58
58
  var selectChoiceSchema = z.union([selectOptionSchema, separatorOptionSchema]);
59
+ var stepReviewConfigSchema = z.object({
60
+ hide: z.boolean().optional(),
61
+ label: z.string().optional(),
62
+ format: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional()
63
+ }).optional();
59
64
  var baseStepFields = {
60
65
  id: z.string(),
61
66
  message: z.string(),
@@ -64,7 +69,8 @@ var baseStepFields = {
64
69
  when: conditionSchema.optional(),
65
70
  keepValuesOnPrevious: z.boolean().optional(),
66
71
  required: z.boolean().optional(),
67
- group: z.string().optional()
72
+ group: z.string().optional(),
73
+ review: stepReviewConfigSchema
68
74
  };
69
75
  var textStepSchema = z.object({
70
76
  ...baseStepFields,
@@ -81,7 +87,8 @@ var selectStepSchema = z.object({
81
87
  default: z.string().optional(),
82
88
  routes: z.record(z.string(), z.string()).optional(),
83
89
  pageSize: z.number().int().positive().optional(),
84
- loop: z.boolean().optional()
90
+ loop: z.boolean().optional(),
91
+ columns: z.number().int().min(1).max(8).optional()
85
92
  });
86
93
  var multiSelectStepSchema = z.object({
87
94
  ...baseStepFields,
@@ -92,7 +99,8 @@ var multiSelectStepSchema = z.object({
92
99
  min: z.number().int().nonnegative().optional(),
93
100
  max: z.number().int().positive().optional(),
94
101
  pageSize: z.number().int().positive().optional(),
95
- loop: z.boolean().optional()
102
+ loop: z.boolean().optional(),
103
+ columns: z.number().int().min(1).max(8).optional()
96
104
  });
97
105
  var confirmStepSchema = z.object({
98
106
  ...baseStepFields,
@@ -120,7 +128,8 @@ var searchStepSchema = z.object({
120
128
  default: z.string().optional(),
121
129
  placeholder: z.string().optional(),
122
130
  pageSize: z.number().int().positive().optional(),
123
- loop: z.boolean().optional()
131
+ loop: z.boolean().optional(),
132
+ columns: z.number().int().min(1).max(8).optional()
124
133
  });
125
134
  var editorStepSchema = z.object({
126
135
  ...baseStepFields,
@@ -148,7 +157,14 @@ var messageStepSchema = z.object({
148
157
  });
149
158
  var noteStepSchema = z.object({
150
159
  ...baseStepFields,
151
- type: z.literal("note")
160
+ type: z.literal("note"),
161
+ style: z.enum(["info", "warning", "error", "success", "code", "banner"]).optional()
162
+ });
163
+ var browserStepSchema = z.object({
164
+ ...baseStepFields,
165
+ type: z.literal("browser"),
166
+ url: z.string(),
167
+ fallback: z.string().optional()
152
168
  });
153
169
  var stepConfigSchema = z.discriminatedUnion("type", [
154
170
  textStepSchema,
@@ -162,12 +178,19 @@ var stepConfigSchema = z.discriminatedUnion("type", [
162
178
  pathStepSchema,
163
179
  toggleStepSchema,
164
180
  messageStepSchema,
165
- noteStepSchema
181
+ noteStepSchema,
182
+ browserStepSchema
166
183
  ]);
167
184
  var hexColorSchema = z.string().regex(
168
185
  /^#[0-9a-fA-F]{6}$/,
169
186
  "Must be a 6-digit hex color (e.g., #FF0000)"
170
187
  );
188
+ var progressBarConfigSchema = z.object({
189
+ width: z.number().int().positive().max(200).optional(),
190
+ filledColor: hexColorSchema.optional(),
191
+ emptyColor: hexColorSchema.optional(),
192
+ style: z.enum(["blocks", "line", "dots", "arrow"]).optional()
193
+ }).optional();
171
194
  var themeConfigSchema = z.object({
172
195
  preset: z.enum(["default", "catppuccin", "dracula", "nord", "tokyonight", "monokai"]).optional(),
173
196
  tokens: z.object({
@@ -177,7 +200,12 @@ var themeConfigSchema = z.object({
177
200
  warning: hexColorSchema.optional(),
178
201
  info: hexColorSchema.optional(),
179
202
  muted: hexColorSchema.optional(),
180
- accent: hexColorSchema.optional()
203
+ accent: hexColorSchema.optional(),
204
+ highlight: hexColorSchema.optional(),
205
+ highlightBg: hexColorSchema.optional(),
206
+ pointer: hexColorSchema.optional(),
207
+ checked: hexColorSchema.optional(),
208
+ dimmed: hexColorSchema.optional()
181
209
  }).optional(),
182
210
  icons: z.object({
183
211
  step: z.string().optional(),
@@ -191,12 +219,15 @@ var themeConfigSchema = z.object({
191
219
  frames: z.array(z.string()).min(1),
192
220
  interval: z.number().positive().optional()
193
221
  })
194
- ]).optional()
222
+ ]).optional(),
223
+ spinnerElapsed: z.boolean().optional(),
224
+ progressBar: progressBarConfigSchema
195
225
  });
196
226
  var preFlightCheckSchema = z.object({
197
227
  name: z.string(),
198
228
  run: z.string(),
199
- message: z.string()
229
+ message: z.string(),
230
+ showOutput: z.boolean().optional()
200
231
  });
201
232
  var actionConfigSchema = z.object({
202
233
  name: z.string().optional(),
@@ -209,7 +240,13 @@ var wizardConfigSchema = z.object({
209
240
  version: z.string().optional(),
210
241
  description: z.string().optional(),
211
242
  review: z.boolean().optional(),
212
- icon: z.string().optional()
243
+ icon: z.string().optional(),
244
+ iconSize: z.union([z.enum(["small", "medium", "large"]), z.number().int().positive()]).optional(),
245
+ font: z.string().min(1).optional(),
246
+ banner: z.union([z.string(), z.function()]).optional(),
247
+ subtitle: z.string().optional(),
248
+ clearBetweenSteps: z.boolean().optional(),
249
+ checksStyle: z.enum(["spinner", "tasklist"]).optional()
213
250
  }),
214
251
  theme: themeConfigSchema.optional(),
215
252
  steps: z.array(stepConfigSchema).min(1),
@@ -490,11 +527,12 @@ async function loadWithInheritance(filePath, seen) {
490
527
  async function loadWizardConfig(filePath) {
491
528
  const config = await loadWithInheritance(filePath, /* @__PURE__ */ new Set());
492
529
  detectCycles(config);
530
+ config._configFilePath = resolve(filePath);
493
531
  return config;
494
532
  }
495
533
 
496
534
  // src/runner.ts
497
- import { execSync } from "child_process";
535
+ import { execSync, execFileSync } from "child_process";
498
536
  import { resolve as resolve2, dirname as dirname2 } from "path";
499
537
  import { pathToFileURL } from "url";
500
538
 
@@ -671,7 +709,8 @@ function wizardReducer(state, transition, config) {
671
709
  errors: { ...state.errors, [state.currentStepId]: validationError }
672
710
  };
673
711
  }
674
- const updatedAnswers = {
712
+ const isDisplayOnly = currentStep.type === "note" || currentStep.type === "message" || currentStep.type === "browser";
713
+ const updatedAnswers = isDisplayOnly ? { ...state.answers } : {
675
714
  ...state.answers,
676
715
  [state.currentStepId]: transition.value
677
716
  };
@@ -879,7 +918,12 @@ var DEFAULT_TOKENS = {
879
918
  warning: "#FFD93D",
880
919
  info: "#4D96FF",
881
920
  muted: "#888888",
882
- accent: "#C084FC"
921
+ accent: "#C084FC",
922
+ highlight: "#5B9BD5",
923
+ highlightBg: "#1E1E2E",
924
+ pointer: "#C084FC",
925
+ checked: "#6BCB77",
926
+ dimmed: "#555555"
883
927
  };
884
928
  var DEFAULT_ICONS = {
885
929
  step: "\u25CF",
@@ -887,6 +931,25 @@ var DEFAULT_ICONS = {
887
931
  stepPending: "\u25CB",
888
932
  pointer: "\u203A"
889
933
  };
934
+ var PROGRESS_BAR_CHARS = {
935
+ blocks: { filled: "\u2588", empty: "\u2591" },
936
+ line: { filled: "\u2500", empty: "\u2500" },
937
+ dots: { filled: "\u2022", empty: "\xB7" },
938
+ arrow: { filled: "\u2550", empty: "\u2500" }
939
+ };
940
+ function resolveProgressBar(config, tokens) {
941
+ const style = config?.style ?? "blocks";
942
+ const chars = PROGRESS_BAR_CHARS[style];
943
+ const filledHex = config?.filledColor ?? tokens?.success ?? DEFAULT_TOKENS.success;
944
+ const emptyHex = config?.emptyColor ?? tokens?.muted ?? DEFAULT_TOKENS.muted;
945
+ return {
946
+ width: config?.width ?? 20,
947
+ filledColor: chalk.hex(filledHex),
948
+ emptyColor: chalk.hex(emptyHex),
949
+ style,
950
+ chars
951
+ };
952
+ }
890
953
  function resolveTheme(themeConfig) {
891
954
  const presetTokens = themeConfig?.preset ? THEME_PRESETS[themeConfig.preset] : void 0;
892
955
  const tokens = { ...DEFAULT_TOKENS, ...presetTokens, ...themeConfig?.tokens };
@@ -899,9 +962,16 @@ function resolveTheme(themeConfig) {
899
962
  info: chalk.hex(tokens.info),
900
963
  muted: chalk.hex(tokens.muted),
901
964
  accent: chalk.hex(tokens.accent),
965
+ highlight: chalk.hex(tokens.highlight),
966
+ highlightBg: chalk.bgHex(tokens.highlightBg),
967
+ pointer: chalk.hex(tokens.pointer),
968
+ checked: chalk.hex(tokens.checked),
969
+ dimmed: chalk.hex(tokens.dimmed),
902
970
  bold: chalk.bold,
903
971
  icons,
904
- spinner: resolveSpinner(themeConfig?.spinner)
972
+ spinner: resolveSpinner(themeConfig?.spinner),
973
+ spinnerElapsed: themeConfig?.spinnerElapsed ?? false,
974
+ progressBar: resolveProgressBar(themeConfig?.progressBar, tokens)
905
975
  };
906
976
  }
907
977
 
@@ -919,11 +989,11 @@ import {
919
989
  } from "@inquirer/prompts";
920
990
  var InquirerRenderer = class {
921
991
  renderStepHeader(stepIndex, totalVisible, message, theme, description) {
922
- const barWidth = 20;
923
- const filledCount = totalVisible > 0 ? Math.round(stepIndex / totalVisible * barWidth) : 0;
924
- const remainingCount = barWidth - filledCount;
925
- const filledBar = theme.success("\u2588".repeat(filledCount));
926
- const remainingBar = theme.muted("\u2591".repeat(remainingCount));
992
+ const pb = theme.progressBar;
993
+ const filledCount = totalVisible > 0 ? Math.round(stepIndex / totalVisible * pb.width) : 0;
994
+ const remainingCount = pb.width - filledCount;
995
+ const filledBar = pb.filledColor(pb.chars.filled.repeat(filledCount));
996
+ const remainingBar = pb.emptyColor(pb.chars.empty.repeat(remainingCount));
927
997
  const counter = theme.muted(`Step ${String(stepIndex + 1)}/${String(totalVisible)}`);
928
998
  const stepMessage = theme.muted(`\u2014 ${message}`);
929
999
  console.log(`
@@ -932,6 +1002,39 @@ var InquirerRenderer = class {
932
1002
  console.log(` ${theme.muted(description)}`);
933
1003
  }
934
1004
  }
1005
+ renderNote(step, theme) {
1006
+ const title = step.message;
1007
+ const body = step.description ?? "";
1008
+ const style = step.style ?? "info";
1009
+ const { colorFn, icon } = getNoteStyleConfig(style, theme);
1010
+ if (style === "banner") {
1011
+ const width = Math.max(4, (process.stdout.columns ?? 60) - 4);
1012
+ const rule = colorFn("\u2500".repeat(width));
1013
+ console.log();
1014
+ console.log(` ${rule}`);
1015
+ console.log(` ${colorFn(icon)} ${theme.bold(title)}`);
1016
+ if (body) console.log(` ${theme.muted(body)}`);
1017
+ console.log(` ${rule}`);
1018
+ console.log();
1019
+ return;
1020
+ }
1021
+ const border = colorFn("\u2502");
1022
+ const topRule = colorFn(`\u250C${"\u2500".repeat(2)}`);
1023
+ const bottomRule = colorFn(`\u2514${"\u2500".repeat(2)}`);
1024
+ console.log();
1025
+ console.log(` ${topRule} ${colorFn(icon)} ${theme.bold(title)}`);
1026
+ if (body) {
1027
+ for (const line of body.split("\n")) {
1028
+ if (style === "code") {
1029
+ console.log(` ${border} ${theme.muted(line)}`);
1030
+ } else {
1031
+ console.log(` ${border} ${line}`);
1032
+ }
1033
+ }
1034
+ }
1035
+ console.log(` ${bottomRule}`);
1036
+ console.log();
1037
+ }
935
1038
  async renderText(step, state, theme) {
936
1039
  const existingAnswer = state.answers[step.id];
937
1040
  const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
@@ -944,7 +1047,7 @@ var InquirerRenderer = class {
944
1047
  async renderSelect(step, state, theme) {
945
1048
  const existingAnswer = state.answers[step.id];
946
1049
  const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
947
- const choices = step.options.map((opt) => {
1050
+ const rawChoices = step.options.map((opt) => {
948
1051
  if ("separator" in opt) {
949
1052
  return new Separator(opt.separator);
950
1053
  }
@@ -955,19 +1058,27 @@ var InquirerRenderer = class {
955
1058
  disabled: opt.disabled
956
1059
  };
957
1060
  });
1061
+ const itemChoices = rawChoices.filter((c) => !(c instanceof Separator));
1062
+ const columnedItems = step.columns ? applyColumns(itemChoices, step.columns) : itemChoices;
1063
+ let itemIdx = 0;
1064
+ const choices = rawChoices.map((c) => c instanceof Separator ? c : columnedItems[itemIdx++] ?? c);
958
1065
  return select({
959
1066
  message: step.message,
960
1067
  choices,
961
1068
  default: defaultValue,
962
1069
  pageSize: step.pageSize,
963
1070
  loop: step.loop,
964
- theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
1071
+ theme: {
1072
+ prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone },
1073
+ icon: { cursor: theme.icons.pointer },
1074
+ style: { disabled: theme.dimmed }
1075
+ }
965
1076
  });
966
1077
  }
967
1078
  async renderMultiSelect(step, state, theme) {
968
1079
  const existingAnswer = state.answers[step.id];
969
1080
  const previousSelections = Array.isArray(existingAnswer) ? existingAnswer.filter((v) => typeof v === "string") : step.default;
970
- const choices = step.options.map((opt) => {
1081
+ const rawChoices = step.options.map((opt) => {
971
1082
  if ("separator" in opt) {
972
1083
  return new Separator(opt.separator);
973
1084
  }
@@ -978,12 +1089,20 @@ var InquirerRenderer = class {
978
1089
  disabled: opt.disabled
979
1090
  };
980
1091
  });
1092
+ const itemChoices = rawChoices.filter((c) => !(c instanceof Separator));
1093
+ const columnedItems = step.columns ? applyColumns(itemChoices, step.columns) : itemChoices;
1094
+ let itemIdx = 0;
1095
+ const choices = rawChoices.map((c) => c instanceof Separator ? c : columnedItems[itemIdx++] ?? c);
981
1096
  return checkbox({
982
1097
  message: step.message,
983
1098
  choices,
984
1099
  pageSize: step.pageSize,
985
1100
  loop: step.loop,
986
- theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
1101
+ theme: {
1102
+ prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone },
1103
+ icon: { cursor: theme.icons.pointer, checked: theme.checked("\u2714"), unchecked: theme.muted("\u25CB") },
1104
+ style: { disabledChoice: theme.dimmed }
1105
+ }
987
1106
  });
988
1107
  }
989
1108
  async renderConfirm(step, state, theme) {
@@ -1015,15 +1134,17 @@ var InquirerRenderer = class {
1015
1134
  return result ?? defaultValue ?? 0;
1016
1135
  }
1017
1136
  async renderSearch(step, _state, theme) {
1137
+ const message = step.placeholder ? `${step.message} ${theme.muted(`(${step.placeholder})`)}` : step.message;
1018
1138
  return search({
1019
- message: step.message,
1139
+ message,
1020
1140
  source: (input4) => {
1021
1141
  const term = (input4 ?? "").toLowerCase();
1022
- return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(term)).map((opt) => ({
1142
+ const filtered = step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(term)).map((opt) => ({
1023
1143
  name: opt.label,
1024
1144
  value: opt.value,
1025
1145
  description: opt.hint
1026
1146
  }));
1147
+ return step.columns ? applyColumns(filtered, step.columns) : filtered;
1027
1148
  },
1028
1149
  pageSize: step.pageSize,
1029
1150
  theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
@@ -1091,6 +1212,42 @@ ${theme.muted("\u2500".repeat(40))}`);
1091
1212
  process.stdout.write("\x1B[2J\x1B[0f");
1092
1213
  }
1093
1214
  };
1215
+ var NOTE_STYLE_MAP = {
1216
+ info: { tokenKey: "info", icon: "\u2139" },
1217
+ warning: { tokenKey: "warning", icon: "\u26A0" },
1218
+ error: { tokenKey: "error", icon: "\u2716" },
1219
+ success: { tokenKey: "success", icon: "\u2714" },
1220
+ code: { tokenKey: "muted", icon: "\u276F" },
1221
+ banner: { tokenKey: "primary", icon: "\u2605" }
1222
+ };
1223
+ function getNoteStyleConfig(style, theme) {
1224
+ const entry = NOTE_STYLE_MAP[style];
1225
+ return {
1226
+ colorFn: theme[entry.tokenKey],
1227
+ icon: entry.icon
1228
+ };
1229
+ }
1230
+ function applyColumns(items, columns) {
1231
+ const cols = Math.max(1, Math.floor(columns));
1232
+ if (cols <= 1) return items;
1233
+ if (items.length === 0) return items;
1234
+ const maxLabel = Math.max(...items.map((i) => i.name.length));
1235
+ const colWidth = maxLabel + 4;
1236
+ const grouped = [];
1237
+ for (let i = 0; i < items.length; i += cols) {
1238
+ grouped.push(items.slice(i, i + cols));
1239
+ }
1240
+ const result = [];
1241
+ for (const row of grouped) {
1242
+ for (let c = 0; c < row.length; c++) {
1243
+ const item = row[c];
1244
+ const isLast = c === row.length - 1;
1245
+ const paddedName = isLast ? item.name : item.name.padEnd(colWidth);
1246
+ result.push({ ...item, name: paddedName });
1247
+ }
1248
+ }
1249
+ return result;
1250
+ }
1094
1251
 
1095
1252
  // src/resolve.ts
1096
1253
  function resolveEnvDefault(value) {
@@ -1145,19 +1302,71 @@ function renderBanner(name, theme, options) {
1145
1302
  const icon = options?.icon;
1146
1303
  const prefix = icon ? `${icon} ` : "";
1147
1304
  if (options?.plain) {
1148
- return ` ${prefix}${theme.bold(name)}`;
1305
+ const line = ` ${prefix}${theme.bold(name)}`;
1306
+ return options?.subtitle ? `${line}
1307
+ ${theme.muted(options.subtitle)}` : line;
1308
+ }
1309
+ if (options?.banner) {
1310
+ const bannerStr = (typeof options.banner === "function" ? options.banner(theme) : options.banner).replace(/\n$/, "");
1311
+ const raw = bannerStr.split("\n");
1312
+ const mid = Math.floor(raw.length / 2);
1313
+ const bannerLines = raw.map((l, i) => {
1314
+ if (icon && i === mid) return ` ${icon} ${l}`;
1315
+ return icon ? ` ${l}` : ` ${l}`;
1316
+ }).join("\n");
1317
+ const colored = typeof options.banner === "function" ? bannerLines : GRIMOIRE_GRADIENT(bannerLines);
1318
+ return options?.subtitle ? `${colored}
1319
+ ${theme.muted(options.subtitle)}` : colored;
1149
1320
  }
1150
1321
  try {
1151
- const art = figlet.textSync(name, {
1152
- font: "Small",
1153
- horizontalLayout: "default"
1154
- });
1155
- const lines = art.split("\n").map((line) => ` ${line}`).join("\n");
1156
- const banner = GRIMOIRE_GRADIENT(lines);
1157
- return icon ? ` ${icon}
1158
- ${banner}` : banner;
1322
+ const requestedFont = options?.font ?? "Small";
1323
+ let art;
1324
+ try {
1325
+ art = figlet.textSync(name, { font: requestedFont, horizontalLayout: "default" });
1326
+ } catch {
1327
+ art = figlet.textSync(name, { font: "Small", horizontalLayout: "default" });
1328
+ }
1329
+ const artLines = art.split("\n");
1330
+ const lines = artLines.map((line, i) => {
1331
+ const iconStr = getIconForLine(icon, options?.iconSize, i, artLines.length);
1332
+ if (iconStr !== void 0) {
1333
+ return ` ${iconStr}${line}`;
1334
+ }
1335
+ return icon ? ` ${line}` : ` ${line}`;
1336
+ }).join("\n");
1337
+ const colored = GRIMOIRE_GRADIENT(lines);
1338
+ return options?.subtitle ? `${colored}
1339
+ ${theme.muted(options.subtitle)}` : colored;
1159
1340
  } catch {
1160
- return ` ${prefix}${theme.bold(name)}`;
1341
+ const line = ` ${prefix}${theme.bold(name)}`;
1342
+ return options?.subtitle ? `${line}
1343
+ ${theme.muted(options.subtitle)}` : line;
1344
+ }
1345
+ }
1346
+ function getIconForLine(icon, iconSize, lineIndex, totalLines) {
1347
+ if (!icon) return void 0;
1348
+ const mid = Math.floor(totalLines / 2);
1349
+ const span = resolveIconSpan(iconSize, totalLines);
1350
+ const halfSpan = Math.floor(span / 2);
1351
+ const start = Math.max(0, mid - halfSpan);
1352
+ const end = Math.min(totalLines - 1, start + span - 1);
1353
+ if (lineIndex >= start && lineIndex <= end) {
1354
+ return lineIndex === mid ? `${icon} ` : ` `;
1355
+ }
1356
+ return void 0;
1357
+ }
1358
+ function resolveIconSpan(iconSize, totalLines) {
1359
+ if (typeof iconSize === "number") {
1360
+ const clamped = Math.max(1, Math.floor(isFinite(iconSize) ? iconSize : 1));
1361
+ return Math.min(clamped, totalLines);
1362
+ }
1363
+ switch (iconSize ?? "small") {
1364
+ case "small":
1365
+ return 1;
1366
+ case "medium":
1367
+ return 3;
1368
+ case "large":
1369
+ return totalLines;
1161
1370
  }
1162
1371
  }
1163
1372
 
@@ -1387,7 +1596,14 @@ function emitEvent(renderer, event, theme) {
1387
1596
  renderer.onEvent(event, theme);
1388
1597
  }
1389
1598
  }
1390
- function runPreFlightChecks(checks, theme, renderer) {
1599
+ function runPreFlightChecks(checks, theme, renderer, checksStyle) {
1600
+ if (checksStyle === "tasklist") {
1601
+ runPreFlightChecksTasklist(checks, theme, renderer);
1602
+ } else {
1603
+ runPreFlightChecksSpinner(checks, theme, renderer);
1604
+ }
1605
+ }
1606
+ function runPreFlightChecksSpinner(checks, theme, renderer) {
1391
1607
  if (renderer) emitEvent(renderer, { type: "checks:start", checks }, theme);
1392
1608
  for (const check of checks) {
1393
1609
  if (renderer) emitEvent(renderer, { type: "spinner:start", message: check.name }, theme);
@@ -1405,20 +1621,69 @@ function runPreFlightChecks(checks, theme, renderer) {
1405
1621
  }
1406
1622
  console.log();
1407
1623
  }
1624
+ function runPreFlightChecksTasklist(checks, theme, renderer) {
1625
+ if (!process.stdout.isTTY) {
1626
+ runPreFlightChecksSpinner(checks, theme, renderer);
1627
+ return;
1628
+ }
1629
+ if (renderer) emitEvent(renderer, { type: "checks:start", checks, mode: "tasklist" }, theme);
1630
+ const statuses = checks.map(() => "pending");
1631
+ const icons = theme.icons;
1632
+ function renderTasklist() {
1633
+ process.stdout.write(`\x1B[${checks.length}A\x1B[0G`);
1634
+ for (let i = 0; i < checks.length; i++) {
1635
+ const check = checks[i];
1636
+ const s = statuses[i];
1637
+ let icon;
1638
+ if (s === "pass") icon = theme.success(icons.stepDone);
1639
+ else if (s === "fail") icon = theme.error("\u2717");
1640
+ else if (s === "running") icon = theme.info("\u280B");
1641
+ else icon = theme.muted(icons.stepPending);
1642
+ process.stdout.write(` ${icon} ${check.name}\x1B[K
1643
+ `);
1644
+ }
1645
+ }
1646
+ for (let i = 0; i < checks.length; i++) {
1647
+ process.stdout.write(` ${theme.muted(icons.stepPending)} ${checks[i].name}
1648
+ `);
1649
+ }
1650
+ let failedCheck;
1651
+ for (let i = 0; i < checks.length; i++) {
1652
+ const check = checks[i];
1653
+ statuses[i] = "running";
1654
+ renderTasklist();
1655
+ try {
1656
+ execSync(check.run, { stdio: "pipe" });
1657
+ statuses[i] = "pass";
1658
+ renderTasklist();
1659
+ if (renderer) emitEvent(renderer, { type: "check:pass", name: check.name }, theme);
1660
+ } catch {
1661
+ statuses[i] = "fail";
1662
+ renderTasklist();
1663
+ failedCheck = check;
1664
+ if (renderer) emitEvent(renderer, { type: "check:fail", name: check.name, message: check.message }, theme);
1665
+ break;
1666
+ }
1667
+ }
1668
+ renderTasklist();
1669
+ console.log();
1670
+ if (failedCheck) {
1671
+ throw new Error(`Pre-flight check failed: ${failedCheck.name} \u2014 ${failedCheck.message}`);
1672
+ }
1673
+ }
1674
+ var MOCK_MISS = /* @__PURE__ */ Symbol("mock-miss");
1408
1675
  function getMockValue(step, mockAnswers) {
1409
1676
  if (step.id in mockAnswers) {
1410
1677
  return mockAnswers[step.id];
1411
1678
  }
1412
- if (step.type === "message" || step.type === "note") {
1679
+ if (step.type === "message" || step.type === "note" || step.type === "browser") {
1413
1680
  return true;
1414
1681
  }
1415
1682
  const defaultValue = getStepDefault(step);
1416
1683
  if (defaultValue !== void 0) {
1417
1684
  return defaultValue;
1418
1685
  }
1419
- throw new Error(
1420
- `Mock mode: no answer provided for step "${step.id}" and no default available`
1421
- );
1686
+ return MOCK_MISS;
1422
1687
  }
1423
1688
  function getStepDefault(step) {
1424
1689
  switch (step.type) {
@@ -1438,6 +1703,7 @@ function getStepDefault(step) {
1438
1703
  case "password":
1439
1704
  case "message":
1440
1705
  case "note":
1706
+ case "browser":
1441
1707
  return void 0;
1442
1708
  }
1443
1709
  }
@@ -1473,9 +1739,34 @@ async function runWizard(config, options) {
1473
1739
  registerPlugin(plugin);
1474
1740
  }
1475
1741
  }
1742
+ let cancelFired = false;
1743
+ const performCancel = async () => {
1744
+ if (cancelFired) return;
1745
+ cancelFired = true;
1746
+ state = wizardReducer(state, { type: "CANCEL" }, config);
1747
+ const passwordStepIds = config.steps.filter((s) => s.type === "password").map((s) => s.id);
1748
+ saveProgress(config.meta.name, {
1749
+ currentStepId: state.currentStepId,
1750
+ answers: state.answers,
1751
+ history: state.history
1752
+ }, void 0, passwordStepIds);
1753
+ try {
1754
+ await options?.onCancel?.(state);
1755
+ } catch {
1756
+ }
1757
+ emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: true }, theme);
1758
+ if (!quiet) console.log(theme.warning("\n Wizard cancelled.\n"));
1759
+ };
1760
+ const signalHandler = !isMock ? () => {
1761
+ performCancel().finally(() => process.exit(130));
1762
+ } : void 0;
1763
+ if (signalHandler) {
1764
+ process.once("SIGINT", signalHandler);
1765
+ process.once("SIGTERM", signalHandler);
1766
+ }
1476
1767
  try {
1477
1768
  if (!isMock && config.checks && config.checks.length > 0) {
1478
- runPreFlightChecks(config.checks, theme, renderer);
1769
+ runPreFlightChecks(config.checks, theme, renderer, config.meta.checksStyle);
1479
1770
  }
1480
1771
  if (!quiet) {
1481
1772
  printWizardHeader(config, theme, options?.plain);
@@ -1485,7 +1776,62 @@ async function runWizard(config, options) {
1485
1776
  let needsReview = true;
1486
1777
  while (needsReview) {
1487
1778
  let previousGroup;
1779
+ let stepsCompleted = 0;
1488
1780
  while (state.status === "running") {
1781
+ if (!isMock && config.meta.clearBetweenSteps && stepsCompleted > 0) {
1782
+ renderer.clear();
1783
+ if (!quiet) {
1784
+ printWizardHeader(config, theme, options?.plain);
1785
+ }
1786
+ previousGroup = void 0;
1787
+ }
1788
+ let nextStepOverride;
1789
+ const createHookContext = (stateOverride) => ({
1790
+ answers: { ...(stateOverride ?? state).answers },
1791
+ state: stateOverride ?? state,
1792
+ showNote: (title, body) => {
1793
+ emitEvent(renderer, { type: "note", title, body }, theme);
1794
+ if (!("onEvent" in renderer) && process.stdout.isTTY) {
1795
+ console.log(`
1796
+ ${theme.bold(title)}`);
1797
+ if (body) console.log(` ${theme.muted(body)}`);
1798
+ console.log();
1799
+ }
1800
+ },
1801
+ setNextStep: (stepId) => {
1802
+ nextStepOverride = stepId;
1803
+ },
1804
+ openBrowser: async (url) => {
1805
+ if (isMock) return;
1806
+ try {
1807
+ new URL(url);
1808
+ } catch {
1809
+ return;
1810
+ }
1811
+ if (process.platform === "darwin") {
1812
+ execFileSync("open", [url], { stdio: "ignore" });
1813
+ } else if (process.platform === "win32") {
1814
+ execFileSync("powershell", ["-NoProfile", "-Command", `Start-Process '${url.replace(/'/g, "''")}'`], { stdio: "ignore" });
1815
+ } else {
1816
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
1817
+ }
1818
+ },
1819
+ prompt: async (promptConfig) => {
1820
+ if (isMock) {
1821
+ if (promptConfig.default !== void 0) return promptConfig.default;
1822
+ throw new Error("Mock mode: context.prompt() requires a default value");
1823
+ }
1824
+ const contextState = stateOverride ?? state;
1825
+ const tempStep = {
1826
+ id: "__hook_prompt__",
1827
+ message: promptConfig.message,
1828
+ type: promptConfig.type,
1829
+ ...promptConfig.type === "select" ? { options: promptConfig.options } : {},
1830
+ default: promptConfig.default
1831
+ };
1832
+ return renderStep(renderer, tempStep, contextState, theme);
1833
+ }
1834
+ });
1489
1835
  const visibleSteps = getVisibleSteps(config, state.answers);
1490
1836
  const currentStep = config.steps.find((s) => s.id === state.currentStepId);
1491
1837
  if (!currentStep) {
@@ -1508,9 +1854,13 @@ async function runWizard(config, options) {
1508
1854
  emitEvent(renderer, { type: "step:start", stepId: currentStep.id, stepIndex, totalVisible: visibleSteps.length, step: currentStep }, theme);
1509
1855
  if (currentStep.type === "note") {
1510
1856
  emitEvent(renderer, { type: "note", title: resolvedMessage, body: resolvedDescription ?? "" }, theme);
1857
+ if (!isMock && "style" in currentStep && currentStep.style && "renderNote" in renderer && !("onEvent" in renderer)) {
1858
+ const resolvedStep2 = { ...currentStep, message: resolvedMessage, description: resolvedDescription };
1859
+ renderer.renderNote(resolvedStep2, theme);
1860
+ }
1511
1861
  }
1512
1862
  if (options?.onBeforeStep) {
1513
- await options.onBeforeStep(currentStep.id, currentStep, state);
1863
+ await options.onBeforeStep(currentStep.id, currentStep, createHookContext());
1514
1864
  }
1515
1865
  const pluginStep = getPluginStep(currentStep.type);
1516
1866
  const resolvedStep = pluginStep ? currentStep : resolveStepDefaults(currentStep, cachedAnswers);
@@ -1527,7 +1877,19 @@ async function runWizard(config, options) {
1527
1877
  }
1528
1878
  }
1529
1879
  try {
1530
- const value = isMock ? getMockValue(finalStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(finalStep), state, theme) : await renderStep(renderer, finalStep, state, theme);
1880
+ let value;
1881
+ if (isMock) {
1882
+ const mockResult = getMockValue(finalStep, mockAnswers);
1883
+ if (mockResult === MOCK_MISS) {
1884
+ throw new Error(
1885
+ `Mock mode: no answer provided for step "${finalStep.id}" and no default available`
1886
+ );
1887
+ } else {
1888
+ value = mockResult;
1889
+ }
1890
+ } else {
1891
+ value = pluginStep ? await pluginStep.render(toStepRecord(finalStep), state, theme) : await renderStep(renderer, finalStep, state, theme);
1892
+ }
1531
1893
  if (pluginStep?.validate) {
1532
1894
  const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
1533
1895
  if (pluginError) {
@@ -1568,28 +1930,31 @@ async function runWizard(config, options) {
1568
1930
  }
1569
1931
  }
1570
1932
  if (options?.onAfterStep) {
1571
- await options.onAfterStep(currentStep.id, value, nextState);
1933
+ await options.onAfterStep(currentStep.id, value, createHookContext(nextState));
1934
+ }
1935
+ if (nextStepOverride) {
1936
+ if (nextStepOverride === "__done__") {
1937
+ state = { ...nextState, status: "done" };
1938
+ } else {
1939
+ const targetExists = config.steps.some((s) => s.id === nextStepOverride);
1940
+ if (!targetExists) {
1941
+ throw new Error(`setNextStep: step "${nextStepOverride}" does not exist`);
1942
+ }
1943
+ state = { ...nextState, status: "running", currentStepId: nextStepOverride };
1944
+ }
1945
+ nextStepOverride = void 0;
1946
+ } else {
1947
+ state = nextState;
1572
1948
  }
1573
- state = nextState;
1574
1949
  emitEvent(renderer, { type: "step:complete", stepId: currentStep.id, value, step: currentStep }, theme);
1575
1950
  if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1576
1951
  recordSelection(config.meta.name, currentStep.id, value);
1577
1952
  }
1578
1953
  options?.onStepComplete?.(currentStep.id, value, state);
1954
+ stepsCompleted++;
1579
1955
  } catch (error) {
1580
1956
  if (!isMock && isUserCancel(error)) {
1581
- state = wizardReducer(state, { type: "CANCEL" }, config);
1582
- options?.onCancel?.(state);
1583
- const passwordStepIds = config.steps.filter((s) => s.type === "password").map((s) => s.id);
1584
- saveProgress(config.meta.name, {
1585
- currentStepId: state.currentStepId,
1586
- answers: state.answers,
1587
- history: state.history
1588
- }, void 0, passwordStepIds);
1589
- emitEvent(renderer, { type: "session:end", answers: state.answers, cancelled: true }, theme);
1590
- if (!quiet) {
1591
- console.log(theme.warning("\n Wizard cancelled.\n"));
1592
- }
1957
+ await performCancel();
1593
1958
  return state.answers;
1594
1959
  }
1595
1960
  throw error;
@@ -1598,10 +1963,14 @@ async function runWizard(config, options) {
1598
1963
  if (config.meta.review && !isMock && state.status === "done") {
1599
1964
  const reviewLines = [];
1600
1965
  for (const step of config.steps) {
1966
+ if (step.review?.hide) continue;
1967
+ if (step.type === "note" || step.type === "message") continue;
1601
1968
  const answer = state.answers[step.id];
1602
1969
  if (answer === void 0) continue;
1603
- const display = step.type === "password" ? "****" : Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
1604
- reviewLines.push(`${step.id}: ${display}`);
1970
+ const label = step.review?.label ?? step.id;
1971
+ const raw = step.type === "password" ? "****" : Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
1972
+ const display = formatReviewValue(raw, step.review?.format);
1973
+ reviewLines.push(`${label}: ${display}`);
1605
1974
  }
1606
1975
  emitEvent(renderer, { type: "note", title: "Review your answers", body: reviewLines.join("\n") }, theme);
1607
1976
  console.log(`
@@ -1621,12 +1990,16 @@ async function runWizard(config, options) {
1621
1990
  } else {
1622
1991
  const { select: selectPrompt } = await import("@inquirer/prompts");
1623
1992
  const stepsWithAnswers = config.steps.filter(
1624
- (s) => state.answers[s.id] !== void 0 && s.type !== "note" && s.type !== "message"
1993
+ (s) => state.answers[s.id] !== void 0 && s.type !== "note" && s.type !== "message" && !s.review?.hide
1625
1994
  );
1995
+ if (stepsWithAnswers.length === 0) {
1996
+ needsReview = false;
1997
+ break;
1998
+ }
1626
1999
  const stepToRevisit = await selectPrompt({
1627
2000
  message: "Which step would you like to change?",
1628
2001
  choices: stepsWithAnswers.map((s) => ({
1629
- name: `${s.id}: ${s.type === "password" ? "****" : String(state.answers[s.id] ?? "")}`,
2002
+ name: `${s.review?.label ?? s.id}: ${s.type === "password" ? "****" : String(state.answers[s.id] ?? "")}`,
1630
2003
  value: s.id
1631
2004
  }))
1632
2005
  });
@@ -1669,6 +2042,10 @@ async function runWizard(config, options) {
1669
2042
  }
1670
2043
  return state.answers;
1671
2044
  } finally {
2045
+ if (signalHandler) {
2046
+ process.removeListener("SIGINT", signalHandler);
2047
+ process.removeListener("SIGTERM", signalHandler);
2048
+ }
1672
2049
  if (userPlugins) {
1673
2050
  clearPlugins();
1674
2051
  }
@@ -1708,6 +2085,18 @@ function renderStep(renderer, step, state, theme) {
1708
2085
  return Promise.resolve(true);
1709
2086
  case "note":
1710
2087
  return Promise.resolve(true);
2088
+ case "browser": {
2089
+ const resolvedUrl = resolveTemplate(step.url, state.answers);
2090
+ console.log(`
2091
+ ${theme.info("\u2192")} ${step.message}`);
2092
+ console.log(` ${theme.muted(resolvedUrl)}`);
2093
+ openUrl(resolvedUrl);
2094
+ if (step.fallback) {
2095
+ console.log(` ${theme.muted(step.fallback)}`);
2096
+ }
2097
+ console.log();
2098
+ return Promise.resolve(true);
2099
+ }
1711
2100
  }
1712
2101
  }
1713
2102
  function resolveStepDefaults(step, cachedAnswers) {
@@ -1759,6 +2148,7 @@ function resolveStepDefaults(step, cachedAnswers) {
1759
2148
  case "password":
1760
2149
  case "message":
1761
2150
  case "note":
2151
+ case "browser":
1762
2152
  return step;
1763
2153
  }
1764
2154
  }
@@ -1768,7 +2158,7 @@ function getCachedDefault(stepId, cachedAnswers) {
1768
2158
  }
1769
2159
  function applyTemplateDefaults(step, templateAnswers) {
1770
2160
  if (!(step.id in templateAnswers)) return step;
1771
- if (step.type === "password" || step.type === "message" || step.type === "note") return step;
2161
+ if (step.type === "password" || step.type === "message" || step.type === "note" || step.type === "browser") return step;
1772
2162
  const value = templateAnswers[step.id];
1773
2163
  switch (step.type) {
1774
2164
  case "text":
@@ -1858,6 +2248,7 @@ function resolveStepTemplates(step, answers) {
1858
2248
  case "toggle":
1859
2249
  case "message":
1860
2250
  case "note":
2251
+ case "browser":
1861
2252
  return {
1862
2253
  ...step,
1863
2254
  description: step.description ? resolveTemplate(step.description, answers) : void 0
@@ -1915,18 +2306,50 @@ async function executeActions(actions, answers, theme, renderer) {
1915
2306
  }
1916
2307
  function printWizardHeader(config, theme, plain) {
1917
2308
  console.log();
1918
- console.log(renderBanner(config.meta.name, theme, { plain, icon: config.meta.icon }));
2309
+ console.log(renderBanner(config.meta.name, theme, {
2310
+ plain,
2311
+ icon: config.meta.icon,
2312
+ iconSize: config.meta.iconSize,
2313
+ font: config.meta.font,
2314
+ banner: config.meta.banner,
2315
+ subtitle: config.meta.subtitle
2316
+ }));
1919
2317
  if (config.meta.description) {
1920
2318
  console.log(` ${theme.muted(config.meta.description)}`);
1921
2319
  }
1922
2320
  console.log();
1923
2321
  }
2322
+ function openUrl(url) {
2323
+ try {
2324
+ if (process.platform === "darwin") {
2325
+ execFileSync("open", [url], { stdio: "ignore" });
2326
+ } else if (process.platform === "win32") {
2327
+ const safeUrl = url.replace(/'/g, "''");
2328
+ execFileSync("powershell", ["-NoProfile", "-Command", `Start-Process '${safeUrl}'`], { stdio: "ignore" });
2329
+ } else {
2330
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
2331
+ }
2332
+ } catch {
2333
+ }
2334
+ }
1924
2335
  function isUserCancel(error) {
1925
2336
  if (error instanceof Error) {
1926
2337
  return error.message.includes("User force closed") || error.name === "ExitPromptError";
1927
2338
  }
1928
2339
  return false;
1929
2340
  }
2341
+ function formatReviewValue(value, format) {
2342
+ switch (format) {
2343
+ case "uppercase":
2344
+ return value.toUpperCase();
2345
+ case "lowercase":
2346
+ return value.toLowerCase();
2347
+ case "capitalize":
2348
+ return value.charAt(0).toUpperCase() + value.slice(1);
2349
+ default:
2350
+ return value;
2351
+ }
2352
+ }
1930
2353
 
1931
2354
  // src/scaffolder.ts
1932
2355
  import { input as input2, select as select2, confirm as confirm2, number as number2 } from "@inquirer/prompts";
@@ -2396,8 +2819,9 @@ var InkRenderer = class {
2396
2819
  return result ?? defaultValue ?? 0;
2397
2820
  }
2398
2821
  async renderSearch(step, _state, theme) {
2822
+ const message = step.placeholder ? `${step.message} ${theme.muted(`(${step.placeholder})`)}` : step.message;
2399
2823
  return search2({
2400
- message: step.message,
2824
+ message,
2401
2825
  source: (term) => {
2402
2826
  const query = (term ?? "").toLowerCase();
2403
2827
  return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(query)).map((opt) => ({
@@ -2501,6 +2925,8 @@ var S_BAR_H = u("\u2500", "-");
2501
2925
  var ClackRenderer = class extends InquirerRenderer {
2502
2926
  spinnerInterval;
2503
2927
  spinnerFrameIndex = 0;
2928
+ spinnerStartTime = 0;
2929
+ checksMode = void 0;
2504
2930
  renderStepHeader() {
2505
2931
  }
2506
2932
  renderGroupHeader() {
@@ -2557,18 +2983,25 @@ var ClackRenderer = class extends InquirerRenderer {
2557
2983
  this.stopSpinner(event.message, theme);
2558
2984
  break;
2559
2985
  case "checks:start":
2560
- process.stdout.write(`${chalk2.gray(S_BAR)}
2986
+ this.checksMode = event.mode === "tasklist" ? "tasklist" : "spinner";
2987
+ if (this.checksMode !== "tasklist") {
2988
+ process.stdout.write(`${chalk2.gray(S_BAR)}
2561
2989
  `);
2562
- process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running checks...")}
2990
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.bold("Running checks...")}
2563
2991
  `);
2992
+ }
2564
2993
  break;
2565
2994
  case "check:pass":
2566
- process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2995
+ if (this.checksMode !== "tasklist") {
2996
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${event.name}
2567
2997
  `);
2998
+ }
2568
2999
  break;
2569
3000
  case "check:fail":
2570
- process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}: ${event.message}
3001
+ if (this.checksMode !== "tasklist") {
3002
+ process.stdout.write(`${chalk2.gray(S_BAR)} ${theme.error(S_STEP_ERROR)} ${event.name}: ${event.message}
2571
3003
  `);
3004
+ }
2572
3005
  break;
2573
3006
  case "actions:start":
2574
3007
  process.stdout.write(`${chalk2.gray(S_BAR)}
@@ -2635,11 +3068,17 @@ var ClackRenderer = class extends InquirerRenderer {
2635
3068
  `);
2636
3069
  }
2637
3070
  startSpinner(message, theme) {
3071
+ if (this.spinnerInterval) {
3072
+ clearInterval(this.spinnerInterval);
3073
+ this.spinnerInterval = void 0;
3074
+ }
2638
3075
  this.spinnerFrameIndex = 0;
3076
+ this.spinnerStartTime = Date.now();
2639
3077
  const { frames, interval } = theme.spinner;
2640
3078
  this.spinnerInterval = setInterval(() => {
2641
3079
  const frame = frames[this.spinnerFrameIndex % frames.length];
2642
- process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}`);
3080
+ const elapsed = theme.spinnerElapsed ? ` ${chalk2.gray(`(${((Date.now() - this.spinnerStartTime) / 1e3).toFixed(1)}s)`)}` : "";
3081
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${chalk2.cyan(frame ?? "")} ${message}${elapsed}`);
2643
3082
  this.spinnerFrameIndex++;
2644
3083
  }, interval);
2645
3084
  }
@@ -2649,7 +3088,7 @@ var ClackRenderer = class extends InquirerRenderer {
2649
3088
  this.spinnerInterval = void 0;
2650
3089
  }
2651
3090
  const finalMessage = message ?? "Done";
2652
- process.stdout.write(`\r${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${finalMessage}
3091
+ process.stdout.write(`\r${chalk2.gray(S_BAR)} ${theme.success(S_STEP_SUBMIT)} ${finalMessage}\x1B[K
2653
3092
  `);
2654
3093
  }
2655
3094
  };