@timeax/scaffold 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs DELETED
@@ -1,848 +0,0 @@
1
- import path2 from 'path';
2
- import fs2 from 'fs';
3
- import os from 'os';
4
- import crypto from 'crypto';
5
- import { pathToFileURL } from 'url';
6
- import { transform } from 'esbuild';
7
- import { minimatch } from 'minimatch';
8
-
9
- // src/schema/index.ts
10
- var SCAFFOLD_ROOT_DIR = ".scaffold";
11
-
12
- // src/util/logger.ts
13
- var supportsColor = typeof process !== "undefined" && process.stdout && process.stdout.isTTY && process.env.NO_COLOR !== "1";
14
- function wrap(code) {
15
- const open = `\x1B[${code}m`;
16
- const close = `\x1B[0m`;
17
- return (text) => supportsColor ? `${open}${text}${close}` : text;
18
- }
19
- var color = {
20
- red: wrap(31),
21
- yellow: wrap(33),
22
- green: wrap(32),
23
- cyan: wrap(36),
24
- magenta: wrap(35),
25
- dim: wrap(2),
26
- bold: wrap(1),
27
- gray: wrap(90)
28
- };
29
- function colorForLevel(level) {
30
- switch (level) {
31
- case "error":
32
- return color.red;
33
- case "warn":
34
- return color.yellow;
35
- case "info":
36
- return color.cyan;
37
- case "debug":
38
- return color.gray;
39
- default:
40
- return (s) => s;
41
- }
42
- }
43
- var Logger = class _Logger {
44
- level;
45
- prefix;
46
- constructor(options = {}) {
47
- this.level = options.level ?? "info";
48
- this.prefix = options.prefix;
49
- }
50
- setLevel(level) {
51
- this.level = level;
52
- }
53
- getLevel() {
54
- return this.level;
55
- }
56
- /**
57
- * Create a child logger with an additional prefix.
58
- */
59
- child(prefix) {
60
- const combined = this.prefix ? `${this.prefix}${prefix}` : prefix;
61
- return new _Logger({ level: this.level, prefix: combined });
62
- }
63
- formatMessage(msg, lvl) {
64
- const text = typeof msg === "string" ? msg : msg instanceof Error ? msg.message : String(msg);
65
- const levelColor = colorForLevel(lvl);
66
- const prefixColored = this.prefix ? color.magenta(this.prefix) : void 0;
67
- const textColored = lvl === "debug" ? color.dim(text) : levelColor(text);
68
- if (prefixColored) {
69
- return `${prefixColored} ${textColored}`;
70
- }
71
- return textColored;
72
- }
73
- shouldLog(targetLevel) {
74
- const order = ["silent", "error", "warn", "info", "debug"];
75
- const currentIdx = order.indexOf(this.level);
76
- const targetIdx = order.indexOf(targetLevel);
77
- if (currentIdx === -1 || targetIdx === -1) return true;
78
- if (this.level === "silent") return false;
79
- return targetIdx <= currentIdx || targetLevel === "error";
80
- }
81
- error(msg, ...rest) {
82
- if (!this.shouldLog("error")) return;
83
- console.error(this.formatMessage(msg, "error"), ...rest);
84
- }
85
- warn(msg, ...rest) {
86
- if (!this.shouldLog("warn")) return;
87
- console.warn(this.formatMessage(msg, "warn"), ...rest);
88
- }
89
- info(msg, ...rest) {
90
- if (!this.shouldLog("info")) return;
91
- console.log(this.formatMessage(msg, "info"), ...rest);
92
- }
93
- debug(msg, ...rest) {
94
- if (!this.shouldLog("debug")) return;
95
- console.debug(this.formatMessage(msg, "debug"), ...rest);
96
- }
97
- };
98
- var defaultLogger = new Logger({
99
- level: process.env.SCAFFOLD_LOG_LEVEL ?? "info",
100
- prefix: "[scaffold]"
101
- });
102
- function toPosixPath(p) {
103
- return p.replace(/\\/g, "/");
104
- }
105
- function ensureDirSync(dirPath) {
106
- if (!fs2.existsSync(dirPath)) {
107
- fs2.mkdirSync(dirPath, { recursive: true });
108
- }
109
- return dirPath;
110
- }
111
- function statSafeSync(targetPath) {
112
- try {
113
- return fs2.statSync(targetPath);
114
- } catch {
115
- return null;
116
- }
117
- }
118
- function toProjectRelativePath(projectRoot, absolutePath) {
119
- const absRoot = path2.resolve(projectRoot);
120
- const absTarget = path2.resolve(absolutePath);
121
- const rootWithSep = absRoot.endsWith(path2.sep) ? absRoot : absRoot + path2.sep;
122
- if (!absTarget.startsWith(rootWithSep) && absTarget !== absRoot) {
123
- throw new Error(
124
- `Path "${absTarget}" is not inside project root "${absRoot}".`
125
- );
126
- }
127
- const rel = path2.relative(absRoot, absTarget);
128
- return toPosixPath(rel);
129
- }
130
-
131
- // src/core/config-loader.ts
132
- var logger = defaultLogger.child("[config]");
133
- async function loadScaffoldConfig(cwd, options = {}) {
134
- const absCwd = path2.resolve(cwd);
135
- const initialScaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(absCwd, SCAFFOLD_ROOT_DIR);
136
- const configPath = options.configPath ?? resolveConfigPath(initialScaffoldDir);
137
- const config = await importConfig(configPath);
138
- let configRoot = absCwd;
139
- if (config.root) {
140
- configRoot = path2.resolve(absCwd, config.root);
141
- }
142
- const scaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(configRoot, SCAFFOLD_ROOT_DIR);
143
- const baseRoot = config.base ? path2.resolve(configRoot, config.base) : configRoot;
144
- logger.debug(
145
- `Loaded config: configRoot=${configRoot}, baseRoot=${baseRoot}, scaffoldDir=${scaffoldDir}`
146
- );
147
- return {
148
- config,
149
- scaffoldDir,
150
- projectRoot: baseRoot
151
- };
152
- }
153
- function resolveConfigPath(scaffoldDir) {
154
- const candidates = [
155
- "config.ts",
156
- "config.mts",
157
- "config.mjs",
158
- "config.js",
159
- "config.cjs"
160
- ];
161
- for (const file of candidates) {
162
- const full = path2.join(scaffoldDir, file);
163
- if (fs2.existsSync(full)) {
164
- return full;
165
- }
166
- }
167
- throw new Error(
168
- `Could not find scaffold config in ${scaffoldDir}. Looked for: ${candidates.join(
169
- ", "
170
- )}`
171
- );
172
- }
173
- async function importConfig(configPath) {
174
- const ext = path2.extname(configPath).toLowerCase();
175
- if (ext === ".ts" || ext === ".tsx") {
176
- return importTsConfig(configPath);
177
- }
178
- const url = pathToFileURL(configPath).href;
179
- const mod = await import(url);
180
- return mod.default ?? mod;
181
- }
182
- async function importTsConfig(configPath) {
183
- const source = fs2.readFileSync(configPath, "utf8");
184
- const stat = fs2.statSync(configPath);
185
- const hash = crypto.createHash("sha1").update(configPath).update(String(stat.mtimeMs)).digest("hex");
186
- const tmpDir = path2.join(os.tmpdir(), "timeax-scaffold-config");
187
- ensureDirSync(tmpDir);
188
- const tmpFile = path2.join(tmpDir, `${hash}.mjs`);
189
- if (!fs2.existsSync(tmpFile)) {
190
- const result = await transform(source, {
191
- loader: "ts",
192
- format: "esm",
193
- sourcemap: "inline",
194
- target: "ESNext",
195
- tsconfigRaw: {
196
- compilerOptions: {}
197
- }
198
- });
199
- fs2.writeFileSync(tmpFile, result.code, "utf8");
200
- }
201
- const url = pathToFileURL(tmpFile).href;
202
- const mod = await import(url);
203
- return mod.default ?? mod;
204
- }
205
-
206
- // src/core/structure-txt.ts
207
- function stripInlineComment(content) {
208
- let cutIndex = -1;
209
- const len = content.length;
210
- for (let i = 0; i < len; i++) {
211
- const ch = content[i];
212
- const prev = i > 0 ? content[i - 1] : "";
213
- if (ch === "#") {
214
- if (i === 0) continue;
215
- if (prev === " " || prev === " ") {
216
- cutIndex = i;
217
- break;
218
- }
219
- }
220
- if (ch === "/" && i + 1 < len && content[i + 1] === "/" && (prev === " " || prev === " ")) {
221
- cutIndex = i;
222
- break;
223
- }
224
- }
225
- if (cutIndex === -1) {
226
- return content.trimEnd();
227
- }
228
- return content.slice(0, cutIndex).trimEnd();
229
- }
230
- function parseLine(line, lineNo) {
231
- const match = line.match(/^(\s*)(.*)$/);
232
- if (!match) return null;
233
- const indentSpaces = match[1].length;
234
- let rest = match[2];
235
- if (!rest.trim()) return null;
236
- const trimmedRest = rest.trimStart();
237
- if (trimmedRest.startsWith("#") || trimmedRest.startsWith("//")) {
238
- return null;
239
- }
240
- const stripped = stripInlineComment(rest);
241
- const trimmed = stripped.trim();
242
- if (!trimmed) return null;
243
- const parts = trimmed.split(/\s+/);
244
- if (!parts.length) return null;
245
- const pathToken = parts[0];
246
- if (pathToken.includes(":")) {
247
- throw new Error(
248
- `structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). Invalid path "${pathToken}" on line ${lineNo}.`
249
- );
250
- }
251
- let stub;
252
- const include = [];
253
- const exclude = [];
254
- for (const token of parts.slice(1)) {
255
- if (token.startsWith("@stub:")) {
256
- stub = token.slice("@stub:".length);
257
- } else if (token.startsWith("@include:")) {
258
- const val = token.slice("@include:".length);
259
- if (val) {
260
- include.push(
261
- ...val.split(",").map((s) => s.trim()).filter(Boolean)
262
- );
263
- }
264
- } else if (token.startsWith("@exclude:")) {
265
- const val = token.slice("@exclude:".length);
266
- if (val) {
267
- exclude.push(
268
- ...val.split(",").map((s) => s.trim()).filter(Boolean)
269
- );
270
- }
271
- }
272
- }
273
- return {
274
- lineNo,
275
- indentSpaces,
276
- rawPath: pathToken,
277
- stub,
278
- include: include.length ? include : void 0,
279
- exclude: exclude.length ? exclude : void 0
280
- };
281
- }
282
- function parseStructureText(text, indentStep = 2) {
283
- const lines = text.split(/\r?\n/);
284
- const parsed = [];
285
- for (let i = 0; i < lines.length; i++) {
286
- const lineNo = i + 1;
287
- const p = parseLine(lines[i], lineNo);
288
- if (p) parsed.push(p);
289
- }
290
- const rootEntries = [];
291
- const stack = [];
292
- for (const p of parsed) {
293
- const { indentSpaces, lineNo } = p;
294
- if (indentSpaces % indentStep !== 0) {
295
- throw new Error(
296
- `structure.txt: Invalid indent on line ${lineNo}. Indent must be multiples of ${indentStep} spaces.`
297
- );
298
- }
299
- const level = indentSpaces / indentStep;
300
- if (level > stack.length) {
301
- if (level !== stack.length + 1) {
302
- throw new Error(
303
- `structure.txt: Invalid indentation on line ${lineNo}. You cannot jump more than one level at a time. Previous depth: ${stack.length}, this line depth: ${level}.`
304
- );
305
- }
306
- }
307
- if (level > 0) {
308
- const parent2 = stack[level - 1];
309
- if (!parent2) {
310
- throw new Error(
311
- `structure.txt: Indented entry without a parent on line ${lineNo}.`
312
- );
313
- }
314
- if (!parent2.isDir) {
315
- throw new Error(
316
- `structure.txt: Cannot indent under a file on line ${lineNo}. Files cannot have children. Parent: "${parent2.entry.path}".`
317
- );
318
- }
319
- }
320
- const isDir = p.rawPath.endsWith("/");
321
- const clean = p.rawPath.replace(/\/$/, "");
322
- const basePath = toPosixPath(clean);
323
- while (stack.length > level) {
324
- stack.pop();
325
- }
326
- const parent = stack[stack.length - 1]?.entry;
327
- const parentPath = parent ? parent.path.replace(/\/$/, "") : "";
328
- const fullPath = parentPath ? `${parentPath}/${basePath}${isDir ? "/" : ""}` : `${basePath}${isDir ? "/" : ""}`;
329
- if (isDir) {
330
- const dirEntry = {
331
- type: "dir",
332
- path: fullPath,
333
- children: [],
334
- ...p.stub ? { stub: p.stub } : {},
335
- ...p.include ? { include: p.include } : {},
336
- ...p.exclude ? { exclude: p.exclude } : {}
337
- };
338
- if (parent && parent.type === "dir") {
339
- parent.children = parent.children ?? [];
340
- parent.children.push(dirEntry);
341
- } else if (!parent) {
342
- rootEntries.push(dirEntry);
343
- }
344
- stack.push({ level, entry: dirEntry, isDir: true });
345
- } else {
346
- const fileEntry = {
347
- type: "file",
348
- path: fullPath,
349
- ...p.stub ? { stub: p.stub } : {},
350
- ...p.include ? { include: p.include } : {},
351
- ...p.exclude ? { exclude: p.exclude } : {}
352
- };
353
- if (parent && parent.type === "dir") {
354
- parent.children = parent.children ?? [];
355
- parent.children.push(fileEntry);
356
- } else if (!parent) {
357
- rootEntries.push(fileEntry);
358
- }
359
- stack.push({ level, entry: fileEntry, isDir: false });
360
- }
361
- }
362
- return rootEntries;
363
- }
364
-
365
- // src/core/resolve-structure.ts
366
- var logger2 = defaultLogger.child("[structure]");
367
- function resolveGroupStructure(scaffoldDir, group) {
368
- if (group.structure && group.structure.length) {
369
- logger2.debug(`Using inline structure for group "${group.name}"`);
370
- return group.structure;
371
- }
372
- const fileName = group.structureFile ?? `${group.name}.txt`;
373
- const filePath = path2.join(scaffoldDir, fileName);
374
- if (!fs2.existsSync(filePath)) {
375
- throw new Error(
376
- `@timeax/scaffold: Group "${group.name}" has no structure. Expected file "${fileName}" in "${scaffoldDir}".`
377
- );
378
- }
379
- logger2.debug(`Reading structure for group "${group.name}" from ${filePath}`);
380
- const raw = fs2.readFileSync(filePath, "utf8");
381
- return parseStructureText(raw);
382
- }
383
- function resolveSingleStructure(scaffoldDir, config) {
384
- if (config.structure && config.structure.length) {
385
- logger2.debug("Using inline single structure (no groups)");
386
- return config.structure;
387
- }
388
- const fileName = config.structureFile ?? "structure.txt";
389
- const filePath = path2.join(scaffoldDir, fileName);
390
- if (!fs2.existsSync(filePath)) {
391
- throw new Error(
392
- `@timeax/scaffold: No structure defined. Expected "${fileName}" in "${scaffoldDir}".`
393
- );
394
- }
395
- logger2.debug(`Reading single structure from ${filePath}`);
396
- const raw = fs2.readFileSync(filePath, "utf8");
397
- return parseStructureText(raw);
398
- }
399
- var logger3 = defaultLogger.child("[cache]");
400
- var DEFAULT_CACHE = {
401
- version: 1,
402
- entries: {}
403
- };
404
- var CacheManager = class {
405
- constructor(projectRoot, cacheFileRelPath) {
406
- this.projectRoot = projectRoot;
407
- this.cacheFileRelPath = cacheFileRelPath;
408
- }
409
- cache = DEFAULT_CACHE;
410
- get cachePathAbs() {
411
- return path2.resolve(this.projectRoot, this.cacheFileRelPath);
412
- }
413
- load() {
414
- const cachePath = this.cachePathAbs;
415
- if (!fs2.existsSync(cachePath)) {
416
- this.cache = { ...DEFAULT_CACHE, entries: {} };
417
- return;
418
- }
419
- try {
420
- const raw = fs2.readFileSync(cachePath, "utf8");
421
- const parsed = JSON.parse(raw);
422
- if (parsed.version === 1 && parsed.entries) {
423
- this.cache = parsed;
424
- } else {
425
- logger3.warn("Cache file version mismatch or invalid, resetting cache.");
426
- this.cache = { ...DEFAULT_CACHE, entries: {} };
427
- }
428
- } catch (err) {
429
- logger3.warn("Failed to read cache file, resetting cache.", err);
430
- this.cache = { ...DEFAULT_CACHE, entries: {} };
431
- }
432
- }
433
- save() {
434
- const cachePath = this.cachePathAbs;
435
- const dir = path2.dirname(cachePath);
436
- ensureDirSync(dir);
437
- fs2.writeFileSync(cachePath, JSON.stringify(this.cache, null, 2), "utf8");
438
- }
439
- get(relPath) {
440
- const key = toPosixPath(relPath);
441
- return this.cache.entries[key];
442
- }
443
- set(entry) {
444
- const key = toPosixPath(entry.path);
445
- this.cache.entries[key] = {
446
- ...entry,
447
- path: key
448
- };
449
- }
450
- delete(relPath) {
451
- const key = toPosixPath(relPath);
452
- delete this.cache.entries[key];
453
- }
454
- allPaths() {
455
- return Object.keys(this.cache.entries);
456
- }
457
- allEntries() {
458
- return Object.values(this.cache.entries);
459
- }
460
- };
461
- function matchesFilter(pathRel, cfg) {
462
- const { include, exclude, files } = cfg;
463
- const patterns = [];
464
- if (include?.length) patterns.push(...include);
465
- if (files?.length) patterns.push(...files);
466
- if (patterns.length) {
467
- const ok = patterns.some((p) => minimatch(pathRel, p));
468
- if (!ok) return false;
469
- }
470
- if (exclude?.length) {
471
- const blocked = exclude.some((p) => minimatch(pathRel, p));
472
- if (blocked) return false;
473
- }
474
- return true;
475
- }
476
- var HookRunner = class {
477
- constructor(config) {
478
- this.config = config;
479
- }
480
- async runRegular(kind, ctx) {
481
- const configs = this.config.hooks?.[kind] ?? [];
482
- for (const cfg of configs) {
483
- if (!matchesFilter(ctx.targetPath, cfg)) continue;
484
- await cfg.fn(ctx);
485
- }
486
- }
487
- getStubConfig(stubName) {
488
- if (!stubName) return void 0;
489
- return this.config.stubs?.[stubName];
490
- }
491
- async runStub(kind, ctx) {
492
- const stub = this.getStubConfig(ctx.stubName);
493
- if (!stub?.hooks) return;
494
- const configs = kind === "preStub" ? stub.hooks.preStub ?? [] : stub.hooks.postStub ?? [];
495
- for (const cfg of configs) {
496
- if (!matchesFilter(ctx.targetPath, cfg)) continue;
497
- await cfg.fn(ctx);
498
- }
499
- }
500
- async renderStubContent(ctx) {
501
- const stub = this.getStubConfig(ctx.stubName);
502
- if (!stub?.getContent) return void 0;
503
- return stub.getContent(ctx);
504
- }
505
- };
506
- async function applyStructure(opts) {
507
- const {
508
- config,
509
- projectRoot,
510
- baseDir,
511
- structure,
512
- cache,
513
- hooks,
514
- groupName,
515
- groupRoot,
516
- sizePromptThreshold,
517
- interactiveDelete
518
- } = opts;
519
- const logger5 = opts.logger ?? defaultLogger.child(groupName ? `[apply:${groupName}]` : "[apply]");
520
- const desiredPaths = /* @__PURE__ */ new Set();
521
- const threshold = sizePromptThreshold ?? config.sizePromptThreshold ?? 128 * 1024;
522
- async function walk(entry, inheritedStub) {
523
- const effectiveStub = entry.stub ?? inheritedStub;
524
- if (entry.type === "dir") {
525
- await handleDir(entry, effectiveStub);
526
- } else {
527
- await handleFile(entry, effectiveStub);
528
- }
529
- }
530
- async function handleDir(entry, inheritedStub) {
531
- const relFromBase = entry.path.replace(/^[./]+/, "");
532
- const absDir = path2.resolve(baseDir, relFromBase);
533
- const relFromRoot = toPosixPath(
534
- toProjectRelativePath(projectRoot, absDir)
535
- );
536
- desiredPaths.add(relFromRoot);
537
- ensureDirSync(absDir);
538
- const nextStub = entry.stub ?? inheritedStub;
539
- if (entry.children) {
540
- for (const child of entry.children) {
541
- await walk(child, nextStub);
542
- }
543
- }
544
- }
545
- async function handleFile(entry, inheritedStub) {
546
- const relFromBase = entry.path.replace(/^[./]+/, "");
547
- const absFile = path2.resolve(baseDir, relFromBase);
548
- const relFromRoot = toPosixPath(
549
- toProjectRelativePath(projectRoot, absFile)
550
- );
551
- desiredPaths.add(relFromRoot);
552
- const stubName = entry.stub ?? inheritedStub;
553
- const ctx = {
554
- projectRoot,
555
- targetPath: relFromRoot,
556
- absolutePath: absFile,
557
- isDirectory: false,
558
- stubName
559
- };
560
- if (fs2.existsSync(absFile)) {
561
- return;
562
- }
563
- await hooks.runRegular("preCreateFile", ctx);
564
- const dir = path2.dirname(absFile);
565
- ensureDirSync(dir);
566
- if (stubName) {
567
- await hooks.runStub("preStub", ctx);
568
- }
569
- let content = "";
570
- const stubContent = await hooks.renderStubContent(ctx);
571
- if (typeof stubContent === "string") {
572
- content = stubContent;
573
- }
574
- fs2.writeFileSync(absFile, content, "utf8");
575
- const stats = fs2.statSync(absFile);
576
- cache.set({
577
- path: relFromRoot,
578
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
579
- sizeAtCreate: stats.size,
580
- createdByStub: stubName,
581
- groupName,
582
- groupRoot
583
- });
584
- logger5.info(`created ${relFromRoot}`);
585
- if (stubName) {
586
- await hooks.runStub("postStub", ctx);
587
- }
588
- await hooks.runRegular("postCreateFile", ctx);
589
- }
590
- for (const entry of structure) {
591
- await walk(entry);
592
- }
593
- for (const cachedPath of cache.allPaths()) {
594
- if (desiredPaths.has(cachedPath)) continue;
595
- const abs = path2.resolve(projectRoot, cachedPath);
596
- const stats = statSafeSync(abs);
597
- if (!stats) {
598
- cache.delete(cachedPath);
599
- continue;
600
- }
601
- if (!stats.isFile()) {
602
- cache.delete(cachedPath);
603
- continue;
604
- }
605
- const entry = cache.get(cachedPath);
606
- const ctx = {
607
- projectRoot,
608
- targetPath: cachedPath,
609
- absolutePath: abs,
610
- isDirectory: false,
611
- stubName: entry?.createdByStub
612
- };
613
- await hooks.runRegular("preDeleteFile", ctx);
614
- let shouldDelete = true;
615
- if (stats.size > threshold && interactiveDelete) {
616
- const res = await interactiveDelete({
617
- absolutePath: abs,
618
- relativePath: cachedPath,
619
- size: stats.size,
620
- createdByStub: entry?.createdByStub,
621
- groupName: entry?.groupName
622
- });
623
- if (res === "keep") {
624
- shouldDelete = false;
625
- cache.delete(cachedPath);
626
- logger5.info(`keeping ${cachedPath} (removed from cache)`);
627
- }
628
- }
629
- if (shouldDelete) {
630
- try {
631
- fs2.unlinkSync(abs);
632
- logger5.info(`deleted ${cachedPath}`);
633
- } catch (err) {
634
- logger5.warn(`failed to delete ${cachedPath}`, err);
635
- }
636
- cache.delete(cachedPath);
637
- await hooks.runRegular("postDeleteFile", ctx);
638
- }
639
- }
640
- }
641
-
642
- // src/core/runner.ts
643
- async function runOnce(cwd, options = {}) {
644
- const logger5 = options.logger ?? defaultLogger.child("[runner]");
645
- const { config, scaffoldDir, projectRoot } = await loadScaffoldConfig(cwd, {
646
- scaffoldDir: options.scaffoldDir,
647
- configPath: options.configPath
648
- });
649
- const cachePath = config.cacheFile ?? ".scaffold-cache.json";
650
- const cache = new CacheManager(projectRoot, cachePath);
651
- cache.load();
652
- const hooks = new HookRunner(config);
653
- if (config.groups && config.groups.length > 0) {
654
- for (const group of config.groups) {
655
- const groupRootAbs = path2.resolve(projectRoot, group.root);
656
- const structure = resolveGroupStructure(scaffoldDir, group);
657
- const groupLogger = logger5.child(`[group:${group.name}]`);
658
- await applyStructure({
659
- config,
660
- projectRoot,
661
- baseDir: groupRootAbs,
662
- structure,
663
- cache,
664
- hooks,
665
- groupName: group.name,
666
- groupRoot: group.root,
667
- interactiveDelete: options.interactiveDelete,
668
- logger: groupLogger
669
- });
670
- }
671
- } else {
672
- const structure = resolveSingleStructure(scaffoldDir, config);
673
- const baseLogger = logger5.child("[group:default]");
674
- await applyStructure({
675
- config,
676
- projectRoot,
677
- baseDir: projectRoot,
678
- structure,
679
- cache,
680
- hooks,
681
- groupName: "default",
682
- groupRoot: ".",
683
- interactiveDelete: options.interactiveDelete,
684
- logger: baseLogger
685
- });
686
- }
687
- cache.save();
688
- }
689
- var logger4 = defaultLogger.child("[scan]");
690
- var DEFAULT_IGNORE = [
691
- "node_modules/**",
692
- ".git/**",
693
- "dist/**",
694
- "build/**",
695
- ".turbo/**",
696
- ".next/**",
697
- "coverage/**"
698
- ];
699
- function scanDirectoryToStructureText(rootDir, options = {}) {
700
- const absRoot = path2.resolve(rootDir);
701
- const lines = [];
702
- const ignorePatterns = options.ignore ?? DEFAULT_IGNORE;
703
- const maxDepth = options.maxDepth ?? Infinity;
704
- function isIgnored(absPath) {
705
- const rel = toPosixPath(path2.relative(absRoot, absPath));
706
- if (!rel || rel === ".") return false;
707
- return ignorePatterns.some(
708
- (pattern) => minimatch(rel, pattern, { dot: true })
709
- );
710
- }
711
- function walk(currentAbs, depth) {
712
- if (depth > maxDepth) return;
713
- let dirents;
714
- try {
715
- dirents = fs2.readdirSync(currentAbs, { withFileTypes: true });
716
- } catch {
717
- return;
718
- }
719
- dirents.sort((a, b) => {
720
- if (a.isDirectory() && !b.isDirectory()) return -1;
721
- if (!a.isDirectory() && b.isDirectory()) return 1;
722
- return a.name.localeCompare(b.name);
723
- });
724
- for (const dirent of dirents) {
725
- const name = dirent.name;
726
- const absPath = path2.join(currentAbs, name);
727
- if (isIgnored(absPath)) continue;
728
- const indent = " ".repeat(depth);
729
- if (dirent.isDirectory()) {
730
- lines.push(`${indent}${name}/`);
731
- walk(absPath, depth + 1);
732
- } else if (dirent.isFile()) {
733
- lines.push(`${indent}${name}`);
734
- }
735
- }
736
- }
737
- walk(absRoot, 0);
738
- return lines.join("\n");
739
- }
740
- async function scanProjectFromConfig(cwd, options = {}) {
741
- const { config, scaffoldDir, projectRoot } = await loadScaffoldConfig(cwd, {
742
- scaffoldDir: options.scaffoldDir
743
- });
744
- const ignorePatterns = options.ignore ?? DEFAULT_IGNORE;
745
- const maxDepth = options.maxDepth ?? Infinity;
746
- const onlyGroups = options.groups;
747
- const results = [];
748
- function scanGroup(cfg, group) {
749
- const rootAbs = path2.resolve(projectRoot, group.root);
750
- const text = scanDirectoryToStructureText(rootAbs, {
751
- ignore: ignorePatterns,
752
- maxDepth
753
- });
754
- const structureFileName = group.structureFile ?? `${group.name}.txt`;
755
- const structureFilePath = path2.join(scaffoldDir, structureFileName);
756
- return {
757
- groupName: group.name,
758
- groupRoot: group.root,
759
- structureFileName,
760
- structureFilePath,
761
- text
762
- };
763
- }
764
- if (config.groups && config.groups.length > 0) {
765
- logger4.debug(
766
- `Scanning project from config with ${config.groups.length} group(s).`
767
- );
768
- for (const group of config.groups) {
769
- if (onlyGroups && !onlyGroups.includes(group.name)) {
770
- continue;
771
- }
772
- const result = scanGroup(config, group);
773
- results.push(result);
774
- }
775
- } else {
776
- logger4.debug("Scanning project in single-root mode (no groups).");
777
- const text = scanDirectoryToStructureText(projectRoot, {
778
- ignore: ignorePatterns,
779
- maxDepth
780
- });
781
- const structureFileName = config.structureFile ?? "structure.txt";
782
- const structureFilePath = path2.join(scaffoldDir, structureFileName);
783
- results.push({
784
- groupName: "default",
785
- groupRoot: ".",
786
- structureFileName,
787
- structureFilePath,
788
- text
789
- });
790
- }
791
- return results;
792
- }
793
- async function writeScannedStructuresFromConfig(cwd, options = {}) {
794
- const { scaffoldDir } = await loadScaffoldConfig(cwd, {
795
- scaffoldDir: options.scaffoldDir
796
- });
797
- ensureDirSync(scaffoldDir);
798
- const results = await scanProjectFromConfig(cwd, options);
799
- for (const result of results) {
800
- fs2.writeFileSync(result.structureFilePath, result.text, "utf8");
801
- logger4.info(
802
- `Wrote structure for group "${result.groupName}" to ${result.structureFilePath}`
803
- );
804
- }
805
- }
806
- async function ensureStructureFilesFromConfig(cwd, options = {}) {
807
- const { config, scaffoldDir } = await loadScaffoldConfig(cwd, {
808
- scaffoldDir: options.scaffoldDirOverride
809
- });
810
- ensureDirSync(scaffoldDir);
811
- const created = [];
812
- const existing = [];
813
- const seen = /* @__PURE__ */ new Set();
814
- const ensureFile = (fileName) => {
815
- if (!fileName) return;
816
- const filePath = path2.join(scaffoldDir, fileName);
817
- const key = path2.resolve(filePath);
818
- if (seen.has(key)) return;
819
- seen.add(key);
820
- if (fs2.existsSync(filePath)) {
821
- existing.push(filePath);
822
- return;
823
- }
824
- const header = `# ${fileName}
825
- # Structure file for @timeax/scaffold
826
- # Define your desired folders/files here.
827
- `;
828
- fs2.writeFileSync(filePath, header, "utf8");
829
- created.push(filePath);
830
- };
831
- if (config.groups && config.groups.length > 0) {
832
- for (const group of config.groups) {
833
- const fileName = group.structureFile ?? `${group.name}.txt`;
834
- ensureFile(fileName);
835
- }
836
- } else {
837
- const fileName = config.structureFile ?? "structure.txt";
838
- ensureFile(fileName);
839
- }
840
- logger4.debug(
841
- `ensureStructureFilesFromConfig: created=${created.length}, existing=${existing.length}`
842
- );
843
- return { created, existing };
844
- }
845
-
846
- export { SCAFFOLD_ROOT_DIR, ensureStructureFilesFromConfig, loadScaffoldConfig, parseStructureText, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
847
- //# sourceMappingURL=index.mjs.map
848
- //# sourceMappingURL=index.mjs.map