simple-scaffold 3.0.0 → 3.1.1

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.
@@ -0,0 +1,1263 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_path = require("node:path");
24
+ node_path = __toESM(node_path);
25
+ let node_os = require("node:os");
26
+ node_os = __toESM(node_os);
27
+ let node_child_process = require("node:child_process");
28
+ let node_fs_promises = require("node:fs/promises");
29
+ node_fs_promises = __toESM(node_fs_promises);
30
+ let glob = require("glob");
31
+ let util = require("util");
32
+ util = __toESM(util);
33
+ let handlebars = require("handlebars");
34
+ handlebars = __toESM(handlebars);
35
+ let date_fns = require("date-fns");
36
+ let node_constants = require("node:constants");
37
+ let _inquirer_input = require("@inquirer/input");
38
+ _inquirer_input = __toESM(_inquirer_input);
39
+ let _inquirer_select = require("@inquirer/select");
40
+ _inquirer_select = __toESM(_inquirer_select);
41
+ let _inquirer_confirm = require("@inquirer/confirm");
42
+ _inquirer_confirm = __toESM(_inquirer_confirm);
43
+ let _inquirer_number = require("@inquirer/number");
44
+ _inquirer_number = __toESM(_inquirer_number);
45
+ let minimatch = require("minimatch");
46
+ let zod_v4 = require("zod/v4");
47
+ //#region src/colors.ts
48
+ /** ANSI color code mapping for terminal output. */
49
+ var colorMap = {
50
+ reset: 0,
51
+ dim: 2,
52
+ bold: 1,
53
+ italic: 3,
54
+ underline: 4,
55
+ red: 31,
56
+ green: 32,
57
+ yellow: 33,
58
+ blue: 34,
59
+ magenta: 35,
60
+ cyan: 36,
61
+ white: 37,
62
+ gray: 90
63
+ };
64
+ function _colorize(text, color) {
65
+ const c = colorMap[color];
66
+ let r;
67
+ if (c > 1 && c < 30) r = c + 20;
68
+ else if (c === 1) r = 23;
69
+ else r = 0;
70
+ return `\x1b[${c}m${text}\x1b[${r}m`;
71
+ }
72
+ function isTemplateStringArray(template) {
73
+ return Array.isArray(template) && typeof template[0] === "string";
74
+ }
75
+ var createColorize = (color) => (template, ...params) => {
76
+ return isTemplateStringArray(template) ? _colorize(template.reduce((acc, str, i) => acc + str + (params[i] ?? ""), ""), color) : _colorize(String(template), color);
77
+ };
78
+ /**
79
+ * Colorize text for terminal output.
80
+ *
81
+ * Can be used as a function: `colorize("text", "red")`
82
+ * Or via named helpers: `colorize.red("text")` / `colorize.red\`template\``
83
+ */
84
+ var colorize = Object.assign(_colorize, Object.entries(colorMap).reduce((acc, [key]) => {
85
+ acc[key] = createColorize(key);
86
+ return acc;
87
+ }, {}));
88
+ //#endregion
89
+ //#region src/utils.ts
90
+ /** Throws the error if non-null, no-ops otherwise. */
91
+ function handleErr(err) {
92
+ if (err) throw err;
93
+ }
94
+ /** Resolves a value that may be either a static value or a function that produces one. */
95
+ function resolve(resolver, arg) {
96
+ return typeof resolver === "function" ? resolver(arg) : resolver;
97
+ }
98
+ /** Wraps a static value in a resolver function. If already a function, returns as-is. */
99
+ function wrapNoopResolver(value) {
100
+ if (typeof value === "function") return value;
101
+ return (_) => value;
102
+ }
103
+ //#endregion
104
+ //#region src/types.ts
105
+ /**
106
+ * The amount of information to log when generating scaffold.
107
+ * When not `none`, the selected level will be the lowest level included.
108
+ *
109
+ * For example, level `info` will include `info`, `warning` and `error`, but not `debug`; and `warning` will only
110
+ * show `warning` and `error`, but not `info` or `debug`.
111
+ *
112
+ * @default `info`
113
+ *
114
+ * @category Logging (const)
115
+ */
116
+ var LogLevel = {
117
+ none: "none",
118
+ debug: "debug",
119
+ info: "info",
120
+ warning: "warning",
121
+ error: "error"
122
+ };
123
+ //#endregion
124
+ //#region src/logger.ts
125
+ /** Priority ordering for log levels (higher = more severe). */
126
+ var LOG_PRIORITY = {
127
+ [LogLevel.none]: 0,
128
+ [LogLevel.debug]: 1,
129
+ [LogLevel.info]: 2,
130
+ [LogLevel.warning]: 3,
131
+ [LogLevel.error]: 4
132
+ };
133
+ /** Maps each log level to a terminal color. */
134
+ var LOG_LEVEL_COLOR = {
135
+ [LogLevel.none]: "reset",
136
+ [LogLevel.debug]: "dim",
137
+ [LogLevel.info]: "reset",
138
+ [LogLevel.warning]: "yellow",
139
+ [LogLevel.error]: "red"
140
+ };
141
+ /** Logs a message at the given level, respecting the configured log level filter. */
142
+ function log(config, level, ...obj) {
143
+ if (config.logLevel === LogLevel.none || LOG_PRIORITY[level] < LOG_PRIORITY[config.logLevel ?? LogLevel.info]) return;
144
+ const colorFn = colorize[LOG_LEVEL_COLOR[level]];
145
+ const key = level === LogLevel.error ? "error" : level === LogLevel.warning ? "warn" : "log";
146
+ const logFn = console[key];
147
+ logFn(...obj.map((i) => i instanceof Error ? colorFn(i, JSON.stringify(i, void 0, 1), i.stack) : typeof i === "object" ? util.default.inspect(i, {
148
+ depth: null,
149
+ colors: true
150
+ }) : colorFn(i)));
151
+ }
152
+ /** Logs the full scaffold configuration at debug level. */
153
+ function logInitStep(config) {
154
+ log(config, LogLevel.debug, "Full config:", {
155
+ name: config.name,
156
+ templates: config.templates,
157
+ output: config.output,
158
+ subdir: config.subdir,
159
+ data: config.data,
160
+ overwrite: config.overwrite,
161
+ subdirHelper: config.subdirHelper,
162
+ helpers: Object.keys(config.helpers ?? {}),
163
+ logLevel: config.logLevel,
164
+ dryRun: config.dryRun,
165
+ beforeWrite: config.beforeWrite
166
+ });
167
+ }
168
+ /**
169
+ * Logs a tree of created files, grouped by directory.
170
+ */
171
+ function logFileTree(config, files) {
172
+ if (files.length === 0) return;
173
+ const commonDir = files.reduce((prefix, file) => {
174
+ while (!file.startsWith(prefix)) prefix = node_path.default.dirname(prefix);
175
+ return prefix;
176
+ }, node_path.default.dirname(files[0]));
177
+ log(config, LogLevel.info, "");
178
+ log(config, LogLevel.info, colorize.bold(`📁 ${commonDir}`));
179
+ const relPaths = files.map((f) => node_path.default.relative(commonDir, f)).sort();
180
+ for (let i = 0; i < relPaths.length; i++) {
181
+ const prefix = i === relPaths.length - 1 ? "└── " : "├── ";
182
+ log(config, LogLevel.info, colorize.dim(prefix) + relPaths[i]);
183
+ }
184
+ }
185
+ /**
186
+ * Logs a final summary line with file count and elapsed time.
187
+ */
188
+ function logSummary(config, fileCount, elapsedMs, dryRun) {
189
+ const timeStr = elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(2)}s`;
190
+ log(config, LogLevel.info, "");
191
+ if (dryRun) log(config, LogLevel.info, colorize.yellow(`🏜️ Dry run complete — ${fileCount} file(s) would be created (${timeStr})`));
192
+ else if (fileCount === 0) log(config, LogLevel.info, colorize.yellow(`⚠️ No files created (${timeStr})`));
193
+ else log(config, LogLevel.info, colorize.green(`✅ Created ${fileCount} file(s) in ${timeStr}`));
194
+ }
195
+ //#endregion
196
+ //#region src/parser.ts
197
+ var dateFns = {
198
+ add: date_fns.add,
199
+ format: date_fns.format,
200
+ parseISO: date_fns.parseISO
201
+ };
202
+ var defaultHelpers = {
203
+ camelCase,
204
+ snakeCase,
205
+ startCase,
206
+ kebabCase,
207
+ hyphenCase: kebabCase,
208
+ pascalCase,
209
+ lowerCase: (text) => text.toLowerCase(),
210
+ upperCase: (text) => text.toUpperCase(),
211
+ now: nowHelper,
212
+ date: dateHelper
213
+ };
214
+ function _dateHelper(date, formatString, durationDifference, durationType) {
215
+ if (durationType && durationDifference !== void 0) return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString);
216
+ return dateFns.format(date, formatString);
217
+ }
218
+ function nowHelper(formatString, durationDifference, durationType) {
219
+ return _dateHelper(/* @__PURE__ */ new Date(), formatString, durationDifference, durationType);
220
+ }
221
+ function dateHelper(date, formatString, durationDifference, durationType) {
222
+ return _dateHelper(dateFns.parseISO(date), formatString, durationDifference, durationType);
223
+ }
224
+ function toWordParts(string) {
225
+ return string.split(/[^a-zA-Z0-9]/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)).filter((s) => s.length > 0);
226
+ }
227
+ function camelCase(s) {
228
+ return toWordParts(s).reduce((acc, part, i) => {
229
+ if (i === 0) return part.toLowerCase();
230
+ return acc + part[0].toUpperCase() + part.slice(1).toLowerCase();
231
+ }, "");
232
+ }
233
+ function snakeCase(s) {
234
+ return toWordParts(s).join("_").toLowerCase();
235
+ }
236
+ function kebabCase(s) {
237
+ return toWordParts(s).join("-").toLowerCase();
238
+ }
239
+ function startCase(s) {
240
+ return toWordParts(s).map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase()).join(" ");
241
+ }
242
+ function pascalCase(s) {
243
+ return startCase(s).replace(/\s+/g, "");
244
+ }
245
+ function registerHelpers(config) {
246
+ const _helpers = {
247
+ ...defaultHelpers,
248
+ ...config.helpers
249
+ };
250
+ for (const helperName in _helpers) {
251
+ log(config, LogLevel.debug, `Registering helper: ${helperName}`);
252
+ handlebars.default.registerHelper(helperName, _helpers[helperName]);
253
+ }
254
+ }
255
+ function handlebarsParse(config, templateBuffer, { asPath = false } = {}) {
256
+ const { data } = config;
257
+ try {
258
+ let str = templateBuffer.toString();
259
+ if (asPath) str = str.replace(/\\/g, "/");
260
+ let outputContents = handlebars.default.compile(str, { noEscape: true })(data);
261
+ if (asPath && node_path.default.sep !== "/") outputContents = outputContents.replace(/\//g, "\\");
262
+ return Buffer.from(outputContents);
263
+ } catch (e) {
264
+ log(config, LogLevel.debug, e);
265
+ log(config, LogLevel.debug, "Couldn't parse file with handlebars, returning original content");
266
+ return Buffer.from(templateBuffer);
267
+ }
268
+ }
269
+ //#endregion
270
+ //#region src/fs-utils.ts
271
+ var { stat, access, mkdir } = node_fs_promises.default;
272
+ /** Recursively creates a directory and its parents if they don't exist. */
273
+ async function createDirIfNotExists(dir, config) {
274
+ if (config.dryRun) {
275
+ log(config, LogLevel.info, `Dry Run. Not creating dir ${dir}`);
276
+ return;
277
+ }
278
+ const parentDir = node_path.default.dirname(dir);
279
+ if (!await pathExists(parentDir)) await createDirIfNotExists(parentDir, config);
280
+ if (!await pathExists(dir)) try {
281
+ log(config, LogLevel.debug, `Creating dir ${dir}`);
282
+ await mkdir(dir);
283
+ return;
284
+ } catch (e) {
285
+ if (e && e.code !== "EEXIST") throw e;
286
+ return;
287
+ }
288
+ }
289
+ /** Checks whether a file or directory exists at the given path. */
290
+ async function pathExists(filePath) {
291
+ try {
292
+ await access(filePath, node_constants.F_OK);
293
+ return true;
294
+ } catch (e) {
295
+ if (e && e.code === "ENOENT") return false;
296
+ throw e;
297
+ }
298
+ }
299
+ /** Returns true if the given path is a directory. */
300
+ async function isDir(dirPath) {
301
+ return (await stat(dirPath)).isDirectory();
302
+ }
303
+ /** Generates a unique temporary directory path for scaffold operations. @internal */
304
+ function getUniqueTmpPath() {
305
+ return node_path.default.resolve(node_os.default.tmpdir(), `scaffold-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
306
+ }
307
+ //#endregion
308
+ //#region src/path-utils.ts
309
+ /** Strips glob wildcard characters from a template path. */
310
+ function removeGlob(template) {
311
+ return node_path.default.normalize(template.replace(/\*/g, ""));
312
+ }
313
+ /** Removes a leading path separator, making the path relative. */
314
+ function makeRelativePath(str) {
315
+ return str.startsWith(node_path.default.sep) ? str.slice(1) : str;
316
+ }
317
+ /** Computes a base path relative to the current working directory. */
318
+ function getBasePath(relPath) {
319
+ return node_path.default.resolve(process.cwd(), relPath).replace(process.cwd() + node_path.default.sep, "").replace(process.cwd(), "");
320
+ }
321
+ //#endregion
322
+ //#region src/file.ts
323
+ var { readFile, writeFile } = node_fs_promises.default;
324
+ /**
325
+ * Resolves a config option that may be either a static value or a per-file function.
326
+ * For function values, the file path is parsed through Handlebars before being passed.
327
+ * @internal
328
+ */
329
+ function getOptionValueForFile(config, filePath, fn, defaultValue) {
330
+ if (typeof fn !== "function") return defaultValue ?? fn;
331
+ return fn(filePath, node_path.default.dirname(handlebarsParse(config, filePath, { asPath: true }).toString()), node_path.default.basename(handlebarsParse(config, filePath, { asPath: true }).toString()));
332
+ }
333
+ /** Expands a list of glob patterns into a flat list of matching file paths. */
334
+ async function getFileList(config, templates) {
335
+ log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`);
336
+ return (await (0, glob.glob)(templates, {
337
+ dot: true,
338
+ nodir: true
339
+ })).map(node_path.default.normalize);
340
+ }
341
+ /** Analyzes a template path to determine if it's a glob, directory, or single file. */
342
+ async function getTemplateGlobInfo(config, template) {
343
+ const _isGlob = (0, glob.hasMagic)(template);
344
+ log(config, LogLevel.debug, "before isDir", "isGlob:", _isGlob, template);
345
+ let resolvedTemplate = template;
346
+ let baseTemplatePath = _isGlob ? removeGlob(template) : template;
347
+ baseTemplatePath = node_path.default.normalize(baseTemplatePath);
348
+ const isDirOrGlob = _isGlob ? true : await isDir(template);
349
+ const shouldAddGlob = !_isGlob && isDirOrGlob;
350
+ log(config, LogLevel.debug, "after", {
351
+ isDirOrGlob,
352
+ shouldAddGlob
353
+ });
354
+ if (shouldAddGlob) resolvedTemplate = node_path.default.join(template, "**", "*");
355
+ return {
356
+ baseTemplatePath,
357
+ origTemplate: template,
358
+ isDirOrGlob,
359
+ isGlob: _isGlob,
360
+ template: resolvedTemplate
361
+ };
362
+ }
363
+ /** Computes the full output path and metadata for a single template file. */
364
+ async function getTemplateFileInfo(config, { templatePath, basePath }) {
365
+ const inputPath = node_path.default.resolve(process.cwd(), templatePath);
366
+ const outputPathOpt = getOptionValueForFile(config, inputPath, config.output);
367
+ const outputDir = getOutputDir(config, outputPathOpt, basePath.replace(config.tmpDir, "./"));
368
+ const outputPath = handlebarsParse(config, node_path.default.join(outputDir, node_path.default.basename(inputPath)), { asPath: true }).toString();
369
+ return {
370
+ inputPath,
371
+ outputPathOpt,
372
+ outputDir,
373
+ outputPath,
374
+ exists: await pathExists(outputPath)
375
+ };
376
+ }
377
+ /**
378
+ * Reads a template file, applies Handlebars parsing, runs the beforeWrite hook,
379
+ * and writes the result to the output path.
380
+ */
381
+ async function copyFileTransformed(config, { exists, overwrite, outputPath, inputPath }) {
382
+ if (!exists || overwrite) {
383
+ if (exists && overwrite) log(config, LogLevel.debug, `Overwriting ${outputPath}`);
384
+ log(config, LogLevel.debug, `Processing file ${inputPath}`);
385
+ const templateBuffer = await readFile(inputPath);
386
+ const unprocessedOutputContents = handlebarsParse(config, templateBuffer);
387
+ const finalOutputContents = await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath) ?? unprocessedOutputContents;
388
+ if (!config.dryRun) await writeFile(outputPath, finalOutputContents);
389
+ else {
390
+ log(config, LogLevel.debug, "Dry run — output would be:");
391
+ log(config, LogLevel.debug, finalOutputContents.toString());
392
+ }
393
+ } else if (exists) log(config, LogLevel.debug, `Skipped ${outputPath} (already exists)`);
394
+ }
395
+ /** Computes the output directory for a file, combining the output path, base path, and optional subdir. */
396
+ function getOutputDir(config, outputPathOpt, basePath) {
397
+ return node_path.default.resolve(process.cwd(), ...[
398
+ outputPathOpt,
399
+ basePath,
400
+ config.subdir ? config.subdirHelper ? handlebarsParse(config, `{{ ${config.subdirHelper} name }}`).toString() : config.name : void 0
401
+ ].filter(Boolean));
402
+ }
403
+ /**
404
+ * Processes a single template file: resolves output paths, creates directories,
405
+ * and writes the transformed output.
406
+ * Returns the output path if the file was written, or null if skipped.
407
+ */
408
+ async function handleTemplateFile(config, { templatePath, basePath }) {
409
+ try {
410
+ const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
411
+ templatePath,
412
+ basePath
413
+ });
414
+ const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false);
415
+ log(config, LogLevel.debug, `\nParsing ${templatePath}`, `\nBase path: ${basePath}`, `\nFull input path: ${inputPath}`, `\nOutput Path Opt: ${outputPathOpt}`, `\nFull output dir: ${outputDir}`, `\nFull output path: ${outputPath}`, `\n`);
416
+ await createDirIfNotExists(node_path.default.dirname(outputPath), config);
417
+ const shouldWrite = (!exists || overwrite) && !config.dryRun;
418
+ log(config, LogLevel.debug, `Writing to ${outputPath}`);
419
+ await copyFileTransformed(config, {
420
+ exists,
421
+ overwrite,
422
+ outputPath,
423
+ inputPath
424
+ });
425
+ return shouldWrite ? outputPath : null;
426
+ } catch (e) {
427
+ handleErr(e);
428
+ throw e;
429
+ }
430
+ }
431
+ //#endregion
432
+ //#region src/git.ts
433
+ async function getGitConfig(url, file, tmpPath, logConfig) {
434
+ const repoUrl = `${url.protocol}//${url.host}${url.pathname}`;
435
+ log(logConfig, LogLevel.debug, `Cloning git repo ${repoUrl}`);
436
+ return new Promise((res, reject) => {
437
+ log(logConfig, LogLevel.debug, `Cloning git repo to ${tmpPath}`);
438
+ const clone = (0, node_child_process.spawn)("git", [
439
+ "clone",
440
+ "--recurse-submodules",
441
+ "--depth",
442
+ "1",
443
+ repoUrl,
444
+ tmpPath
445
+ ]);
446
+ clone.on("error", reject);
447
+ clone.on("close", async (code) => {
448
+ if (code === 0) {
449
+ res(await loadGitConfig({
450
+ logConfig,
451
+ url: repoUrl,
452
+ file,
453
+ tmpPath
454
+ }));
455
+ return;
456
+ }
457
+ reject(/* @__PURE__ */ new Error(`Git clone failed with code ${code}`));
458
+ });
459
+ });
460
+ }
461
+ /** @internal */
462
+ async function loadGitConfig({ logConfig, url: repoUrl, file, tmpPath }) {
463
+ log(logConfig, LogLevel.debug, `Loading config from git repo: ${repoUrl}`);
464
+ const filename = file || await findConfigFile(tmpPath);
465
+ const absolutePath = node_path.default.resolve(tmpPath, filename);
466
+ log(logConfig, LogLevel.debug, `Resolving config file: ${absolutePath}`);
467
+ const loadedConfig = await resolve(async () => (await import(absolutePath)).default, logConfig);
468
+ log(logConfig, LogLevel.debug, `Loaded config from git`);
469
+ log(logConfig, LogLevel.debug, `Raw config:`, loadedConfig);
470
+ const fixedConfig = {};
471
+ for (const [k, v] of Object.entries(loadedConfig)) fixedConfig[k] = {
472
+ ...v,
473
+ templates: v.templates.map((t) => node_path.default.resolve(tmpPath, t))
474
+ };
475
+ return wrapNoopResolver(fixedConfig);
476
+ }
477
+ //#endregion
478
+ //#region src/before-write.ts
479
+ /**
480
+ * Wraps a CLI beforeWrite command string into a beforeWrite callback function.
481
+ * The command receives the processed content via a temp file and can return modified content via stdout.
482
+ * @internal
483
+ */
484
+ function wrapBeforeWrite(config, beforeWrite) {
485
+ return async (content, rawContent, outputFile) => {
486
+ const tmpDir = node_path.default.join(getUniqueTmpPath(), node_path.default.basename(outputFile));
487
+ await createDirIfNotExists(node_path.default.dirname(tmpDir), config);
488
+ const ext = node_path.default.extname(outputFile);
489
+ const rawTmpPath = tmpDir.replace(ext, ".raw" + ext);
490
+ try {
491
+ log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite);
492
+ const cmd = await prepareBeforeWriteCmd({
493
+ beforeWrite,
494
+ tmpDir,
495
+ content,
496
+ rawTmpPath,
497
+ rawContent
498
+ });
499
+ return await new Promise((resolve, reject) => {
500
+ log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd);
501
+ const proc = (0, node_child_process.exec)(cmd);
502
+ proc.stdout.on("data", (data) => {
503
+ if (data.trim()) resolve(data.toString());
504
+ else resolve(void 0);
505
+ });
506
+ proc.stderr.on("data", (data) => {
507
+ reject(data.toString());
508
+ });
509
+ });
510
+ } catch (e) {
511
+ log(config, LogLevel.debug, e);
512
+ log(config, LogLevel.warning, "Error running beforeWrite command, returning original content");
513
+ return;
514
+ } finally {
515
+ await node_fs_promises.default.rm(tmpDir, { force: true });
516
+ await node_fs_promises.default.rm(rawTmpPath, { force: true });
517
+ }
518
+ };
519
+ }
520
+ async function prepareBeforeWriteCmd({ beforeWrite, tmpDir, content, rawTmpPath, rawContent }) {
521
+ let cmd = "";
522
+ const pathReg = /\{\{\s*path\s*\}\}/gi;
523
+ const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi;
524
+ if (pathReg.test(beforeWrite)) {
525
+ await node_fs_promises.default.writeFile(tmpDir, content);
526
+ cmd = beforeWrite.replaceAll(pathReg, tmpDir);
527
+ }
528
+ if (rawPathReg.test(beforeWrite)) {
529
+ await node_fs_promises.default.writeFile(rawTmpPath, rawContent);
530
+ cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath);
531
+ }
532
+ if (!cmd) {
533
+ await node_fs_promises.default.writeFile(tmpDir, content);
534
+ cmd = [beforeWrite, tmpDir].join(" ");
535
+ }
536
+ return cmd;
537
+ }
538
+ //#endregion
539
+ //#region src/config.ts
540
+ /** Parses CLI append-data syntax (`key=value` or `key:=jsonValue`) into a data object. @internal */
541
+ function parseAppendData(value, options) {
542
+ const data = options.data ?? {};
543
+ const [key, val] = value.split(/:?=/);
544
+ if (value.includes(":=") && !val.includes(":=")) return {
545
+ ...data,
546
+ [key]: JSON.parse(val)
547
+ };
548
+ return {
549
+ ...data,
550
+ [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val
551
+ };
552
+ }
553
+ function isWrappedWithQuotes(string) {
554
+ return string.startsWith("\"") && string.endsWith("\"") || string.startsWith("'") && string.endsWith("'");
555
+ }
556
+ /** Loads and resolves a config file (local or remote). @internal */
557
+ async function getConfigFile(config) {
558
+ if (config.git && !config.git.includes("://")) {
559
+ log(config, LogLevel.debug, `Loading config from GitHub ${config.git}`);
560
+ config.git = githubPartToUrl(config.git);
561
+ }
562
+ const isGit = Boolean(config.git);
563
+ const configFilename = config.config;
564
+ const configPath = isGit ? config.git : configFilename;
565
+ log(config, LogLevel.debug, `Loading config from file ${configFilename}`);
566
+ let configImport = await resolve(await (isGit ? getRemoteConfig({
567
+ git: configPath,
568
+ config: configFilename,
569
+ logLevel: config.logLevel,
570
+ tmpDir: config.tmpDir
571
+ }) : getLocalConfig({
572
+ config: configFilename,
573
+ logLevel: config.logLevel
574
+ })), config);
575
+ if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
576
+ log(config, LogLevel.debug, "Config is a function or promise, resolving...");
577
+ configImport = await resolve(configImport.default, config);
578
+ }
579
+ return configImport;
580
+ }
581
+ /**
582
+ * Parses a CLI config into a full ScaffoldConfig by merging CLI args, config file values,
583
+ * and append-data overrides. @internal
584
+ */
585
+ async function parseConfigFile(config) {
586
+ let output = {
587
+ name: config.name,
588
+ templates: config.templates ?? [],
589
+ output: config.output,
590
+ logLevel: config.logLevel,
591
+ dryRun: config.dryRun,
592
+ data: config.data,
593
+ subdir: config.subdir,
594
+ overwrite: config.overwrite,
595
+ subdirHelper: config.subdirHelper,
596
+ beforeWrite: void 0,
597
+ tmpDir: config.tmpDir
598
+ };
599
+ if (config.quiet) config.logLevel = LogLevel.none;
600
+ if (Boolean(config.config || config.git)) {
601
+ const key = config.key ?? "default";
602
+ const configImport = await getConfigFile(config);
603
+ if (!configImport[key]) throw new Error(`Template "${key}" not found in ${config.config}`);
604
+ const imported = configImport[key];
605
+ log(config, LogLevel.debug, "Imported result", imported);
606
+ output = {
607
+ ...output,
608
+ ...imported,
609
+ beforeWrite: void 0,
610
+ templates: config.templates || imported.templates,
611
+ output: config.output || imported.output,
612
+ data: {
613
+ ...imported.data,
614
+ ...config.data
615
+ }
616
+ };
617
+ }
618
+ output.data = {
619
+ ...output.data,
620
+ ...config.appendData
621
+ };
622
+ const cmdBeforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : void 0;
623
+ output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite;
624
+ if (config.afterScaffold) output.afterScaffold = config.afterScaffold;
625
+ if (!output.name) throw new Error("simple-scaffold: Missing required option: name");
626
+ log(output, LogLevel.debug, "Parsed config", output);
627
+ return output;
628
+ }
629
+ /** Converts a GitHub shorthand (user/repo) to a full HTTPS git URL. @internal */
630
+ function githubPartToUrl(part) {
631
+ const gitUrl = new URL(`https://github.com/${part}`);
632
+ if (!gitUrl.pathname.endsWith(".git")) gitUrl.pathname += ".git";
633
+ return gitUrl.toString();
634
+ }
635
+ /** Loads a scaffold config from a local file or directory. @internal */
636
+ async function getLocalConfig(config) {
637
+ const { config: configFile, ...logConfig } = config;
638
+ const absolutePath = node_path.default.resolve(process.cwd(), configFile);
639
+ if (await isDir(absolutePath)) {
640
+ log(logConfig, LogLevel.debug, `Resolving config file from directory ${absolutePath}`);
641
+ const file = await findConfigFile(absolutePath);
642
+ if (!await pathExists(file)) throw new Error(`Could not find config file in directory ${absolutePath}`);
643
+ log(logConfig, LogLevel.debug, `Loading config from: ${node_path.default.resolve(absolutePath, file)}`);
644
+ return wrapNoopResolver(import(node_path.default.resolve(absolutePath, file)));
645
+ }
646
+ log(logConfig, LogLevel.debug, `Loading config from: ${absolutePath}`);
647
+ return wrapNoopResolver(import(absolutePath));
648
+ }
649
+ /** Loads a scaffold config from a remote git repository. @internal */
650
+ async function getRemoteConfig(config) {
651
+ const { config: configFile, git, tmpDir, ...logConfig } = config;
652
+ log(logConfig, LogLevel.debug, `Loading config from remote ${git}, config file ${configFile || "<auto-detect>"}`);
653
+ const url = new URL(git);
654
+ const isHttp = url.protocol === "http:" || url.protocol === "https:";
655
+ if (!(url.protocol === "git:" || isHttp && url.pathname.endsWith(".git"))) throw new Error(`Unsupported protocol ${url.protocol}`);
656
+ return getGitConfig(url, configFile, tmpDir, logConfig);
657
+ }
658
+ /** Searches for a scaffold config file in the given directory, trying known filenames in order. @internal */
659
+ async function findConfigFile(root) {
660
+ const allowed = [
661
+ "mjs",
662
+ "cjs",
663
+ "js",
664
+ "json"
665
+ ].reduce((acc, ext) => {
666
+ acc.push(`scaffold.config.${ext}`);
667
+ acc.push(`scaffold.${ext}`);
668
+ acc.push(`.scaffold.${ext}`);
669
+ return acc;
670
+ }, []);
671
+ for (const file of allowed) if (await pathExists(node_path.default.resolve(root, file))) return file;
672
+ throw new Error(`Could not find config file in git repo`);
673
+ }
674
+ //#endregion
675
+ //#region src/prompts.ts
676
+ /** Prompts the user for a scaffold name. */
677
+ async function promptForName() {
678
+ return (0, _inquirer_input.default)({
679
+ message: colorize.cyan("Scaffold name:"),
680
+ required: true,
681
+ validate: (value) => {
682
+ if (!value.trim()) return "Name cannot be empty";
683
+ return true;
684
+ }
685
+ });
686
+ }
687
+ /** Prompts the user to select a template key from the available config keys. */
688
+ async function promptForTemplateKey(configMap) {
689
+ const keys = Object.keys(configMap);
690
+ if (keys.length === 0) throw new Error("No templates found in config file");
691
+ if (keys.length === 1) return keys[0];
692
+ return (0, _inquirer_select.default)({
693
+ message: colorize.cyan("Select a template:"),
694
+ choices: keys.map((key) => ({
695
+ name: key,
696
+ value: key
697
+ }))
698
+ });
699
+ }
700
+ /** Prompts the user for an output directory path. */
701
+ async function promptForOutput() {
702
+ return (0, _inquirer_input.default)({
703
+ message: colorize.cyan("Output directory:"),
704
+ required: true,
705
+ default: ".",
706
+ validate: (value) => {
707
+ if (!value.trim()) return "Output directory cannot be empty";
708
+ return true;
709
+ }
710
+ });
711
+ }
712
+ /** Prompts the user for template paths (comma-separated). */
713
+ async function promptForTemplates() {
714
+ return (await (0, _inquirer_input.default)({
715
+ message: colorize.cyan("Template paths (comma-separated):"),
716
+ required: true,
717
+ validate: (value) => {
718
+ if (!value.trim()) return "At least one template path is required";
719
+ return true;
720
+ }
721
+ })).split(",").map((t) => t.trim()).filter(Boolean);
722
+ }
723
+ /** Prompts for a single input based on its type. */
724
+ async function promptSingleInput(key, def) {
725
+ const type = def.type ?? "text";
726
+ const message = colorize.cyan(def.message ?? `${key}:`);
727
+ switch (type) {
728
+ case "text": return (0, _inquirer_input.default)({
729
+ message,
730
+ required: def.required,
731
+ default: def.default,
732
+ validate: def.required ? (value) => {
733
+ if (!value.trim()) return `${key} is required`;
734
+ return true;
735
+ } : void 0
736
+ });
737
+ case "select": {
738
+ const choices = (def.options ?? []).map((opt) => typeof opt === "string" ? {
739
+ name: opt,
740
+ value: opt
741
+ } : opt);
742
+ if (choices.length === 0) throw new Error(`Input "${key}" has type "select" but no options defined`);
743
+ return (0, _inquirer_select.default)({
744
+ message,
745
+ choices,
746
+ default: def.default
747
+ });
748
+ }
749
+ case "confirm": return (0, _inquirer_confirm.default)({
750
+ message,
751
+ default: def.default ?? false
752
+ });
753
+ case "number": return await (0, _inquirer_number.default)({
754
+ message,
755
+ required: def.required,
756
+ default: def.default
757
+ }) ?? def.default;
758
+ }
759
+ }
760
+ /**
761
+ * Prompts the user for any required scaffold inputs that are not already provided in data.
762
+ * Also applies default values for optional inputs that have one.
763
+ * Returns the merged data object.
764
+ */
765
+ async function promptForInputs(inputs, existingData = {}) {
766
+ const data = { ...existingData };
767
+ for (const [key, def] of Object.entries(inputs)) {
768
+ if (key in data && data[key] !== void 0 && data[key] !== "") continue;
769
+ if (def.required || def.type === "select" || def.type === "confirm") data[key] = await promptSingleInput(key, def);
770
+ else if (def.default !== void 0 && !(key in data)) data[key] = def.default;
771
+ }
772
+ return data;
773
+ }
774
+ /** Returns true if the process is running in an interactive terminal. */
775
+ function isInteractive() {
776
+ return Boolean(process.stdin.isTTY);
777
+ }
778
+ /**
779
+ * Prompts for name and template key before the config file is parsed.
780
+ * These are needed by parseConfigFile to know which template to load.
781
+ */
782
+ async function promptBeforeConfig(config, configMap) {
783
+ if (!isInteractive()) return config;
784
+ if (!config.name) config.name = await promptForName();
785
+ if (configMap && !config.key) {
786
+ if (Object.keys(configMap).length > 1) config.key = await promptForTemplateKey(configMap);
787
+ }
788
+ return config;
789
+ }
790
+ /**
791
+ * Prompts for any values still missing after the config file has been parsed.
792
+ * Only prompts in interactive mode.
793
+ */
794
+ async function promptAfterConfig(config) {
795
+ if (!isInteractive()) return config;
796
+ if (!config.output || typeof config.output === "string" && !config.output) config.output = await promptForOutput();
797
+ if (!config.templates || config.templates.length === 0) config.templates = await promptForTemplates();
798
+ return config;
799
+ }
800
+ /**
801
+ * Prompts for any required inputs defined in the scaffold config and merges them into data.
802
+ * Only prompts in interactive mode; in non-interactive mode, only applies defaults.
803
+ */
804
+ async function resolveInputs(config) {
805
+ if (!config.inputs) return config;
806
+ if (isInteractive()) config.data = await promptForInputs(config.inputs, config.data);
807
+ else {
808
+ const data = { ...config.data };
809
+ for (const [key, def] of Object.entries(config.inputs)) if (def.default !== void 0 && !(key in data)) data[key] = def.default;
810
+ config.data = data;
811
+ }
812
+ return config;
813
+ }
814
+ //#endregion
815
+ //#region src/ignore.ts
816
+ var IGNORE_FILENAME = ".scaffoldignore";
817
+ /**
818
+ * Reads a `.scaffoldignore` file from the given directory and returns
819
+ * the parsed patterns for filtering.
820
+ *
821
+ * Lines starting with `#` are comments. Empty lines are skipped.
822
+ *
823
+ * @param dir The directory to search for `.scaffoldignore`
824
+ * @returns Array of glob patterns to ignore
825
+ */
826
+ async function loadIgnorePatterns(dir) {
827
+ const ignorePath = node_path.default.resolve(dir, IGNORE_FILENAME);
828
+ if (!await pathExists(ignorePath)) return [];
829
+ return parseIgnoreFile(await node_fs_promises.default.readFile(ignorePath, "utf-8"));
830
+ }
831
+ /**
832
+ * Parses the contents of a `.scaffoldignore` file into glob patterns.
833
+ * @internal
834
+ */
835
+ function parseIgnoreFile(content) {
836
+ return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
837
+ }
838
+ /**
839
+ * Filters a list of file paths, removing any that match the ignore patterns.
840
+ * Patterns are matched against the relative path from baseDir.
841
+ * Also always excludes `.scaffoldignore` itself.
842
+ */
843
+ function filterIgnoredFiles(files, ignorePatterns, baseDir) {
844
+ return files.filter((file) => {
845
+ const basename = node_path.default.basename(file);
846
+ if (basename === IGNORE_FILENAME) return false;
847
+ const relPath = node_path.default.relative(baseDir, file);
848
+ for (const pattern of ignorePatterns) if ((0, minimatch.minimatch)(relPath, pattern, { dot: true }) || (0, minimatch.minimatch)(basename, pattern, { dot: true })) return false;
849
+ return true;
850
+ });
851
+ }
852
+ //#endregion
853
+ //#region src/validate.ts
854
+ /** Schema for a JavaScript function value. */
855
+ var functionSchema = zod_v4.z.any().refine((v) => typeof v === "function", { message: "Expected a function" });
856
+ /** Schema for a value that can be either a string or a function. */
857
+ var stringOrFunctionSchema = zod_v4.z.union([zod_v4.z.string(), functionSchema]);
858
+ /** Schema for a value that can be either a boolean or a function. */
859
+ var booleanOrFunctionSchema = zod_v4.z.union([zod_v4.z.boolean(), functionSchema]);
860
+ /** Schema for a select input option — either a plain string or a `{ name, value }` object. */
861
+ var selectOptionSchema = zod_v4.z.union([zod_v4.z.string(), zod_v4.z.object({
862
+ name: zod_v4.z.string(),
863
+ value: zod_v4.z.string()
864
+ })]);
865
+ /** Schema for the input type enum. */
866
+ var inputTypeSchema = zod_v4.z.enum([
867
+ "text",
868
+ "select",
869
+ "confirm",
870
+ "number"
871
+ ]);
872
+ /** Schema for the log level enum. */
873
+ var logLevelSchema = zod_v4.z.enum([
874
+ "none",
875
+ "debug",
876
+ "info",
877
+ "warning",
878
+ "error"
879
+ ]);
880
+ /** Zod schema for a single scaffold input definition. */
881
+ var scaffoldInputSchema = zod_v4.z.object({
882
+ type: inputTypeSchema.optional(),
883
+ message: zod_v4.z.string().optional(),
884
+ required: zod_v4.z.boolean().optional(),
885
+ default: zod_v4.z.union([
886
+ zod_v4.z.string(),
887
+ zod_v4.z.boolean(),
888
+ zod_v4.z.number()
889
+ ]).optional(),
890
+ options: zod_v4.z.array(selectOptionSchema).optional()
891
+ });
892
+ function validateInputSemantics(key, input) {
893
+ const issues = [];
894
+ if (input.type === "select" && (!input.options || input.options.length === 0)) issues.push({
895
+ path: [
896
+ "inputs",
897
+ key,
898
+ "options"
899
+ ],
900
+ message: "select input must have a non-empty options array"
901
+ });
902
+ if (input.type === "confirm" && input.default !== void 0 && typeof input.default !== "boolean") issues.push({
903
+ path: [
904
+ "inputs",
905
+ key,
906
+ "default"
907
+ ],
908
+ message: "confirm input default must be a boolean"
909
+ });
910
+ if (input.type === "number" && input.default !== void 0 && typeof input.default !== "number") issues.push({
911
+ path: [
912
+ "inputs",
913
+ key,
914
+ "default"
915
+ ],
916
+ message: "number input default must be a number"
917
+ });
918
+ return issues;
919
+ }
920
+ /** Zod schema for ScaffoldConfig. */
921
+ var scaffoldConfigSchema = zod_v4.z.object({
922
+ name: zod_v4.z.string().min(1, "name is required"),
923
+ templates: zod_v4.z.array(zod_v4.z.string()).min(1, "templates must contain at least one entry"),
924
+ output: stringOrFunctionSchema,
925
+ subdir: zod_v4.z.boolean().optional(),
926
+ data: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional(),
927
+ overwrite: booleanOrFunctionSchema.optional(),
928
+ logLevel: logLevelSchema.optional(),
929
+ dryRun: zod_v4.z.boolean().optional(),
930
+ helpers: zod_v4.z.record(zod_v4.z.string(), functionSchema).optional(),
931
+ subdirHelper: zod_v4.z.string().optional(),
932
+ inputs: zod_v4.z.record(zod_v4.z.string(), scaffoldInputSchema).optional(),
933
+ beforeWrite: functionSchema.optional(),
934
+ afterScaffold: stringOrFunctionSchema.optional(),
935
+ tmpDir: zod_v4.z.string().optional()
936
+ }).check((ctx) => {
937
+ const config = ctx.value;
938
+ if (config.subdirHelper && !config.subdir) ctx.issues.push({
939
+ code: "custom",
940
+ message: "subdirHelper is set but subdir is not enabled",
941
+ path: ["subdirHelper"],
942
+ input: config
943
+ });
944
+ if (config.inputs) for (const [key, val] of Object.entries(config.inputs)) for (const issue of validateInputSemantics(key, val)) ctx.issues.push({
945
+ code: "custom",
946
+ ...issue,
947
+ input: config
948
+ });
949
+ });
950
+ /**
951
+ * Validates a scaffold config and returns a list of human-readable errors.
952
+ * Returns an empty array if the config is valid.
953
+ */
954
+ function validateConfig(config) {
955
+ const result = scaffoldConfigSchema.safeParse(config);
956
+ if (result.success) return [];
957
+ return result.error.issues.map((issue) => {
958
+ return `${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
959
+ });
960
+ }
961
+ /**
962
+ * Validates template paths exist on disk.
963
+ * Only checks non-glob, non-negation paths.
964
+ */
965
+ async function validateTemplatePaths(templates) {
966
+ const errors = [];
967
+ for (const tpl of templates) {
968
+ if (tpl.startsWith("!") || tpl.includes("*")) continue;
969
+ if (!await pathExists(tpl)) errors.push(`templates: path does not exist: ${tpl}`);
970
+ }
971
+ return errors;
972
+ }
973
+ /**
974
+ * Validates the config and throws a formatted error if any issues are found.
975
+ * Checks both schema validity and template path existence.
976
+ */
977
+ async function assertConfigValid(config) {
978
+ const schemaErrors = validateConfig(config);
979
+ const pathErrors = config && typeof config === "object" && "templates" in config && Array.isArray(config.templates) ? await validateTemplatePaths(config.templates) : [];
980
+ const allErrors = [...schemaErrors, ...pathErrors];
981
+ if (allErrors.length > 0) {
982
+ const lines = allErrors.map((e) => ` - ${e}`);
983
+ throw new Error(`Invalid scaffold config:\n${lines.join("\n")}`);
984
+ }
985
+ }
986
+ //#endregion
987
+ //#region src/scaffold.ts
988
+ /**
989
+ * @module
990
+ * Simple Scaffold
991
+ *
992
+ * See [readme](README.md)
993
+ */
994
+ /**
995
+ * Create a scaffold using given `options`.
996
+ *
997
+ * #### Create files
998
+ * To create a file structure to output, use any directory and file structure you would like.
999
+ * Inside folder names, file names or file contents, you may place `{{ var }}` where `var` is either
1000
+ * `name` which is the scaffold name you provided or one of the keys you provided in the `data` option.
1001
+ *
1002
+ * The contents and names will be replaced with the transformed values so you can use your original structure as a
1003
+ * boilerplate for other projects, components, modules, or even single files.
1004
+ *
1005
+ * The files will maintain their structure, starting from the directory containing the template (or the template itself
1006
+ * if it is already a directory), and will output from that directory into the directory defined by `config.output`.
1007
+ *
1008
+ * #### Helpers
1009
+ * Helpers are functions you can use to transform your `{{ var }}` contents into other values without having to
1010
+ * pre-define the data and use a duplicated key.
1011
+ *
1012
+ * Any functions you provide in `helpers` option will also be available to you to make custom formatting as you see fit
1013
+ * (for example, formatting a date)
1014
+ *
1015
+ * For available default values, see {@link DefaultHelpers}.
1016
+ *
1017
+ * @param {ScaffoldConfig} config The main configuration object
1018
+ * @return {Promise<void>} A promise that resolves when the scaffold is complete
1019
+ *
1020
+ * @see {@link DefaultHelpers}
1021
+ * @see {@link CaseHelpers}
1022
+ * @see {@link DateHelpers}
1023
+ *
1024
+ * @category Main
1025
+ */
1026
+ async function Scaffold(config) {
1027
+ config.output ??= process.cwd();
1028
+ await assertConfigValid(config);
1029
+ config = await resolveInputs(config);
1030
+ registerHelpers(config);
1031
+ const startTime = performance.now();
1032
+ const writtenFiles = [];
1033
+ try {
1034
+ config.data = {
1035
+ name: config.name,
1036
+ ...config.data
1037
+ };
1038
+ logInitStep(config);
1039
+ log(config, LogLevel.info, `Scaffolding "${config.name}"...`);
1040
+ const excludes = config.templates.filter((t) => t.startsWith("!"));
1041
+ const includes = config.templates.filter((t) => !t.startsWith("!"));
1042
+ const templates = await resolveTemplateGlobs(config, includes);
1043
+ for (const tpl of templates) {
1044
+ const files = await processTemplateGlob(config, tpl, excludes);
1045
+ writtenFiles.push(...files);
1046
+ }
1047
+ } catch (e) {
1048
+ log(config, LogLevel.error, e);
1049
+ throw e;
1050
+ }
1051
+ const elapsed = performance.now() - startTime;
1052
+ logFileTree(config, writtenFiles);
1053
+ logSummary(config, writtenFiles.length, elapsed, config.dryRun);
1054
+ if (config.afterScaffold) await runAfterScaffoldHook(config, writtenFiles);
1055
+ }
1056
+ /** Resolves included template paths into GlobInfo objects. */
1057
+ async function resolveTemplateGlobs(config, includes) {
1058
+ const templates = [];
1059
+ for (const includedTemplate of includes) try {
1060
+ templates.push(await getTemplateGlobInfo(config, includedTemplate));
1061
+ } catch (e) {
1062
+ handleErr(e);
1063
+ }
1064
+ return templates;
1065
+ }
1066
+ /** Processes all files matching a single template glob pattern. Returns paths of written files. */
1067
+ async function processTemplateGlob(config, tpl, excludes) {
1068
+ const written = [];
1069
+ const ignorePatterns = await loadIgnorePatterns(tpl.baseTemplatePath);
1070
+ if (ignorePatterns.length > 0) log(config, LogLevel.debug, `Loaded .scaffoldignore patterns:`, ignorePatterns);
1071
+ const files = filterIgnoredFiles(await getFileList(config, [tpl.template, ...excludes]), ignorePatterns, tpl.baseTemplatePath);
1072
+ for (const file of files) {
1073
+ if (await isDir(file)) continue;
1074
+ log(config, LogLevel.debug, "Iterating files", {
1075
+ files,
1076
+ file
1077
+ });
1078
+ const relPath = makeRelativePath(node_path.default.dirname(removeGlob(file).replace(tpl.baseTemplatePath, "")));
1079
+ const basePath = getBasePath(relPath);
1080
+ log(config, LogLevel.debug, {
1081
+ originalTemplate: tpl.origTemplate,
1082
+ relativePath: relPath,
1083
+ parsedTemplate: tpl.template,
1084
+ inputFilePath: file,
1085
+ baseTemplatePath: tpl.baseTemplatePath,
1086
+ basePath,
1087
+ isDirOrGlob: tpl.isDirOrGlob,
1088
+ isGlob: tpl.isGlob
1089
+ });
1090
+ const outputPath = await handleTemplateFile(config, {
1091
+ templatePath: file,
1092
+ basePath
1093
+ });
1094
+ if (outputPath) written.push(outputPath);
1095
+ }
1096
+ return written;
1097
+ }
1098
+ /** Executes the afterScaffold hook — either a function or a shell command string. */
1099
+ async function runAfterScaffoldHook(config, files) {
1100
+ const hook = config.afterScaffold;
1101
+ if (typeof hook === "function") {
1102
+ log(config, LogLevel.debug, "Running afterScaffold function hook");
1103
+ await hook({
1104
+ config,
1105
+ files
1106
+ });
1107
+ return;
1108
+ }
1109
+ const outputDir = typeof config.output === "string" ? config.output : process.cwd();
1110
+ const cwd = node_path.default.resolve(process.cwd(), outputDir);
1111
+ log(config, LogLevel.info, `Running afterScaffold command: ${hook}`);
1112
+ await new Promise((resolve, reject) => {
1113
+ const proc = (0, node_child_process.exec)(hook, { cwd });
1114
+ proc.stdout?.on("data", (data) => {
1115
+ log(config, LogLevel.info, data.toString().trimEnd());
1116
+ });
1117
+ proc.stderr?.on("data", (data) => {
1118
+ log(config, LogLevel.warning, data.toString().trimEnd());
1119
+ });
1120
+ proc.on("close", (code) => {
1121
+ if (code === 0) resolve();
1122
+ else reject(/* @__PURE__ */ new Error(`afterScaffold command exited with code ${code}`));
1123
+ });
1124
+ proc.on("error", reject);
1125
+ });
1126
+ }
1127
+ /**
1128
+ * Create a scaffold based on a config file or URL.
1129
+ *
1130
+ * @param {string} pathOrUrl The path or URL to the config file
1131
+ * @param {Record<string, string>} config Information needed before loading the config
1132
+ * @param {Partial<Omit<ScaffoldConfig, 'name'>>} overrides Any overrides to the loaded config
1133
+ *
1134
+ * @see {@link Scaffold}
1135
+ * @category Main
1136
+ * @return {Promise<void>} A promise that resolves when the scaffold is complete
1137
+ */
1138
+ Scaffold.fromConfig = async function(pathOrUrl, config, overrides) {
1139
+ const tmpPath = node_path.default.resolve(node_os.default.tmpdir(), `scaffold-config-${Date.now()}`);
1140
+ const _cmdConfig = {
1141
+ dryRun: false,
1142
+ output: process.cwd(),
1143
+ logLevel: LogLevel.info,
1144
+ overwrite: false,
1145
+ templates: [],
1146
+ subdir: false,
1147
+ quiet: false,
1148
+ config: pathOrUrl,
1149
+ version: false,
1150
+ tmpDir: tmpPath,
1151
+ ...config
1152
+ };
1153
+ const _overrides = resolve(overrides, _cmdConfig);
1154
+ return Scaffold({
1155
+ ...await parseConfigFile(_cmdConfig),
1156
+ ..._overrides
1157
+ });
1158
+ };
1159
+ //#endregion
1160
+ Object.defineProperty(exports, "LogLevel", {
1161
+ enumerable: true,
1162
+ get: function() {
1163
+ return LogLevel;
1164
+ }
1165
+ });
1166
+ Object.defineProperty(exports, "Scaffold", {
1167
+ enumerable: true,
1168
+ get: function() {
1169
+ return Scaffold;
1170
+ }
1171
+ });
1172
+ Object.defineProperty(exports, "__toESM", {
1173
+ enumerable: true,
1174
+ get: function() {
1175
+ return __toESM;
1176
+ }
1177
+ });
1178
+ Object.defineProperty(exports, "assertConfigValid", {
1179
+ enumerable: true,
1180
+ get: function() {
1181
+ return assertConfigValid;
1182
+ }
1183
+ });
1184
+ Object.defineProperty(exports, "colorize", {
1185
+ enumerable: true,
1186
+ get: function() {
1187
+ return colorize;
1188
+ }
1189
+ });
1190
+ Object.defineProperty(exports, "findConfigFile", {
1191
+ enumerable: true,
1192
+ get: function() {
1193
+ return findConfigFile;
1194
+ }
1195
+ });
1196
+ Object.defineProperty(exports, "getConfigFile", {
1197
+ enumerable: true,
1198
+ get: function() {
1199
+ return getConfigFile;
1200
+ }
1201
+ });
1202
+ Object.defineProperty(exports, "getUniqueTmpPath", {
1203
+ enumerable: true,
1204
+ get: function() {
1205
+ return getUniqueTmpPath;
1206
+ }
1207
+ });
1208
+ Object.defineProperty(exports, "log", {
1209
+ enumerable: true,
1210
+ get: function() {
1211
+ return log;
1212
+ }
1213
+ });
1214
+ Object.defineProperty(exports, "parseAppendData", {
1215
+ enumerable: true,
1216
+ get: function() {
1217
+ return parseAppendData;
1218
+ }
1219
+ });
1220
+ Object.defineProperty(exports, "parseConfigFile", {
1221
+ enumerable: true,
1222
+ get: function() {
1223
+ return parseConfigFile;
1224
+ }
1225
+ });
1226
+ Object.defineProperty(exports, "pathExists", {
1227
+ enumerable: true,
1228
+ get: function() {
1229
+ return pathExists;
1230
+ }
1231
+ });
1232
+ Object.defineProperty(exports, "promptAfterConfig", {
1233
+ enumerable: true,
1234
+ get: function() {
1235
+ return promptAfterConfig;
1236
+ }
1237
+ });
1238
+ Object.defineProperty(exports, "promptBeforeConfig", {
1239
+ enumerable: true,
1240
+ get: function() {
1241
+ return promptBeforeConfig;
1242
+ }
1243
+ });
1244
+ Object.defineProperty(exports, "resolveInputs", {
1245
+ enumerable: true,
1246
+ get: function() {
1247
+ return resolveInputs;
1248
+ }
1249
+ });
1250
+ Object.defineProperty(exports, "scaffoldConfigSchema", {
1251
+ enumerable: true,
1252
+ get: function() {
1253
+ return scaffoldConfigSchema;
1254
+ }
1255
+ });
1256
+ Object.defineProperty(exports, "validateConfig", {
1257
+ enumerable: true,
1258
+ get: function() {
1259
+ return validateConfig;
1260
+ }
1261
+ });
1262
+
1263
+ //# sourceMappingURL=scaffold-DOzCgpZT.js.map