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/LICENSE +21 -0
- package/README.md +1399 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2485 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +354 -0
- package/dist/index.js +1922 -0
- package/dist/index.js.map +1 -0
- package/examples/all-features.yaml +54 -0
- package/examples/base.yaml +28 -0
- package/examples/basic.yaml +61 -0
- package/examples/conditional.yaml +126 -0
- package/examples/demo.yaml +112 -0
- package/examples/ebay-mcp-setup.yaml +171 -0
- package/examples/extended.yaml +34 -0
- package/examples/themed.yaml +92 -0
- package/examples/with-checks.yaml +34 -0
- package/package.json +73 -0
- package/schema/grimoire.schema.json +964 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1922 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var conditionSchema = z.lazy(() => {
|
|
4
|
+
const fieldEquals = z.object({ field: z.string(), equals: z.unknown() }).strict();
|
|
5
|
+
const fieldNotEquals = z.object({ field: z.string(), notEquals: z.unknown() }).strict();
|
|
6
|
+
const fieldIncludes = z.object({ field: z.string(), includes: z.unknown() }).strict();
|
|
7
|
+
const fieldNotIncludes = z.object({ field: z.string(), notIncludes: z.unknown() }).strict();
|
|
8
|
+
const fieldGreaterThan = z.object({ field: z.string(), greaterThan: z.number() }).strict();
|
|
9
|
+
const fieldLessThan = z.object({ field: z.string(), lessThan: z.number() }).strict();
|
|
10
|
+
const fieldIsEmpty = z.object({ field: z.string(), isEmpty: z.literal(true) }).strict();
|
|
11
|
+
const fieldIsNotEmpty = z.object({ field: z.string(), isNotEmpty: z.literal(true) }).strict();
|
|
12
|
+
const allCondition = z.object({ all: z.array(conditionSchema) }).strict();
|
|
13
|
+
const anyCondition = z.object({ any: z.array(conditionSchema) }).strict();
|
|
14
|
+
const notCondition = z.object({ not: conditionSchema }).strict();
|
|
15
|
+
return z.union([
|
|
16
|
+
fieldEquals,
|
|
17
|
+
fieldNotEquals,
|
|
18
|
+
fieldIncludes,
|
|
19
|
+
fieldNotIncludes,
|
|
20
|
+
fieldGreaterThan,
|
|
21
|
+
fieldLessThan,
|
|
22
|
+
fieldIsEmpty,
|
|
23
|
+
fieldIsNotEmpty,
|
|
24
|
+
allCondition,
|
|
25
|
+
anyCondition,
|
|
26
|
+
notCondition
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
var validationRuleSchema = z.discriminatedUnion("rule", [
|
|
30
|
+
z.object({ rule: z.literal("required"), message: z.string().optional() }),
|
|
31
|
+
z.object({ rule: z.literal("minLength"), value: z.number(), message: z.string().optional() }),
|
|
32
|
+
z.object({ rule: z.literal("maxLength"), value: z.number(), message: z.string().optional() }),
|
|
33
|
+
z.object({ rule: z.literal("pattern"), value: z.string(), message: z.string().optional() }),
|
|
34
|
+
z.object({ rule: z.literal("min"), value: z.number(), message: z.string().optional() }),
|
|
35
|
+
z.object({ rule: z.literal("max"), value: z.number(), message: z.string().optional() })
|
|
36
|
+
]);
|
|
37
|
+
var selectOptionSchema = z.object({
|
|
38
|
+
value: z.string(),
|
|
39
|
+
label: z.string(),
|
|
40
|
+
hint: z.string().optional(),
|
|
41
|
+
disabled: z.union([z.boolean(), z.string()]).optional()
|
|
42
|
+
});
|
|
43
|
+
var separatorOptionSchema = z.object({
|
|
44
|
+
separator: z.string()
|
|
45
|
+
});
|
|
46
|
+
var selectChoiceSchema = z.union([selectOptionSchema, separatorOptionSchema]);
|
|
47
|
+
var baseStepFields = {
|
|
48
|
+
id: z.string(),
|
|
49
|
+
message: z.string(),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
next: z.string().optional(),
|
|
52
|
+
when: conditionSchema.optional(),
|
|
53
|
+
keepValuesOnPrevious: z.boolean().optional(),
|
|
54
|
+
required: z.boolean().optional(),
|
|
55
|
+
group: z.string().optional()
|
|
56
|
+
};
|
|
57
|
+
var textStepSchema = z.object({
|
|
58
|
+
...baseStepFields,
|
|
59
|
+
type: z.literal("text"),
|
|
60
|
+
placeholder: z.string().optional(),
|
|
61
|
+
default: z.string().optional(),
|
|
62
|
+
validate: z.array(validationRuleSchema).optional()
|
|
63
|
+
});
|
|
64
|
+
var selectStepSchema = z.object({
|
|
65
|
+
...baseStepFields,
|
|
66
|
+
type: z.literal("select"),
|
|
67
|
+
options: z.array(selectChoiceSchema).min(1).optional(),
|
|
68
|
+
optionsFrom: z.string().optional(),
|
|
69
|
+
default: z.string().optional(),
|
|
70
|
+
routes: z.record(z.string(), z.string()).optional(),
|
|
71
|
+
pageSize: z.number().int().positive().optional(),
|
|
72
|
+
loop: z.boolean().optional()
|
|
73
|
+
});
|
|
74
|
+
var multiSelectStepSchema = z.object({
|
|
75
|
+
...baseStepFields,
|
|
76
|
+
type: z.literal("multiselect"),
|
|
77
|
+
options: z.array(selectChoiceSchema).min(1).optional(),
|
|
78
|
+
optionsFrom: z.string().optional(),
|
|
79
|
+
default: z.array(z.string()).optional(),
|
|
80
|
+
min: z.number().int().nonnegative().optional(),
|
|
81
|
+
max: z.number().int().positive().optional(),
|
|
82
|
+
pageSize: z.number().int().positive().optional(),
|
|
83
|
+
loop: z.boolean().optional()
|
|
84
|
+
});
|
|
85
|
+
var confirmStepSchema = z.object({
|
|
86
|
+
...baseStepFields,
|
|
87
|
+
type: z.literal("confirm"),
|
|
88
|
+
default: z.boolean().optional()
|
|
89
|
+
});
|
|
90
|
+
var passwordStepSchema = z.object({
|
|
91
|
+
...baseStepFields,
|
|
92
|
+
type: z.literal("password"),
|
|
93
|
+
validate: z.array(validationRuleSchema).optional()
|
|
94
|
+
});
|
|
95
|
+
var numberStepSchema = z.object({
|
|
96
|
+
...baseStepFields,
|
|
97
|
+
type: z.literal("number"),
|
|
98
|
+
default: z.number().optional(),
|
|
99
|
+
min: z.number().optional(),
|
|
100
|
+
max: z.number().optional(),
|
|
101
|
+
step: z.number().positive().optional()
|
|
102
|
+
});
|
|
103
|
+
var searchStepSchema = z.object({
|
|
104
|
+
...baseStepFields,
|
|
105
|
+
type: z.literal("search"),
|
|
106
|
+
options: z.array(selectChoiceSchema).min(1).optional(),
|
|
107
|
+
optionsFrom: z.string().optional(),
|
|
108
|
+
default: z.string().optional(),
|
|
109
|
+
placeholder: z.string().optional(),
|
|
110
|
+
pageSize: z.number().int().positive().optional(),
|
|
111
|
+
loop: z.boolean().optional()
|
|
112
|
+
});
|
|
113
|
+
var editorStepSchema = z.object({
|
|
114
|
+
...baseStepFields,
|
|
115
|
+
type: z.literal("editor"),
|
|
116
|
+
default: z.string().optional(),
|
|
117
|
+
validate: z.array(validationRuleSchema).optional()
|
|
118
|
+
});
|
|
119
|
+
var pathStepSchema = z.object({
|
|
120
|
+
...baseStepFields,
|
|
121
|
+
type: z.literal("path"),
|
|
122
|
+
default: z.string().optional(),
|
|
123
|
+
placeholder: z.string().optional(),
|
|
124
|
+
validate: z.array(validationRuleSchema).optional()
|
|
125
|
+
});
|
|
126
|
+
var toggleStepSchema = z.object({
|
|
127
|
+
...baseStepFields,
|
|
128
|
+
type: z.literal("toggle"),
|
|
129
|
+
default: z.boolean().optional(),
|
|
130
|
+
active: z.string().optional(),
|
|
131
|
+
inactive: z.string().optional()
|
|
132
|
+
});
|
|
133
|
+
var messageStepSchema = z.object({
|
|
134
|
+
...baseStepFields,
|
|
135
|
+
type: z.literal("message")
|
|
136
|
+
});
|
|
137
|
+
var stepConfigSchema = z.discriminatedUnion("type", [
|
|
138
|
+
textStepSchema,
|
|
139
|
+
selectStepSchema,
|
|
140
|
+
multiSelectStepSchema,
|
|
141
|
+
confirmStepSchema,
|
|
142
|
+
passwordStepSchema,
|
|
143
|
+
numberStepSchema,
|
|
144
|
+
searchStepSchema,
|
|
145
|
+
editorStepSchema,
|
|
146
|
+
pathStepSchema,
|
|
147
|
+
toggleStepSchema,
|
|
148
|
+
messageStepSchema
|
|
149
|
+
]);
|
|
150
|
+
var hexColorSchema = z.string().regex(
|
|
151
|
+
/^#[0-9a-fA-F]{6}$/,
|
|
152
|
+
"Must be a 6-digit hex color (e.g., #FF0000)"
|
|
153
|
+
);
|
|
154
|
+
var themeConfigSchema = z.object({
|
|
155
|
+
tokens: z.object({
|
|
156
|
+
primary: hexColorSchema.optional(),
|
|
157
|
+
success: hexColorSchema.optional(),
|
|
158
|
+
error: hexColorSchema.optional(),
|
|
159
|
+
warning: hexColorSchema.optional(),
|
|
160
|
+
info: hexColorSchema.optional(),
|
|
161
|
+
muted: hexColorSchema.optional(),
|
|
162
|
+
accent: hexColorSchema.optional()
|
|
163
|
+
}).optional(),
|
|
164
|
+
icons: z.object({
|
|
165
|
+
step: z.string().optional(),
|
|
166
|
+
stepDone: z.string().optional(),
|
|
167
|
+
stepPending: z.string().optional(),
|
|
168
|
+
pointer: z.string().optional()
|
|
169
|
+
}).optional()
|
|
170
|
+
});
|
|
171
|
+
var preFlightCheckSchema = z.object({
|
|
172
|
+
name: z.string(),
|
|
173
|
+
run: z.string(),
|
|
174
|
+
message: z.string()
|
|
175
|
+
});
|
|
176
|
+
var actionConfigSchema = z.object({
|
|
177
|
+
name: z.string().optional(),
|
|
178
|
+
run: z.string(),
|
|
179
|
+
when: conditionSchema.optional()
|
|
180
|
+
});
|
|
181
|
+
var wizardConfigSchema = z.object({
|
|
182
|
+
meta: z.object({
|
|
183
|
+
name: z.string(),
|
|
184
|
+
version: z.string().optional(),
|
|
185
|
+
description: z.string().optional()
|
|
186
|
+
}),
|
|
187
|
+
theme: themeConfigSchema.optional(),
|
|
188
|
+
steps: z.array(stepConfigSchema).min(1),
|
|
189
|
+
output: z.object({
|
|
190
|
+
format: z.enum(["json", "env", "yaml"]),
|
|
191
|
+
path: z.string().optional()
|
|
192
|
+
}).optional(),
|
|
193
|
+
extends: z.string().optional(),
|
|
194
|
+
checks: z.array(preFlightCheckSchema).optional(),
|
|
195
|
+
actions: z.array(actionConfigSchema).optional()
|
|
196
|
+
}).superRefine((config, ctx) => {
|
|
197
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const step of config.steps) {
|
|
199
|
+
if (stepIds.has(step.id)) {
|
|
200
|
+
ctx.addIssue({
|
|
201
|
+
code: z.ZodIssueCode.custom,
|
|
202
|
+
message: `Duplicate step ID: "${step.id}"`,
|
|
203
|
+
path: ["steps"]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
stepIds.add(step.id);
|
|
207
|
+
}
|
|
208
|
+
config.steps.forEach((step, i) => {
|
|
209
|
+
if (step.next && step.next !== "__done__" && !stepIds.has(step.next)) {
|
|
210
|
+
ctx.addIssue({
|
|
211
|
+
code: z.ZodIssueCode.custom,
|
|
212
|
+
message: `Step "${step.id}" references unknown next step: "${step.next}"`,
|
|
213
|
+
path: ["steps", i, "next"]
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (step.type === "select" && step.routes) {
|
|
217
|
+
for (const [key, target] of Object.entries(step.routes)) {
|
|
218
|
+
if (target !== "__done__" && !stepIds.has(target)) {
|
|
219
|
+
ctx.addIssue({
|
|
220
|
+
code: z.ZodIssueCode.custom,
|
|
221
|
+
message: `Step "${step.id}" route "${key}" references unknown step: "${target}"`,
|
|
222
|
+
path: ["steps", i, "routes", key]
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (step.when) {
|
|
228
|
+
collectConditionFieldIssues(step.when, stepIds, ctx, ["steps", i, "when"]);
|
|
229
|
+
}
|
|
230
|
+
if (step.type === "select" && step.routes && step.options) {
|
|
231
|
+
const optionValues = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const o of step.options) {
|
|
233
|
+
if ("value" in o) {
|
|
234
|
+
optionValues.add(o.value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const routeKey of Object.keys(step.routes)) {
|
|
238
|
+
if (!optionValues.has(routeKey)) {
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
message: `Step "${step.id}" route key "${routeKey}" does not match any option value`,
|
|
242
|
+
path: ["steps", i, "routes", routeKey]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (step.type === "select" || step.type === "multiselect" || step.type === "search") {
|
|
248
|
+
const hasOptions = step.options !== void 0;
|
|
249
|
+
const hasOptionsFrom = step.optionsFrom !== void 0;
|
|
250
|
+
if (hasOptions && hasOptionsFrom) {
|
|
251
|
+
ctx.addIssue({
|
|
252
|
+
code: z.ZodIssueCode.custom,
|
|
253
|
+
message: `Step "${step.id}" has both "options" and "optionsFrom" \u2014 only one is allowed`,
|
|
254
|
+
path: ["steps", i]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (!hasOptions && !hasOptionsFrom) {
|
|
258
|
+
ctx.addIssue({
|
|
259
|
+
code: z.ZodIssueCode.custom,
|
|
260
|
+
message: `Step "${step.id}" must have either "options" or "optionsFrom"`,
|
|
261
|
+
path: ["steps", i]
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if ((step.type === "number" || step.type === "multiselect") && step.min !== void 0 && step.max !== void 0 && step.min > step.max) {
|
|
266
|
+
ctx.addIssue({
|
|
267
|
+
code: z.ZodIssueCode.custom,
|
|
268
|
+
message: `Step "${step.id}" has min (${String(step.min)}) greater than max (${String(step.max)})`,
|
|
269
|
+
path: ["steps", i]
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (config.actions) {
|
|
274
|
+
config.actions.forEach((action, i) => {
|
|
275
|
+
if (action.when) {
|
|
276
|
+
collectConditionFieldIssues(action.when, stepIds, ctx, ["actions", i, "when"]);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
function collectConditionFieldIssues(condition, validIds, ctx, path) {
|
|
282
|
+
if ("field" in condition) {
|
|
283
|
+
const fieldRoot = condition.field.split(".")[0];
|
|
284
|
+
if (fieldRoot && !validIds.has(fieldRoot)) {
|
|
285
|
+
ctx.addIssue({
|
|
286
|
+
code: z.ZodIssueCode.custom,
|
|
287
|
+
message: `Condition references unknown step ID: "${fieldRoot}"`,
|
|
288
|
+
path
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if ("all" in condition) {
|
|
294
|
+
condition.all.forEach((child, i) => {
|
|
295
|
+
collectConditionFieldIssues(child, validIds, ctx, [...path, "all", i]);
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if ("any" in condition) {
|
|
300
|
+
condition.any.forEach((child, i) => {
|
|
301
|
+
collectConditionFieldIssues(child, validIds, ctx, [...path, "any", i]);
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if ("not" in condition) {
|
|
306
|
+
collectConditionFieldIssues(condition.not, validIds, ctx, [...path, "not"]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function parseWizardConfig(raw) {
|
|
310
|
+
const result = wizardConfigSchema.parse(raw);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/parser.ts
|
|
315
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
316
|
+
import { readFileSync } from "fs";
|
|
317
|
+
import { dirname, resolve, isAbsolute } from "path";
|
|
318
|
+
import { parse as parseYAML } from "yaml";
|
|
319
|
+
var DONE_SENTINEL = "__done__";
|
|
320
|
+
function buildStepGraph(steps) {
|
|
321
|
+
const graph = /* @__PURE__ */ new Map();
|
|
322
|
+
for (let i = 0; i < steps.length; i++) {
|
|
323
|
+
const step = steps[i];
|
|
324
|
+
const edges = [];
|
|
325
|
+
if (step.next && step.next !== DONE_SENTINEL) {
|
|
326
|
+
edges.push(step.next);
|
|
327
|
+
}
|
|
328
|
+
if (step.type === "select" && step.routes) {
|
|
329
|
+
for (const target of Object.values(step.routes)) {
|
|
330
|
+
if (target !== DONE_SENTINEL) {
|
|
331
|
+
edges.push(target);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!step.next && !(step.type === "select" && step.routes)) {
|
|
336
|
+
const nextStep = steps[i + 1];
|
|
337
|
+
if (nextStep) {
|
|
338
|
+
edges.push(nextStep.id);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
graph.set(step.id, edges);
|
|
342
|
+
}
|
|
343
|
+
return graph;
|
|
344
|
+
}
|
|
345
|
+
function detectCycles(config) {
|
|
346
|
+
const graph = buildStepGraph(config.steps);
|
|
347
|
+
const UNVISITED = 0;
|
|
348
|
+
const IN_STACK = 1;
|
|
349
|
+
const DONE = 2;
|
|
350
|
+
const nodeState = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const id of graph.keys()) {
|
|
352
|
+
nodeState.set(id, UNVISITED);
|
|
353
|
+
}
|
|
354
|
+
function dfs(nodeId, path) {
|
|
355
|
+
nodeState.set(nodeId, IN_STACK);
|
|
356
|
+
const currentPath = [...path, nodeId];
|
|
357
|
+
for (const neighbor of graph.get(nodeId) ?? []) {
|
|
358
|
+
const state = nodeState.get(neighbor);
|
|
359
|
+
if (state === IN_STACK) {
|
|
360
|
+
const cycleStart = currentPath.indexOf(neighbor);
|
|
361
|
+
const cycle = [...currentPath.slice(cycleStart), neighbor];
|
|
362
|
+
throw new Error(`Cycle detected in wizard steps: ${cycle.join(" \u2192 ")}`);
|
|
363
|
+
}
|
|
364
|
+
if (state === UNVISITED) {
|
|
365
|
+
dfs(neighbor, currentPath);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
nodeState.set(nodeId, DONE);
|
|
369
|
+
}
|
|
370
|
+
for (const id of graph.keys()) {
|
|
371
|
+
if (nodeState.get(id) === UNVISITED) {
|
|
372
|
+
dfs(id, []);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function deepMergeTheme(parent, child) {
|
|
377
|
+
if (!parent && !child) return void 0;
|
|
378
|
+
if (!parent) return child;
|
|
379
|
+
if (!child) return parent;
|
|
380
|
+
return {
|
|
381
|
+
tokens: { ...parent.tokens, ...child.tokens },
|
|
382
|
+
icons: { ...parent.icons, ...child.icons }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function mergeConfigs(parent, child) {
|
|
386
|
+
return {
|
|
387
|
+
meta: { ...parent.meta, ...child.meta },
|
|
388
|
+
theme: deepMergeTheme(parent.theme, child.theme),
|
|
389
|
+
steps: child.steps,
|
|
390
|
+
output: child.output ?? parent.output,
|
|
391
|
+
checks: [
|
|
392
|
+
...parent.checks ?? [],
|
|
393
|
+
...child.checks ?? []
|
|
394
|
+
],
|
|
395
|
+
actions: child.actions ?? parent.actions
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
var OPTION_STEP_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "search"]);
|
|
399
|
+
function resolveOptionsFromSteps(raw, configDir) {
|
|
400
|
+
const steps = raw["steps"];
|
|
401
|
+
if (!Array.isArray(steps)) return;
|
|
402
|
+
for (const step of steps) {
|
|
403
|
+
if (typeof step !== "object" || step === null) continue;
|
|
404
|
+
const stepObj = step;
|
|
405
|
+
const optionsFrom = stepObj["optionsFrom"];
|
|
406
|
+
if (typeof optionsFrom !== "string") continue;
|
|
407
|
+
const stepId = String(stepObj["id"] ?? "unknown");
|
|
408
|
+
const stepType = String(stepObj["type"] ?? "unknown");
|
|
409
|
+
if (!OPTION_STEP_TYPES.has(stepType)) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Step "${stepId}" has "optionsFrom" but type "${stepType}" does not support dynamic options`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const fullPath = isAbsolute(optionsFrom) ? optionsFrom : resolve(configDir, optionsFrom);
|
|
415
|
+
let content;
|
|
416
|
+
try {
|
|
417
|
+
content = readFileSync(fullPath, "utf-8");
|
|
418
|
+
} catch {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Step "${stepId}": failed to read optionsFrom file "${fullPath}"`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
let parsed;
|
|
424
|
+
try {
|
|
425
|
+
if (fullPath.endsWith(".yaml") || fullPath.endsWith(".yml")) {
|
|
426
|
+
parsed = parseYAML(content);
|
|
427
|
+
} else {
|
|
428
|
+
parsed = JSON.parse(content);
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Step "${stepId}": optionsFrom file "${fullPath}" contains invalid JSON/YAML`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (!Array.isArray(parsed)) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Step "${stepId}": optionsFrom file "${fullPath}" must contain an array`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
stepObj["options"] = parsed;
|
|
441
|
+
delete stepObj["optionsFrom"];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function loadWithInheritance(filePath, seen) {
|
|
445
|
+
const resolvedPath = resolve(filePath);
|
|
446
|
+
if (seen.has(resolvedPath)) {
|
|
447
|
+
throw new Error(`Circular extends detected: "${resolvedPath}" was already loaded`);
|
|
448
|
+
}
|
|
449
|
+
seen.add(resolvedPath);
|
|
450
|
+
const explorer = cosmiconfig("grimoire");
|
|
451
|
+
const result = await explorer.load(resolvedPath);
|
|
452
|
+
if (!result || result.isEmpty) {
|
|
453
|
+
throw new Error(`No configuration found at: ${resolvedPath}`);
|
|
454
|
+
}
|
|
455
|
+
const raw = result.config;
|
|
456
|
+
const extendsPath = typeof raw["extends"] === "string" ? raw["extends"] : void 0;
|
|
457
|
+
resolveOptionsFromSteps(raw, dirname(resolvedPath));
|
|
458
|
+
const config = parseWizardConfig(raw);
|
|
459
|
+
if (!extendsPath) {
|
|
460
|
+
return config;
|
|
461
|
+
}
|
|
462
|
+
const parentPath = isAbsolute(extendsPath) ? extendsPath : resolve(dirname(resolvedPath), extendsPath);
|
|
463
|
+
const parentConfig = await loadWithInheritance(parentPath, seen);
|
|
464
|
+
return mergeConfigs(parentConfig, config);
|
|
465
|
+
}
|
|
466
|
+
async function loadWizardConfig(filePath) {
|
|
467
|
+
const config = await loadWithInheritance(filePath, /* @__PURE__ */ new Set());
|
|
468
|
+
detectCycles(config);
|
|
469
|
+
return config;
|
|
470
|
+
}
|
|
471
|
+
function parseWizardYAML(yamlString) {
|
|
472
|
+
const raw = parseYAML(yamlString);
|
|
473
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
474
|
+
if ("extends" in raw && typeof raw.extends === "string") {
|
|
475
|
+
throw new Error(
|
|
476
|
+
'"extends" is not supported in parseWizardYAML \u2014 use loadWizardConfig with a file path'
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const steps = raw["steps"];
|
|
480
|
+
if (Array.isArray(steps)) {
|
|
481
|
+
for (const step of steps) {
|
|
482
|
+
if (typeof step === "object" && step !== null && "optionsFrom" in step) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
'"optionsFrom" is not supported in parseWizardYAML \u2014 use loadWizardConfig with a file path'
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const config = parseWizardConfig(raw);
|
|
491
|
+
detectCycles(config);
|
|
492
|
+
return config;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/conditions.ts
|
|
496
|
+
function isRecord(value) {
|
|
497
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
498
|
+
}
|
|
499
|
+
function getValueByPath(obj, path) {
|
|
500
|
+
const segments = path.split(".");
|
|
501
|
+
let current = obj;
|
|
502
|
+
for (const segment of segments) {
|
|
503
|
+
if (!isRecord(current)) {
|
|
504
|
+
return void 0;
|
|
505
|
+
}
|
|
506
|
+
current = current[segment];
|
|
507
|
+
}
|
|
508
|
+
return current;
|
|
509
|
+
}
|
|
510
|
+
function evaluateCondition(condition, answers) {
|
|
511
|
+
if ("all" in condition) {
|
|
512
|
+
return condition.all.every((c) => evaluateCondition(c, answers));
|
|
513
|
+
}
|
|
514
|
+
if ("any" in condition) {
|
|
515
|
+
return condition.any.some((c) => evaluateCondition(c, answers));
|
|
516
|
+
}
|
|
517
|
+
if ("not" in condition) {
|
|
518
|
+
return !evaluateCondition(condition.not, answers);
|
|
519
|
+
}
|
|
520
|
+
const value = getValueByPath(answers, condition.field);
|
|
521
|
+
if ("isEmpty" in condition) {
|
|
522
|
+
if (value === void 0 || value === null) return true;
|
|
523
|
+
if (typeof value === "string") return value.length === 0;
|
|
524
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if ("isNotEmpty" in condition) {
|
|
528
|
+
if (value === void 0 || value === null) return false;
|
|
529
|
+
if (typeof value === "string") return value.length > 0;
|
|
530
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
if (value === void 0 || value === null) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
if ("equals" in condition) {
|
|
537
|
+
return value === condition.equals;
|
|
538
|
+
}
|
|
539
|
+
if ("notEquals" in condition) {
|
|
540
|
+
return value !== condition.notEquals;
|
|
541
|
+
}
|
|
542
|
+
if ("includes" in condition) {
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
return value.includes(condition.includes);
|
|
545
|
+
}
|
|
546
|
+
if (typeof value === "string" && typeof condition.includes === "string") {
|
|
547
|
+
return value.includes(condition.includes);
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if ("notIncludes" in condition) {
|
|
552
|
+
if (Array.isArray(value)) {
|
|
553
|
+
return !value.includes(condition.notIncludes);
|
|
554
|
+
}
|
|
555
|
+
if (typeof value === "string" && typeof condition.notIncludes === "string") {
|
|
556
|
+
return !value.includes(condition.notIncludes);
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if ("greaterThan" in condition) {
|
|
561
|
+
return typeof value === "number" && value > condition.greaterThan;
|
|
562
|
+
}
|
|
563
|
+
if ("lessThan" in condition) {
|
|
564
|
+
return typeof value === "number" && value < condition.lessThan;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
function isStepVisible(step, answers) {
|
|
569
|
+
if (!step.when) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return evaluateCondition(step.when, answers);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/engine.ts
|
|
576
|
+
function createWizardState(config) {
|
|
577
|
+
const firstVisible = config.steps.find((s) => isStepVisible(s, {}));
|
|
578
|
+
if (!firstVisible) {
|
|
579
|
+
throw new Error("No visible steps in wizard configuration");
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
currentStepId: firstVisible.id,
|
|
583
|
+
answers: {},
|
|
584
|
+
history: [],
|
|
585
|
+
status: "running",
|
|
586
|
+
errors: {}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function validateStepAnswer(step, value) {
|
|
590
|
+
if (step.type === "message") {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const isRequired = step.required !== false;
|
|
594
|
+
if (isRequired) {
|
|
595
|
+
if (value === void 0 || value === null || value === "") {
|
|
596
|
+
return "This field is required";
|
|
597
|
+
}
|
|
598
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
599
|
+
return "This field is required";
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if ((step.type === "text" || step.type === "password" || step.type === "editor" || step.type === "path") && step.validate) {
|
|
603
|
+
const strValue = typeof value === "string" ? value : String(value ?? "");
|
|
604
|
+
for (const rule of step.validate) {
|
|
605
|
+
const error = applyValidationRule(rule, strValue);
|
|
606
|
+
if (error) return error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (step.type === "number" && typeof value === "number") {
|
|
610
|
+
if (step.min !== void 0 && value < step.min) {
|
|
611
|
+
return `Must be at least ${String(step.min)}`;
|
|
612
|
+
}
|
|
613
|
+
if (step.max !== void 0 && value > step.max) {
|
|
614
|
+
return `Must be at most ${String(step.max)}`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (step.type === "multiselect" && Array.isArray(value)) {
|
|
618
|
+
if (step.min !== void 0 && value.length < step.min) {
|
|
619
|
+
return `Select at least ${String(step.min)} option${step.min === 1 ? "" : "s"}`;
|
|
620
|
+
}
|
|
621
|
+
if (step.max !== void 0 && value.length > step.max) {
|
|
622
|
+
return `Select at most ${String(step.max)} option${step.max === 1 ? "" : "s"}`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
function resolveNextStep(config, currentStep, answer, answers) {
|
|
628
|
+
let targetId;
|
|
629
|
+
if (currentStep.type === "select" && currentStep.routes) {
|
|
630
|
+
const route = currentStep.routes[String(answer)];
|
|
631
|
+
if (route === "__done__") return "__done__";
|
|
632
|
+
if (route) targetId = route;
|
|
633
|
+
}
|
|
634
|
+
if (!targetId && currentStep.next) {
|
|
635
|
+
if (currentStep.next === "__done__") return "__done__";
|
|
636
|
+
targetId = currentStep.next;
|
|
637
|
+
}
|
|
638
|
+
if (!targetId) {
|
|
639
|
+
const currentIndex = config.steps.findIndex((s) => s.id === currentStep.id);
|
|
640
|
+
const nextInArray = config.steps[currentIndex + 1];
|
|
641
|
+
if (!nextInArray) return "__done__";
|
|
642
|
+
targetId = nextInArray.id;
|
|
643
|
+
}
|
|
644
|
+
const targetIndex = config.steps.findIndex((s) => s.id === targetId);
|
|
645
|
+
if (targetIndex < 0) return "__done__";
|
|
646
|
+
for (let i = targetIndex; i < config.steps.length; i++) {
|
|
647
|
+
const step = config.steps[i];
|
|
648
|
+
if (step && isStepVisible(step, answers)) {
|
|
649
|
+
return step.id;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return "__done__";
|
|
653
|
+
}
|
|
654
|
+
function getVisibleSteps(config, answers) {
|
|
655
|
+
return config.steps.filter((s) => isStepVisible(s, answers));
|
|
656
|
+
}
|
|
657
|
+
function wizardReducer(state, transition, config) {
|
|
658
|
+
switch (transition.type) {
|
|
659
|
+
case "NEXT": {
|
|
660
|
+
const currentStep = findStepOrThrow(config, state.currentStepId);
|
|
661
|
+
const validationError = validateStepAnswer(currentStep, transition.value);
|
|
662
|
+
if (validationError) {
|
|
663
|
+
return {
|
|
664
|
+
...state,
|
|
665
|
+
errors: { ...state.errors, [state.currentStepId]: validationError }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const updatedAnswers = {
|
|
669
|
+
...state.answers,
|
|
670
|
+
[state.currentStepId]: transition.value
|
|
671
|
+
};
|
|
672
|
+
const nextStepId = resolveNextStep(
|
|
673
|
+
config,
|
|
674
|
+
currentStep,
|
|
675
|
+
transition.value,
|
|
676
|
+
updatedAnswers
|
|
677
|
+
);
|
|
678
|
+
if (nextStepId === "__done__") {
|
|
679
|
+
return {
|
|
680
|
+
...state,
|
|
681
|
+
answers: updatedAnswers,
|
|
682
|
+
history: [...state.history, state.currentStepId],
|
|
683
|
+
status: "done",
|
|
684
|
+
errors: {}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const finalAnswers = cleanOrphanedAnswers(
|
|
688
|
+
config,
|
|
689
|
+
updatedAnswers,
|
|
690
|
+
state.currentStepId,
|
|
691
|
+
nextStepId
|
|
692
|
+
);
|
|
693
|
+
return {
|
|
694
|
+
...state,
|
|
695
|
+
currentStepId: nextStepId,
|
|
696
|
+
answers: finalAnswers,
|
|
697
|
+
history: [...state.history, state.currentStepId],
|
|
698
|
+
status: "running",
|
|
699
|
+
errors: {}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
case "BACK": {
|
|
703
|
+
if (state.history.length === 0) {
|
|
704
|
+
return state;
|
|
705
|
+
}
|
|
706
|
+
const previousStepId = state.history[state.history.length - 1];
|
|
707
|
+
const currentStep = config.steps.find((s) => s.id === state.currentStepId);
|
|
708
|
+
const newAnswers = { ...state.answers };
|
|
709
|
+
if (currentStep && currentStep.keepValuesOnPrevious === false) {
|
|
710
|
+
delete newAnswers[currentStep.id];
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...state,
|
|
714
|
+
currentStepId: previousStepId,
|
|
715
|
+
answers: newAnswers,
|
|
716
|
+
history: state.history.slice(0, -1),
|
|
717
|
+
status: "running",
|
|
718
|
+
errors: {}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
case "JUMP": {
|
|
722
|
+
findStepOrThrow(config, transition.stepId);
|
|
723
|
+
return {
|
|
724
|
+
...state,
|
|
725
|
+
currentStepId: transition.stepId,
|
|
726
|
+
history: [...state.history, state.currentStepId],
|
|
727
|
+
status: "running",
|
|
728
|
+
errors: {}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
case "CANCEL": {
|
|
732
|
+
return { ...state, status: "cancelled" };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function findStepOrThrow(config, stepId) {
|
|
737
|
+
const step = config.steps.find((s) => s.id === stepId);
|
|
738
|
+
if (!step) {
|
|
739
|
+
throw new Error(`Step not found: "${stepId}"`);
|
|
740
|
+
}
|
|
741
|
+
return step;
|
|
742
|
+
}
|
|
743
|
+
function cleanOrphanedAnswers(config, answers, _fromStepId, _toStepId) {
|
|
744
|
+
const cleaned = { ...answers };
|
|
745
|
+
for (const step of config.steps) {
|
|
746
|
+
if (step.id in cleaned && !isStepVisible(step, cleaned)) {
|
|
747
|
+
delete cleaned[step.id];
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return cleaned;
|
|
751
|
+
}
|
|
752
|
+
function applyValidationRule(rule, value) {
|
|
753
|
+
switch (rule.rule) {
|
|
754
|
+
case "required":
|
|
755
|
+
return !value.trim() ? rule.message ?? "This field is required" : null;
|
|
756
|
+
case "minLength":
|
|
757
|
+
return value.length < rule.value ? rule.message ?? `Must be at least ${String(rule.value)} characters` : null;
|
|
758
|
+
case "maxLength":
|
|
759
|
+
return value.length > rule.value ? rule.message ?? `Must be at most ${String(rule.value)} characters` : null;
|
|
760
|
+
case "pattern": {
|
|
761
|
+
const regex = new RegExp(rule.value);
|
|
762
|
+
return !regex.test(value) ? rule.message ?? `Must match pattern: ${rule.value}` : null;
|
|
763
|
+
}
|
|
764
|
+
case "min":
|
|
765
|
+
case "max":
|
|
766
|
+
default:
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/theme.ts
|
|
772
|
+
import chalk from "chalk";
|
|
773
|
+
var DEFAULT_TOKENS = {
|
|
774
|
+
primary: "#5B9BD5",
|
|
775
|
+
success: "#6BCB77",
|
|
776
|
+
error: "#FF6B6B",
|
|
777
|
+
warning: "#FFD93D",
|
|
778
|
+
info: "#4D96FF",
|
|
779
|
+
muted: "#888888",
|
|
780
|
+
accent: "#C084FC"
|
|
781
|
+
};
|
|
782
|
+
var DEFAULT_ICONS = {
|
|
783
|
+
step: "\u25CF",
|
|
784
|
+
stepDone: "\u2713",
|
|
785
|
+
stepPending: "\u25CB",
|
|
786
|
+
pointer: "\u203A"
|
|
787
|
+
};
|
|
788
|
+
function resolveTheme(themeConfig) {
|
|
789
|
+
const tokens = { ...DEFAULT_TOKENS, ...themeConfig?.tokens };
|
|
790
|
+
const icons = { ...DEFAULT_ICONS, ...themeConfig?.icons };
|
|
791
|
+
return {
|
|
792
|
+
primary: chalk.hex(tokens.primary),
|
|
793
|
+
success: chalk.hex(tokens.success),
|
|
794
|
+
error: chalk.hex(tokens.error),
|
|
795
|
+
warning: chalk.hex(tokens.warning),
|
|
796
|
+
info: chalk.hex(tokens.info),
|
|
797
|
+
muted: chalk.hex(tokens.muted),
|
|
798
|
+
accent: chalk.hex(tokens.accent),
|
|
799
|
+
bold: chalk.bold,
|
|
800
|
+
icons
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/resolve.ts
|
|
805
|
+
function resolveEnvDefault(value) {
|
|
806
|
+
if (typeof value !== "string") return value;
|
|
807
|
+
if (!value.startsWith("$")) return value;
|
|
808
|
+
const envKey = value.slice(1);
|
|
809
|
+
return process.env[envKey] ?? value;
|
|
810
|
+
}
|
|
811
|
+
function resolveEnvDefaultNumber(value) {
|
|
812
|
+
if (typeof value === "number") return value;
|
|
813
|
+
const resolved = resolveEnvDefault(typeof value === "string" ? value : void 0);
|
|
814
|
+
if (resolved === void 0) return void 0;
|
|
815
|
+
const num = Number(resolved);
|
|
816
|
+
return Number.isNaN(num) ? void 0 : num;
|
|
817
|
+
}
|
|
818
|
+
function resolveEnvDefaultBoolean(value) {
|
|
819
|
+
if (typeof value === "boolean") return value;
|
|
820
|
+
const resolved = resolveEnvDefault(typeof value === "string" ? value : void 0);
|
|
821
|
+
if (resolved === void 0) return void 0;
|
|
822
|
+
return resolved === "true" || resolved === "1";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/runner.ts
|
|
826
|
+
import { execSync } from "child_process";
|
|
827
|
+
|
|
828
|
+
// src/renderers/inquirer.ts
|
|
829
|
+
import {
|
|
830
|
+
input,
|
|
831
|
+
select,
|
|
832
|
+
checkbox,
|
|
833
|
+
confirm,
|
|
834
|
+
password,
|
|
835
|
+
number,
|
|
836
|
+
search,
|
|
837
|
+
editor,
|
|
838
|
+
Separator
|
|
839
|
+
} from "@inquirer/prompts";
|
|
840
|
+
var InquirerRenderer = class {
|
|
841
|
+
renderStepHeader(stepIndex, totalVisible, message, theme, description) {
|
|
842
|
+
const barWidth = 20;
|
|
843
|
+
const filledCount = totalVisible > 0 ? Math.round(stepIndex / totalVisible * barWidth) : 0;
|
|
844
|
+
const remainingCount = barWidth - filledCount;
|
|
845
|
+
const filledBar = theme.success("\u2588".repeat(filledCount));
|
|
846
|
+
const remainingBar = theme.muted("\u2591".repeat(remainingCount));
|
|
847
|
+
const counter = theme.muted(`Step ${String(stepIndex + 1)}/${String(totalVisible)}`);
|
|
848
|
+
const stepMessage = theme.muted(`\u2014 ${message}`);
|
|
849
|
+
console.log(`
|
|
850
|
+
[${filledBar}${remainingBar}] ${counter} ${stepMessage}`);
|
|
851
|
+
if (description) {
|
|
852
|
+
console.log(` ${theme.muted(description)}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async renderText(step, state, theme) {
|
|
856
|
+
const existingAnswer = state.answers[step.id];
|
|
857
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
858
|
+
return input({
|
|
859
|
+
message: step.message,
|
|
860
|
+
default: defaultValue,
|
|
861
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
async renderSelect(step, state, theme) {
|
|
865
|
+
const existingAnswer = state.answers[step.id];
|
|
866
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
867
|
+
const choices = step.options.map((opt) => {
|
|
868
|
+
if ("separator" in opt) {
|
|
869
|
+
return new Separator(opt.separator);
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
name: opt.label,
|
|
873
|
+
value: opt.value,
|
|
874
|
+
description: opt.hint,
|
|
875
|
+
disabled: opt.disabled
|
|
876
|
+
};
|
|
877
|
+
});
|
|
878
|
+
return select({
|
|
879
|
+
message: step.message,
|
|
880
|
+
choices,
|
|
881
|
+
default: defaultValue,
|
|
882
|
+
pageSize: step.pageSize,
|
|
883
|
+
loop: step.loop,
|
|
884
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async renderMultiSelect(step, state, theme) {
|
|
888
|
+
const existingAnswer = state.answers[step.id];
|
|
889
|
+
const previousSelections = Array.isArray(existingAnswer) ? existingAnswer.filter((v) => typeof v === "string") : step.default;
|
|
890
|
+
const choices = step.options.map((opt) => {
|
|
891
|
+
if ("separator" in opt) {
|
|
892
|
+
return new Separator(opt.separator);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
name: opt.label,
|
|
896
|
+
value: opt.value,
|
|
897
|
+
checked: previousSelections?.includes(opt.value) ?? false,
|
|
898
|
+
disabled: opt.disabled
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
return checkbox({
|
|
902
|
+
message: step.message,
|
|
903
|
+
choices,
|
|
904
|
+
pageSize: step.pageSize,
|
|
905
|
+
loop: step.loop,
|
|
906
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
async renderConfirm(step, state, theme) {
|
|
910
|
+
const existingAnswer = state.answers[step.id];
|
|
911
|
+
const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer : step.default;
|
|
912
|
+
return confirm({
|
|
913
|
+
message: step.message,
|
|
914
|
+
default: defaultValue ?? true,
|
|
915
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async renderPassword(step, _state, theme) {
|
|
919
|
+
return password({
|
|
920
|
+
message: step.message,
|
|
921
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
async renderNumber(step, state, theme) {
|
|
925
|
+
const existingAnswer = state.answers[step.id];
|
|
926
|
+
const defaultValue = typeof existingAnswer === "number" ? existingAnswer : step.default;
|
|
927
|
+
const result = await number({
|
|
928
|
+
message: step.message,
|
|
929
|
+
default: defaultValue,
|
|
930
|
+
min: step.min,
|
|
931
|
+
max: step.max,
|
|
932
|
+
step: step.step,
|
|
933
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
934
|
+
});
|
|
935
|
+
return result ?? defaultValue ?? 0;
|
|
936
|
+
}
|
|
937
|
+
async renderSearch(step, _state, theme) {
|
|
938
|
+
return search({
|
|
939
|
+
message: step.message,
|
|
940
|
+
source: (input3) => {
|
|
941
|
+
const term = (input3 ?? "").toLowerCase();
|
|
942
|
+
return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(term)).map((opt) => ({
|
|
943
|
+
name: opt.label,
|
|
944
|
+
value: opt.value,
|
|
945
|
+
description: opt.hint
|
|
946
|
+
}));
|
|
947
|
+
},
|
|
948
|
+
pageSize: step.pageSize,
|
|
949
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
async renderEditor(step, _state, theme) {
|
|
953
|
+
return editor({
|
|
954
|
+
message: step.message,
|
|
955
|
+
default: step.default,
|
|
956
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async renderPath(step, state, theme) {
|
|
960
|
+
const existingAnswer = state.answers[step.id];
|
|
961
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
962
|
+
return input({
|
|
963
|
+
message: step.message,
|
|
964
|
+
default: defaultValue,
|
|
965
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
async renderToggle(step, state, theme) {
|
|
969
|
+
const existingAnswer = state.answers[step.id];
|
|
970
|
+
const activeLabel = step.active ?? "On";
|
|
971
|
+
const inactiveLabel = step.inactive ?? "Off";
|
|
972
|
+
const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer ? activeLabel : inactiveLabel : step.default === true ? activeLabel : inactiveLabel;
|
|
973
|
+
const result = await select({
|
|
974
|
+
message: step.message,
|
|
975
|
+
choices: [
|
|
976
|
+
{ name: activeLabel, value: activeLabel },
|
|
977
|
+
{ name: inactiveLabel, value: inactiveLabel }
|
|
978
|
+
],
|
|
979
|
+
default: defaultValue,
|
|
980
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
981
|
+
});
|
|
982
|
+
return result === activeLabel;
|
|
983
|
+
}
|
|
984
|
+
renderMessage(step, _state, theme) {
|
|
985
|
+
if (step.description) {
|
|
986
|
+
console.log(` ${theme.muted(step.description)}`);
|
|
987
|
+
}
|
|
988
|
+
console.log();
|
|
989
|
+
}
|
|
990
|
+
renderGroupHeader(group, theme) {
|
|
991
|
+
console.log(`
|
|
992
|
+
${theme.accent("\u2500\u2500")} ${theme.bold(group)} ${theme.accent("\u2500\u2500")}
|
|
993
|
+
`);
|
|
994
|
+
}
|
|
995
|
+
renderSummary(answers, steps, theme) {
|
|
996
|
+
console.log(`
|
|
997
|
+
${theme.muted("\u2500".repeat(40))}`);
|
|
998
|
+
console.log(` ${theme.bold("Summary")}
|
|
999
|
+
`);
|
|
1000
|
+
for (const step of steps) {
|
|
1001
|
+
const answer = answers[step.id];
|
|
1002
|
+
if (answer === void 0) continue;
|
|
1003
|
+
const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
|
|
1004
|
+
console.log(
|
|
1005
|
+
` ${theme.muted(step.id.padEnd(20))} ${theme.primary(display)}`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
console.log(theme.muted("\u2500".repeat(40)));
|
|
1009
|
+
}
|
|
1010
|
+
clear() {
|
|
1011
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// src/template.ts
|
|
1016
|
+
function resolveTemplate(template, answers) {
|
|
1017
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
|
|
1018
|
+
const trimmedKey = key.trim();
|
|
1019
|
+
if (trimmedKey in answers) {
|
|
1020
|
+
const value = answers[trimmedKey];
|
|
1021
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
1022
|
+
return String(value);
|
|
1023
|
+
}
|
|
1024
|
+
return _match;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/banner.ts
|
|
1029
|
+
import figlet from "figlet";
|
|
1030
|
+
import gradient from "gradient-string";
|
|
1031
|
+
var GRIMOIRE_GRADIENT = gradient(["#C084FC", "#5B9BD5", "#6BCB77"]);
|
|
1032
|
+
function renderBanner(name, theme, options) {
|
|
1033
|
+
if (options?.plain) {
|
|
1034
|
+
return ` ${theme.bold(name)}`;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const art = figlet.textSync(name, {
|
|
1038
|
+
font: "Small",
|
|
1039
|
+
horizontalLayout: "default"
|
|
1040
|
+
});
|
|
1041
|
+
const lines = art.split("\n").map((line) => ` ${line}`).join("\n");
|
|
1042
|
+
return GRIMOIRE_GRADIENT(lines);
|
|
1043
|
+
} catch {
|
|
1044
|
+
return ` ${theme.bold(name)}`;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/plugins.ts
|
|
1049
|
+
var BUILT_IN_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
1050
|
+
"text",
|
|
1051
|
+
"select",
|
|
1052
|
+
"multiselect",
|
|
1053
|
+
"confirm",
|
|
1054
|
+
"password",
|
|
1055
|
+
"number",
|
|
1056
|
+
"search",
|
|
1057
|
+
"editor",
|
|
1058
|
+
"path",
|
|
1059
|
+
"toggle"
|
|
1060
|
+
]);
|
|
1061
|
+
var pluginStepRegistry = /* @__PURE__ */ new Map();
|
|
1062
|
+
function registerPlugin(plugin) {
|
|
1063
|
+
for (const [stepType, stepPlugin] of Object.entries(plugin.steps)) {
|
|
1064
|
+
if (BUILT_IN_STEP_TYPES.has(stepType)) {
|
|
1065
|
+
throw new Error(`Cannot override built-in step type "${stepType}"`);
|
|
1066
|
+
}
|
|
1067
|
+
if (pluginStepRegistry.has(stepType)) {
|
|
1068
|
+
throw new Error(`Step type "${stepType}" is already registered`);
|
|
1069
|
+
}
|
|
1070
|
+
pluginStepRegistry.set(stepType, stepPlugin);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function getPluginStep(stepType) {
|
|
1074
|
+
return pluginStepRegistry.get(stepType);
|
|
1075
|
+
}
|
|
1076
|
+
function clearPlugins() {
|
|
1077
|
+
pluginStepRegistry.clear();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/cache.ts
|
|
1081
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync, unlinkSync, readdirSync } from "fs";
|
|
1082
|
+
import { join } from "path";
|
|
1083
|
+
import { homedir } from "os";
|
|
1084
|
+
var DEFAULT_CACHE_DIR = join(homedir(), ".config", "grimoire", "cache");
|
|
1085
|
+
function slugify(name) {
|
|
1086
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1087
|
+
}
|
|
1088
|
+
function getCacheDir(customDir) {
|
|
1089
|
+
return customDir ?? DEFAULT_CACHE_DIR;
|
|
1090
|
+
}
|
|
1091
|
+
function getCacheFilePath(wizardName, customDir) {
|
|
1092
|
+
return join(getCacheDir(customDir), `${slugify(wizardName)}.json`);
|
|
1093
|
+
}
|
|
1094
|
+
function loadCachedAnswers(wizardName, customDir) {
|
|
1095
|
+
try {
|
|
1096
|
+
const filePath = getCacheFilePath(wizardName, customDir);
|
|
1097
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
1098
|
+
const parsed = JSON.parse(raw);
|
|
1099
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1100
|
+
return parsed;
|
|
1101
|
+
}
|
|
1102
|
+
return void 0;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function saveCachedAnswers(wizardName, answers, customDir) {
|
|
1108
|
+
try {
|
|
1109
|
+
const dir = getCacheDir(customDir);
|
|
1110
|
+
mkdirSync(dir, { recursive: true });
|
|
1111
|
+
const filePath = getCacheFilePath(wizardName, customDir);
|
|
1112
|
+
writeFileSync(filePath, JSON.stringify(answers, null, 2) + "\n", "utf-8");
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function clearCache(wizardName, customDir) {
|
|
1117
|
+
try {
|
|
1118
|
+
const dir = getCacheDir(customDir);
|
|
1119
|
+
if (wizardName) {
|
|
1120
|
+
const filePath = getCacheFilePath(wizardName, customDir);
|
|
1121
|
+
unlinkSync(filePath);
|
|
1122
|
+
} else {
|
|
1123
|
+
const files = readdirSync(dir);
|
|
1124
|
+
for (const file of files) {
|
|
1125
|
+
if (file.endsWith(".json")) {
|
|
1126
|
+
unlinkSync(join(dir, file));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/mru.ts
|
|
1135
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
1136
|
+
import { join as join2 } from "path";
|
|
1137
|
+
import { homedir as homedir2 } from "os";
|
|
1138
|
+
var MRU_DIR = join2(homedir2(), ".config", "grimoire", "mru");
|
|
1139
|
+
function getMruFilePath(wizardName) {
|
|
1140
|
+
const safeName = wizardName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1141
|
+
return join2(MRU_DIR, `${safeName}.json`);
|
|
1142
|
+
}
|
|
1143
|
+
function loadMruData(wizardName) {
|
|
1144
|
+
const filePath = getMruFilePath(wizardName);
|
|
1145
|
+
try {
|
|
1146
|
+
if (!existsSync(filePath)) {
|
|
1147
|
+
return {};
|
|
1148
|
+
}
|
|
1149
|
+
const raw = readFileSync3(filePath, "utf-8");
|
|
1150
|
+
const parsed = JSON.parse(raw);
|
|
1151
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1152
|
+
return {};
|
|
1153
|
+
}
|
|
1154
|
+
return parsed;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return {};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function saveMruData(wizardName, data) {
|
|
1160
|
+
const filePath = getMruFilePath(wizardName);
|
|
1161
|
+
try {
|
|
1162
|
+
mkdirSync2(MRU_DIR, { recursive: true });
|
|
1163
|
+
writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1164
|
+
} catch {
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function recordSelection(wizardName, stepId, value) {
|
|
1168
|
+
const data = loadMruData(wizardName);
|
|
1169
|
+
const stepData = data[stepId] ?? {};
|
|
1170
|
+
const values = Array.isArray(value) ? value : [value];
|
|
1171
|
+
for (const v of values) {
|
|
1172
|
+
stepData[v] = (stepData[v] ?? 0) + 1;
|
|
1173
|
+
}
|
|
1174
|
+
data[stepId] = stepData;
|
|
1175
|
+
saveMruData(wizardName, data);
|
|
1176
|
+
}
|
|
1177
|
+
function getOrderedOptions(wizardName, stepId, options) {
|
|
1178
|
+
const data = loadMruData(wizardName);
|
|
1179
|
+
const stepData = data[stepId] ?? {};
|
|
1180
|
+
if (Object.keys(stepData).length === 0) {
|
|
1181
|
+
return options;
|
|
1182
|
+
}
|
|
1183
|
+
const separatorIndices = /* @__PURE__ */ new Map();
|
|
1184
|
+
const selectableOptions = [];
|
|
1185
|
+
for (let i = 0; i < options.length; i++) {
|
|
1186
|
+
const opt = options[i];
|
|
1187
|
+
if ("separator" in opt) {
|
|
1188
|
+
separatorIndices.set(i, opt);
|
|
1189
|
+
} else {
|
|
1190
|
+
selectableOptions.push(opt);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const sorted = [...selectableOptions].sort((a, b) => {
|
|
1194
|
+
if (!("value" in a) || !("value" in b)) return 0;
|
|
1195
|
+
const countA = stepData[a.value] ?? 0;
|
|
1196
|
+
const countB = stepData[b.value] ?? 0;
|
|
1197
|
+
return countB - countA;
|
|
1198
|
+
});
|
|
1199
|
+
const result = [];
|
|
1200
|
+
let sortedIndex = 0;
|
|
1201
|
+
for (let i = 0; i < options.length; i++) {
|
|
1202
|
+
const sep = separatorIndices.get(i);
|
|
1203
|
+
if (sep) {
|
|
1204
|
+
result.push(sep);
|
|
1205
|
+
} else {
|
|
1206
|
+
const next = sorted[sortedIndex];
|
|
1207
|
+
if (next) {
|
|
1208
|
+
result.push(next);
|
|
1209
|
+
}
|
|
1210
|
+
sortedIndex++;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return result;
|
|
1214
|
+
}
|
|
1215
|
+
function clearMruData(wizardName) {
|
|
1216
|
+
const filePath = getMruFilePath(wizardName);
|
|
1217
|
+
try {
|
|
1218
|
+
if (existsSync(filePath)) {
|
|
1219
|
+
writeFileSync2(filePath, "{}", "utf-8");
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/runner.ts
|
|
1226
|
+
function runPreFlightChecks(checks, theme) {
|
|
1227
|
+
for (const check of checks) {
|
|
1228
|
+
try {
|
|
1229
|
+
execSync(check.run, { stdio: "pipe" });
|
|
1230
|
+
console.log(` ${theme.success("\u2713")} ${check.name}`);
|
|
1231
|
+
} catch {
|
|
1232
|
+
console.log(` ${theme.error("\u2717")} ${check.name}: ${check.message}`);
|
|
1233
|
+
throw new Error(`Pre-flight check failed: ${check.name} \u2014 ${check.message}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
console.log();
|
|
1237
|
+
}
|
|
1238
|
+
function getMockValue(step, mockAnswers) {
|
|
1239
|
+
if (step.id in mockAnswers) {
|
|
1240
|
+
return mockAnswers[step.id];
|
|
1241
|
+
}
|
|
1242
|
+
if (step.type === "message") {
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
const defaultValue = getStepDefault(step);
|
|
1246
|
+
if (defaultValue !== void 0) {
|
|
1247
|
+
return defaultValue;
|
|
1248
|
+
}
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`Mock mode: no answer provided for step "${step.id}" and no default available`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
function getStepDefault(step) {
|
|
1254
|
+
switch (step.type) {
|
|
1255
|
+
case "text":
|
|
1256
|
+
case "select":
|
|
1257
|
+
case "search":
|
|
1258
|
+
case "editor":
|
|
1259
|
+
case "path":
|
|
1260
|
+
return step.default;
|
|
1261
|
+
case "number":
|
|
1262
|
+
return step.default;
|
|
1263
|
+
case "confirm":
|
|
1264
|
+
case "toggle":
|
|
1265
|
+
return step.default;
|
|
1266
|
+
case "multiselect":
|
|
1267
|
+
return step.default;
|
|
1268
|
+
case "password":
|
|
1269
|
+
case "message":
|
|
1270
|
+
return void 0;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
async function runWizard(config, options) {
|
|
1274
|
+
const renderer = options?.renderer ?? new InquirerRenderer();
|
|
1275
|
+
const theme = resolveTheme(config.theme);
|
|
1276
|
+
const mockAnswers = options?.mockAnswers;
|
|
1277
|
+
const isMock = mockAnswers !== void 0;
|
|
1278
|
+
const quiet = options?.quiet ?? isMock;
|
|
1279
|
+
const cacheEnabled = !isMock && options?.cache !== false;
|
|
1280
|
+
const cacheDir = typeof options?.cache === "object" ? options.cache.dir : void 0;
|
|
1281
|
+
const mruEnabled = !isMock && options?.mru !== false;
|
|
1282
|
+
let state = createWizardState(config);
|
|
1283
|
+
const cachedAnswers = cacheEnabled ? loadCachedAnswers(config.meta.name, cacheDir) : void 0;
|
|
1284
|
+
const userPlugins = options?.plugins;
|
|
1285
|
+
if (userPlugins) {
|
|
1286
|
+
for (const plugin of userPlugins) {
|
|
1287
|
+
registerPlugin(plugin);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
if (!isMock && config.checks && config.checks.length > 0) {
|
|
1292
|
+
runPreFlightChecks(config.checks, theme);
|
|
1293
|
+
}
|
|
1294
|
+
if (!quiet) {
|
|
1295
|
+
printWizardHeader(config, theme, options?.plain);
|
|
1296
|
+
}
|
|
1297
|
+
let previousGroup;
|
|
1298
|
+
while (state.status === "running") {
|
|
1299
|
+
const visibleSteps = getVisibleSteps(config, state.answers);
|
|
1300
|
+
const currentStep = config.steps.find((s) => s.id === state.currentStepId);
|
|
1301
|
+
if (!currentStep) {
|
|
1302
|
+
throw new Error(`Current step not found: "${state.currentStepId}"`);
|
|
1303
|
+
}
|
|
1304
|
+
if (!isMock) {
|
|
1305
|
+
if (currentStep.group !== void 0 && currentStep.group !== previousGroup) {
|
|
1306
|
+
const resolvedGroup = resolveTemplate(currentStep.group, state.answers);
|
|
1307
|
+
renderer.renderGroupHeader(resolvedGroup, theme);
|
|
1308
|
+
}
|
|
1309
|
+
previousGroup = currentStep.group;
|
|
1310
|
+
const stepIndex = visibleSteps.findIndex((s) => s.id === state.currentStepId);
|
|
1311
|
+
const resolvedMessage = resolveTemplate(currentStep.message, state.answers);
|
|
1312
|
+
const resolvedDescription = currentStep.description ? resolveTemplate(currentStep.description, state.answers) : void 0;
|
|
1313
|
+
renderer.renderStepHeader(stepIndex, visibleSteps.length, resolvedMessage, theme, resolvedDescription);
|
|
1314
|
+
}
|
|
1315
|
+
if (options?.onBeforeStep) {
|
|
1316
|
+
await options.onBeforeStep(currentStep.id, currentStep, state);
|
|
1317
|
+
}
|
|
1318
|
+
const pluginStep = getPluginStep(currentStep.type);
|
|
1319
|
+
const resolvedStep = pluginStep ? currentStep : resolveStepDefaults(currentStep, cachedAnswers);
|
|
1320
|
+
const withTemplate = options?.templateAnswers ? applyTemplateDefaults(resolvedStep, options.templateAnswers) : resolvedStep;
|
|
1321
|
+
const templatedStep = resolveStepTemplates(withTemplate, state.answers);
|
|
1322
|
+
const mruStep = mruEnabled ? applyMruOrdering(templatedStep, config.meta.name) : templatedStep;
|
|
1323
|
+
try {
|
|
1324
|
+
const value = isMock ? getMockValue(mruStep, mockAnswers) : pluginStep ? await pluginStep.render(toStepRecord(mruStep), state, theme) : await renderStep(renderer, mruStep, state, theme);
|
|
1325
|
+
if (pluginStep?.validate) {
|
|
1326
|
+
const pluginError = pluginStep.validate(value, toStepRecord(templatedStep));
|
|
1327
|
+
if (pluginError) {
|
|
1328
|
+
if (isMock) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`Mock mode: validation failed for step "${currentStep.id}": ${pluginError}`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
console.log(theme.error(`
|
|
1334
|
+
${pluginError}
|
|
1335
|
+
`));
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const nextState = wizardReducer(state, { type: "NEXT", value }, config);
|
|
1340
|
+
if (nextState.errors[currentStep.id]) {
|
|
1341
|
+
const errorMsg = resolveTemplate(nextState.errors[currentStep.id] ?? "", state.answers);
|
|
1342
|
+
if (isMock) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Mock mode: validation failed for step "${currentStep.id}": ${errorMsg}`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
console.log(theme.error(`
|
|
1348
|
+
${errorMsg}
|
|
1349
|
+
`));
|
|
1350
|
+
state = { ...nextState, errors: {} };
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (!isMock && options?.asyncValidate) {
|
|
1354
|
+
const asyncError = await options.asyncValidate(currentStep.id, value, nextState.answers);
|
|
1355
|
+
if (asyncError !== null) {
|
|
1356
|
+
console.log(theme.error(`
|
|
1357
|
+
${asyncError}
|
|
1358
|
+
`));
|
|
1359
|
+
state = { ...nextState, errors: {} };
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (options?.onAfterStep) {
|
|
1364
|
+
await options.onAfterStep(currentStep.id, value, nextState);
|
|
1365
|
+
}
|
|
1366
|
+
state = nextState;
|
|
1367
|
+
if (mruEnabled && isSelectLikeStep(currentStep.type)) {
|
|
1368
|
+
recordSelection(config.meta.name, currentStep.id, value);
|
|
1369
|
+
}
|
|
1370
|
+
options?.onStepComplete?.(currentStep.id, value, state);
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
if (!isMock && isUserCancel(error)) {
|
|
1373
|
+
state = wizardReducer(state, { type: "CANCEL" }, config);
|
|
1374
|
+
options?.onCancel?.(state);
|
|
1375
|
+
if (!quiet) {
|
|
1376
|
+
console.log(theme.warning("\n Wizard cancelled.\n"));
|
|
1377
|
+
}
|
|
1378
|
+
return state.answers;
|
|
1379
|
+
}
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (state.status === "done" && !quiet) {
|
|
1384
|
+
renderer.renderSummary(state.answers, config.steps, theme);
|
|
1385
|
+
}
|
|
1386
|
+
if (state.status === "done" && config.actions && config.actions.length > 0 && !isMock) {
|
|
1387
|
+
await executeActions(config.actions, state.answers, theme);
|
|
1388
|
+
}
|
|
1389
|
+
if (state.status === "done" && cacheEnabled) {
|
|
1390
|
+
const passwordStepIds = new Set(
|
|
1391
|
+
config.steps.filter((s) => s.type === "password").map((s) => s.id)
|
|
1392
|
+
);
|
|
1393
|
+
const answersToCache = {};
|
|
1394
|
+
for (const [key, value] of Object.entries(state.answers)) {
|
|
1395
|
+
if (!passwordStepIds.has(key)) {
|
|
1396
|
+
answersToCache[key] = value;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
saveCachedAnswers(config.meta.name, answersToCache, cacheDir);
|
|
1400
|
+
}
|
|
1401
|
+
return state.answers;
|
|
1402
|
+
} finally {
|
|
1403
|
+
if (userPlugins) {
|
|
1404
|
+
clearPlugins();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function toStepRecord(step) {
|
|
1409
|
+
const record = {};
|
|
1410
|
+
for (const [key, val] of Object.entries(step)) {
|
|
1411
|
+
record[key] = val;
|
|
1412
|
+
}
|
|
1413
|
+
return record;
|
|
1414
|
+
}
|
|
1415
|
+
function renderStep(renderer, step, state, theme) {
|
|
1416
|
+
switch (step.type) {
|
|
1417
|
+
case "text":
|
|
1418
|
+
return renderer.renderText(step, state, theme);
|
|
1419
|
+
case "select":
|
|
1420
|
+
return renderer.renderSelect(step, state, theme);
|
|
1421
|
+
case "multiselect":
|
|
1422
|
+
return renderer.renderMultiSelect(step, state, theme);
|
|
1423
|
+
case "confirm":
|
|
1424
|
+
return renderer.renderConfirm(step, state, theme);
|
|
1425
|
+
case "password":
|
|
1426
|
+
return renderer.renderPassword(step, state, theme);
|
|
1427
|
+
case "number":
|
|
1428
|
+
return renderer.renderNumber(step, state, theme);
|
|
1429
|
+
case "search":
|
|
1430
|
+
return renderer.renderSearch(step, state, theme);
|
|
1431
|
+
case "editor":
|
|
1432
|
+
return renderer.renderEditor(step, state, theme);
|
|
1433
|
+
case "path":
|
|
1434
|
+
return renderer.renderPath(step, state, theme);
|
|
1435
|
+
case "toggle":
|
|
1436
|
+
return renderer.renderToggle(step, state, theme);
|
|
1437
|
+
case "message":
|
|
1438
|
+
renderer.renderMessage(step, state, theme);
|
|
1439
|
+
return Promise.resolve(true);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function resolveStepDefaults(step, cachedAnswers) {
|
|
1443
|
+
switch (step.type) {
|
|
1444
|
+
case "text": {
|
|
1445
|
+
const envResolved = resolveEnvDefault(step.default);
|
|
1446
|
+
const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1447
|
+
return { ...step, default: fallback };
|
|
1448
|
+
}
|
|
1449
|
+
case "search": {
|
|
1450
|
+
const envResolved = resolveEnvDefault(step.default);
|
|
1451
|
+
const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1452
|
+
return { ...step, default: fallback };
|
|
1453
|
+
}
|
|
1454
|
+
case "editor": {
|
|
1455
|
+
const envResolved = resolveEnvDefault(step.default);
|
|
1456
|
+
const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1457
|
+
return { ...step, default: fallback };
|
|
1458
|
+
}
|
|
1459
|
+
case "path": {
|
|
1460
|
+
const envResolved = resolveEnvDefault(step.default);
|
|
1461
|
+
const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1462
|
+
return { ...step, default: fallback };
|
|
1463
|
+
}
|
|
1464
|
+
case "select": {
|
|
1465
|
+
const envResolved = resolveEnvDefault(step.default);
|
|
1466
|
+
const fallback = envResolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1467
|
+
return { ...step, default: fallback };
|
|
1468
|
+
}
|
|
1469
|
+
case "number": {
|
|
1470
|
+
const resolved = resolveEnvDefaultNumber(step.default);
|
|
1471
|
+
const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1472
|
+
return { ...step, default: fallback };
|
|
1473
|
+
}
|
|
1474
|
+
case "confirm": {
|
|
1475
|
+
const resolved = resolveEnvDefaultBoolean(step.default);
|
|
1476
|
+
const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1477
|
+
return { ...step, default: fallback };
|
|
1478
|
+
}
|
|
1479
|
+
case "toggle": {
|
|
1480
|
+
const resolved = resolveEnvDefaultBoolean(step.default);
|
|
1481
|
+
const fallback = resolved ?? getCachedDefault(step.id, cachedAnswers);
|
|
1482
|
+
return { ...step, default: fallback };
|
|
1483
|
+
}
|
|
1484
|
+
case "multiselect": {
|
|
1485
|
+
const fallback = step.default ?? getCachedDefault(step.id, cachedAnswers);
|
|
1486
|
+
return { ...step, default: fallback };
|
|
1487
|
+
}
|
|
1488
|
+
case "password":
|
|
1489
|
+
case "message":
|
|
1490
|
+
return step;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function getCachedDefault(stepId, cachedAnswers) {
|
|
1494
|
+
if (!cachedAnswers || !(stepId in cachedAnswers)) return void 0;
|
|
1495
|
+
return cachedAnswers[stepId];
|
|
1496
|
+
}
|
|
1497
|
+
function applyTemplateDefaults(step, templateAnswers) {
|
|
1498
|
+
if (!(step.id in templateAnswers)) return step;
|
|
1499
|
+
if (step.type === "password" || step.type === "message") return step;
|
|
1500
|
+
const value = templateAnswers[step.id];
|
|
1501
|
+
switch (step.type) {
|
|
1502
|
+
case "text":
|
|
1503
|
+
case "select":
|
|
1504
|
+
case "search":
|
|
1505
|
+
case "editor":
|
|
1506
|
+
case "path":
|
|
1507
|
+
return { ...step, default: typeof value === "string" ? value : step.default };
|
|
1508
|
+
case "number":
|
|
1509
|
+
return { ...step, default: typeof value === "number" ? value : step.default };
|
|
1510
|
+
case "confirm":
|
|
1511
|
+
case "toggle":
|
|
1512
|
+
return { ...step, default: typeof value === "boolean" ? value : step.default };
|
|
1513
|
+
case "multiselect":
|
|
1514
|
+
return { ...step, default: Array.isArray(value) ? value : step.default };
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function isSelectLikeStep(type) {
|
|
1518
|
+
return type === "select" || type === "multiselect" || type === "search";
|
|
1519
|
+
}
|
|
1520
|
+
function applyMruOrdering(step, wizardName) {
|
|
1521
|
+
if (step.type === "select") {
|
|
1522
|
+
return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
|
|
1523
|
+
}
|
|
1524
|
+
if (step.type === "multiselect") {
|
|
1525
|
+
return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
|
|
1526
|
+
}
|
|
1527
|
+
if (step.type === "search") {
|
|
1528
|
+
return { ...step, options: getOrderedOptions(wizardName, step.id, step.options) };
|
|
1529
|
+
}
|
|
1530
|
+
return step;
|
|
1531
|
+
}
|
|
1532
|
+
function resolveChoiceTemplates(options, answers) {
|
|
1533
|
+
return options.map((opt) => {
|
|
1534
|
+
if ("separator" in opt) return opt;
|
|
1535
|
+
return {
|
|
1536
|
+
...opt,
|
|
1537
|
+
label: resolveTemplate(opt.label, answers),
|
|
1538
|
+
hint: opt.hint ? resolveTemplate(opt.hint, answers) : void 0
|
|
1539
|
+
};
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function resolveStepTemplates(step, answers) {
|
|
1543
|
+
switch (step.type) {
|
|
1544
|
+
case "text":
|
|
1545
|
+
return {
|
|
1546
|
+
...step,
|
|
1547
|
+
placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
|
|
1548
|
+
default: step.default ? resolveTemplate(step.default, answers) : void 0,
|
|
1549
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1550
|
+
};
|
|
1551
|
+
case "select":
|
|
1552
|
+
return {
|
|
1553
|
+
...step,
|
|
1554
|
+
options: resolveChoiceTemplates(step.options, answers),
|
|
1555
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1556
|
+
};
|
|
1557
|
+
case "multiselect":
|
|
1558
|
+
return {
|
|
1559
|
+
...step,
|
|
1560
|
+
options: resolveChoiceTemplates(step.options, answers),
|
|
1561
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1562
|
+
};
|
|
1563
|
+
case "search":
|
|
1564
|
+
return {
|
|
1565
|
+
...step,
|
|
1566
|
+
placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
|
|
1567
|
+
options: resolveChoiceTemplates(step.options, answers),
|
|
1568
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1569
|
+
};
|
|
1570
|
+
case "path":
|
|
1571
|
+
return {
|
|
1572
|
+
...step,
|
|
1573
|
+
placeholder: step.placeholder ? resolveTemplate(step.placeholder, answers) : void 0,
|
|
1574
|
+
default: step.default ? resolveTemplate(step.default, answers) : void 0,
|
|
1575
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1576
|
+
};
|
|
1577
|
+
case "editor":
|
|
1578
|
+
return {
|
|
1579
|
+
...step,
|
|
1580
|
+
default: step.default ? resolveTemplate(step.default, answers) : void 0,
|
|
1581
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1582
|
+
};
|
|
1583
|
+
case "password":
|
|
1584
|
+
case "number":
|
|
1585
|
+
case "confirm":
|
|
1586
|
+
case "toggle":
|
|
1587
|
+
case "message":
|
|
1588
|
+
return {
|
|
1589
|
+
...step,
|
|
1590
|
+
description: step.description ? resolveTemplate(step.description, answers) : void 0
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
async function executeActions(actions, answers, theme) {
|
|
1595
|
+
console.log(`
|
|
1596
|
+
${theme.bold("Running actions...")}
|
|
1597
|
+
`);
|
|
1598
|
+
for (const action of actions) {
|
|
1599
|
+
if (action.when && !evaluateCondition(action.when, answers)) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
const resolvedCommand = resolveTemplate(action.run, answers);
|
|
1603
|
+
const resolvedName = action.name ? resolveTemplate(action.name, answers) : void 0;
|
|
1604
|
+
const label = resolvedName ?? resolvedCommand;
|
|
1605
|
+
try {
|
|
1606
|
+
execSync(resolvedCommand, { stdio: "pipe" });
|
|
1607
|
+
console.log(` ${theme.success("\u2713")} ${label}`);
|
|
1608
|
+
} catch {
|
|
1609
|
+
console.log(` ${theme.error("\u2717")} ${label}`);
|
|
1610
|
+
throw new Error(`Action failed: ${label}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
console.log();
|
|
1614
|
+
}
|
|
1615
|
+
function printWizardHeader(config, theme, plain) {
|
|
1616
|
+
console.log();
|
|
1617
|
+
console.log(renderBanner(config.meta.name, theme, { plain }));
|
|
1618
|
+
if (config.meta.description) {
|
|
1619
|
+
console.log(` ${theme.muted(config.meta.description)}`);
|
|
1620
|
+
}
|
|
1621
|
+
console.log();
|
|
1622
|
+
}
|
|
1623
|
+
function isUserCancel(error) {
|
|
1624
|
+
if (error instanceof Error) {
|
|
1625
|
+
return error.message.includes("User force closed") || error.name === "ExitPromptError";
|
|
1626
|
+
}
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/define.ts
|
|
1631
|
+
function defineWizard(config) {
|
|
1632
|
+
return config;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/renderers/ink.ts
|
|
1636
|
+
import {
|
|
1637
|
+
input as input2,
|
|
1638
|
+
select as select2,
|
|
1639
|
+
checkbox as checkbox2,
|
|
1640
|
+
confirm as confirm2,
|
|
1641
|
+
password as password2,
|
|
1642
|
+
number as number2,
|
|
1643
|
+
search as search2,
|
|
1644
|
+
editor as editor2,
|
|
1645
|
+
Separator as Separator2
|
|
1646
|
+
} from "@inquirer/prompts";
|
|
1647
|
+
function boxLine(text, theme) {
|
|
1648
|
+
const line = "\u2500".repeat(Math.max(0, 40 - text.length));
|
|
1649
|
+
return `${theme.accent("\u250C\u2500")} ${theme.bold(text)} ${theme.accent(`${line}\u2510`)}`;
|
|
1650
|
+
}
|
|
1651
|
+
var InkRenderer = class {
|
|
1652
|
+
renderStepHeader(stepIndex, totalVisible, message, theme, description) {
|
|
1653
|
+
const barWidth = 30;
|
|
1654
|
+
const progress = totalVisible > 0 ? stepIndex / totalVisible : 0;
|
|
1655
|
+
const filledCount = Math.round(progress * barWidth);
|
|
1656
|
+
const remainingCount = barWidth - filledCount;
|
|
1657
|
+
const filledBar = theme.success("\u2593".repeat(filledCount));
|
|
1658
|
+
const remainingBar = theme.muted("\u2591".repeat(remainingCount));
|
|
1659
|
+
const pct = `${String(Math.round(progress * 100))}%`;
|
|
1660
|
+
const counter = theme.muted(`Step ${String(stepIndex + 1)}/${String(totalVisible)}`);
|
|
1661
|
+
console.log();
|
|
1662
|
+
console.log(` ${theme.accent("\u250C")} ${counter} ${theme.muted(pct)}`);
|
|
1663
|
+
console.log(` ${theme.accent("\u2502")} [${filledBar}${remainingBar}]`);
|
|
1664
|
+
console.log(` ${theme.accent("\u2514\u2500")} ${theme.primary(message)}`);
|
|
1665
|
+
if (description) {
|
|
1666
|
+
console.log(` ${theme.muted(description)}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async renderText(step, state, theme) {
|
|
1670
|
+
const existingAnswer = state.answers[step.id];
|
|
1671
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
1672
|
+
return input2({
|
|
1673
|
+
message: step.message,
|
|
1674
|
+
default: defaultValue,
|
|
1675
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
async renderSelect(step, state, theme) {
|
|
1679
|
+
const existingAnswer = state.answers[step.id];
|
|
1680
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
1681
|
+
const choices = step.options.map((opt) => {
|
|
1682
|
+
if ("separator" in opt) {
|
|
1683
|
+
return new Separator2(opt.separator);
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
name: opt.label,
|
|
1687
|
+
value: opt.value,
|
|
1688
|
+
description: opt.hint,
|
|
1689
|
+
disabled: opt.disabled
|
|
1690
|
+
};
|
|
1691
|
+
});
|
|
1692
|
+
return select2({
|
|
1693
|
+
message: step.message,
|
|
1694
|
+
choices,
|
|
1695
|
+
default: defaultValue,
|
|
1696
|
+
pageSize: step.pageSize,
|
|
1697
|
+
loop: step.loop,
|
|
1698
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
async renderMultiSelect(step, state, theme) {
|
|
1702
|
+
const existingAnswer = state.answers[step.id];
|
|
1703
|
+
const previousSelections = Array.isArray(existingAnswer) ? existingAnswer.filter((v) => typeof v === "string") : step.default;
|
|
1704
|
+
const choices = step.options.map((opt) => {
|
|
1705
|
+
if ("separator" in opt) {
|
|
1706
|
+
return new Separator2(opt.separator);
|
|
1707
|
+
}
|
|
1708
|
+
return {
|
|
1709
|
+
name: opt.label,
|
|
1710
|
+
value: opt.value,
|
|
1711
|
+
checked: previousSelections?.includes(opt.value) ?? false,
|
|
1712
|
+
disabled: opt.disabled
|
|
1713
|
+
};
|
|
1714
|
+
});
|
|
1715
|
+
return checkbox2({
|
|
1716
|
+
message: step.message,
|
|
1717
|
+
choices,
|
|
1718
|
+
pageSize: step.pageSize,
|
|
1719
|
+
loop: step.loop,
|
|
1720
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
async renderConfirm(step, state, theme) {
|
|
1724
|
+
const existingAnswer = state.answers[step.id];
|
|
1725
|
+
const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer : step.default;
|
|
1726
|
+
return confirm2({
|
|
1727
|
+
message: step.message,
|
|
1728
|
+
default: defaultValue ?? true,
|
|
1729
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
async renderPassword(step, _state, theme) {
|
|
1733
|
+
return password2({
|
|
1734
|
+
message: step.message,
|
|
1735
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
async renderNumber(step, state, theme) {
|
|
1739
|
+
const existingAnswer = state.answers[step.id];
|
|
1740
|
+
const defaultValue = typeof existingAnswer === "number" ? existingAnswer : step.default;
|
|
1741
|
+
const result = await number2({
|
|
1742
|
+
message: step.message,
|
|
1743
|
+
default: defaultValue,
|
|
1744
|
+
min: step.min,
|
|
1745
|
+
max: step.max,
|
|
1746
|
+
step: step.step,
|
|
1747
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1748
|
+
});
|
|
1749
|
+
return result ?? defaultValue ?? 0;
|
|
1750
|
+
}
|
|
1751
|
+
async renderSearch(step, _state, theme) {
|
|
1752
|
+
return search2({
|
|
1753
|
+
message: step.message,
|
|
1754
|
+
source: (term) => {
|
|
1755
|
+
const query = (term ?? "").toLowerCase();
|
|
1756
|
+
return step.options.filter((opt) => "value" in opt).filter((opt) => !opt.disabled && opt.label.toLowerCase().includes(query)).map((opt) => ({
|
|
1757
|
+
name: opt.label,
|
|
1758
|
+
value: opt.value,
|
|
1759
|
+
description: opt.hint
|
|
1760
|
+
}));
|
|
1761
|
+
},
|
|
1762
|
+
pageSize: step.pageSize,
|
|
1763
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
async renderEditor(step, _state, theme) {
|
|
1767
|
+
return editor2({
|
|
1768
|
+
message: step.message,
|
|
1769
|
+
default: step.default,
|
|
1770
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
async renderPath(step, state, theme) {
|
|
1774
|
+
const existingAnswer = state.answers[step.id];
|
|
1775
|
+
const defaultValue = typeof existingAnswer === "string" ? existingAnswer : step.default;
|
|
1776
|
+
return input2({
|
|
1777
|
+
message: step.message,
|
|
1778
|
+
default: defaultValue,
|
|
1779
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
async renderToggle(step, state, theme) {
|
|
1783
|
+
const existingAnswer = state.answers[step.id];
|
|
1784
|
+
const activeLabel = step.active ?? "On";
|
|
1785
|
+
const inactiveLabel = step.inactive ?? "Off";
|
|
1786
|
+
const defaultValue = typeof existingAnswer === "boolean" ? existingAnswer ? activeLabel : inactiveLabel : step.default === true ? activeLabel : inactiveLabel;
|
|
1787
|
+
const result = await select2({
|
|
1788
|
+
message: step.message,
|
|
1789
|
+
choices: [
|
|
1790
|
+
{ name: activeLabel, value: activeLabel },
|
|
1791
|
+
{ name: inactiveLabel, value: inactiveLabel }
|
|
1792
|
+
],
|
|
1793
|
+
default: defaultValue,
|
|
1794
|
+
theme: { prefix: { idle: theme.icons.pointer, done: theme.icons.stepDone } }
|
|
1795
|
+
});
|
|
1796
|
+
return result === activeLabel;
|
|
1797
|
+
}
|
|
1798
|
+
renderMessage(step, _state, theme) {
|
|
1799
|
+
if (step.description) {
|
|
1800
|
+
console.log(` ${theme.muted(step.description)}`);
|
|
1801
|
+
}
|
|
1802
|
+
console.log();
|
|
1803
|
+
}
|
|
1804
|
+
renderGroupHeader(group, theme) {
|
|
1805
|
+
console.log();
|
|
1806
|
+
console.log(` ${boxLine(group, theme)}`);
|
|
1807
|
+
console.log();
|
|
1808
|
+
}
|
|
1809
|
+
renderSummary(answers, steps, theme) {
|
|
1810
|
+
const divider = theme.accent("\u2500".repeat(50));
|
|
1811
|
+
console.log();
|
|
1812
|
+
console.log(` ${divider}`);
|
|
1813
|
+
console.log(` ${theme.accent("\u2502")} ${theme.bold("Summary")}`);
|
|
1814
|
+
console.log(` ${divider}`);
|
|
1815
|
+
for (const step of steps) {
|
|
1816
|
+
const answer = answers[step.id];
|
|
1817
|
+
if (answer === void 0) continue;
|
|
1818
|
+
const display = Array.isArray(answer) ? answer.map(String).join(", ") : String(answer);
|
|
1819
|
+
const label = theme.muted(step.id.padEnd(24));
|
|
1820
|
+
const value = theme.primary(display);
|
|
1821
|
+
console.log(` ${theme.accent("\u2502")} ${label} ${value}`);
|
|
1822
|
+
}
|
|
1823
|
+
console.log(` ${divider}`);
|
|
1824
|
+
}
|
|
1825
|
+
clear() {
|
|
1826
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
// src/templates.ts
|
|
1831
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
1832
|
+
import { join as join3 } from "path";
|
|
1833
|
+
import { homedir as homedir3 } from "os";
|
|
1834
|
+
var TEMPLATES_DIR = join3(homedir3(), ".config", "grimoire", "templates");
|
|
1835
|
+
function getWizardTemplateDir(wizardName) {
|
|
1836
|
+
return join3(TEMPLATES_DIR, slugify(wizardName));
|
|
1837
|
+
}
|
|
1838
|
+
function getTemplateFilePath(wizardName, templateName) {
|
|
1839
|
+
return join3(getWizardTemplateDir(wizardName), `${slugify(templateName)}.json`);
|
|
1840
|
+
}
|
|
1841
|
+
function saveTemplate(wizardName, templateName, answers, excludeKeys) {
|
|
1842
|
+
try {
|
|
1843
|
+
const dir = getWizardTemplateDir(wizardName);
|
|
1844
|
+
mkdirSync3(dir, { recursive: true });
|
|
1845
|
+
const filePath = getTemplateFilePath(wizardName, templateName);
|
|
1846
|
+
let answersToSave = answers;
|
|
1847
|
+
if (excludeKeys && excludeKeys.length > 0) {
|
|
1848
|
+
const excluded = new Set(excludeKeys);
|
|
1849
|
+
answersToSave = Object.fromEntries(
|
|
1850
|
+
Object.entries(answers).filter(([k]) => !excluded.has(k))
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
writeFileSync3(filePath, JSON.stringify(answersToSave, null, 2) + "\n", "utf-8");
|
|
1854
|
+
} catch {
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function loadTemplate(wizardName, templateName) {
|
|
1858
|
+
try {
|
|
1859
|
+
const filePath = getTemplateFilePath(wizardName, templateName);
|
|
1860
|
+
const raw = readFileSync4(filePath, "utf-8");
|
|
1861
|
+
const parsed = JSON.parse(raw);
|
|
1862
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1863
|
+
return parsed;
|
|
1864
|
+
}
|
|
1865
|
+
return void 0;
|
|
1866
|
+
} catch {
|
|
1867
|
+
return void 0;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function listTemplates(wizardName) {
|
|
1871
|
+
try {
|
|
1872
|
+
const dir = getWizardTemplateDir(wizardName);
|
|
1873
|
+
if (!existsSync2(dir)) return [];
|
|
1874
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
1875
|
+
} catch {
|
|
1876
|
+
return [];
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function deleteTemplate(wizardName, templateName) {
|
|
1880
|
+
try {
|
|
1881
|
+
const filePath = getTemplateFilePath(wizardName, templateName);
|
|
1882
|
+
unlinkSync2(filePath);
|
|
1883
|
+
} catch {
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
export {
|
|
1887
|
+
InkRenderer,
|
|
1888
|
+
InquirerRenderer,
|
|
1889
|
+
clearCache,
|
|
1890
|
+
clearMruData,
|
|
1891
|
+
clearPlugins,
|
|
1892
|
+
createWizardState,
|
|
1893
|
+
defineWizard,
|
|
1894
|
+
deleteTemplate,
|
|
1895
|
+
evaluateCondition,
|
|
1896
|
+
getCacheDir,
|
|
1897
|
+
getOrderedOptions,
|
|
1898
|
+
getPluginStep,
|
|
1899
|
+
getVisibleSteps,
|
|
1900
|
+
isStepVisible,
|
|
1901
|
+
listTemplates,
|
|
1902
|
+
loadCachedAnswers,
|
|
1903
|
+
loadTemplate,
|
|
1904
|
+
loadWizardConfig,
|
|
1905
|
+
parseWizardConfig,
|
|
1906
|
+
parseWizardYAML,
|
|
1907
|
+
recordSelection,
|
|
1908
|
+
registerPlugin,
|
|
1909
|
+
renderBanner,
|
|
1910
|
+
resolveEnvDefault,
|
|
1911
|
+
resolveNextStep,
|
|
1912
|
+
resolveTemplate,
|
|
1913
|
+
resolveTheme,
|
|
1914
|
+
runPreFlightChecks,
|
|
1915
|
+
runWizard,
|
|
1916
|
+
saveCachedAnswers,
|
|
1917
|
+
saveTemplate,
|
|
1918
|
+
slugify,
|
|
1919
|
+
validateStepAnswer,
|
|
1920
|
+
wizardReducer
|
|
1921
|
+
};
|
|
1922
|
+
//# sourceMappingURL=index.js.map
|