grimoire-wizard 0.3.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 ADDED
@@ -0,0 +1,2485 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import chalk2 from "chalk";
5
+ import { program } from "commander";
6
+
7
+ // src/parser.ts
8
+ import { cosmiconfig } from "cosmiconfig";
9
+ import { readFileSync } from "fs";
10
+ import { dirname, resolve, isAbsolute } from "path";
11
+ import { parse as parseYAML } from "yaml";
12
+
13
+ // src/schema.ts
14
+ import { z } from "zod";
15
+ var conditionSchema = z.lazy(() => {
16
+ const fieldEquals = z.object({ field: z.string(), equals: z.unknown() }).strict();
17
+ const fieldNotEquals = z.object({ field: z.string(), notEquals: z.unknown() }).strict();
18
+ const fieldIncludes = z.object({ field: z.string(), includes: z.unknown() }).strict();
19
+ const fieldNotIncludes = z.object({ field: z.string(), notIncludes: z.unknown() }).strict();
20
+ const fieldGreaterThan = z.object({ field: z.string(), greaterThan: z.number() }).strict();
21
+ const fieldLessThan = z.object({ field: z.string(), lessThan: z.number() }).strict();
22
+ const fieldIsEmpty = z.object({ field: z.string(), isEmpty: z.literal(true) }).strict();
23
+ const fieldIsNotEmpty = z.object({ field: z.string(), isNotEmpty: z.literal(true) }).strict();
24
+ const allCondition = z.object({ all: z.array(conditionSchema) }).strict();
25
+ const anyCondition = z.object({ any: z.array(conditionSchema) }).strict();
26
+ const notCondition = z.object({ not: conditionSchema }).strict();
27
+ return z.union([
28
+ fieldEquals,
29
+ fieldNotEquals,
30
+ fieldIncludes,
31
+ fieldNotIncludes,
32
+ fieldGreaterThan,
33
+ fieldLessThan,
34
+ fieldIsEmpty,
35
+ fieldIsNotEmpty,
36
+ allCondition,
37
+ anyCondition,
38
+ notCondition
39
+ ]);
40
+ });
41
+ var validationRuleSchema = z.discriminatedUnion("rule", [
42
+ z.object({ rule: z.literal("required"), message: z.string().optional() }),
43
+ z.object({ rule: z.literal("minLength"), value: z.number(), message: z.string().optional() }),
44
+ z.object({ rule: z.literal("maxLength"), value: z.number(), message: z.string().optional() }),
45
+ z.object({ rule: z.literal("pattern"), value: z.string(), message: z.string().optional() }),
46
+ z.object({ rule: z.literal("min"), value: z.number(), message: z.string().optional() }),
47
+ z.object({ rule: z.literal("max"), value: z.number(), message: z.string().optional() })
48
+ ]);
49
+ var selectOptionSchema = z.object({
50
+ value: z.string(),
51
+ label: z.string(),
52
+ hint: z.string().optional(),
53
+ disabled: z.union([z.boolean(), z.string()]).optional()
54
+ });
55
+ var separatorOptionSchema = z.object({
56
+ separator: z.string()
57
+ });
58
+ var selectChoiceSchema = z.union([selectOptionSchema, separatorOptionSchema]);
59
+ var baseStepFields = {
60
+ id: z.string(),
61
+ message: z.string(),
62
+ description: z.string().optional(),
63
+ next: z.string().optional(),
64
+ when: conditionSchema.optional(),
65
+ keepValuesOnPrevious: z.boolean().optional(),
66
+ required: z.boolean().optional(),
67
+ group: z.string().optional()
68
+ };
69
+ var textStepSchema = z.object({
70
+ ...baseStepFields,
71
+ type: z.literal("text"),
72
+ placeholder: z.string().optional(),
73
+ default: z.string().optional(),
74
+ validate: z.array(validationRuleSchema).optional()
75
+ });
76
+ var selectStepSchema = z.object({
77
+ ...baseStepFields,
78
+ type: z.literal("select"),
79
+ options: z.array(selectChoiceSchema).min(1).optional(),
80
+ optionsFrom: z.string().optional(),
81
+ default: z.string().optional(),
82
+ routes: z.record(z.string(), z.string()).optional(),
83
+ pageSize: z.number().int().positive().optional(),
84
+ loop: z.boolean().optional()
85
+ });
86
+ var multiSelectStepSchema = z.object({
87
+ ...baseStepFields,
88
+ type: z.literal("multiselect"),
89
+ options: z.array(selectChoiceSchema).min(1).optional(),
90
+ optionsFrom: z.string().optional(),
91
+ default: z.array(z.string()).optional(),
92
+ min: z.number().int().nonnegative().optional(),
93
+ max: z.number().int().positive().optional(),
94
+ pageSize: z.number().int().positive().optional(),
95
+ loop: z.boolean().optional()
96
+ });
97
+ var confirmStepSchema = z.object({
98
+ ...baseStepFields,
99
+ type: z.literal("confirm"),
100
+ default: z.boolean().optional()
101
+ });
102
+ var passwordStepSchema = z.object({
103
+ ...baseStepFields,
104
+ type: z.literal("password"),
105
+ validate: z.array(validationRuleSchema).optional()
106
+ });
107
+ var numberStepSchema = z.object({
108
+ ...baseStepFields,
109
+ type: z.literal("number"),
110
+ default: z.number().optional(),
111
+ min: z.number().optional(),
112
+ max: z.number().optional(),
113
+ step: z.number().positive().optional()
114
+ });
115
+ var searchStepSchema = z.object({
116
+ ...baseStepFields,
117
+ type: z.literal("search"),
118
+ options: z.array(selectChoiceSchema).min(1).optional(),
119
+ optionsFrom: z.string().optional(),
120
+ default: z.string().optional(),
121
+ placeholder: z.string().optional(),
122
+ pageSize: z.number().int().positive().optional(),
123
+ loop: z.boolean().optional()
124
+ });
125
+ var editorStepSchema = z.object({
126
+ ...baseStepFields,
127
+ type: z.literal("editor"),
128
+ default: z.string().optional(),
129
+ validate: z.array(validationRuleSchema).optional()
130
+ });
131
+ var pathStepSchema = z.object({
132
+ ...baseStepFields,
133
+ type: z.literal("path"),
134
+ default: z.string().optional(),
135
+ placeholder: z.string().optional(),
136
+ validate: z.array(validationRuleSchema).optional()
137
+ });
138
+ var toggleStepSchema = z.object({
139
+ ...baseStepFields,
140
+ type: z.literal("toggle"),
141
+ default: z.boolean().optional(),
142
+ active: z.string().optional(),
143
+ inactive: z.string().optional()
144
+ });
145
+ var messageStepSchema = z.object({
146
+ ...baseStepFields,
147
+ type: z.literal("message")
148
+ });
149
+ var stepConfigSchema = z.discriminatedUnion("type", [
150
+ textStepSchema,
151
+ selectStepSchema,
152
+ multiSelectStepSchema,
153
+ confirmStepSchema,
154
+ passwordStepSchema,
155
+ numberStepSchema,
156
+ searchStepSchema,
157
+ editorStepSchema,
158
+ pathStepSchema,
159
+ toggleStepSchema,
160
+ messageStepSchema
161
+ ]);
162
+ var hexColorSchema = z.string().regex(
163
+ /^#[0-9a-fA-F]{6}$/,
164
+ "Must be a 6-digit hex color (e.g., #FF0000)"
165
+ );
166
+ var themeConfigSchema = z.object({
167
+ tokens: z.object({
168
+ primary: hexColorSchema.optional(),
169
+ success: hexColorSchema.optional(),
170
+ error: hexColorSchema.optional(),
171
+ warning: hexColorSchema.optional(),
172
+ info: hexColorSchema.optional(),
173
+ muted: hexColorSchema.optional(),
174
+ accent: hexColorSchema.optional()
175
+ }).optional(),
176
+ icons: z.object({
177
+ step: z.string().optional(),
178
+ stepDone: z.string().optional(),
179
+ stepPending: z.string().optional(),
180
+ pointer: z.string().optional()
181
+ }).optional()
182
+ });
183
+ var preFlightCheckSchema = z.object({
184
+ name: z.string(),
185
+ run: z.string(),
186
+ message: z.string()
187
+ });
188
+ var actionConfigSchema = z.object({
189
+ name: z.string().optional(),
190
+ run: z.string(),
191
+ when: conditionSchema.optional()
192
+ });
193
+ var wizardConfigSchema = z.object({
194
+ meta: z.object({
195
+ name: z.string(),
196
+ version: z.string().optional(),
197
+ description: z.string().optional()
198
+ }),
199
+ theme: themeConfigSchema.optional(),
200
+ steps: z.array(stepConfigSchema).min(1),
201
+ output: z.object({
202
+ format: z.enum(["json", "env", "yaml"]),
203
+ path: z.string().optional()
204
+ }).optional(),
205
+ extends: z.string().optional(),
206
+ checks: z.array(preFlightCheckSchema).optional(),
207
+ actions: z.array(actionConfigSchema).optional()
208
+ }).superRefine((config, ctx) => {
209
+ const stepIds = /* @__PURE__ */ new Set();
210
+ for (const step of config.steps) {
211
+ if (stepIds.has(step.id)) {
212
+ ctx.addIssue({
213
+ code: z.ZodIssueCode.custom,
214
+ message: `Duplicate step ID: "${step.id}"`,
215
+ path: ["steps"]
216
+ });
217
+ }
218
+ stepIds.add(step.id);
219
+ }
220
+ config.steps.forEach((step, i) => {
221
+ if (step.next && step.next !== "__done__" && !stepIds.has(step.next)) {
222
+ ctx.addIssue({
223
+ code: z.ZodIssueCode.custom,
224
+ message: `Step "${step.id}" references unknown next step: "${step.next}"`,
225
+ path: ["steps", i, "next"]
226
+ });
227
+ }
228
+ if (step.type === "select" && step.routes) {
229
+ for (const [key, target] of Object.entries(step.routes)) {
230
+ if (target !== "__done__" && !stepIds.has(target)) {
231
+ ctx.addIssue({
232
+ code: z.ZodIssueCode.custom,
233
+ message: `Step "${step.id}" route "${key}" references unknown step: "${target}"`,
234
+ path: ["steps", i, "routes", key]
235
+ });
236
+ }
237
+ }
238
+ }
239
+ if (step.when) {
240
+ collectConditionFieldIssues(step.when, stepIds, ctx, ["steps", i, "when"]);
241
+ }
242
+ if (step.type === "select" && step.routes && step.options) {
243
+ const optionValues = /* @__PURE__ */ new Set();
244
+ for (const o of step.options) {
245
+ if ("value" in o) {
246
+ optionValues.add(o.value);
247
+ }
248
+ }
249
+ for (const routeKey of Object.keys(step.routes)) {
250
+ if (!optionValues.has(routeKey)) {
251
+ ctx.addIssue({
252
+ code: z.ZodIssueCode.custom,
253
+ message: `Step "${step.id}" route key "${routeKey}" does not match any option value`,
254
+ path: ["steps", i, "routes", routeKey]
255
+ });
256
+ }
257
+ }
258
+ }
259
+ if (step.type === "select" || step.type === "multiselect" || step.type === "search") {
260
+ const hasOptions = step.options !== void 0;
261
+ const hasOptionsFrom = step.optionsFrom !== void 0;
262
+ if (hasOptions && hasOptionsFrom) {
263
+ ctx.addIssue({
264
+ code: z.ZodIssueCode.custom,
265
+ message: `Step "${step.id}" has both "options" and "optionsFrom" \u2014 only one is allowed`,
266
+ path: ["steps", i]
267
+ });
268
+ }
269
+ if (!hasOptions && !hasOptionsFrom) {
270
+ ctx.addIssue({
271
+ code: z.ZodIssueCode.custom,
272
+ message: `Step "${step.id}" must have either "options" or "optionsFrom"`,
273
+ path: ["steps", i]
274
+ });
275
+ }
276
+ }
277
+ if ((step.type === "number" || step.type === "multiselect") && step.min !== void 0 && step.max !== void 0 && step.min > step.max) {
278
+ ctx.addIssue({
279
+ code: z.ZodIssueCode.custom,
280
+ message: `Step "${step.id}" has min (${String(step.min)}) greater than max (${String(step.max)})`,
281
+ path: ["steps", i]
282
+ });
283
+ }
284
+ });
285
+ if (config.actions) {
286
+ config.actions.forEach((action, i) => {
287
+ if (action.when) {
288
+ collectConditionFieldIssues(action.when, stepIds, ctx, ["actions", i, "when"]);
289
+ }
290
+ });
291
+ }
292
+ });
293
+ function collectConditionFieldIssues(condition, validIds, ctx, path) {
294
+ if ("field" in condition) {
295
+ const fieldRoot = condition.field.split(".")[0];
296
+ if (fieldRoot && !validIds.has(fieldRoot)) {
297
+ ctx.addIssue({
298
+ code: z.ZodIssueCode.custom,
299
+ message: `Condition references unknown step ID: "${fieldRoot}"`,
300
+ path
301
+ });
302
+ }
303
+ return;
304
+ }
305
+ if ("all" in condition) {
306
+ condition.all.forEach((child, i) => {
307
+ collectConditionFieldIssues(child, validIds, ctx, [...path, "all", i]);
308
+ });
309
+ return;
310
+ }
311
+ if ("any" in condition) {
312
+ condition.any.forEach((child, i) => {
313
+ collectConditionFieldIssues(child, validIds, ctx, [...path, "any", i]);
314
+ });
315
+ return;
316
+ }
317
+ if ("not" in condition) {
318
+ collectConditionFieldIssues(condition.not, validIds, ctx, [...path, "not"]);
319
+ }
320
+ }
321
+ function parseWizardConfig(raw) {
322
+ const result = wizardConfigSchema.parse(raw);
323
+ return result;
324
+ }
325
+
326
+ // src/parser.ts
327
+ var DONE_SENTINEL = "__done__";
328
+ function buildStepGraph(steps) {
329
+ const graph = /* @__PURE__ */ new Map();
330
+ for (let i = 0; i < steps.length; i++) {
331
+ const step = steps[i];
332
+ const edges = [];
333
+ if (step.next && step.next !== DONE_SENTINEL) {
334
+ edges.push(step.next);
335
+ }
336
+ if (step.type === "select" && step.routes) {
337
+ for (const target of Object.values(step.routes)) {
338
+ if (target !== DONE_SENTINEL) {
339
+ edges.push(target);
340
+ }
341
+ }
342
+ }
343
+ if (!step.next && !(step.type === "select" && step.routes)) {
344
+ const nextStep = steps[i + 1];
345
+ if (nextStep) {
346
+ edges.push(nextStep.id);
347
+ }
348
+ }
349
+ graph.set(step.id, edges);
350
+ }
351
+ return graph;
352
+ }
353
+ function detectCycles(config) {
354
+ const graph = buildStepGraph(config.steps);
355
+ const UNVISITED = 0;
356
+ const IN_STACK = 1;
357
+ const DONE = 2;
358
+ const nodeState = /* @__PURE__ */ new Map();
359
+ for (const id of graph.keys()) {
360
+ nodeState.set(id, UNVISITED);
361
+ }
362
+ function dfs(nodeId, path) {
363
+ nodeState.set(nodeId, IN_STACK);
364
+ const currentPath = [...path, nodeId];
365
+ for (const neighbor of graph.get(nodeId) ?? []) {
366
+ const state = nodeState.get(neighbor);
367
+ if (state === IN_STACK) {
368
+ const cycleStart = currentPath.indexOf(neighbor);
369
+ const cycle = [...currentPath.slice(cycleStart), neighbor];
370
+ throw new Error(`Cycle detected in wizard steps: ${cycle.join(" \u2192 ")}`);
371
+ }
372
+ if (state === UNVISITED) {
373
+ dfs(neighbor, currentPath);
374
+ }
375
+ }
376
+ nodeState.set(nodeId, DONE);
377
+ }
378
+ for (const id of graph.keys()) {
379
+ if (nodeState.get(id) === UNVISITED) {
380
+ dfs(id, []);
381
+ }
382
+ }
383
+ }
384
+ function deepMergeTheme(parent, child) {
385
+ if (!parent && !child) return void 0;
386
+ if (!parent) return child;
387
+ if (!child) return parent;
388
+ return {
389
+ tokens: { ...parent.tokens, ...child.tokens },
390
+ icons: { ...parent.icons, ...child.icons }
391
+ };
392
+ }
393
+ function mergeConfigs(parent, child) {
394
+ return {
395
+ meta: { ...parent.meta, ...child.meta },
396
+ theme: deepMergeTheme(parent.theme, child.theme),
397
+ steps: child.steps,
398
+ output: child.output ?? parent.output,
399
+ checks: [
400
+ ...parent.checks ?? [],
401
+ ...child.checks ?? []
402
+ ],
403
+ actions: child.actions ?? parent.actions
404
+ };
405
+ }
406
+ var OPTION_STEP_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "search"]);
407
+ function resolveOptionsFromSteps(raw, configDir) {
408
+ const steps = raw["steps"];
409
+ if (!Array.isArray(steps)) return;
410
+ for (const step of steps) {
411
+ if (typeof step !== "object" || step === null) continue;
412
+ const stepObj = step;
413
+ const optionsFrom = stepObj["optionsFrom"];
414
+ if (typeof optionsFrom !== "string") continue;
415
+ const stepId = String(stepObj["id"] ?? "unknown");
416
+ const stepType = String(stepObj["type"] ?? "unknown");
417
+ if (!OPTION_STEP_TYPES.has(stepType)) {
418
+ throw new Error(
419
+ `Step "${stepId}" has "optionsFrom" but type "${stepType}" does not support dynamic options`
420
+ );
421
+ }
422
+ const fullPath = isAbsolute(optionsFrom) ? optionsFrom : resolve(configDir, optionsFrom);
423
+ let content;
424
+ try {
425
+ content = readFileSync(fullPath, "utf-8");
426
+ } catch {
427
+ throw new Error(
428
+ `Step "${stepId}": failed to read optionsFrom file "${fullPath}"`
429
+ );
430
+ }
431
+ let parsed;
432
+ try {
433
+ if (fullPath.endsWith(".yaml") || fullPath.endsWith(".yml")) {
434
+ parsed = parseYAML(content);
435
+ } else {
436
+ parsed = JSON.parse(content);
437
+ }
438
+ } catch {
439
+ throw new Error(
440
+ `Step "${stepId}": optionsFrom file "${fullPath}" contains invalid JSON/YAML`
441
+ );
442
+ }
443
+ if (!Array.isArray(parsed)) {
444
+ throw new Error(
445
+ `Step "${stepId}": optionsFrom file "${fullPath}" must contain an array`
446
+ );
447
+ }
448
+ stepObj["options"] = parsed;
449
+ delete stepObj["optionsFrom"];
450
+ }
451
+ }
452
+ async function loadWithInheritance(filePath, seen) {
453
+ const resolvedPath = resolve(filePath);
454
+ if (seen.has(resolvedPath)) {
455
+ throw new Error(`Circular extends detected: "${resolvedPath}" was already loaded`);
456
+ }
457
+ seen.add(resolvedPath);
458
+ const explorer = cosmiconfig("grimoire");
459
+ const result = await explorer.load(resolvedPath);
460
+ if (!result || result.isEmpty) {
461
+ throw new Error(`No configuration found at: ${resolvedPath}`);
462
+ }
463
+ const raw = result.config;
464
+ const extendsPath = typeof raw["extends"] === "string" ? raw["extends"] : void 0;
465
+ resolveOptionsFromSteps(raw, dirname(resolvedPath));
466
+ const config = parseWizardConfig(raw);
467
+ if (!extendsPath) {
468
+ return config;
469
+ }
470
+ const parentPath = isAbsolute(extendsPath) ? extendsPath : resolve(dirname(resolvedPath), extendsPath);
471
+ const parentConfig = await loadWithInheritance(parentPath, seen);
472
+ return mergeConfigs(parentConfig, config);
473
+ }
474
+ async function loadWizardConfig(filePath) {
475
+ const config = await loadWithInheritance(filePath, /* @__PURE__ */ new Set());
476
+ detectCycles(config);
477
+ return config;
478
+ }
479
+
480
+ // src/runner.ts
481
+ import { execSync } from "child_process";
482
+
483
+ // src/conditions.ts
484
+ function isRecord(value) {
485
+ return value !== null && typeof value === "object" && !Array.isArray(value);
486
+ }
487
+ function getValueByPath(obj, path) {
488
+ const segments = path.split(".");
489
+ let current = obj;
490
+ for (const segment of segments) {
491
+ if (!isRecord(current)) {
492
+ return void 0;
493
+ }
494
+ current = current[segment];
495
+ }
496
+ return current;
497
+ }
498
+ function evaluateCondition(condition, answers) {
499
+ if ("all" in condition) {
500
+ return condition.all.every((c) => evaluateCondition(c, answers));
501
+ }
502
+ if ("any" in condition) {
503
+ return condition.any.some((c) => evaluateCondition(c, answers));
504
+ }
505
+ if ("not" in condition) {
506
+ return !evaluateCondition(condition.not, answers);
507
+ }
508
+ const value = getValueByPath(answers, condition.field);
509
+ if ("isEmpty" in condition) {
510
+ if (value === void 0 || value === null) return true;
511
+ if (typeof value === "string") return value.length === 0;
512
+ if (Array.isArray(value)) return value.length === 0;
513
+ return false;
514
+ }
515
+ if ("isNotEmpty" in condition) {
516
+ if (value === void 0 || value === null) return false;
517
+ if (typeof value === "string") return value.length > 0;
518
+ if (Array.isArray(value)) return value.length > 0;
519
+ return true;
520
+ }
521
+ if (value === void 0 || value === null) {
522
+ return false;
523
+ }
524
+ if ("equals" in condition) {
525
+ return value === condition.equals;
526
+ }
527
+ if ("notEquals" in condition) {
528
+ return value !== condition.notEquals;
529
+ }
530
+ if ("includes" in condition) {
531
+ if (Array.isArray(value)) {
532
+ return value.includes(condition.includes);
533
+ }
534
+ if (typeof value === "string" && typeof condition.includes === "string") {
535
+ return value.includes(condition.includes);
536
+ }
537
+ return false;
538
+ }
539
+ if ("notIncludes" in condition) {
540
+ if (Array.isArray(value)) {
541
+ return !value.includes(condition.notIncludes);
542
+ }
543
+ if (typeof value === "string" && typeof condition.notIncludes === "string") {
544
+ return !value.includes(condition.notIncludes);
545
+ }
546
+ return false;
547
+ }
548
+ if ("greaterThan" in condition) {
549
+ return typeof value === "number" && value > condition.greaterThan;
550
+ }
551
+ if ("lessThan" in condition) {
552
+ return typeof value === "number" && value < condition.lessThan;
553
+ }
554
+ return false;
555
+ }
556
+ function isStepVisible(step, answers) {
557
+ if (!step.when) {
558
+ return true;
559
+ }
560
+ return evaluateCondition(step.when, answers);
561
+ }
562
+
563
+ // src/engine.ts
564
+ function createWizardState(config) {
565
+ const firstVisible = config.steps.find((s) => isStepVisible(s, {}));
566
+ if (!firstVisible) {
567
+ throw new Error("No visible steps in wizard configuration");
568
+ }
569
+ return {
570
+ currentStepId: firstVisible.id,
571
+ answers: {},
572
+ history: [],
573
+ status: "running",
574
+ errors: {}
575
+ };
576
+ }
577
+ function validateStepAnswer(step, value) {
578
+ if (step.type === "message") {
579
+ return null;
580
+ }
581
+ const isRequired = step.required !== false;
582
+ if (isRequired) {
583
+ if (value === void 0 || value === null || value === "") {
584
+ return "This field is required";
585
+ }
586
+ if (Array.isArray(value) && value.length === 0) {
587
+ return "This field is required";
588
+ }
589
+ }
590
+ if ((step.type === "text" || step.type === "password" || step.type === "editor" || step.type === "path") && step.validate) {
591
+ const strValue = typeof value === "string" ? value : String(value ?? "");
592
+ for (const rule of step.validate) {
593
+ const error = applyValidationRule(rule, strValue);
594
+ if (error) return error;
595
+ }
596
+ }
597
+ if (step.type === "number" && typeof value === "number") {
598
+ if (step.min !== void 0 && value < step.min) {
599
+ return `Must be at least ${String(step.min)}`;
600
+ }
601
+ if (step.max !== void 0 && value > step.max) {
602
+ return `Must be at most ${String(step.max)}`;
603
+ }
604
+ }
605
+ if (step.type === "multiselect" && Array.isArray(value)) {
606
+ if (step.min !== void 0 && value.length < step.min) {
607
+ return `Select at least ${String(step.min)} option${step.min === 1 ? "" : "s"}`;
608
+ }
609
+ if (step.max !== void 0 && value.length > step.max) {
610
+ return `Select at most ${String(step.max)} option${step.max === 1 ? "" : "s"}`;
611
+ }
612
+ }
613
+ return null;
614
+ }
615
+ function resolveNextStep(config, currentStep, answer, answers) {
616
+ let targetId;
617
+ if (currentStep.type === "select" && currentStep.routes) {
618
+ const route = currentStep.routes[String(answer)];
619
+ if (route === "__done__") return "__done__";
620
+ if (route) targetId = route;
621
+ }
622
+ if (!targetId && currentStep.next) {
623
+ if (currentStep.next === "__done__") return "__done__";
624
+ targetId = currentStep.next;
625
+ }
626
+ if (!targetId) {
627
+ const currentIndex = config.steps.findIndex((s) => s.id === currentStep.id);
628
+ const nextInArray = config.steps[currentIndex + 1];
629
+ if (!nextInArray) return "__done__";
630
+ targetId = nextInArray.id;
631
+ }
632
+ const targetIndex = config.steps.findIndex((s) => s.id === targetId);
633
+ if (targetIndex < 0) return "__done__";
634
+ for (let i = targetIndex; i < config.steps.length; i++) {
635
+ const step = config.steps[i];
636
+ if (step && isStepVisible(step, answers)) {
637
+ return step.id;
638
+ }
639
+ }
640
+ return "__done__";
641
+ }
642
+ function getVisibleSteps(config, answers) {
643
+ return config.steps.filter((s) => isStepVisible(s, answers));
644
+ }
645
+ function wizardReducer(state, transition, config) {
646
+ switch (transition.type) {
647
+ case "NEXT": {
648
+ const currentStep = findStepOrThrow(config, state.currentStepId);
649
+ const validationError = validateStepAnswer(currentStep, transition.value);
650
+ if (validationError) {
651
+ return {
652
+ ...state,
653
+ errors: { ...state.errors, [state.currentStepId]: validationError }
654
+ };
655
+ }
656
+ const updatedAnswers = {
657
+ ...state.answers,
658
+ [state.currentStepId]: transition.value
659
+ };
660
+ const nextStepId = resolveNextStep(
661
+ config,
662
+ currentStep,
663
+ transition.value,
664
+ updatedAnswers
665
+ );
666
+ if (nextStepId === "__done__") {
667
+ return {
668
+ ...state,
669
+ answers: updatedAnswers,
670
+ history: [...state.history, state.currentStepId],
671
+ status: "done",
672
+ errors: {}
673
+ };
674
+ }
675
+ const finalAnswers = cleanOrphanedAnswers(
676
+ config,
677
+ updatedAnswers,
678
+ state.currentStepId,
679
+ nextStepId
680
+ );
681
+ return {
682
+ ...state,
683
+ currentStepId: nextStepId,
684
+ answers: finalAnswers,
685
+ history: [...state.history, state.currentStepId],
686
+ status: "running",
687
+ errors: {}
688
+ };
689
+ }
690
+ case "BACK": {
691
+ if (state.history.length === 0) {
692
+ return state;
693
+ }
694
+ const previousStepId = state.history[state.history.length - 1];
695
+ const currentStep = config.steps.find((s) => s.id === state.currentStepId);
696
+ const newAnswers = { ...state.answers };
697
+ if (currentStep && currentStep.keepValuesOnPrevious === false) {
698
+ delete newAnswers[currentStep.id];
699
+ }
700
+ return {
701
+ ...state,
702
+ currentStepId: previousStepId,
703
+ answers: newAnswers,
704
+ history: state.history.slice(0, -1),
705
+ status: "running",
706
+ errors: {}
707
+ };
708
+ }
709
+ case "JUMP": {
710
+ findStepOrThrow(config, transition.stepId);
711
+ return {
712
+ ...state,
713
+ currentStepId: transition.stepId,
714
+ history: [...state.history, state.currentStepId],
715
+ status: "running",
716
+ errors: {}
717
+ };
718
+ }
719
+ case "CANCEL": {
720
+ return { ...state, status: "cancelled" };
721
+ }
722
+ }
723
+ }
724
+ function findStepOrThrow(config, stepId) {
725
+ const step = config.steps.find((s) => s.id === stepId);
726
+ if (!step) {
727
+ throw new Error(`Step not found: "${stepId}"`);
728
+ }
729
+ return step;
730
+ }
731
+ function cleanOrphanedAnswers(config, answers, _fromStepId, _toStepId) {
732
+ const cleaned = { ...answers };
733
+ for (const step of config.steps) {
734
+ if (step.id in cleaned && !isStepVisible(step, cleaned)) {
735
+ delete cleaned[step.id];
736
+ }
737
+ }
738
+ return cleaned;
739
+ }
740
+ function applyValidationRule(rule, value) {
741
+ switch (rule.rule) {
742
+ case "required":
743
+ return !value.trim() ? rule.message ?? "This field is required" : null;
744
+ case "minLength":
745
+ return value.length < rule.value ? rule.message ?? `Must be at least ${String(rule.value)} characters` : null;
746
+ case "maxLength":
747
+ return value.length > rule.value ? rule.message ?? `Must be at most ${String(rule.value)} characters` : null;
748
+ case "pattern": {
749
+ const regex = new RegExp(rule.value);
750
+ return !regex.test(value) ? rule.message ?? `Must match pattern: ${rule.value}` : null;
751
+ }
752
+ case "min":
753
+ case "max":
754
+ default:
755
+ return null;
756
+ }
757
+ }
758
+
759
+ // src/theme.ts
760
+ import chalk from "chalk";
761
+ var DEFAULT_TOKENS = {
762
+ primary: "#5B9BD5",
763
+ success: "#6BCB77",
764
+ error: "#FF6B6B",
765
+ warning: "#FFD93D",
766
+ info: "#4D96FF",
767
+ muted: "#888888",
768
+ accent: "#C084FC"
769
+ };
770
+ var DEFAULT_ICONS = {
771
+ step: "\u25CF",
772
+ stepDone: "\u2713",
773
+ stepPending: "\u25CB",
774
+ pointer: "\u203A"
775
+ };
776
+ function resolveTheme(themeConfig) {
777
+ const tokens = { ...DEFAULT_TOKENS, ...themeConfig?.tokens };
778
+ const icons = { ...DEFAULT_ICONS, ...themeConfig?.icons };
779
+ return {
780
+ primary: chalk.hex(tokens.primary),
781
+ success: chalk.hex(tokens.success),
782
+ error: chalk.hex(tokens.error),
783
+ warning: chalk.hex(tokens.warning),
784
+ info: chalk.hex(tokens.info),
785
+ muted: chalk.hex(tokens.muted),
786
+ accent: chalk.hex(tokens.accent),
787
+ bold: chalk.bold,
788
+ icons
789
+ };
790
+ }
791
+
792
+ // src/renderers/inquirer.ts
793
+ import {
794
+ input,
795
+ select,
796
+ checkbox,
797
+ confirm,
798
+ password,
799
+ number,
800
+ search,
801
+ editor,
802
+ Separator
803
+ } from "@inquirer/prompts";
804
+ var InquirerRenderer = class {
805
+ renderStepHeader(stepIndex, totalVisible, message, theme, description) {
806
+ const barWidth = 20;
807
+ const filledCount = totalVisible > 0 ? Math.round(stepIndex / totalVisible * barWidth) : 0;
808
+ const remainingCount = barWidth - filledCount;
809
+ const filledBar = theme.success("\u2588".repeat(filledCount));
810
+ const remainingBar = theme.muted("\u2591".repeat(remainingCount));
811
+ const counter = theme.muted(`Step ${String(stepIndex + 1)}/${String(totalVisible)}`);
812
+ const stepMessage = theme.muted(`\u2014 ${message}`);
813
+ console.log(`
814
+ [${filledBar}${remainingBar}] ${counter} ${stepMessage}`);
815
+ if (description) {
816
+ console.log(` ${theme.muted(description)}`);
817
+ }
818
+ }
819
+ async renderText(step, state, theme) {
820
+ const existingAnswer = state.answers[step.id];
821
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
822
+ return input({
823
+ message: step.message,
824
+ default: defaultValue,
825
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
826
+ });
827
+ }
828
+ async renderSelect(step, state, theme) {
829
+ const existingAnswer = state.answers[step.id];
830
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
831
+ const choices = step.options.map((opt) => {
832
+ if ("separator" in opt) {
833
+ return new Separator(opt.separator);
834
+ }
835
+ return {
836
+ name: opt.label,
837
+ value: opt.value,
838
+ description: opt.hint,
839
+ disabled: opt.disabled
840
+ };
841
+ });
842
+ return select({
843
+ message: step.message,
844
+ choices,
845
+ default: defaultValue,
846
+ pageSize: step.pageSize,
847
+ loop: step.loop,
848
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
849
+ });
850
+ }
851
+ async renderMultiSelect(step, state, theme) {
852
+ const existingAnswer = state.answers[step.id];
853
+ const previousSelections = Array.isArray(existingAnswer) ? existingAnswer.filter((v) => typeof v === "string") : step.default;
854
+ const choices = step.options.map((opt) => {
855
+ if ("separator" in opt) {
856
+ return new Separator(opt.separator);
857
+ }
858
+ return {
859
+ name: opt.label,
860
+ value: opt.value,
861
+ checked: previousSelections?.includes(opt.value) ?? false,
862
+ disabled: opt.disabled
863
+ };
864
+ });
865
+ return checkbox({
866
+ message: step.message,
867
+ choices,
868
+ pageSize: step.pageSize,
869
+ loop: step.loop,
870
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
871
+ });
872
+ }
873
+ async renderConfirm(step, state, theme) {
874
+ const existingAnswer = state.answers[step.id];
875
+ const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer : step.default;
876
+ return confirm({
877
+ message: step.message,
878
+ default: defaultValue ?? true,
879
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
880
+ });
881
+ }
882
+ async renderPassword(step, _state, theme) {
883
+ return password({
884
+ message: step.message,
885
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
886
+ });
887
+ }
888
+ async renderNumber(step, state, theme) {
889
+ const existingAnswer = state.answers[step.id];
890
+ const defaultValue = typeof existingAnswer === "number" ? existingAnswer : step.default;
891
+ const result = await number({
892
+ message: step.message,
893
+ default: defaultValue,
894
+ min: step.min,
895
+ max: step.max,
896
+ step: step.step,
897
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
898
+ });
899
+ return result ?? defaultValue ?? 0;
900
+ }
901
+ async renderSearch(step, _state, theme) {
902
+ return search({
903
+ message: step.message,
904
+ source: (input4) => {
905
+ const term = (input4 ?? "").toLowerCase();
906
+ return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(term)).map((opt) => ({
907
+ name: opt.label,
908
+ value: opt.value,
909
+ description: opt.hint
910
+ }));
911
+ },
912
+ pageSize: step.pageSize,
913
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
914
+ });
915
+ }
916
+ async renderEditor(step, _state, theme) {
917
+ return editor({
918
+ message: step.message,
919
+ default: step.default,
920
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
921
+ });
922
+ }
923
+ async renderPath(step, state, theme) {
924
+ const existingAnswer = state.answers[step.id];
925
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
926
+ return input({
927
+ message: step.message,
928
+ default: defaultValue,
929
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
930
+ });
931
+ }
932
+ async renderToggle(step, state, theme) {
933
+ const existingAnswer = state.answers[step.id];
934
+ const activeLabel = step.active ?? "On";
935
+ const inactiveLabel = step.inactive ?? "Off";
936
+ const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer ? activeLabel : inactiveLabel : step.default === true ? activeLabel : inactiveLabel;
937
+ const result = await select({
938
+ message: step.message,
939
+ choices: [
940
+ { name: activeLabel, value: activeLabel },
941
+ { name: inactiveLabel, value: inactiveLabel }
942
+ ],
943
+ default: defaultValue,
944
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
945
+ });
946
+ return result === activeLabel;
947
+ }
948
+ renderMessage(step, _state, theme) {
949
+ if (step.description) {
950
+ console.log(` ${theme.muted(step.description)}`);
951
+ }
952
+ console.log();
953
+ }
954
+ renderGroupHeader(group, theme) {
955
+ console.log(`
956
+ ${theme.accent("\u2500\u2500")} ${theme.bold(group)} ${theme.accent("\u2500\u2500")}
957
+ `);
958
+ }
959
+ renderSummary(answers, steps, theme) {
960
+ console.log(`
961
+ ${theme.muted("\u2500".repeat(40))}`);
962
+ console.log(` ${theme.bold("Summary")}
963
+ `);
964
+ for (const step of steps) {
965
+ const answer = answers[step.id];
966
+ if (answer === void 0) continue;
967
+ const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
968
+ console.log(
969
+ ` ${theme.muted(step.id.padEnd(20))} ${theme.primary(display)}`
970
+ );
971
+ }
972
+ console.log(theme.muted("\u2500".repeat(40)));
973
+ }
974
+ clear() {
975
+ process.stdout.write("\x1B[2J\x1B[0f");
976
+ }
977
+ };
978
+
979
+ // src/resolve.ts
980
+ function resolveEnvDefault(value) {
981
+ if (typeof value !== "string") return value;
982
+ if (!value.startsWith("$")) return value;
983
+ const envKey = value.slice(1);
984
+ return process.env[envKey] ?? value;
985
+ }
986
+ function resolveEnvDefaultNumber(value) {
987
+ if (typeof value === "number") return value;
988
+ const resolved = resolveEnvDefault(typeof value === "string" ? value : void 0);
989
+ if (resolved === void 0) return void 0;
990
+ const num = Number(resolved);
991
+ return Number.isNaN(num) ? void 0 : num;
992
+ }
993
+ function resolveEnvDefaultBoolean(value) {
994
+ if (typeof value === "boolean") return value;
995
+ const resolved = resolveEnvDefault(typeof value === "string" ? value : void 0);
996
+ if (resolved === void 0) return void 0;
997
+ return resolved === "true" || resolved === "1";
998
+ }
999
+
1000
+ // src/template.ts
1001
+ function resolveTemplate(template, answers) {
1002
+ return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
1003
+ const trimmedKey = key.trim();
1004
+ if (trimmedKey in answers) {
1005
+ const value = answers[trimmedKey];
1006
+ if (Array.isArray(value)) return value.join(", ");
1007
+ return String(value);
1008
+ }
1009
+ return _match;
1010
+ });
1011
+ }
1012
+
1013
+ // src/banner.ts
1014
+ import figlet from "figlet";
1015
+ import gradient from "gradient-string";
1016
+ var GRIMOIRE_GRADIENT = gradient(["#C084FC", "#5B9BD5", "#6BCB77"]);
1017
+ function renderBanner(name, theme, options) {
1018
+ if (options?.plain) {
1019
+ return ` ${theme.bold(name)}`;
1020
+ }
1021
+ try {
1022
+ const art = figlet.textSync(name, {
1023
+ font: "Small",
1024
+ horizontalLayout: "default"
1025
+ });
1026
+ const lines = art.split("\n").map((line) => ` ${line}`).join("\n");
1027
+ return GRIMOIRE_GRADIENT(lines);
1028
+ } catch {
1029
+ return ` ${theme.bold(name)}`;
1030
+ }
1031
+ }
1032
+
1033
+ // src/plugins.ts
1034
+ var BUILT_IN_STEP_TYPES = /* @__PURE__ */ new Set([
1035
+ "text",
1036
+ "select",
1037
+ "multiselect",
1038
+ "confirm",
1039
+ "password",
1040
+ "number",
1041
+ "search",
1042
+ "editor",
1043
+ "path",
1044
+ "toggle"
1045
+ ]);
1046
+ var pluginStepRegistry = /* @__PURE__ */ new Map();
1047
+ function registerPlugin(plugin) {
1048
+ for (const [stepType, stepPlugin] of Object.entries(plugin.steps)) {
1049
+ if (BUILT_IN_STEP_TYPES.has(stepType)) {
1050
+ throw new Error(`Cannot override built-in step type "${stepType}"`);
1051
+ }
1052
+ if (pluginStepRegistry.has(stepType)) {
1053
+ throw new Error(`Step type "${stepType}" is already registered`);
1054
+ }
1055
+ pluginStepRegistry.set(stepType, stepPlugin);
1056
+ }
1057
+ }
1058
+ function getPluginStep(stepType) {
1059
+ return pluginStepRegistry.get(stepType);
1060
+ }
1061
+ function clearPlugins() {
1062
+ pluginStepRegistry.clear();
1063
+ }
1064
+
1065
+ // src/cache.ts
1066
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, unlinkSync, readdirSync } from "fs";
1067
+ import { join } from "path";
1068
+ import { homedir } from "os";
1069
+ var DEFAULT_CACHE_DIR = join(homedir(), ".config", "grimoire", "cache");
1070
+ function slugify(name) {
1071
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1072
+ }
1073
+ function getCacheDir(customDir) {
1074
+ return customDir ?? DEFAULT_CACHE_DIR;
1075
+ }
1076
+ function getCacheFilePath(wizardName, customDir) {
1077
+ return join(getCacheDir(customDir), `${slugify(wizardName)}.json`);
1078
+ }
1079
+ function loadCachedAnswers(wizardName, customDir) {
1080
+ try {
1081
+ const filePath = getCacheFilePath(wizardName, customDir);
1082
+ const raw = readFileSync2(filePath, "utf-8");
1083
+ const parsed = JSON.parse(raw);
1084
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1085
+ return parsed;
1086
+ }
1087
+ return void 0;
1088
+ } catch {
1089
+ return void 0;
1090
+ }
1091
+ }
1092
+ function saveCachedAnswers(wizardName, answers, customDir) {
1093
+ try {
1094
+ const dir = getCacheDir(customDir);
1095
+ mkdirSync(dir, { recursive: true });
1096
+ const filePath = getCacheFilePath(wizardName, customDir);
1097
+ writeFileSync(filePath, JSON.stringify(answers, null, 2) + "\n", "utf-8");
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ function clearCache(wizardName, customDir) {
1102
+ try {
1103
+ const dir = getCacheDir(customDir);
1104
+ if (wizardName) {
1105
+ const filePath = getCacheFilePath(wizardName, customDir);
1106
+ unlinkSync(filePath);
1107
+ } else {
1108
+ const files = readdirSync(dir);
1109
+ for (const file of files) {
1110
+ if (file.endsWith(".json")) {
1111
+ unlinkSync(join(dir, file));
1112
+ }
1113
+ }
1114
+ }
1115
+ } catch {
1116
+ }
1117
+ }
1118
+
1119
+ // src/mru.ts
1120
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
1121
+ import { join as join2 } from "path";
1122
+ import { homedir as homedir2 } from "os";
1123
+ var MRU_DIR = join2(homedir2(), ".config", "grimoire", "mru");
1124
+ function getMruFilePath(wizardName) {
1125
+ const safeName = wizardName.replace(/[^a-zA-Z0-9_-]/g, "_");
1126
+ return join2(MRU_DIR, `${safeName}.json`);
1127
+ }
1128
+ function loadMruData(wizardName) {
1129
+ const filePath = getMruFilePath(wizardName);
1130
+ try {
1131
+ if (!existsSync(filePath)) {
1132
+ return {};
1133
+ }
1134
+ const raw = readFileSync3(filePath, "utf-8");
1135
+ const parsed = JSON.parse(raw);
1136
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1137
+ return {};
1138
+ }
1139
+ return parsed;
1140
+ } catch {
1141
+ return {};
1142
+ }
1143
+ }
1144
+ function saveMruData(wizardName, data) {
1145
+ const filePath = getMruFilePath(wizardName);
1146
+ try {
1147
+ mkdirSync2(MRU_DIR, { recursive: true });
1148
+ writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ function recordSelection(wizardName, stepId, value) {
1153
+ const data = loadMruData(wizardName);
1154
+ const stepData = data[stepId] ?? {};
1155
+ const values = Array.isArray(value) ? value : [value];
1156
+ for (const v of values) {
1157
+ stepData[v] = (stepData[v] ?? 0) + 1;
1158
+ }
1159
+ data[stepId] = stepData;
1160
+ saveMruData(wizardName, data);
1161
+ }
1162
+ function getOrderedOptions(wizardName, stepId, options) {
1163
+ const data = loadMruData(wizardName);
1164
+ const stepData = data[stepId] ?? {};
1165
+ if (Object.keys(stepData).length === 0) {
1166
+ return options;
1167
+ }
1168
+ const separatorIndices = /* @__PURE__ */ new Map();
1169
+ const selectableOptions = [];
1170
+ for (let i = 0; i < options.length; i++) {
1171
+ const opt = options[i];
1172
+ if ("separator" in opt) {
1173
+ separatorIndices.set(i, opt);
1174
+ } else {
1175
+ selectableOptions.push(opt);
1176
+ }
1177
+ }
1178
+ const sorted = [...selectableOptions].sort((a, b) => {
1179
+ if (!("value" in a) || !("value" in b)) return 0;
1180
+ const countA = stepData[a.value] ?? 0;
1181
+ const countB = stepData[b.value] ?? 0;
1182
+ return countB - countA;
1183
+ });
1184
+ const result = [];
1185
+ let sortedIndex = 0;
1186
+ for (let i = 0; i < options.length; i++) {
1187
+ const sep = separatorIndices.get(i);
1188
+ if (sep) {
1189
+ result.push(sep);
1190
+ } else {
1191
+ const next = sorted[sortedIndex];
1192
+ if (next) {
1193
+ result.push(next);
1194
+ }
1195
+ sortedIndex++;
1196
+ }
1197
+ }
1198
+ return result;
1199
+ }
1200
+
1201
+ // src/runner.ts
1202
+ function runPreFlightChecks(checks, theme) {
1203
+ for (const check of checks) {
1204
+ try {
1205
+ execSync(check.run, { stdio: "pipe" });
1206
+ console.log(` ${theme.success("\u2713")} ${check.name}`);
1207
+ } catch {
1208
+ console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
1209
+ throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
1210
+ }
1211
+ }
1212
+ console.log();
1213
+ }
1214
+ function getMockValue(step, mockAnswers) {
1215
+ if (step.id in mockAnswers) {
1216
+ return mockAnswers[step.id];
1217
+ }
1218
+ if (step.type === "message") {
1219
+ return true;
1220
+ }
1221
+ const defaultValue = getStepDefault(step);
1222
+ if (defaultValue !== void 0) {
1223
+ return defaultValue;
1224
+ }
1225
+ throw new Error(
1226
+ `Mock mode: no answer provided for step "${step.id}" and no default available`
1227
+ );
1228
+ }
1229
+ function getStepDefault(step) {
1230
+ switch (step.type) {
1231
+ case "text":
1232
+ case "select":
1233
+ case "search":
1234
+ case "editor":
1235
+ case "path":
1236
+ return step.default;
1237
+ case "number":
1238
+ return step.default;
1239
+ case "confirm":
1240
+ case "toggle":
1241
+ return step.default;
1242
+ case "multiselect":
1243
+ return step.default;
1244
+ case "password":
1245
+ case "message":
1246
+ return void 0;
1247
+ }
1248
+ }
1249
+ async function runWizard(config, options) {
1250
+ const renderer = options?.renderer ?? new InquirerRenderer();
1251
+ const theme = resolveTheme(config.theme);
1252
+ const mockAnswers = options?.mockAnswers;
1253
+ const isMock = mockAnswers !== void 0;
1254
+ const quiet = options?.quiet ?? isMock;
1255
+ const cacheEnabled = !isMock && options?.cache !== false;
1256
+ const cacheDir = typeof options?.cache === "object" ? options.cache.dir : void 0;
1257
+ const mruEnabled = !isMock && options?.mru !== false;
1258
+ let state = createWizardState(config);
1259
+ const cachedAnswers = cacheEnabled ? loadCachedAnswers(config.meta.name, cacheDir) : void 0;
1260
+ const userPlugins = options?.plugins;
1261
+ if (userPlugins) {
1262
+ for (const plugin of userPlugins) {
1263
+ registerPlugin(plugin);
1264
+ }
1265
+ }
1266
+ try {
1267
+ if (!isMock && config.checks && config.checks.length > 0) {
1268
+ runPreFlightChecks(config.checks, theme);
1269
+ }
1270
+ if (!quiet) {
1271
+ printWizardHeader(config, theme, options?.plain);
1272
+ }
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) {
1281
+ if (currentStep.group !== void 0 && currentStep.group !== previousGroup) {
1282
+ const resolvedGroup = resolveTemplate(currentStep.group, state.answers);
1283
+ renderer.renderGroupHeader(resolvedGroup, theme);
1284
+ }
1285
+ previousGroup = currentStep.group;
1286
+ const stepIndex = visibleSteps.findIndex((s) => s.id === state.currentStepId);
1287
+ const resolvedMessage = resolveTemplate(currentStep.message, state.answers);
1288
+ 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) {
1304
+ if (isMock) {
1305
+ throw new Error(
1306
+ `Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
1307
+ );
1308
+ }
1309
+ console.log(theme.error(`
1310
+ ${pluginError}
1311
+ `));
1312
+ continue;
1313
+ }
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(`
1333
+ ${asyncError}
1334
+ `));
1335
+ state = { ...nextState, errors: {} };
1336
+ continue;
1337
+ }
1338
+ }
1339
+ if (options?.onAfterStep) {
1340
+ await options.onAfterStep(currentStep.id, value, nextState);
1341
+ }
1342
+ state = nextState;
1343
+ if (mruEnabled && isSelectLikeStep(currentStep.type)) {
1344
+ recordSelection(config.meta.name, currentStep.id, value);
1345
+ }
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;
1355
+ }
1356
+ throw error;
1357
+ }
1358
+ }
1359
+ if (state.status === "done" && !quiet) {
1360
+ renderer.renderSummary(state.answers, config.steps, theme);
1361
+ }
1362
+ if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
1363
+ await executeActions(config.actions, state.answers, theme);
1364
+ }
1365
+ if (state.status === "done" && cacheEnabled) {
1366
+ const passwordStepIds = new Set(
1367
+ config.steps.filter((s) => s.type === "password").map((s) => s.id)
1368
+ );
1369
+ const answersToCache = {};
1370
+ for (const [key, value] of Object.entries(state.answers)) {
1371
+ if (!passwordStepIds.has(key)) {
1372
+ answersToCache[key] = value;
1373
+ }
1374
+ }
1375
+ saveCachedAnswers(config.meta.name, answersToCache, cacheDir);
1376
+ }
1377
+ return state.answers;
1378
+ } finally {
1379
+ if (userPlugins) {
1380
+ clearPlugins();
1381
+ }
1382
+ }
1383
+ }
1384
+ function toStepRecord(step) {
1385
+ const record = {};
1386
+ for (const [key, val] of Object.entries(step)) {
1387
+ record[key] = val;
1388
+ }
1389
+ return record;
1390
+ }
1391
+ function renderStep(renderer, step, state, theme) {
1392
+ switch (step.type) {
1393
+ case "text":
1394
+ return renderer.renderText(step, state, theme);
1395
+ case "select":
1396
+ return renderer.renderSelect(step, state, theme);
1397
+ case "multiselect":
1398
+ return renderer.renderMultiSelect(step, state, theme);
1399
+ case "confirm":
1400
+ return renderer.renderConfirm(step, state, theme);
1401
+ case "password":
1402
+ return renderer.renderPassword(step, state, theme);
1403
+ case "number":
1404
+ return renderer.renderNumber(step, state, theme);
1405
+ case "search":
1406
+ return renderer.renderSearch(step, state, theme);
1407
+ case "editor":
1408
+ return renderer.renderEditor(step, state, theme);
1409
+ case "path":
1410
+ return renderer.renderPath(step, state, theme);
1411
+ case "toggle":
1412
+ return renderer.renderToggle(step, state, theme);
1413
+ case "message":
1414
+ renderer.renderMessage(step, state, theme);
1415
+ return Promise.resolve(true);
1416
+ }
1417
+ }
1418
+ function resolveStepDefaults(step, cachedAnswers) {
1419
+ switch (step.type) {
1420
+ case "text": {
1421
+ const envResolved = resolveEnvDefault(step.default);
1422
+ const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
1423
+ return { ...step, default: fallback };
1424
+ }
1425
+ case "search": {
1426
+ const envResolved = resolveEnvDefault(step.default);
1427
+ const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
1428
+ return { ...step, default: fallback };
1429
+ }
1430
+ case "editor": {
1431
+ const envResolved = resolveEnvDefault(step.default);
1432
+ const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
1433
+ return { ...step, default: fallback };
1434
+ }
1435
+ case "path": {
1436
+ const envResolved = resolveEnvDefault(step.default);
1437
+ const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
1438
+ return { ...step, default: fallback };
1439
+ }
1440
+ case "select": {
1441
+ const envResolved = resolveEnvDefault(step.default);
1442
+ const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
1443
+ return { ...step, default: fallback };
1444
+ }
1445
+ case "number": {
1446
+ const resolved = resolveEnvDefaultNumber(step.default);
1447
+ const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
1448
+ return { ...step, default: fallback };
1449
+ }
1450
+ case "confirm": {
1451
+ const resolved = resolveEnvDefaultBoolean(step.default);
1452
+ const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
1453
+ return { ...step, default: fallback };
1454
+ }
1455
+ case "toggle": {
1456
+ const resolved = resolveEnvDefaultBoolean(step.default);
1457
+ const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
1458
+ return { ...step, default: fallback };
1459
+ }
1460
+ case "multiselect": {
1461
+ const fallback = step.default ?? getCachedDefault(step.id, cachedAnswers);
1462
+ return { ...step, default: fallback };
1463
+ }
1464
+ case "password":
1465
+ case "message":
1466
+ return step;
1467
+ }
1468
+ }
1469
+ function getCachedDefault(stepId, cachedAnswers) {
1470
+ if (!cachedAnswers || !(stepId in cachedAnswers)) return void 0;
1471
+ return cachedAnswers[stepId];
1472
+ }
1473
+ function applyTemplateDefaults(step, templateAnswers) {
1474
+ if (!(step.id in templateAnswers)) return step;
1475
+ if (step.type === "password" || step.type === "message") return step;
1476
+ const value = templateAnswers[step.id];
1477
+ switch (step.type) {
1478
+ case "text":
1479
+ case "select":
1480
+ case "search":
1481
+ case "editor":
1482
+ case "path":
1483
+ return { ...step, default: typeof value === "string" ? value : step.default };
1484
+ case "number":
1485
+ return { ...step, default: typeof value === "number" ? value : step.default };
1486
+ case "confirm":
1487
+ case "toggle":
1488
+ return { ...step, default: typeof value === "boolean" ? value : step.default };
1489
+ case "multiselect":
1490
+ return { ...step, default: Array.isArray(value) ? value : step.default };
1491
+ }
1492
+ }
1493
+ function isSelectLikeStep(type) {
1494
+ return type === "select" || type === "multiselect" || type === "search";
1495
+ }
1496
+ function applyMruOrdering(step, wizardName) {
1497
+ if (step.type === "select") {
1498
+ return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
1499
+ }
1500
+ if (step.type === "multiselect") {
1501
+ return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
1502
+ }
1503
+ if (step.type === "search") {
1504
+ return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
1505
+ }
1506
+ return step;
1507
+ }
1508
+ function resolveChoiceTemplates(options, answers) {
1509
+ return options.map((opt) => {
1510
+ if ("separator" in opt) return opt;
1511
+ return {
1512
+ ...opt,
1513
+ label: resolveTemplate(opt.label, answers),
1514
+ hint: opt.hint ? resolveTemplate(opt.hint, answers) : void 0
1515
+ };
1516
+ });
1517
+ }
1518
+ function resolveStepTemplates(step, answers) {
1519
+ switch (step.type) {
1520
+ case "text":
1521
+ return {
1522
+ ...step,
1523
+ placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
1524
+ default: step.default ? resolveTemplate(step.default, answers) : void 0,
1525
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1526
+ };
1527
+ case "select":
1528
+ return {
1529
+ ...step,
1530
+ options: resolveChoiceTemplates(step.options, answers),
1531
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1532
+ };
1533
+ case "multiselect":
1534
+ return {
1535
+ ...step,
1536
+ options: resolveChoiceTemplates(step.options, answers),
1537
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1538
+ };
1539
+ case "search":
1540
+ return {
1541
+ ...step,
1542
+ placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
1543
+ options: resolveChoiceTemplates(step.options, answers),
1544
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1545
+ };
1546
+ case "path":
1547
+ return {
1548
+ ...step,
1549
+ placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
1550
+ default: step.default ? resolveTemplate(step.default, answers) : void 0,
1551
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1552
+ };
1553
+ case "editor":
1554
+ return {
1555
+ ...step,
1556
+ default: step.default ? resolveTemplate(step.default, answers) : void 0,
1557
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1558
+ };
1559
+ case "password":
1560
+ case "number":
1561
+ case "confirm":
1562
+ case "toggle":
1563
+ case "message":
1564
+ return {
1565
+ ...step,
1566
+ description: step.description ? resolveTemplate(step.description, answers) : void 0
1567
+ };
1568
+ }
1569
+ }
1570
+ async function executeActions(actions, answers, theme) {
1571
+ console.log(`
1572
+ ${theme.bold("Running actions...")}
1573
+ `);
1574
+ for (const action of actions) {
1575
+ if (action.when && !evaluateCondition(action.when, answers)) {
1576
+ continue;
1577
+ }
1578
+ const resolvedCommand = resolveTemplate(action.run, answers);
1579
+ const resolvedName = action.name ? resolveTemplate(action.name, answers) : void 0;
1580
+ const label = resolvedName ?? resolvedCommand;
1581
+ try {
1582
+ execSync(resolvedCommand, { stdio: "pipe" });
1583
+ console.log(` ${theme.success("\u2713")} ${label}`);
1584
+ } catch {
1585
+ console.log(` ${theme.error("\u2717")} ${label}`);
1586
+ throw new Error(`Action failed: ${label}`);
1587
+ }
1588
+ }
1589
+ console.log();
1590
+ }
1591
+ function printWizardHeader(config, theme, plain) {
1592
+ console.log();
1593
+ console.log(renderBanner(config.meta.name, theme, { plain }));
1594
+ if (config.meta.description) {
1595
+ console.log(` ${theme.muted(config.meta.description)}`);
1596
+ }
1597
+ console.log();
1598
+ }
1599
+ function isUserCancel(error) {
1600
+ if (error instanceof Error) {
1601
+ return error.message.includes("User force closed") || error.name === "ExitPromptError";
1602
+ }
1603
+ return false;
1604
+ }
1605
+
1606
+ // src/scaffolder.ts
1607
+ import { input as input2, select as select2, confirm as confirm2, number as number2 } from "@inquirer/prompts";
1608
+ import { stringify } from "yaml";
1609
+ import { writeFileSync as writeFileSync3 } from "fs";
1610
+ import { relative } from "path";
1611
+ var STEP_TYPE_CHOICES = [
1612
+ { name: "text", value: "text" },
1613
+ { name: "select", value: "select" },
1614
+ { name: "multiselect", value: "multiselect" },
1615
+ { name: "confirm", value: "confirm" },
1616
+ { name: "password", value: "password" },
1617
+ { name: "number", value: "number" },
1618
+ { name: "search", value: "search" },
1619
+ { name: "editor", value: "editor" },
1620
+ { name: "path", value: "path" },
1621
+ { name: "toggle", value: "toggle" }
1622
+ ];
1623
+ var TYPES_WITH_OPTIONS = /* @__PURE__ */ new Set(["select", "multiselect", "search"]);
1624
+ function isUserCancel2(error) {
1625
+ if (error instanceof Error) {
1626
+ return error.message.includes("User force closed") || error.name === "ExitPromptError";
1627
+ }
1628
+ return false;
1629
+ }
1630
+ async function scaffoldWizard(outputPath) {
1631
+ try {
1632
+ const wizardName = await input2({
1633
+ message: "Wizard name",
1634
+ validate: (val) => val.trim().length > 0 || "Name is required"
1635
+ });
1636
+ const description = await input2({
1637
+ message: "Description (optional)"
1638
+ });
1639
+ const rawStepCount = await number2({
1640
+ message: "How many steps?",
1641
+ default: 3,
1642
+ min: 1,
1643
+ max: 20
1644
+ });
1645
+ const stepCount = rawStepCount ?? 3;
1646
+ const steps = [];
1647
+ for (let i = 0; i < stepCount; i++) {
1648
+ console.log(`
1649
+ Step ${String(i + 1)} of ${String(stepCount)}`);
1650
+ const stepId = await input2({
1651
+ message: "Step ID",
1652
+ validate: (val) => /^[a-z][a-z0-9-]*$/.test(val) || "Must be lowercase, start with a letter, hyphens only (e.g., my-step)"
1653
+ });
1654
+ const stepType = await select2({
1655
+ message: "Step type",
1656
+ choices: STEP_TYPE_CHOICES
1657
+ });
1658
+ const stepMessage = await input2({
1659
+ message: "Prompt message",
1660
+ validate: (val) => val.trim().length > 0 || "Message is required"
1661
+ });
1662
+ const step = {
1663
+ id: stepId,
1664
+ type: stepType,
1665
+ message: stepMessage
1666
+ };
1667
+ if (TYPES_WITH_OPTIONS.has(stepType)) {
1668
+ const rawOptions = await input2({
1669
+ message: "Options (comma-separated values)",
1670
+ validate: (val) => val.split(",").some((v) => v.trim().length > 0) || "At least one option is required"
1671
+ });
1672
+ step.options = rawOptions.split(",").map((v) => v.trim()).filter((v) => v.length > 0).map((v) => ({
1673
+ value: v,
1674
+ label: v.charAt(0).toUpperCase() + v.slice(1)
1675
+ }));
1676
+ }
1677
+ const isRequired = await confirm2({
1678
+ message: "Is this a required field?",
1679
+ default: true
1680
+ });
1681
+ if (!isRequired) {
1682
+ step.required = false;
1683
+ }
1684
+ steps.push(step);
1685
+ }
1686
+ const format = await select2({
1687
+ message: "Output format",
1688
+ choices: [
1689
+ { name: "JSON", value: "json" },
1690
+ { name: "YAML", value: "yaml" },
1691
+ { name: "ENV", value: "env" }
1692
+ ]
1693
+ });
1694
+ const outputFilePath = await input2({
1695
+ message: "Output file path (optional)"
1696
+ });
1697
+ const wantTheme = await confirm2({
1698
+ message: "Do you want a theme?",
1699
+ default: false
1700
+ });
1701
+ let primaryColor = "";
1702
+ if (wantTheme) {
1703
+ primaryColor = await input2({
1704
+ message: "Primary color (hex)",
1705
+ default: "#5B9BD5",
1706
+ validate: (val) => /^#[0-9A-Fa-f]{6}$/.test(val) || "Must be a valid hex color (e.g., #5B9BD5)"
1707
+ });
1708
+ }
1709
+ const meta = { name: wizardName };
1710
+ if (description.length > 0) {
1711
+ meta.description = description;
1712
+ }
1713
+ const outputConfig = { format };
1714
+ if (outputFilePath.length > 0) {
1715
+ outputConfig.path = outputFilePath;
1716
+ }
1717
+ const config = {
1718
+ meta,
1719
+ steps,
1720
+ output: outputConfig
1721
+ };
1722
+ if (wantTheme) {
1723
+ config.theme = { tokens: { primary: primaryColor } };
1724
+ }
1725
+ const yamlContent = stringify(config);
1726
+ writeFileSync3(outputPath, yamlContent, "utf-8");
1727
+ const displayPath = relative(process.cwd(), outputPath);
1728
+ console.log(`
1729
+ \u2713 Created wizard config: ${outputPath}`);
1730
+ console.log(` Run it with: grimoire run ${displayPath}
1731
+ `);
1732
+ } catch (error) {
1733
+ if (isUserCancel2(error)) {
1734
+ console.log("\n Wizard creation cancelled.\n");
1735
+ return;
1736
+ }
1737
+ throw error;
1738
+ }
1739
+ }
1740
+
1741
+ // src/completions.ts
1742
+ function bashCompletion() {
1743
+ return `_grimoire() {
1744
+ local cur prev words cword
1745
+ COMPREPLY=()
1746
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1747
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1748
+ words=("\${COMP_WORDS[@]}")
1749
+ cword=\${COMP_CWORD}
1750
+
1751
+ # Get the subcommand (first non-option argument)
1752
+ local subcommand=""
1753
+ local i
1754
+ for ((i = 1; i < cword; i++)); do
1755
+ if [[ "\${words[i]}" != -* ]]; then
1756
+ subcommand="\${words[i]}"
1757
+ break
1758
+ fi
1759
+ done
1760
+
1761
+ # Complete subcommands
1762
+ if [[ \${cword} -eq 1 ]]; then
1763
+ COMPREPLY=($(compgen -W "run validate create demo completion" -- "\${cur}"))
1764
+ return 0
1765
+ fi
1766
+
1767
+ # Complete shell argument for completion command
1768
+ if [[ "\${subcommand}" == "completion" ]]; then
1769
+ if [[ \${cword} -eq 2 ]]; then
1770
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "\${cur}"))
1771
+ fi
1772
+ return 0
1773
+ fi
1774
+
1775
+ # Complete flags for run command
1776
+ if [[ "\${subcommand}" == "run" ]]; then
1777
+ if [[ "\${cur}" == -* ]]; then
1778
+ COMPREPLY=($(compgen -W "-o --output -f --format -q --quiet --dry-run --mock --json" -- "\${cur}"))
1779
+ elif [[ "\${prev}" == "-f" ]] || [[ "\${prev}" == "--format" ]]; then
1780
+ COMPREPLY=($(compgen -W "json yaml env" -- "\${cur}"))
1781
+ elif [[ "\${prev}" == "-o" ]] || [[ "\${prev}" == "--output" ]]; then
1782
+ COMPREPLY=($(compgen -f -- "\${cur}"))
1783
+ elif [[ "\${prev}" == "--mock" ]]; then
1784
+ COMPREPLY=()
1785
+ else
1786
+ # Complete config file path
1787
+ COMPREPLY=($(compgen -f -- "\${cur}"))
1788
+ fi
1789
+ return 0
1790
+ fi
1791
+
1792
+ # Complete flags for validate command
1793
+ if [[ "\${subcommand}" == "validate" ]]; then
1794
+ if [[ "\${cur}" == -* ]]; then
1795
+ COMPREPLY=()
1796
+ else
1797
+ # Complete config file path
1798
+ COMPREPLY=($(compgen -f -- "\${cur}"))
1799
+ fi
1800
+ return 0
1801
+ fi
1802
+
1803
+ # Complete flags for create command
1804
+ if [[ "\${subcommand}" == "create" ]]; then
1805
+ if [[ "\${cur}" == -* ]]; then
1806
+ COMPREPLY=()
1807
+ else
1808
+ # Complete output file path
1809
+ COMPREPLY=($(compgen -f -- "\${cur}"))
1810
+ fi
1811
+ return 0
1812
+ fi
1813
+
1814
+ # Complete flags for demo command
1815
+ if [[ "\${subcommand}" == "demo" ]]; then
1816
+ COMPREPLY=()
1817
+ return 0
1818
+ fi
1819
+
1820
+ return 0
1821
+ }
1822
+
1823
+ complete -o bashdefault -o default -o nospace -F _grimoire grimoire
1824
+ `;
1825
+ }
1826
+ function zshCompletion() {
1827
+ return `#compdef grimoire
1828
+
1829
+ _grimoire() {
1830
+ local -a subcommands
1831
+ subcommands=(
1832
+ 'run:Run a wizard from a config file'
1833
+ 'validate:Validate a wizard config file without running it'
1834
+ 'create:Interactively scaffold a new wizard config file'
1835
+ 'demo:Run a demo wizard showcasing all step types'
1836
+ 'completion:Output shell completion script'
1837
+ )
1838
+
1839
+ local -a run_flags
1840
+ run_flags=(
1841
+ '-o+[Write answers to file]:output file:_files'
1842
+ '--output+[Write answers to file]:output file:_files'
1843
+ '-f+[Output format]:format:(json yaml env)'
1844
+ '--format+[Output format]:format:(json yaml env)'
1845
+ '-q[Suppress header and summary output]'
1846
+ '--quiet[Suppress header and summary output]'
1847
+ '--dry-run[Show step plan without running the wizard]'
1848
+ '--mock+[Run wizard with preset answers]:json string:'
1849
+ '--json[Output structured JSON result to stdout]'
1850
+ )
1851
+
1852
+ local -a completion_shells
1853
+ completion_shells=(
1854
+ 'bash:Bash shell'
1855
+ 'zsh:Zsh shell'
1856
+ 'fish:Fish shell'
1857
+ )
1858
+
1859
+ local context state line
1860
+
1861
+ _arguments -C '1: :->command' '*::args:->args'
1862
+
1863
+ case $state in
1864
+ command)
1865
+ _describe 'command' subcommands
1866
+ ;;
1867
+ args)
1868
+ case \${words[2]} in
1869
+ run)
1870
+ _arguments $run_flags '1:config file:_files'
1871
+ ;;
1872
+ validate)
1873
+ _arguments '1:config file:_files'
1874
+ ;;
1875
+ create)
1876
+ _arguments '1:output file:_files'
1877
+ ;;
1878
+ demo)
1879
+ ;;
1880
+ completion)
1881
+ _describe 'shell' completion_shells
1882
+ ;;
1883
+ esac
1884
+ ;;
1885
+ esac
1886
+ }
1887
+
1888
+ _grimoire
1889
+ `;
1890
+ }
1891
+ function fishCompletion() {
1892
+ return `# Fish completion for grimoire
1893
+
1894
+ # Subcommands
1895
+ complete -c grimoire -f -n "__fish_use_subcommand_from_list" -a "run" -d "Run a wizard from a config file"
1896
+ complete -c grimoire -f -n "__fish_use_subcommand_from_list" -a "validate" -d "Validate a wizard config file without running it"
1897
+ complete -c grimoire -f -n "__fish_use_subcommand_from_list" -a "create" -d "Interactively scaffold a new wizard config file"
1898
+ complete -c grimoire -f -n "__fish_use_subcommand_from_list" -a "demo" -d "Run a demo wizard showcasing all step types"
1899
+ complete -c grimoire -f -n "__fish_use_subcommand_from_list" -a "completion" -d "Output shell completion script"
1900
+
1901
+ # run command flags
1902
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -s o -l output -d "Write answers to file" -r
1903
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -s f -l format -d "Output format" -x -a "json yaml env"
1904
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -s q -l quiet -d "Suppress header and summary output"
1905
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -l dry-run -d "Show step plan without running the wizard"
1906
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -l mock -d "Run wizard with preset answers (JSON string)" -r
1907
+ complete -c grimoire -n "__fish_seen_subcommand_from run" -l json -d "Output structured JSON result to stdout"
1908
+
1909
+ # completion command shells
1910
+ complete -c grimoire -n "__fish_seen_subcommand_from completion" -f -a "bash" -d "Bash shell"
1911
+ complete -c grimoire -n "__fish_seen_subcommand_from completion" -f -a "zsh" -d "Zsh shell"
1912
+ complete -c grimoire -n "__fish_seen_subcommand_from completion" -f -a "fish" -d "Fish shell"
1913
+ `;
1914
+ }
1915
+
1916
+ // 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");
1921
+ function getWizardTemplateDir(wizardName) {
1922
+ return join3(TEMPLATES_DIR, slugify(wizardName));
1923
+ }
1924
+ function getTemplateFilePath(wizardName, templateName) {
1925
+ return join3(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
1926
+ }
1927
+ function loadTemplate(wizardName, templateName) {
1928
+ try {
1929
+ const filePath = getTemplateFilePath(wizardName, templateName);
1930
+ const raw = readFileSync4(filePath, "utf-8");
1931
+ const parsed = JSON.parse(raw);
1932
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1933
+ return parsed;
1934
+ }
1935
+ return void 0;
1936
+ } catch {
1937
+ return void 0;
1938
+ }
1939
+ }
1940
+ function listTemplates(wizardName) {
1941
+ try {
1942
+ const dir = getWizardTemplateDir(wizardName);
1943
+ if (!existsSync2(dir)) return [];
1944
+ return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
1945
+ } catch {
1946
+ return [];
1947
+ }
1948
+ }
1949
+ function deleteTemplate(wizardName, templateName) {
1950
+ try {
1951
+ const filePath = getTemplateFilePath(wizardName, templateName);
1952
+ unlinkSync2(filePath);
1953
+ } catch {
1954
+ }
1955
+ }
1956
+
1957
+ // src/renderers/ink.ts
1958
+ import {
1959
+ input as input3,
1960
+ select as select3,
1961
+ checkbox as checkbox2,
1962
+ confirm as confirm3,
1963
+ password as password2,
1964
+ number as number3,
1965
+ search as search2,
1966
+ editor as editor2,
1967
+ Separator as Separator2
1968
+ } from "@inquirer/prompts";
1969
+ function boxLine(text, theme) {
1970
+ const line = "\u2500".repeat(Math.max(0, 40 - text.length));
1971
+ return `${theme.accent("\u250C\u2500")} ${theme.bold(text)} ${theme.accent(`${line}\u2510`)}`;
1972
+ }
1973
+ var InkRenderer = class {
1974
+ renderStepHeader(stepIndex, totalVisible, message, theme, description) {
1975
+ const barWidth = 30;
1976
+ const progress = totalVisible > 0 ? stepIndex / totalVisible : 0;
1977
+ const filledCount = Math.round(progress * barWidth);
1978
+ const remainingCount = barWidth - filledCount;
1979
+ const filledBar = theme.success("\u2593".repeat(filledCount));
1980
+ const remainingBar = theme.muted("\u2591".repeat(remainingCount));
1981
+ const pct = `${String(Math.round(progress * 100))}%`;
1982
+ const counter = theme.muted(`Step ${String(stepIndex + 1)}/${String(totalVisible)}`);
1983
+ console.log();
1984
+ console.log(` ${theme.accent("\u250C")} ${counter} ${theme.muted(pct)}`);
1985
+ console.log(` ${theme.accent("\u2502")} [${filledBar}${remainingBar}]`);
1986
+ console.log(` ${theme.accent("\u2514\u2500")} ${theme.primary(message)}`);
1987
+ if (description) {
1988
+ console.log(` ${theme.muted(description)}`);
1989
+ }
1990
+ }
1991
+ async renderText(step, state, theme) {
1992
+ const existingAnswer = state.answers[step.id];
1993
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
1994
+ return input3({
1995
+ message: step.message,
1996
+ default: defaultValue,
1997
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
1998
+ });
1999
+ }
2000
+ async renderSelect(step, state, theme) {
2001
+ const existingAnswer = state.answers[step.id];
2002
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
2003
+ const choices = step.options.map((opt) => {
2004
+ if ("separator" in opt) {
2005
+ return new Separator2(opt.separator);
2006
+ }
2007
+ return {
2008
+ name: opt.label,
2009
+ value: opt.value,
2010
+ description: opt.hint,
2011
+ disabled: opt.disabled
2012
+ };
2013
+ });
2014
+ return select3({
2015
+ message: step.message,
2016
+ choices,
2017
+ default: defaultValue,
2018
+ pageSize: step.pageSize,
2019
+ loop: step.loop,
2020
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2021
+ });
2022
+ }
2023
+ async renderMultiSelect(step, state, theme) {
2024
+ const existingAnswer = state.answers[step.id];
2025
+ const previousSelections = Array.isArray(existingAnswer) ? existingAnswer.filter((v) => typeof v === "string") : step.default;
2026
+ const choices = step.options.map((opt) => {
2027
+ if ("separator" in opt) {
2028
+ return new Separator2(opt.separator);
2029
+ }
2030
+ return {
2031
+ name: opt.label,
2032
+ value: opt.value,
2033
+ checked: previousSelections?.includes(opt.value) ?? false,
2034
+ disabled: opt.disabled
2035
+ };
2036
+ });
2037
+ return checkbox2({
2038
+ message: step.message,
2039
+ choices,
2040
+ pageSize: step.pageSize,
2041
+ loop: step.loop,
2042
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2043
+ });
2044
+ }
2045
+ async renderConfirm(step, state, theme) {
2046
+ const existingAnswer = state.answers[step.id];
2047
+ const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer : step.default;
2048
+ return confirm3({
2049
+ message: step.message,
2050
+ default: defaultValue ?? true,
2051
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2052
+ });
2053
+ }
2054
+ async renderPassword(step, _state, theme) {
2055
+ return password2({
2056
+ message: step.message,
2057
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2058
+ });
2059
+ }
2060
+ async renderNumber(step, state, theme) {
2061
+ const existingAnswer = state.answers[step.id];
2062
+ const defaultValue = typeof existingAnswer === "number" ? existingAnswer : step.default;
2063
+ const result = await number3({
2064
+ message: step.message,
2065
+ default: defaultValue,
2066
+ min: step.min,
2067
+ max: step.max,
2068
+ step: step.step,
2069
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2070
+ });
2071
+ return result ?? defaultValue ?? 0;
2072
+ }
2073
+ async renderSearch(step, _state, theme) {
2074
+ return search2({
2075
+ message: step.message,
2076
+ source: (term) => {
2077
+ const query = (term ?? "").toLowerCase();
2078
+ return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(query)).map((opt) => ({
2079
+ name: opt.label,
2080
+ value: opt.value,
2081
+ description: opt.hint
2082
+ }));
2083
+ },
2084
+ pageSize: step.pageSize,
2085
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2086
+ });
2087
+ }
2088
+ async renderEditor(step, _state, theme) {
2089
+ return editor2({
2090
+ message: step.message,
2091
+ default: step.default,
2092
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2093
+ });
2094
+ }
2095
+ async renderPath(step, state, theme) {
2096
+ const existingAnswer = state.answers[step.id];
2097
+ const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
2098
+ return input3({
2099
+ message: step.message,
2100
+ default: defaultValue,
2101
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2102
+ });
2103
+ }
2104
+ async renderToggle(step, state, theme) {
2105
+ const existingAnswer = state.answers[step.id];
2106
+ const activeLabel = step.active ?? "On";
2107
+ const inactiveLabel = step.inactive ?? "Off";
2108
+ const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer ? activeLabel : inactiveLabel : step.default === true ? activeLabel : inactiveLabel;
2109
+ const result = await select3({
2110
+ message: step.message,
2111
+ choices: [
2112
+ { name: activeLabel, value: activeLabel },
2113
+ { name: inactiveLabel, value: inactiveLabel }
2114
+ ],
2115
+ default: defaultValue,
2116
+ theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
2117
+ });
2118
+ return result === activeLabel;
2119
+ }
2120
+ renderMessage(step, _state, theme) {
2121
+ if (step.description) {
2122
+ console.log(` ${theme.muted(step.description)}`);
2123
+ }
2124
+ console.log();
2125
+ }
2126
+ renderGroupHeader(group, theme) {
2127
+ console.log();
2128
+ console.log(` ${boxLine(group, theme)}`);
2129
+ console.log();
2130
+ }
2131
+ renderSummary(answers, steps, theme) {
2132
+ const divider = theme.accent("\u2500".repeat(50));
2133
+ console.log();
2134
+ console.log(` ${divider}`);
2135
+ console.log(` ${theme.accent("\u2502")} ${theme.bold("Summary")}`);
2136
+ console.log(` ${divider}`);
2137
+ for (const step of steps) {
2138
+ const answer = answers[step.id];
2139
+ if (answer === void 0) continue;
2140
+ const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
2141
+ const label = theme.muted(step.id.padEnd(24));
2142
+ const value = theme.primary(display);
2143
+ console.log(` ${theme.accent("\u2502")} ${label} ${value}`);
2144
+ }
2145
+ console.log(` ${divider}`);
2146
+ }
2147
+ clear() {
2148
+ process.stdout.write("\x1B[2J\x1B[0f");
2149
+ }
2150
+ };
2151
+
2152
+ // src/cli.ts
2153
+ import { writeFileSync as writeFileSync5 } from "fs";
2154
+ import { resolve as resolve2 } from "path";
2155
+ import { fileURLToPath } from "url";
2156
+ import { stringify as yamlStringify } from "yaml";
2157
+ var plainMode = false;
2158
+ function applyColorMode(opts) {
2159
+ if (opts.plain || opts.color === false || process.env["NO_COLOR"] !== void 0) {
2160
+ chalk2.level = 0;
2161
+ plainMode = true;
2162
+ }
2163
+ }
2164
+ program.name("grimoire").description("Config-driven CLI wizard framework").version("0.3.0").option("--no-color", "Disable colored output").option("--plain", "Plain output mode (no colors, no banner)").hook("preAction", () => {
2165
+ const globalOpts = program.opts();
2166
+ applyColorMode(globalOpts);
2167
+ });
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) => {
2169
+ try {
2170
+ const fullPath = resolve2(configPath);
2171
+ const config = await loadWizardConfig(fullPath);
2172
+ if (opts.dryRun) {
2173
+ printDryRun(config);
2174
+ return;
2175
+ }
2176
+ const mockAnswers = parseMockAnswers(opts.mock);
2177
+ const isJsonOutput = opts.json === true;
2178
+ const renderer = resolveRenderer(opts.renderer);
2179
+ let templateAnswers;
2180
+ if (opts.template) {
2181
+ templateAnswers = loadTemplate(config.meta.name, opts.template);
2182
+ if (!templateAnswers) {
2183
+ console.error(`
2184
+ Template "${opts.template}" not found for "${config.meta.name}".
2185
+ `);
2186
+ process.exit(1);
2187
+ }
2188
+ }
2189
+ const answers = await runWizard(config, {
2190
+ renderer,
2191
+ quiet: opts.quiet ?? isJsonOutput,
2192
+ plain: plainMode,
2193
+ mockAnswers,
2194
+ templateAnswers,
2195
+ cache: opts.cache
2196
+ });
2197
+ const rawOutputPath = opts.output ?? config.output?.path;
2198
+ const outputPath = rawOutputPath ? resolve2(resolveTemplate(rawOutputPath, answers)) : void 0;
2199
+ if (isJsonOutput) {
2200
+ const stepsCompleted = Object.keys(answers).length;
2201
+ const result = {
2202
+ ok: true,
2203
+ wizard: config.meta.name,
2204
+ answers,
2205
+ stepsCompleted,
2206
+ format: "json"
2207
+ };
2208
+ const jsonStr = JSON.stringify(result, null, 2);
2209
+ console.log(jsonStr);
2210
+ if (outputPath) {
2211
+ writeFileSync5(outputPath, jsonStr + "\n", "utf-8");
2212
+ }
2213
+ return;
2214
+ }
2215
+ if (outputPath) {
2216
+ const format = opts.format ?? config.output?.format ?? "json";
2217
+ const content = formatOutput(answers, format);
2218
+ writeFileSync5(outputPath, content, "utf-8");
2219
+ if (!opts.quiet) {
2220
+ console.log(`
2221
+ Answers written to: ${outputPath}
2222
+ `);
2223
+ }
2224
+ }
2225
+ } catch (error) {
2226
+ if (opts.json) {
2227
+ const result = {
2228
+ ok: false,
2229
+ error: error instanceof Error ? error.message : "Unknown error"
2230
+ };
2231
+ console.log(JSON.stringify(result, null, 2));
2232
+ process.exit(1);
2233
+ }
2234
+ if (error instanceof Error) {
2235
+ console.error(`
2236
+ Error: ${error.message}
2237
+ `);
2238
+ }
2239
+ process.exit(1);
2240
+ }
2241
+ });
2242
+ program.command("validate").description("Validate a wizard config file without running it").argument("<config>", "Path to wizard config file").action(async (configPath) => {
2243
+ try {
2244
+ const fullPath = resolve2(configPath);
2245
+ const config = await loadWizardConfig(fullPath);
2246
+ console.log(`
2247
+ \u2713 Valid wizard config: "${config.meta.name}"`);
2248
+ console.log(` ${String(config.steps.length)} steps defined
2249
+ `);
2250
+ } catch (error) {
2251
+ if (error instanceof Error) {
2252
+ console.error(`
2253
+ \u2717 Invalid config: ${error.message}
2254
+ `);
2255
+ }
2256
+ process.exit(1);
2257
+ }
2258
+ });
2259
+ program.command("create").description("Interactively scaffold a new wizard config file").argument("[output]", "Output file path", "wizard.yaml").action(async (output) => {
2260
+ try {
2261
+ const resolvedPath = resolve2(output);
2262
+ await scaffoldWizard(resolvedPath);
2263
+ } catch (error) {
2264
+ if (error instanceof Error) {
2265
+ console.error(`
2266
+ Error: ${error.message}
2267
+ `);
2268
+ }
2269
+ process.exit(1);
2270
+ }
2271
+ });
2272
+ program.command("demo").description("Run a demo wizard showcasing all step types").action(async () => {
2273
+ try {
2274
+ const demoPath = resolve2(
2275
+ fileURLToPath(import.meta.url),
2276
+ "..",
2277
+ "..",
2278
+ "examples",
2279
+ "demo.yaml"
2280
+ );
2281
+ const config = await loadWizardConfig(demoPath);
2282
+ await runWizard(config);
2283
+ } catch (error) {
2284
+ if (error instanceof Error) {
2285
+ console.error(`
2286
+ Error: ${error.message}
2287
+ `);
2288
+ }
2289
+ process.exit(1);
2290
+ }
2291
+ });
2292
+ program.command("completion").description("Output shell completion script").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
2293
+ switch (shell) {
2294
+ case "bash":
2295
+ console.log(bashCompletion());
2296
+ break;
2297
+ case "zsh":
2298
+ console.log(zshCompletion());
2299
+ break;
2300
+ case "fish":
2301
+ console.log(fishCompletion());
2302
+ break;
2303
+ default:
2304
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
2305
+ process.exit(1);
2306
+ }
2307
+ });
2308
+ var cacheCommand = program.command("cache").description("Manage cached wizard answers");
2309
+ cacheCommand.command("clear").description("Delete cached wizard answers").argument("[name]", "Wizard name to clear (clears all if omitted)").action((name) => {
2310
+ clearCache(name);
2311
+ if (name) {
2312
+ console.log(`
2313
+ Cache cleared for "${name}".
2314
+ `);
2315
+ } else {
2316
+ console.log("\n All cached answers cleared.\n");
2317
+ }
2318
+ });
2319
+ var templateCommand = program.command("template").description("Manage saved wizard answer templates");
2320
+ templateCommand.command("list").description("List saved templates for a wizard").argument("<wizard-name>", "Wizard name").action((wizardName) => {
2321
+ const templates = listTemplates(wizardName);
2322
+ if (templates.length === 0) {
2323
+ console.log(`
2324
+ No templates found for "${wizardName}".
2325
+ `);
2326
+ return;
2327
+ }
2328
+ console.log(`
2329
+ Templates for "${wizardName}":
2330
+ `);
2331
+ for (const t of templates) {
2332
+ console.log(` - ${t}`);
2333
+ }
2334
+ console.log();
2335
+ });
2336
+ templateCommand.command("delete").description("Delete a saved template").argument("<wizard-name>", "Wizard name").argument("<template-name>", "Template name").action((wizardName, templateName) => {
2337
+ deleteTemplate(wizardName, templateName);
2338
+ console.log(`
2339
+ Template "${templateName}" deleted from "${wizardName}".
2340
+ `);
2341
+ });
2342
+ program.parse();
2343
+ function isRecord2(value) {
2344
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2345
+ }
2346
+ function parseMockAnswers(mockJson) {
2347
+ if (mockJson === void 0) {
2348
+ return void 0;
2349
+ }
2350
+ try {
2351
+ const parsed = JSON.parse(mockJson);
2352
+ if (!isRecord2(parsed)) {
2353
+ throw new Error("--mock value must be a JSON object");
2354
+ }
2355
+ return parsed;
2356
+ } catch (error) {
2357
+ if (error instanceof SyntaxError) {
2358
+ throw new Error(`--mock value is not valid JSON: ${error.message}`);
2359
+ }
2360
+ throw error;
2361
+ }
2362
+ }
2363
+ function printDryRun(config) {
2364
+ const theme = resolveTheme(config.theme);
2365
+ const visibleSteps = getVisibleSteps(config, {});
2366
+ console.log();
2367
+ console.log(` ${theme.bold("Dry Run:")} "${config.meta.name}"`);
2368
+ if (config.meta.description) {
2369
+ console.log(` ${theme.muted(config.meta.description)}`);
2370
+ }
2371
+ console.log();
2372
+ if (config.checks && config.checks.length > 0) {
2373
+ console.log(` ${theme.bold("Pre-flight checks:")}`);
2374
+ for (const check of config.checks) {
2375
+ console.log(` ${theme.muted("\u2022")} ${check.name}: ${theme.muted(check.run)}`);
2376
+ }
2377
+ console.log();
2378
+ }
2379
+ for (let i = 0; i < config.steps.length; i++) {
2380
+ const step = config.steps[i];
2381
+ if (!step) continue;
2382
+ const isVisible = visibleSteps.some((v) => v.id === step.id);
2383
+ const num = String(i + 1).padStart(2, " ");
2384
+ const typeStr = step.type.padEnd(12);
2385
+ const idStr = step.id.padEnd(20);
2386
+ const msg = `"${step.message}"`;
2387
+ const parts = [];
2388
+ if (!isVisible) {
2389
+ parts.push("(hidden)");
2390
+ }
2391
+ if (step.required === false) {
2392
+ parts.push("(optional)");
2393
+ }
2394
+ if (step.type === "multiselect") {
2395
+ if (step.min !== void 0) parts.push(`min:${String(step.min)}`);
2396
+ if (step.max !== void 0) parts.push(`max:${String(step.max)}`);
2397
+ }
2398
+ if (step.when) {
2399
+ parts.push(`[when: ${formatCondition(step.when)}]`);
2400
+ }
2401
+ if (step.type === "select" && step.routes) {
2402
+ const routeParts = Object.entries(step.routes).map(([k, v]) => `${k}\u2192${v}`).join(", ");
2403
+ parts.push(`routes: {${routeParts}}`);
2404
+ }
2405
+ if (step.next) {
2406
+ parts.push(`\u2192 ${step.next}`);
2407
+ }
2408
+ const suffix = parts.length > 0 ? ` ${parts.join(" ")}` : "";
2409
+ console.log(` Step ${num} ${typeStr} ${idStr} ${msg}${suffix}`);
2410
+ }
2411
+ console.log();
2412
+ if (config.actions && config.actions.length > 0) {
2413
+ console.log(` ${theme.bold("Post-wizard actions:")}`);
2414
+ for (const action of config.actions) {
2415
+ const label = action.name ?? action.run;
2416
+ const whenStr = action.when ? ` [when: ${formatCondition(action.when)}]` : "";
2417
+ console.log(` ${theme.muted("\u2022")} ${label}: ${theme.muted(action.run)}${whenStr}`);
2418
+ }
2419
+ console.log();
2420
+ }
2421
+ }
2422
+ function formatCondition(condition) {
2423
+ if ("all" in condition) {
2424
+ return `all(${condition.all.map(formatCondition).join(", ")})`;
2425
+ }
2426
+ if ("any" in condition) {
2427
+ return `any(${condition.any.map(formatCondition).join(", ")})`;
2428
+ }
2429
+ if ("not" in condition) {
2430
+ return `not(${formatCondition(condition.not)})`;
2431
+ }
2432
+ if ("equals" in condition) {
2433
+ return `${condition.field} equals ${String(condition.equals)}`;
2434
+ }
2435
+ if ("notEquals" in condition) {
2436
+ return `${condition.field} notEquals ${String(condition.notEquals)}`;
2437
+ }
2438
+ if ("includes" in condition) {
2439
+ return `${condition.field} includes ${String(condition.includes)}`;
2440
+ }
2441
+ if ("notIncludes" in condition) {
2442
+ return `${condition.field} notIncludes ${String(condition.notIncludes)}`;
2443
+ }
2444
+ if ("greaterThan" in condition) {
2445
+ return `${condition.field} > ${String(condition.greaterThan)}`;
2446
+ }
2447
+ if ("lessThan" in condition) {
2448
+ return `${condition.field} < ${String(condition.lessThan)}`;
2449
+ }
2450
+ if ("isEmpty" in condition) {
2451
+ return `${condition.field} isEmpty`;
2452
+ }
2453
+ if ("isNotEmpty" in condition) {
2454
+ return `${condition.field} isNotEmpty`;
2455
+ }
2456
+ return "unknown";
2457
+ }
2458
+ function resolveRenderer(rendererName) {
2459
+ if (!rendererName || rendererName === "inquirer") {
2460
+ return void 0;
2461
+ }
2462
+ if (rendererName === "ink") {
2463
+ return new InkRenderer();
2464
+ }
2465
+ throw new Error(`Unknown renderer: "${rendererName}". Supported: inquirer, ink`);
2466
+ }
2467
+ function toEnvKey(key) {
2468
+ return key.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
2469
+ }
2470
+ function formatOutput(answers, format) {
2471
+ switch (format) {
2472
+ case "json":
2473
+ return JSON.stringify(answers, null, 2) + "\n";
2474
+ case "env":
2475
+ return Object.entries(answers).map(([key, value]) => {
2476
+ const strValue = Array.isArray(value) ? value.join(",") : String(value);
2477
+ return `${toEnvKey(key)}=${strValue}`;
2478
+ }).join("\n") + "\n";
2479
+ case "yaml":
2480
+ return yamlStringify(answers);
2481
+ default:
2482
+ return JSON.stringify(answers, null, 2) + "\n";
2483
+ }
2484
+ }
2485
+ //# sourceMappingURL=cli.js.map