@oamm/textor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3190 @@
1
+ import { readFile, mkdir, writeFile, readdir, stat, rename, rmdir, rm, unlink, open } from 'fs/promises';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { createHash } from 'crypto';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+
8
+ const CONFIG_DIR$1 = '.textor';
9
+ const CONFIG_FILE = 'config.json';
10
+ /**
11
+ * @typedef {Object} TextorConfig
12
+ * @property {Object} paths
13
+ * @property {string} paths.pages
14
+ * @property {string} paths.features
15
+ * @property {string} paths.components
16
+ * @property {string} paths.layouts
17
+ * @property {Object} routing
18
+ * @property {string} routing.mode
19
+ * @property {string} routing.indexFile
20
+ * @property {Object} importAliases
21
+ * @property {string} importAliases.layouts
22
+ * @property {string} importAliases.features
23
+ * @property {Object} naming
24
+ * @property {string} naming.routeExtension
25
+ * @property {string} naming.featureExtension
26
+ * @property {string} naming.componentExtension
27
+ * @property {string} naming.hookExtension
28
+ * @property {string} naming.testExtension
29
+ * @property {Object} signatures
30
+ * @property {string} signatures.astro
31
+ * @property {string} signatures.typescript
32
+ * @property {string} signatures.javascript
33
+ * @property {Object} features
34
+ * @property {string} features.entry
35
+ * @property {boolean} features.createSubComponentsDir
36
+ * @property {boolean} features.createScriptsDir
37
+ * @property {string} features.scriptsIndexFile
38
+ * @property {Object} components
39
+ * @property {boolean} components.createSubComponentsDir
40
+ * @property {boolean} components.createContext
41
+ * @property {boolean} components.createHook
42
+ * @property {boolean} components.createTests
43
+ * @property {boolean} components.createConfig
44
+ * @property {boolean} components.createConstants
45
+ * @property {boolean} components.createTypes
46
+ * @property {Object} formatting
47
+ * @property {string} formatting.tool
48
+ * @property {Object} git
49
+ * @property {boolean} git.requireCleanRepo
50
+ * @property {boolean} git.stageChanges
51
+ * @property {Object} presets
52
+ * @property {string} defaultPreset
53
+ */
54
+ /**
55
+ * Default configuration for Textor
56
+ * @type {TextorConfig}
57
+ */
58
+ const DEFAULT_CONFIG = {
59
+ paths: {
60
+ pages: 'src/pages',
61
+ features: 'src/features',
62
+ components: 'src/components',
63
+ layouts: 'src/layouts'
64
+ },
65
+ routing: {
66
+ mode: 'flat', // 'flat' | 'nested'
67
+ indexFile: 'index.astro'
68
+ },
69
+ importAliases: {},
70
+ naming: {
71
+ routeExtension: '.astro',
72
+ featureExtension: '.astro',
73
+ componentExtension: '.tsx',
74
+ hookExtension: '.ts',
75
+ testExtension: '.test.tsx'
76
+ },
77
+ signatures: {
78
+ astro: '<!-- @generated by Textor -->',
79
+ typescript: '// @generated by Textor',
80
+ javascript: '// @generated by Textor',
81
+ tsx: '// @generated by Textor'
82
+ },
83
+ features: {
84
+ framework: 'astro',
85
+ entry: 'pascal', // 'index' | 'pascal'
86
+ createSubComponentsDir: true,
87
+ createScriptsDir: true,
88
+ scriptsIndexFile: 'scripts/index.ts',
89
+ createApi: false,
90
+ createServices: false,
91
+ createSchemas: false,
92
+ createHooks: false,
93
+ createContext: false,
94
+ createTests: false,
95
+ createTypes: false,
96
+ createReadme: false,
97
+ createStories: false,
98
+ createIndex: false
99
+ },
100
+ components: {
101
+ framework: 'react',
102
+ createSubComponentsDir: true,
103
+ createContext: true,
104
+ createHook: true,
105
+ createTests: true,
106
+ createConfig: true,
107
+ createConstants: true,
108
+ createTypes: true,
109
+ createApi: false,
110
+ createServices: false,
111
+ createSchemas: false,
112
+ createReadme: false,
113
+ createStories: false
114
+ },
115
+ formatting: {
116
+ tool: 'none' // 'prettier' | 'biome' | 'none'
117
+ },
118
+ hashing: {
119
+ normalization: 'normalizeEOL', // 'none' | 'normalizeEOL' | 'stripGeneratedRegions'
120
+ useMarkers: false
121
+ },
122
+ git: {
123
+ requireCleanRepo: false,
124
+ stageChanges: false
125
+ },
126
+ kindRules: [],
127
+ presets: {
128
+ minimal: {
129
+ features: {
130
+ createSubComponentsDir: false,
131
+ createScriptsDir: false
132
+ },
133
+ components: {
134
+ createSubComponentsDir: false,
135
+ createContext: false,
136
+ createHook: false,
137
+ createTests: false,
138
+ createConfig: false,
139
+ createConstants: false,
140
+ createTypes: false
141
+ }
142
+ },
143
+ standard: {
144
+ features: {
145
+ createSubComponentsDir: true,
146
+ createScriptsDir: true
147
+ },
148
+ components: {
149
+ createSubComponentsDir: true,
150
+ createContext: true,
151
+ createHook: true,
152
+ createTests: true,
153
+ createConfig: true,
154
+ createConstants: true,
155
+ createTypes: true
156
+ }
157
+ },
158
+ senior: {
159
+ features: {
160
+ createSubComponentsDir: true,
161
+ createScriptsDir: true,
162
+ createApi: true,
163
+ createServices: true,
164
+ createSchemas: true,
165
+ createHooks: true,
166
+ createContext: true,
167
+ createTests: true,
168
+ createTypes: true,
169
+ createReadme: true,
170
+ createStories: true,
171
+ createIndex: true
172
+ },
173
+ components: {
174
+ createSubComponentsDir: true,
175
+ createContext: true,
176
+ createHook: true,
177
+ createTests: true,
178
+ createConfig: true,
179
+ createConstants: true,
180
+ createTypes: true,
181
+ createApi: true,
182
+ createServices: true,
183
+ createSchemas: true,
184
+ createReadme: true,
185
+ createStories: true
186
+ }
187
+ }
188
+ },
189
+ defaultPreset: 'standard'
190
+ };
191
+ /**
192
+ * Gets the absolute path to the .textor directory.
193
+ * @returns {string}
194
+ */
195
+ function getConfigDir() {
196
+ return path.join(process.cwd(), CONFIG_DIR$1);
197
+ }
198
+ /**
199
+ * Gets the absolute path to the config.json file.
200
+ * @returns {string}
201
+ */
202
+ function getConfigPath() {
203
+ return path.join(getConfigDir(), CONFIG_FILE);
204
+ }
205
+ /**
206
+ * Loads the configuration from config.json and merges it with DEFAULT_CONFIG.
207
+ * @returns {Promise<TextorConfig>}
208
+ * @throws {Error} If configuration not found or invalid
209
+ */
210
+ async function loadConfig() {
211
+ const configPath = getConfigPath();
212
+ if (!existsSync(configPath)) {
213
+ throw new Error(`Textor configuration not found at ${configPath}\n` +
214
+ `Run 'textor init' to create it.`);
215
+ }
216
+ try {
217
+ const content = await readFile(configPath, 'utf-8');
218
+ const config = JSON.parse(content);
219
+ const merged = deepMerge(DEFAULT_CONFIG, config);
220
+ validateConfig(merged);
221
+ return merged;
222
+ }
223
+ catch (error) {
224
+ if (error instanceof SyntaxError) {
225
+ throw new Error(`Failed to parse config: Invalid JSON at ${configPath}`);
226
+ }
227
+ throw new Error(`Failed to load config: ${error.message}`);
228
+ }
229
+ }
230
+ /**
231
+ * Saves the configuration to config.json.
232
+ * @param {TextorConfig} config Configuration to save
233
+ * @param {boolean} [force=false] Whether to overwrite existing config
234
+ * @returns {Promise<string>} Path to the saved config file
235
+ * @throws {Error} If config exists and force is false
236
+ */
237
+ async function saveConfig(config, force = false) {
238
+ const configPath = getConfigPath();
239
+ if (existsSync(configPath) && !force) {
240
+ throw new Error(`Configuration already exists at ${configPath}\n` +
241
+ `Use --force to overwrite.`);
242
+ }
243
+ validateConfig(config);
244
+ const configDir = getConfigDir();
245
+ if (!existsSync(configDir)) {
246
+ await mkdir(configDir, { recursive: true });
247
+ }
248
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
249
+ return configPath;
250
+ }
251
+ /**
252
+ * Deeply merges source object into target object.
253
+ * @param {Object} target
254
+ * @param {Object} source
255
+ * @returns {Object} Merged object
256
+ */
257
+ function deepMerge(target, source) {
258
+ const result = { ...target };
259
+ for (const key in source) {
260
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
261
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
262
+ result[key] = deepMerge(target[key] || {}, source[key]);
263
+ }
264
+ else {
265
+ result[key] = source[key];
266
+ }
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ /**
272
+ * Validates the configuration object structure.
273
+ * @param {any} config
274
+ * @throws {Error} If config is invalid
275
+ */
276
+ function validateConfig(config) {
277
+ if (!config || typeof config !== 'object') {
278
+ throw new Error('Invalid configuration: must be an object');
279
+ }
280
+ const requiredSections = ['paths', 'naming', 'signatures', 'importAliases'];
281
+ for (const section of requiredSections) {
282
+ if (!config[section] || typeof config[section] !== 'object') {
283
+ throw new Error(`Invalid configuration: missing or invalid "${section}" section`);
284
+ }
285
+ }
286
+ if (config.kindRules && !Array.isArray(config.kindRules)) {
287
+ throw new Error('Invalid configuration: "kindRules" must be an array');
288
+ }
289
+ // Validate paths are strings
290
+ for (const [key, value] of Object.entries(config.paths)) {
291
+ if (typeof value !== 'string') {
292
+ throw new Error(`Invalid configuration: "paths.${key}" must be a string`);
293
+ }
294
+ }
295
+ // Validate naming extensions start with dot
296
+ for (const [key, value] of Object.entries(config.naming)) {
297
+ if (typeof value === 'string' && !value.startsWith('.') && value !== '') {
298
+ throw new Error(`Invalid configuration: "naming.${key}" should start with a dot (e.g., ".astro")`);
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Resolves a configured path key to an absolute path.
304
+ * @param {TextorConfig} config
305
+ * @param {keyof TextorConfig['paths']} pathKey
306
+ * @returns {string}
307
+ */
308
+ function resolvePath(config, pathKey) {
309
+ const configuredPath = config.paths[pathKey];
310
+ if (!configuredPath) {
311
+ throw new Error(`Path "${pathKey}" not found in configuration`);
312
+ }
313
+ return path.resolve(process.cwd(), configuredPath);
314
+ }
315
+ /**
316
+ * Merges CLI options, preset defaults, and global config.
317
+ * @param {Object} cmdOptions
318
+ * @param {TextorConfig} config
319
+ * @param {'features' | 'components'} type
320
+ */
321
+ function getEffectiveOptions(cmdOptions, config, type) {
322
+ const presetName = cmdOptions.preset || config.defaultPreset || 'standard';
323
+ const preset = config.presets[presetName] || config.presets['standard'] || {};
324
+ const presetTypeOptions = preset[type] || {};
325
+ const configTypeOptions = config[type] || {};
326
+ const merged = { ...configTypeOptions, ...presetTypeOptions };
327
+ // Explicit CLI flags should override
328
+ // Commander uses camelCase for flags like --no-sub-components-dir -> subComponentsDir
329
+ for (const key in merged) {
330
+ if (cmdOptions[key] !== undefined) {
331
+ merged[key] = cmdOptions[key];
332
+ }
333
+ else if (key.startsWith('create')) {
334
+ // Try mapping short flags to "create" prefix
335
+ // e.g., CLI --api (cmdOptions.api) -> config createApi
336
+ const shortKey = key.slice(6).charAt(0).toLowerCase() + key.slice(7);
337
+ if (cmdOptions[shortKey] !== undefined) {
338
+ merged[key] = cmdOptions[shortKey];
339
+ }
340
+ }
341
+ }
342
+ return merged;
343
+ }
344
+
345
+ async function initCommand(options) {
346
+ try {
347
+ const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
348
+ console.log('✓ Textor configuration created at:', configPath);
349
+ console.log('\nDefault configuration:');
350
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
351
+ console.log('\nYou can now use Textor commands like:');
352
+ console.log(' textor add-section /users users/catalog --layout Main');
353
+ }
354
+ catch (error) {
355
+ console.error('Error:', error.message);
356
+ process.exit(1);
357
+ }
358
+ }
359
+
360
+ function toPascalCase(input) {
361
+ return input
362
+ .split(/[/\\_-]/)
363
+ .filter(Boolean)
364
+ .map(segment => {
365
+ if (segment === segment.toUpperCase() && segment.length > 1) {
366
+ segment = segment.toLowerCase();
367
+ }
368
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
369
+ })
370
+ .join('');
371
+ }
372
+ function getFeatureComponentName(featurePath) {
373
+ return toPascalCase(featurePath);
374
+ }
375
+ function getHookFunctionName(componentName) {
376
+ return 'use' + componentName;
377
+ }
378
+ function getHookFileName(componentName, extension = '.ts') {
379
+ return getHookFunctionName(componentName) + extension;
380
+ }
381
+ function normalizeComponentName(name) {
382
+ return toPascalCase(name);
383
+ }
384
+ function normalizeRoute(route) {
385
+ let normalized = route.trim();
386
+ if (!normalized.startsWith('/')) {
387
+ normalized = '/' + normalized;
388
+ }
389
+ if (normalized.length > 1 && normalized.endsWith('/')) {
390
+ normalized = normalized.slice(0, -1);
391
+ }
392
+ return normalized;
393
+ }
394
+ function routeToFilePath(route, options = {}) {
395
+ const { extension = '.astro', mode = 'flat', indexFile = 'index.astro' } = options;
396
+ const normalized = normalizeRoute(route);
397
+ if (normalized === '/') {
398
+ return indexFile;
399
+ }
400
+ const routePath = normalized.slice(1);
401
+ if (mode === 'nested') {
402
+ return path.join(routePath, indexFile);
403
+ }
404
+ return routePath + extension;
405
+ }
406
+ function featureToDirectoryPath(featurePath) {
407
+ return featurePath.replace(/^\/+/, '').replace(/\/+$/, '');
408
+ }
409
+ function getFeatureFileName(featurePath, options = {}) {
410
+ const { extension = '.astro', strategy = 'index' } = options;
411
+ if (strategy === 'pascal') {
412
+ return getFeatureComponentName(featurePath) + extension;
413
+ }
414
+ return 'index' + extension;
415
+ }
416
+ /**
417
+ * Calculates a relative import path from one file to another.
418
+ * @param {string} fromFile The absolute path of the file containing the import
419
+ * @param {string} toFile The absolute path of the file being imported
420
+ * @returns {string} The relative import path
421
+ */
422
+ function getRelativeImportPath(fromFile, toFile) {
423
+ let relativePath = path.relative(path.dirname(fromFile), toFile);
424
+ // Convert backslashes to forward slashes for imports
425
+ relativePath = relativePath.split(path.sep).join('/');
426
+ // Ensure it starts with ./ or ../
427
+ if (!relativePath.startsWith('.')) {
428
+ relativePath = './' + relativePath;
429
+ }
430
+ return relativePath;
431
+ }
432
+
433
+ const execAsync$1 = promisify(exec);
434
+ function calculateHash(content, normalization = 'normalizeEOL') {
435
+ let normalizedContent = content;
436
+ if (normalization === 'stripGeneratedRegions') {
437
+ // Extract all content between :begin and :end markers
438
+ const beginMarker = /@generated by Textor:begin/g;
439
+ const endMarker = /@generated by Textor:end/g;
440
+ const regions = [];
441
+ let match;
442
+ const beginIndices = [];
443
+ while ((match = beginMarker.exec(content)) !== null) {
444
+ beginIndices.push(match.index + match[0].length);
445
+ }
446
+ const endIndices = [];
447
+ while ((match = endMarker.exec(content)) !== null) {
448
+ endIndices.push(match.index);
449
+ }
450
+ if (beginIndices.length > 0 && beginIndices.length === endIndices.length) {
451
+ for (let i = 0; i < beginIndices.length; i++) {
452
+ regions.push(content.slice(beginIndices[i], endIndices[i]));
453
+ }
454
+ normalizedContent = regions.join('\n');
455
+ }
456
+ // Fall back to full content if markers are missing or mismatched
457
+ }
458
+ if (normalization === 'normalizeEOL' || normalization === 'stripGeneratedRegions') {
459
+ normalizedContent = normalizedContent.replace(/\r\n/g, '\n');
460
+ }
461
+ return createHash('sha256').update(normalizedContent).digest('hex');
462
+ }
463
+ async function isTextorGenerated(filePath, customSignatures = []) {
464
+ if (!existsSync(filePath)) {
465
+ return false;
466
+ }
467
+ try {
468
+ const content = await readFile(filePath, 'utf-8');
469
+ const signatures = ['@generated by Textor', ...customSignatures];
470
+ return signatures.some(sig => content.includes(sig));
471
+ }
472
+ catch {
473
+ return false;
474
+ }
475
+ }
476
+ async function verifyFileIntegrity(filePath, expectedHash, options = {}) {
477
+ const { force = false, acceptChanges = false, normalization = 'normalizeEOL', owner = null, actualOwner = null } = options;
478
+ if (force)
479
+ return { valid: true };
480
+ if (owner && actualOwner && owner !== actualOwner) {
481
+ return {
482
+ valid: false,
483
+ reason: 'wrong-owner',
484
+ message: `Refusing to operate on ${filePath} - owned by ${actualOwner}, but requested by ${owner}. Use --force to override.`
485
+ };
486
+ }
487
+ const isGenerated = await isTextorGenerated(filePath);
488
+ if (!isGenerated) {
489
+ return {
490
+ valid: false,
491
+ reason: 'not-generated',
492
+ message: `Refusing to operate on ${filePath} - not generated by Textor. Use --force to override.`
493
+ };
494
+ }
495
+ if (!expectedHash) {
496
+ return {
497
+ valid: false,
498
+ reason: 'not-in-state',
499
+ message: `Refusing to operate on ${filePath} - not found in Textor state. Use --force to override.`
500
+ };
501
+ }
502
+ if (!acceptChanges) {
503
+ const content = await readFile(filePath, 'utf-8');
504
+ const currentHash = calculateHash(content, normalization);
505
+ if (currentHash !== expectedHash) {
506
+ return {
507
+ valid: false,
508
+ reason: 'hash-mismatch',
509
+ message: `Refusing to operate on ${filePath} - content has been modified. Use --accept-changes or --force to override.`
510
+ };
511
+ }
512
+ }
513
+ return { valid: true };
514
+ }
515
+ async function safeDelete(filePath, options = {}) {
516
+ const { force = false, expectedHash = null, acceptChanges = false, owner = null, actualOwner = null } = options;
517
+ if (!existsSync(filePath)) {
518
+ return { deleted: false, reason: 'not-found' };
519
+ }
520
+ const integrity = await verifyFileIntegrity(filePath, expectedHash, {
521
+ force,
522
+ acceptChanges,
523
+ owner,
524
+ actualOwner
525
+ });
526
+ if (!integrity.valid) {
527
+ return { deleted: false, reason: integrity.reason, message: integrity.message };
528
+ }
529
+ await unlink(filePath);
530
+ return { deleted: true };
531
+ }
532
+ async function ensureNotExists(filePath, force = false) {
533
+ if (existsSync(filePath)) {
534
+ if (!force) {
535
+ throw new Error(`File already exists: ${filePath}\n` +
536
+ `Use --force to overwrite.`);
537
+ }
538
+ }
539
+ }
540
+ async function ensureDir(dirPath) {
541
+ await mkdir(dirPath, { recursive: true });
542
+ }
543
+ async function isSafeToDeleteDir(dirPath, stateFiles = {}, options = {}) {
544
+ try {
545
+ const files = await readdir(dirPath);
546
+ const results = await Promise.all(files.map(async (file) => {
547
+ const filePath = path.join(dirPath, file);
548
+ const stats = await stat(filePath);
549
+ if (stats.isDirectory()) {
550
+ return await isSafeToDeleteDir(filePath, stateFiles, options);
551
+ }
552
+ const normalizedPath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
553
+ const fileState = stateFiles[normalizedPath];
554
+ const integrity = await verifyFileIntegrity(filePath, fileState?.hash, {
555
+ ...options,
556
+ actualOwner: fileState?.owner
557
+ });
558
+ return integrity.valid;
559
+ }));
560
+ return results.every(Boolean);
561
+ }
562
+ catch {
563
+ return false;
564
+ }
565
+ }
566
+ async function safeDeleteDir(dirPath, options = {}) {
567
+ const { force = false, stateFiles = {} } = options;
568
+ if (!existsSync(dirPath)) {
569
+ return { deleted: false, reason: 'not-found' };
570
+ }
571
+ const isSafe = force || await isSafeToDeleteDir(dirPath, stateFiles, options);
572
+ if (isSafe) {
573
+ await rm(dirPath, { recursive: true, force: true });
574
+ return { deleted: true };
575
+ }
576
+ return {
577
+ deleted: false,
578
+ reason: 'contains-non-generated-or-modified',
579
+ message: `Directory contains non-generated or modified files: ${dirPath}. Use --force to override.`
580
+ };
581
+ }
582
+ async function writeFileWithSignature(filePath, content, signature, normalization = 'normalizeEOL') {
583
+ await ensureDir(path.dirname(filePath));
584
+ let finalContent = signature + '\n' + content;
585
+ if (signature.includes(':begin')) {
586
+ const endSignature = signature.replace(':begin', ':end');
587
+ finalContent = signature + '\n' + content + '\n' + endSignature;
588
+ }
589
+ await writeFile(filePath, finalContent, 'utf-8');
590
+ return calculateHash(finalContent, normalization);
591
+ }
592
+ function getSignature(config, type) {
593
+ const base = config.signatures[type] || config.signatures.typescript;
594
+ if (config.hashing?.useMarkers) {
595
+ return base.replace('Textor', 'Textor:begin');
596
+ }
597
+ return base;
598
+ }
599
+ async function updateSignature(filePath, oldPath, newPath) {
600
+ if (!existsSync(filePath)) {
601
+ return;
602
+ }
603
+ let content = await readFile(filePath, 'utf-8');
604
+ content = content.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath);
605
+ await writeFile(filePath, content, 'utf-8');
606
+ }
607
+ async function safeMove(fromPath, toPath, options = {}) {
608
+ const { force = false, expectedHash = null, acceptChanges = false, normalization = 'normalizeEOL', owner = null, actualOwner = null } = options;
609
+ if (!existsSync(fromPath)) {
610
+ throw new Error(`Source file not found: ${fromPath}`);
611
+ }
612
+ if (existsSync(toPath) && !force) {
613
+ throw new Error(`Destination already exists: ${toPath}\n` +
614
+ `Use --force to overwrite.`);
615
+ }
616
+ const integrity = await verifyFileIntegrity(fromPath, expectedHash, {
617
+ force,
618
+ acceptChanges,
619
+ normalization,
620
+ owner,
621
+ actualOwner
622
+ });
623
+ if (!integrity.valid) {
624
+ throw new Error(integrity.message);
625
+ }
626
+ await ensureDir(path.dirname(toPath));
627
+ await rename(fromPath, toPath);
628
+ await updateSignature(toPath, fromPath, toPath);
629
+ // Return new hash because updateSignature might have changed it
630
+ const content = await readFile(toPath, 'utf-8');
631
+ return calculateHash(content, normalization);
632
+ }
633
+ async function isEmptyDir(dirPath) {
634
+ if (!existsSync(dirPath)) {
635
+ return true;
636
+ }
637
+ const files = await readdir(dirPath);
638
+ return files.length === 0;
639
+ }
640
+ async function scanDirectory(dir, fileSet) {
641
+ const files = await readdir(dir);
642
+ for (const file of files) {
643
+ const fullPath = path.join(dir, file);
644
+ const stats = await stat(fullPath);
645
+ if (stats.isDirectory()) {
646
+ if (file === 'node_modules' || file === '.git' || file === '.textor')
647
+ continue;
648
+ await scanDirectory(fullPath, fileSet);
649
+ }
650
+ else {
651
+ const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
652
+ fileSet.add(relativePath);
653
+ }
654
+ }
655
+ }
656
+ function inferKind(filePath, config) {
657
+ const normalizedFilePath = path.resolve(filePath).replace(/\\/g, '/');
658
+ const relativeFromCwd = path.relative(process.cwd(), normalizedFilePath).replace(/\\/g, '/');
659
+ // Check kindRules first (precedence)
660
+ if (config.kindRules && Array.isArray(config.kindRules)) {
661
+ for (const rule of config.kindRules) {
662
+ if (rule.match && rule.kind) {
663
+ // Simple glob-to-regex conversion for ** and *
664
+ const regexStr = rule.match
665
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
666
+ .replace(/\*\*/g, '(.+)') // ** matches anything
667
+ .replace(/\*/g, '([^/]+)'); // * matches one segment
668
+ const regex = new RegExp(`^${regexStr}$`);
669
+ if (regex.test(relativeFromCwd) || regex.test(normalizedFilePath)) {
670
+ return rule.kind;
671
+ }
672
+ }
673
+ }
674
+ }
675
+ const pagesRoot = path.resolve(process.cwd(), config.paths.pages || 'src/pages').replace(/\\/g, '/');
676
+ const featuresRoot = path.resolve(process.cwd(), config.paths.features || 'src/features').replace(/\\/g, '/');
677
+ const componentsRoot = path.resolve(process.cwd(), config.paths.components || 'src/components').replace(/\\/g, '/');
678
+ const ext = path.extname(normalizedFilePath);
679
+ if (normalizedFilePath.startsWith(pagesRoot)) {
680
+ if (ext === '.ts' || ext === '.js')
681
+ return 'endpoint';
682
+ return 'route';
683
+ }
684
+ if (normalizedFilePath.startsWith(featuresRoot)) {
685
+ const relativePath = path.relative(featuresRoot, normalizedFilePath).replace(/\\/g, '/');
686
+ const parts = relativePath.split('/');
687
+ // If it's directly in the features root
688
+ if (parts.length === 1)
689
+ return 'feature';
690
+ const fileName = parts[parts.length - 1];
691
+ const featureDir = path.dirname(relativePath).replace(/\\/g, '/');
692
+ // Main feature file can be FeatureName.astro or index.astro
693
+ const featureName = toPascalCase(featureDir);
694
+ const possiblePascalName = featureName + (config.naming.featureExtension || '.astro');
695
+ const possibleIndexName = 'index' + (config.naming.featureExtension || '.astro');
696
+ if (path.dirname(relativePath) !== '.' && (fileName === possiblePascalName || fileName === possibleIndexName)) {
697
+ // It's in the top level of its feature directory
698
+ if (parts.length === (featureDir.split('/').length + 1)) {
699
+ return 'feature';
700
+ }
701
+ }
702
+ return 'feature-file';
703
+ }
704
+ if (normalizedFilePath.startsWith(componentsRoot)) {
705
+ const relativePath = path.relative(componentsRoot, normalizedFilePath).replace(/\\/g, '/');
706
+ const parts = relativePath.split('/');
707
+ if (parts.length === 1)
708
+ return 'component';
709
+ const componentDir = parts[0];
710
+ const fileName = parts[parts.length - 1];
711
+ const componentName = toPascalCase(componentDir);
712
+ const possibleComponentName = componentName + (config.naming.componentExtension || '.tsx');
713
+ const possibleIndexName = 'index' + (config.naming.componentExtension || '.tsx');
714
+ if (parts.length === 2 && (fileName === possibleComponentName || fileName === possibleIndexName)) {
715
+ return 'component';
716
+ }
717
+ return 'component-file';
718
+ }
719
+ return 'unknown';
720
+ }
721
+ /**
722
+ * Safely joins path segments and ensures the result is within the basePath.
723
+ * @param {string} basePath The base directory that must contain the result
724
+ * @param {...string} segments Path segments to join
725
+ * @returns {string} The joined path
726
+ * @throws {Error} If a path traversal attempt is detected
727
+ */
728
+ function secureJoin(basePath, ...segments) {
729
+ const joinedPath = path.join(basePath, ...segments);
730
+ const resolvedBase = path.resolve(basePath);
731
+ const resolvedJoined = path.resolve(joinedPath);
732
+ const relative = path.relative(resolvedBase, resolvedJoined);
733
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
734
+ throw new Error(`Security error: Path traversal attempt detected: ${joinedPath} is outside of ${basePath}`);
735
+ }
736
+ return joinedPath;
737
+ }
738
+ async function cleanupEmptyDirs(dirPath, rootPath) {
739
+ const normalizedDir = path.resolve(dirPath);
740
+ const normalizedRoot = path.resolve(rootPath);
741
+ if (normalizedDir === normalizedRoot || !normalizedDir.startsWith(normalizedRoot)) {
742
+ return;
743
+ }
744
+ if (await isEmptyDir(normalizedDir)) {
745
+ await rmdir(normalizedDir);
746
+ await cleanupEmptyDirs(path.dirname(normalizedDir), rootPath);
747
+ }
748
+ }
749
+ /**
750
+ * Formats a list of files using the specified tool.
751
+ * @param {string[]} filePaths
752
+ * @param {'prettier' | 'biome' | 'none'} tool
753
+ */
754
+ async function formatFiles(filePaths, tool) {
755
+ if (tool === 'none' || !filePaths.length)
756
+ return;
757
+ // Quote paths to handle spaces
758
+ const paths = filePaths.map(p => `"${p}"`).join(' ');
759
+ if (tool === 'prettier') {
760
+ try {
761
+ await execAsync$1(`npx prettier --write ${paths}`);
762
+ }
763
+ catch (error) {
764
+ // Silently fail if prettier is not available or fails
765
+ }
766
+ }
767
+ else if (tool === 'biome') {
768
+ try {
769
+ await execAsync$1(`npx biome format --write ${paths}`);
770
+ }
771
+ catch (error) {
772
+ // Silently fail
773
+ }
774
+ }
775
+ }
776
+
777
+ var filesystem = /*#__PURE__*/Object.freeze({
778
+ __proto__: null,
779
+ calculateHash: calculateHash,
780
+ cleanupEmptyDirs: cleanupEmptyDirs,
781
+ ensureDir: ensureDir,
782
+ ensureNotExists: ensureNotExists,
783
+ formatFiles: formatFiles,
784
+ getSignature: getSignature,
785
+ inferKind: inferKind,
786
+ isEmptyDir: isEmptyDir,
787
+ isTextorGenerated: isTextorGenerated,
788
+ safeDelete: safeDelete,
789
+ safeDeleteDir: safeDeleteDir,
790
+ safeMove: safeMove,
791
+ scanDirectory: scanDirectory,
792
+ secureJoin: secureJoin,
793
+ updateSignature: updateSignature,
794
+ verifyFileIntegrity: verifyFileIntegrity,
795
+ writeFileWithSignature: writeFileWithSignature
796
+ });
797
+
798
+ function getTemplateOverride(templateName, extension, data = {}) {
799
+ const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
800
+ if (existsSync(overridePath)) {
801
+ let content = readFileSync(overridePath, 'utf-8');
802
+ for (const [key, value] of Object.entries(data)) {
803
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
804
+ }
805
+ return content;
806
+ }
807
+ return null;
808
+ }
809
+ /**
810
+ * Route Template Variables:
811
+ * - layoutName: The name of the layout component
812
+ * - layoutImportPath: Path to import the layout
813
+ * - featureImportPath: Path to import the feature component
814
+ * - featureComponentName: Name of the feature component
815
+ */
816
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
817
+ const override = getTemplateOverride('route', '.astro', {
818
+ layoutName,
819
+ layoutImportPath,
820
+ featureImportPath,
821
+ featureComponentName
822
+ });
823
+ if (override)
824
+ return override;
825
+ if (layoutName === 'none') {
826
+ return `---
827
+ import ${featureComponentName} from '${featureImportPath}';
828
+ ---
829
+
830
+ <${featureComponentName} />
831
+ `;
832
+ }
833
+ return `---
834
+ import ${layoutName} from '${layoutImportPath}';
835
+ import ${featureComponentName} from '${featureImportPath}';
836
+ ---
837
+
838
+ <${layoutName}>
839
+ <${featureComponentName} />
840
+ </${layoutName}>
841
+ `;
842
+ }
843
+ /**
844
+ * Feature Template Variables:
845
+ * - componentName: Name of the feature component
846
+ * - scriptImportPath: Path to the feature's client-side script
847
+ */
848
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
849
+ const extension = framework === 'astro' ? '.astro' : '.tsx';
850
+ const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
851
+ if (override)
852
+ return override;
853
+ if (framework === 'react') {
854
+ return `export type ${componentName}Props = {
855
+ // Add props here
856
+ }
857
+
858
+ export default function ${componentName}({ }: ${componentName}Props) {
859
+ return (
860
+ <div className="${componentName.toLowerCase()}">
861
+ <h1>${componentName}</h1>
862
+ </div>
863
+ );
864
+ }
865
+ `;
866
+ }
867
+ const scriptTag = scriptImportPath ? `\n<script src="${scriptImportPath}"></script>` : '';
868
+ return `---
869
+ // Feature: ${componentName}
870
+ ---
871
+
872
+ <div class="${componentName.toLowerCase()}">
873
+ <h1>${componentName}</h1>
874
+ </div>${scriptTag}
875
+ `;
876
+ }
877
+ /**
878
+ * Scripts Index Template (no variables)
879
+ */
880
+ function generateScriptsIndexTemplate() {
881
+ const override = getTemplateOverride('scripts-index', '.ts');
882
+ if (override)
883
+ return override;
884
+ return `export {};
885
+ `;
886
+ }
887
+ /**
888
+ * Component Template Variables:
889
+ * - componentName: Name of the component
890
+ */
891
+ function generateComponentTemplate(componentName, framework = 'react') {
892
+ const extension = framework === 'astro' ? '.astro' : '.tsx';
893
+ const override = getTemplateOverride('component', extension, { componentName });
894
+ if (override)
895
+ return override;
896
+ if (framework === 'react') {
897
+ return `export type ${componentName}Props = {
898
+ // Add props here
899
+ }
900
+
901
+ export default function ${componentName}({ }: ${componentName}Props) {
902
+ return (
903
+ <div className="${componentName.toLowerCase()}">
904
+ {/* ${componentName} implementation */}
905
+ </div>
906
+ );
907
+ }
908
+ `;
909
+ }
910
+ return `---
911
+ export type Props = {
912
+ // Add props here
913
+ }
914
+
915
+ const props = Astro.props;
916
+ ---
917
+
918
+ <div class="${componentName.toLowerCase()}">
919
+ <!-- ${componentName} implementation -->
920
+ </div>
921
+ `;
922
+ }
923
+ /**
924
+ * Hook Template Variables:
925
+ * - componentName: Name of the component
926
+ * - hookName: Name of the hook function (e.g., useButton)
927
+ */
928
+ function generateHookTemplate(componentName, hookName) {
929
+ const override = getTemplateOverride('hook', '.ts', { componentName, hookName });
930
+ if (override)
931
+ return override;
932
+ return `import { useState } from 'react';
933
+
934
+ export function ${hookName}() {
935
+ // Add hook logic here
936
+
937
+ return {
938
+ // Return hook values
939
+ };
940
+ }
941
+ `;
942
+ }
943
+ /**
944
+ * Context Template Variables:
945
+ * - componentName: Name of the component
946
+ */
947
+ function generateContextTemplate(componentName) {
948
+ const override = getTemplateOverride('context', '.tsx', { componentName });
949
+ if (override)
950
+ return override;
951
+ return `import { createContext, useContext } from 'react';
952
+
953
+ //@ts-ignore
954
+ type ${componentName}ContextValue = {
955
+ // Add context value types here
956
+ }
957
+
958
+ const ${componentName}Context = createContext<${componentName}ContextValue | undefined>(undefined);
959
+
960
+ export function ${componentName}Provider({ children }: { children: React.ReactNode }) {
961
+ const value: ${componentName}ContextValue = {
962
+ // Provide context values
963
+ };
964
+
965
+ return (
966
+ <${componentName}Context.Provider value={value}>
967
+ {children}
968
+ </${componentName}Context.Provider>
969
+ );
970
+ }
971
+
972
+ export function use${componentName}Context() {
973
+ const context = useContext(${componentName}Context);
974
+ if (context === undefined) {
975
+ throw new Error('use${componentName}Context must be used within ${componentName}Provider');
976
+ }
977
+ return context;
978
+ }
979
+ `;
980
+ }
981
+ /**
982
+ * Test Template Variables:
983
+ * - componentName: Name of the component
984
+ * - componentPath: Relative path to the component file
985
+ */
986
+ function generateTestTemplate(componentName, componentPath) {
987
+ const override = getTemplateOverride('test', '.tsx', { componentName, componentPath });
988
+ if (override)
989
+ return override;
990
+ return `import { describe, it, expect } from 'vitest';
991
+ import { render, screen } from '@testing-library/react';
992
+ import ${componentName} from '${componentPath}';
993
+
994
+ describe('${componentName}', () => {
995
+ it('renders without crashing', () => {
996
+ render(<${componentName} />);
997
+ expect(screen.getByText('${componentName}')).toBeInTheDocument();
998
+ });
999
+ });
1000
+ `;
1001
+ }
1002
+ /**
1003
+ * Config Template Variables:
1004
+ * - componentName: Name of the component
1005
+ */
1006
+ function generateConfigTemplate(componentName) {
1007
+ const override = getTemplateOverride('config', '.ts', { componentName });
1008
+ if (override)
1009
+ return override;
1010
+ return `export const ${componentName}Config = {
1011
+ // Add configuration here
1012
+ };
1013
+ `;
1014
+ }
1015
+ /**
1016
+ * Constants Template Variables:
1017
+ * - componentName: Name of the component
1018
+ */
1019
+ function generateConstantsTemplate(componentName) {
1020
+ const override = getTemplateOverride('constants', '.ts', { componentName });
1021
+ if (override)
1022
+ return override;
1023
+ return `export const ${componentName.toUpperCase()}_CONSTANTS = {
1024
+ // Add constants here
1025
+ };
1026
+ `;
1027
+ }
1028
+ /**
1029
+ * Index Template Variables:
1030
+ * - componentName: Name of the component
1031
+ * - componentExtension: File extension of the component
1032
+ */
1033
+ function generateIndexTemplate(componentName, componentExtension) {
1034
+ const override = getTemplateOverride('index', '.ts', { componentName, componentExtension });
1035
+ if (override)
1036
+ return override;
1037
+ return `export { default as ${componentName} } from './${componentName}${componentExtension}';
1038
+ export * from './types';
1039
+ `;
1040
+ }
1041
+ /**
1042
+ * Types Template Variables:
1043
+ * - componentName: Name of the component
1044
+ */
1045
+ function generateTypesTemplate(componentName) {
1046
+ const override = getTemplateOverride('types', '.ts', { componentName });
1047
+ if (override)
1048
+ return override;
1049
+ return `export type ${componentName}Props = {
1050
+ // Add props types here
1051
+ };
1052
+ `;
1053
+ }
1054
+ /**
1055
+ * API Template Variables:
1056
+ * - componentName: Name of the component
1057
+ */
1058
+ function generateApiTemplate(componentName) {
1059
+ const override = getTemplateOverride('api', '.ts', { componentName });
1060
+ if (override)
1061
+ return override;
1062
+ return `export function GET({ params, request }) {
1063
+ return new Response(
1064
+ JSON.stringify({
1065
+ name: "${componentName}",
1066
+ url: "https://astro.build/",
1067
+ }),
1068
+ );
1069
+ }
1070
+ `;
1071
+ }
1072
+ /**
1073
+ * Endpoint Template Variables:
1074
+ * - componentName: Name of the component
1075
+ */
1076
+ function generateEndpointTemplate(componentName) {
1077
+ const override = getTemplateOverride('endpoint', '.ts', { componentName });
1078
+ if (override)
1079
+ return override;
1080
+ return `export function GET({ params, request }) {
1081
+ return new Response(
1082
+ JSON.stringify({
1083
+ name: "${componentName}",
1084
+ url: "https://astro.build/",
1085
+ }),
1086
+ );
1087
+ }
1088
+ `;
1089
+ }
1090
+ /**
1091
+ * Service Template Variables:
1092
+ * - componentName: Name of the component
1093
+ */
1094
+ function generateServiceTemplate(componentName) {
1095
+ const override = getTemplateOverride('service', '.ts', { componentName });
1096
+ if (override)
1097
+ return override;
1098
+ return `// ${componentName} business logic and transformers
1099
+ export async function get${componentName}Data() {
1100
+ // Encapsulated logic for data processing
1101
+ return [];
1102
+ }
1103
+
1104
+ export function transform${componentName}Data(data: any) {
1105
+ // Domain-specific data transformations
1106
+ return data;
1107
+ }
1108
+ `;
1109
+ }
1110
+ /**
1111
+ * Schema Template Variables:
1112
+ * - componentName: Name of the component
1113
+ */
1114
+ function generateSchemaTemplate(componentName) {
1115
+ const override = getTemplateOverride('schema', '.ts', { componentName });
1116
+ if (override)
1117
+ return override;
1118
+ return `import { z } from 'zod';
1119
+
1120
+ export const ${componentName}Schema = z.object({
1121
+ id: z.string().uuid(),
1122
+ createdAt: z.string().datetime(),
1123
+ updatedAt: z.string().datetime().optional(),
1124
+ });
1125
+
1126
+ export type ${componentName} = z.infer<typeof ${componentName}Schema>;
1127
+ `;
1128
+ }
1129
+ /**
1130
+ * Readme Template Variables:
1131
+ * - componentName: Name of the component
1132
+ */
1133
+ function generateReadmeTemplate(componentName) {
1134
+ const override = getTemplateOverride('readme', '.md', { componentName });
1135
+ if (override)
1136
+ return override;
1137
+ return `# ${componentName}
1138
+
1139
+ ## Description
1140
+ Brief description of what this feature/component does.
1141
+
1142
+ ## Props/Usage
1143
+ How to use this and what are its requirements.
1144
+ `;
1145
+ }
1146
+ /**
1147
+ * Stories Template Variables:
1148
+ * - componentName: Name of the component
1149
+ * - componentPath: Relative path to the component file
1150
+ */
1151
+ function generateStoriesTemplate(componentName, componentPath) {
1152
+ const override = getTemplateOverride('stories', '.tsx', { componentName, componentPath });
1153
+ if (override)
1154
+ return override;
1155
+ return `import type { Meta, StoryObj } from '@storybook/react';
1156
+ import ${componentName} from '${componentPath}';
1157
+
1158
+ const meta: Meta<typeof ${componentName}> = {
1159
+ title: 'Components/${componentName}',
1160
+ component: ${componentName},
1161
+ };
1162
+
1163
+ export default meta;
1164
+ type Story = StoryObj<typeof ${componentName}>;
1165
+
1166
+ export const Default: Story = {
1167
+ args: {
1168
+ // Default props
1169
+ },
1170
+ };
1171
+ `;
1172
+ }
1173
+
1174
+ const CONFIG_DIR = '.textor';
1175
+ const STATE_FILE = 'state.json';
1176
+ function getStatePath() {
1177
+ return path.join(process.cwd(), CONFIG_DIR, STATE_FILE);
1178
+ }
1179
+ async function loadState() {
1180
+ const statePath = getStatePath();
1181
+ if (!existsSync(statePath)) {
1182
+ return { sections: [], components: [], files: {} };
1183
+ }
1184
+ try {
1185
+ const content = await readFile(statePath, 'utf-8');
1186
+ const state = JSON.parse(content);
1187
+ if (!state.files)
1188
+ state.files = {};
1189
+ return state;
1190
+ }
1191
+ catch (error) {
1192
+ return { sections: [], components: [], files: {} };
1193
+ }
1194
+ }
1195
+ let saveQueue = Promise.resolve();
1196
+ async function saveState(state) {
1197
+ const result = saveQueue.then(async () => {
1198
+ const statePath = getStatePath();
1199
+ const dir = path.dirname(statePath);
1200
+ if (!existsSync(dir)) {
1201
+ await mkdir(dir, { recursive: true });
1202
+ }
1203
+ const tempPath = statePath + '.' + Math.random().toString(36).slice(2) + '.tmp';
1204
+ const content = JSON.stringify(state, null, 2);
1205
+ const handle = await open(tempPath, 'w');
1206
+ await handle.writeFile(content, 'utf-8');
1207
+ await handle.sync();
1208
+ await handle.close();
1209
+ await rename(tempPath, statePath);
1210
+ });
1211
+ saveQueue = result.catch(() => { });
1212
+ return result;
1213
+ }
1214
+ async function registerFile(filePath, { kind, template, hash, templateVersion = '1.0.0', owner = null }) {
1215
+ const state = await loadState();
1216
+ const normalizedPath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
1217
+ state.files[normalizedPath] = {
1218
+ kind,
1219
+ template,
1220
+ hash,
1221
+ templateVersion,
1222
+ owner,
1223
+ timestamp: new Date().toISOString()
1224
+ };
1225
+ await saveState(state);
1226
+ }
1227
+ async function addSectionToState(section) {
1228
+ const state = await loadState();
1229
+ // Avoid duplicates by route
1230
+ state.sections = state.sections.filter(s => s.route !== section.route);
1231
+ state.sections.push(section);
1232
+ await saveState(state);
1233
+ }
1234
+ async function addComponentToState(component) {
1235
+ const state = await loadState();
1236
+ // Avoid duplicates by name
1237
+ state.components = state.components.filter(c => c.name !== component.name);
1238
+ state.components.push(component);
1239
+ await saveState(state);
1240
+ }
1241
+ function findSection(state, identifier) {
1242
+ return state.sections.find(s => s.route === identifier || s.name === identifier || s.featurePath === identifier);
1243
+ }
1244
+ function findComponent(state, name) {
1245
+ return state.components.find(c => c.name === name);
1246
+ }
1247
+ function reconstructComponents(files, config) {
1248
+ const componentsRoot = (config.paths.components || 'src/components').replace(/\\/g, '/');
1249
+ const components = new Map();
1250
+ for (const filePath in files) {
1251
+ const normalizedPath = filePath.replace(/\\/g, '/');
1252
+ if (normalizedPath === componentsRoot || normalizedPath.startsWith(componentsRoot + '/')) {
1253
+ const relativePath = normalizedPath === componentsRoot ? '' : normalizedPath.slice(componentsRoot.length + 1);
1254
+ if (relativePath === '')
1255
+ continue; // skip the root itself if it's in files for some reason
1256
+ const parts = relativePath.split('/');
1257
+ if (parts.length >= 1) {
1258
+ const componentName = parts[0];
1259
+ const componentPath = `${componentsRoot}/${componentName}`;
1260
+ if (!components.has(componentName)) {
1261
+ components.set(componentName, {
1262
+ name: componentName,
1263
+ path: componentPath
1264
+ });
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ return Array.from(components.values());
1270
+ }
1271
+ function reconstructSections(state, config) {
1272
+ const pagesRoot = (config.paths.pages || 'src/pages').replace(/\\/g, '/');
1273
+ const featuresRoot = (config.paths.features || 'src/features').replace(/\\/g, '/');
1274
+ const files = state.files;
1275
+ // Keep existing sections if their files still exist
1276
+ const validSections = (state.sections || []).filter(section => {
1277
+ // Check if route file exists in state.files
1278
+ const routeFile = Object.keys(files).find(f => {
1279
+ const normalizedF = f.replace(/\\/g, '/');
1280
+ const routePath = section.route === '/' ? 'index' : section.route.slice(1);
1281
+ return normalizedF.startsWith(pagesRoot + '/' + routePath + '.') ||
1282
+ normalizedF === pagesRoot + '/' + routePath + '/index.astro'; // nested mode
1283
+ });
1284
+ // Check if feature directory has at least one file in state.files
1285
+ const hasFeatureFiles = Object.keys(files).some(f => f.replace(/\\/g, '/').startsWith(section.featurePath.replace(/\\/g, '/') + '/'));
1286
+ return routeFile && hasFeatureFiles;
1287
+ });
1288
+ const sections = new Map();
1289
+ validSections.forEach(s => sections.set(s.route, s));
1290
+ // Try to discover new sections
1291
+ for (const filePath in files) {
1292
+ const normalizedPath = filePath.replace(/\\/g, '/');
1293
+ if (normalizedPath.startsWith(pagesRoot + '/')) {
1294
+ const relativePath = normalizedPath.slice(pagesRoot.length + 1);
1295
+ const route = '/' + relativePath.replace(/\.(astro|ts|js|tsx|jsx)$/, '').replace(/\/index$/, '');
1296
+ const finalRoute = route === '' ? '/' : route;
1297
+ if (!sections.has(finalRoute)) {
1298
+ // Try to find a matching feature by name
1299
+ const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute);
1300
+ // Look for a directory in features with same name or similar
1301
+ const possibleFeaturePath = Object.keys(files).find(f => {
1302
+ const nf = f.replace(/\\/g, '/');
1303
+ return nf.startsWith(featuresRoot + '/') && nf.includes('/' + routeName + '/');
1304
+ });
1305
+ if (possibleFeaturePath) {
1306
+ const featurePathParts = possibleFeaturePath.replace(/\\/g, '/').split('/');
1307
+ const featuresBase = path.basename(featuresRoot);
1308
+ const featureIndex = featurePathParts.indexOf(featuresBase) + 1;
1309
+ if (featureIndex > 0 && featureIndex < featurePathParts.length) {
1310
+ const featureName = featurePathParts[featureIndex];
1311
+ const featurePath = `${featuresRoot}/${featureName}`;
1312
+ sections.set(finalRoute, {
1313
+ name: featureName,
1314
+ route: finalRoute,
1315
+ featurePath: featurePath,
1316
+ extension: path.extname(filePath)
1317
+ });
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ return Array.from(sections.values());
1324
+ }
1325
+
1326
+ const execAsync = promisify(exec);
1327
+ async function isRepoClean() {
1328
+ try {
1329
+ const { stdout } = await execAsync('git status --porcelain');
1330
+ return stdout.trim() === '';
1331
+ }
1332
+ catch (error) {
1333
+ // If not a git repo, we consider it clean or at least we can't check
1334
+ return true;
1335
+ }
1336
+ }
1337
+ async function stageFiles(filePaths) {
1338
+ if (!filePaths.length)
1339
+ return;
1340
+ try {
1341
+ const paths = filePaths.map(p => `"${p}"`).join(' ');
1342
+ await execAsync(`git add ${paths}`);
1343
+ }
1344
+ catch (error) {
1345
+ // Ignore errors if git is not available
1346
+ }
1347
+ }
1348
+
1349
+ async function addSectionCommand(route, featurePath, options) {
1350
+ try {
1351
+ const config = await loadConfig();
1352
+ const effectiveOptions = getEffectiveOptions(options, config, 'features');
1353
+ const normalizedRoute = normalizeRoute(route);
1354
+ const normalizedFeaturePath = featureToDirectoryPath(featurePath);
1355
+ const pagesRoot = resolvePath(config, 'pages');
1356
+ const featuresRoot = resolvePath(config, 'features');
1357
+ const layoutsRoot = resolvePath(config, 'layouts');
1358
+ const routeExtension = options.endpoint ? '.ts' : config.naming.routeExtension;
1359
+ // Check if we should use nested mode even if config says flat
1360
+ // (because the directory already exists, suggesting it should be an index file)
1361
+ let effectiveRoutingMode = config.routing.mode;
1362
+ if (effectiveRoutingMode === 'flat') {
1363
+ const routeDirName = routeToFilePath(normalizedRoute, {
1364
+ extension: '',
1365
+ mode: 'flat'
1366
+ });
1367
+ const routeDirPath = secureJoin(pagesRoot, routeDirName);
1368
+ if (existsSync(routeDirPath)) {
1369
+ effectiveRoutingMode = 'nested';
1370
+ }
1371
+ }
1372
+ const routeFileName = routeToFilePath(normalizedRoute, {
1373
+ extension: routeExtension,
1374
+ mode: effectiveRoutingMode,
1375
+ indexFile: config.routing.indexFile
1376
+ });
1377
+ const featureComponentName = getFeatureComponentName(normalizedFeaturePath);
1378
+ const featureFileName = getFeatureFileName(normalizedFeaturePath, {
1379
+ extension: config.naming.featureExtension,
1380
+ strategy: effectiveOptions.entry
1381
+ });
1382
+ const routeFilePath = secureJoin(pagesRoot, routeFileName);
1383
+ const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
1384
+ const featureFilePath = secureJoin(featureDirPath, featureFileName);
1385
+ const scriptsIndexPath = secureJoin(featureDirPath, config.features.scriptsIndexFile);
1386
+ const subComponentsDir = secureJoin(featureDirPath, 'sub-components');
1387
+ const testsDir = secureJoin(featureDirPath, '__tests__');
1388
+ const contextDirInside = secureJoin(featureDirPath, 'context');
1389
+ const hooksDirInside = secureJoin(featureDirPath, 'hooks');
1390
+ const typesDirInside = secureJoin(featureDirPath, 'types');
1391
+ const apiDirInside = secureJoin(featureDirPath, 'api');
1392
+ const servicesDirInside = secureJoin(featureDirPath, 'services');
1393
+ const schemasDirInside = secureJoin(featureDirPath, 'schemas');
1394
+ const { framework, createSubComponentsDir: shouldCreateSubComponentsDir, createScriptsDir: shouldCreateScriptsDir, createApi: shouldCreateApi, createServices: shouldCreateServices, createSchemas: shouldCreateSchemas, createHooks: shouldCreateHooks, createContext: shouldCreateContext, createTests: shouldCreateTests, createTypes: shouldCreateTypes, createReadme: shouldCreateReadme, createStories: shouldCreateStories, createIndex: shouldCreateIndex } = effectiveOptions;
1395
+ const indexFilePath = path.join(featureDirPath, 'index.ts');
1396
+ const contextFilePath = path.join(contextDirInside, `${featureComponentName}Context.tsx`);
1397
+ const hookFilePath = path.join(hooksDirInside, getHookFileName(featureComponentName, config.naming.hookExtension));
1398
+ const testFilePath = path.join(testsDir, `${featureComponentName}${config.naming.testExtension}`);
1399
+ const typesFilePath = path.join(typesDirInside, 'index.ts');
1400
+ const apiFilePath = path.join(apiDirInside, 'index.ts');
1401
+ const servicesFilePath = path.join(servicesDirInside, 'index.ts');
1402
+ const schemasFilePath = path.join(schemasDirInside, 'index.ts');
1403
+ const readmeFilePath = path.join(featureDirPath, 'README.md');
1404
+ const storiesFilePath = path.join(featureDirPath, `${featureComponentName}.stories.tsx`);
1405
+ const routeParts = normalizedRoute.split('/').filter(Boolean);
1406
+ const reorganizations = [];
1407
+ if (routeParts.length > 1 && config.routing.mode === 'flat') {
1408
+ const possibleExtensions = ['.astro', '.ts', '.js', '.md', '.mdx', '.html'];
1409
+ for (let i = 1; i < routeParts.length; i++) {
1410
+ const parentRoute = '/' + routeParts.slice(0, i).join('/');
1411
+ for (const ext of possibleExtensions) {
1412
+ const parentRouteFileName = routeToFilePath(parentRoute, {
1413
+ extension: ext,
1414
+ mode: 'flat'
1415
+ });
1416
+ const parentRouteFilePath = secureJoin(pagesRoot, parentRouteFileName);
1417
+ if (existsSync(parentRouteFilePath)) {
1418
+ const indexFile = ext === '.astro' ? config.routing.indexFile : `index${ext}`;
1419
+ const newParentRouteFileName = routeToFilePath(parentRoute, {
1420
+ extension: ext,
1421
+ mode: 'nested',
1422
+ indexFile: indexFile
1423
+ });
1424
+ const newParentRouteFilePath = secureJoin(pagesRoot, newParentRouteFileName);
1425
+ if (!existsSync(newParentRouteFilePath)) {
1426
+ reorganizations.push({
1427
+ from: parentRouteFilePath,
1428
+ to: newParentRouteFilePath,
1429
+ route: parentRoute
1430
+ });
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ }
1436
+ if (options.dryRun) {
1437
+ console.log('Dry run - would create:');
1438
+ console.log(` Route: ${routeFilePath}`);
1439
+ console.log(` Feature: ${featureFilePath}`);
1440
+ for (const reorg of reorganizations) {
1441
+ console.log(` Reorganize: ${reorg.from} -> ${reorg.to}`);
1442
+ }
1443
+ if (shouldCreateIndex)
1444
+ console.log(` Index: ${indexFilePath}`);
1445
+ if (shouldCreateSubComponentsDir)
1446
+ console.log(` Sub-components: ${subComponentsDir}/`);
1447
+ if (shouldCreateScriptsDir)
1448
+ console.log(` Scripts: ${scriptsIndexPath}`);
1449
+ if (shouldCreateApi)
1450
+ console.log(` Api: ${apiFilePath}`);
1451
+ if (shouldCreateServices)
1452
+ console.log(` Services: ${servicesFilePath}`);
1453
+ if (shouldCreateSchemas)
1454
+ console.log(` Schemas: ${schemasFilePath}`);
1455
+ if (shouldCreateHooks)
1456
+ console.log(` Hooks: ${hookFilePath}`);
1457
+ if (shouldCreateContext)
1458
+ console.log(` Context: ${contextFilePath}`);
1459
+ if (shouldCreateTests)
1460
+ console.log(` Tests: ${testFilePath}`);
1461
+ if (shouldCreateTypes)
1462
+ console.log(` Types: ${typesFilePath}`);
1463
+ if (shouldCreateReadme)
1464
+ console.log(` Readme: ${readmeFilePath}`);
1465
+ if (shouldCreateStories)
1466
+ console.log(` Stories: ${storiesFilePath}`);
1467
+ return;
1468
+ }
1469
+ if (reorganizations.length > 0) {
1470
+ const state = await loadState();
1471
+ for (const reorg of reorganizations) {
1472
+ await ensureDir(path.dirname(reorg.to));
1473
+ await rename(reorg.from, reorg.to);
1474
+ const oldRelative = path.relative(process.cwd(), reorg.from).replace(/\\/g, '/');
1475
+ const newRelative = path.relative(process.cwd(), reorg.to).replace(/\\/g, '/');
1476
+ if (state.files[oldRelative]) {
1477
+ state.files[newRelative] = { ...state.files[oldRelative] };
1478
+ delete state.files[oldRelative];
1479
+ }
1480
+ // Update imports in the moved file
1481
+ await updateImportsInFile(reorg.to, reorg.from, reorg.to);
1482
+ console.log(`✓ Reorganized ${oldRelative} to ${newRelative}`);
1483
+ }
1484
+ await saveState(state);
1485
+ }
1486
+ await ensureNotExists(routeFilePath, options.force);
1487
+ await ensureNotExists(featureFilePath, options.force);
1488
+ if (shouldCreateIndex)
1489
+ await ensureNotExists(indexFilePath, options.force);
1490
+ if (shouldCreateContext)
1491
+ await ensureNotExists(contextFilePath, options.force);
1492
+ if (shouldCreateHooks)
1493
+ await ensureNotExists(hookFilePath, options.force);
1494
+ if (shouldCreateTests)
1495
+ await ensureNotExists(testFilePath, options.force);
1496
+ if (shouldCreateTypes)
1497
+ await ensureNotExists(typesFilePath, options.force);
1498
+ if (shouldCreateApi)
1499
+ await ensureNotExists(apiFilePath, options.force);
1500
+ if (shouldCreateServices)
1501
+ await ensureNotExists(servicesFilePath, options.force);
1502
+ if (shouldCreateSchemas)
1503
+ await ensureNotExists(schemasFilePath, options.force);
1504
+ if (shouldCreateReadme)
1505
+ await ensureNotExists(readmeFilePath, options.force);
1506
+ if (shouldCreateStories)
1507
+ await ensureNotExists(storiesFilePath, options.force);
1508
+ if (shouldCreateScriptsDir)
1509
+ await ensureNotExists(scriptsIndexPath, options.force);
1510
+ let layoutImportPath = null;
1511
+ if (options.layout !== 'none') {
1512
+ if (config.importAliases.layouts) {
1513
+ layoutImportPath = `${config.importAliases.layouts}/${options.layout}.astro`;
1514
+ }
1515
+ else {
1516
+ const layoutFilePath = secureJoin(layoutsRoot, `${options.layout}.astro`);
1517
+ layoutImportPath = getRelativeImportPath(routeFilePath, layoutFilePath);
1518
+ }
1519
+ }
1520
+ let featureImportPath;
1521
+ if (config.importAliases.features) {
1522
+ const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
1523
+ // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
1524
+ // However, to be safe, we use the configured extension.
1525
+ featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
1526
+ }
1527
+ else {
1528
+ const relativeFeatureFile = getRelativeImportPath(routeFilePath, featureFilePath);
1529
+ // Remove extension for import
1530
+ featureImportPath = relativeFeatureFile.replace(/\.[^/.]+$/, '');
1531
+ }
1532
+ let scriptImportPath;
1533
+ if (shouldCreateScriptsDir) {
1534
+ scriptImportPath = getRelativeImportPath(featureFilePath, scriptsIndexPath);
1535
+ }
1536
+ let routeContent;
1537
+ let routeSignature;
1538
+ if (options.endpoint) {
1539
+ routeContent = generateEndpointTemplate(featureComponentName);
1540
+ routeSignature = getSignature(config, 'typescript');
1541
+ }
1542
+ else {
1543
+ routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName);
1544
+ routeSignature = getSignature(config, 'astro');
1545
+ }
1546
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
1547
+ const routeHash = await writeFileWithSignature(routeFilePath, routeContent, routeSignature, config.hashing?.normalization);
1548
+ await registerFile(routeFilePath, {
1549
+ kind: 'route',
1550
+ template: options.endpoint ? 'endpoint' : 'route',
1551
+ hash: routeHash,
1552
+ owner: normalizedRoute
1553
+ });
1554
+ const writtenFiles = [routeFilePath];
1555
+ await ensureDir(featureDirPath);
1556
+ if (shouldCreateSubComponentsDir)
1557
+ await ensureDir(subComponentsDir);
1558
+ if (shouldCreateApi)
1559
+ await ensureDir(apiDirInside);
1560
+ if (shouldCreateServices)
1561
+ await ensureDir(servicesDirInside);
1562
+ if (shouldCreateSchemas)
1563
+ await ensureDir(schemasDirInside);
1564
+ if (shouldCreateHooks)
1565
+ await ensureDir(hooksDirInside);
1566
+ if (shouldCreateContext)
1567
+ await ensureDir(contextDirInside);
1568
+ if (shouldCreateTests)
1569
+ await ensureDir(testsDir);
1570
+ if (shouldCreateTypes)
1571
+ await ensureDir(typesDirInside);
1572
+ const featureSignature = getSignature(config, config.naming.featureExtension === '.astro' ? 'astro' : 'tsx');
1573
+ const featureHash = await writeFileWithSignature(featureFilePath, featureContent, featureSignature, config.hashing?.normalization);
1574
+ await registerFile(featureFilePath, {
1575
+ kind: 'feature',
1576
+ template: 'feature',
1577
+ hash: featureHash,
1578
+ owner: normalizedRoute
1579
+ });
1580
+ writtenFiles.push(featureFilePath);
1581
+ if (shouldCreateScriptsDir) {
1582
+ const hash = await writeFileWithSignature(scriptsIndexPath, generateScriptsIndexTemplate(), getSignature(config, 'typescript'), config.hashing?.normalization);
1583
+ await registerFile(scriptsIndexPath, {
1584
+ kind: 'feature-file',
1585
+ template: 'scripts-index',
1586
+ hash,
1587
+ owner: normalizedRoute
1588
+ });
1589
+ writtenFiles.push(scriptsIndexPath);
1590
+ }
1591
+ if (shouldCreateIndex) {
1592
+ const indexContent = generateIndexTemplate(featureComponentName, config.naming.featureExtension);
1593
+ const hash = await writeFileWithSignature(indexFilePath, indexContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1594
+ await registerFile(indexFilePath, {
1595
+ kind: 'feature-file',
1596
+ template: 'index',
1597
+ hash,
1598
+ owner: normalizedRoute
1599
+ });
1600
+ writtenFiles.push(indexFilePath);
1601
+ }
1602
+ if (shouldCreateApi) {
1603
+ const apiContent = generateApiTemplate(featureComponentName);
1604
+ const hash = await writeFileWithSignature(apiFilePath, apiContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1605
+ await registerFile(apiFilePath, {
1606
+ kind: 'feature-file',
1607
+ template: 'api',
1608
+ hash,
1609
+ owner: normalizedRoute
1610
+ });
1611
+ writtenFiles.push(apiFilePath);
1612
+ }
1613
+ if (shouldCreateServices) {
1614
+ const servicesContent = generateServiceTemplate(featureComponentName);
1615
+ const hash = await writeFileWithSignature(servicesFilePath, servicesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1616
+ await registerFile(servicesFilePath, {
1617
+ kind: 'feature-file',
1618
+ template: 'service',
1619
+ hash,
1620
+ owner: normalizedRoute
1621
+ });
1622
+ writtenFiles.push(servicesFilePath);
1623
+ }
1624
+ if (shouldCreateSchemas) {
1625
+ const schemasContent = generateSchemaTemplate(featureComponentName);
1626
+ const hash = await writeFileWithSignature(schemasFilePath, schemasContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1627
+ await registerFile(schemasFilePath, {
1628
+ kind: 'feature-file',
1629
+ template: 'schema',
1630
+ hash,
1631
+ owner: normalizedRoute
1632
+ });
1633
+ writtenFiles.push(schemasFilePath);
1634
+ }
1635
+ if (shouldCreateHooks) {
1636
+ const hookName = getHookFunctionName(featureComponentName);
1637
+ const hookContent = generateHookTemplate(featureComponentName, hookName);
1638
+ const hash = await writeFileWithSignature(hookFilePath, hookContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1639
+ await registerFile(hookFilePath, {
1640
+ kind: 'feature-file',
1641
+ template: 'hook',
1642
+ hash,
1643
+ owner: normalizedRoute
1644
+ });
1645
+ writtenFiles.push(hookFilePath);
1646
+ }
1647
+ if (shouldCreateContext) {
1648
+ const contextContent = generateContextTemplate(featureComponentName);
1649
+ const hash = await writeFileWithSignature(contextFilePath, contextContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1650
+ await registerFile(contextFilePath, {
1651
+ kind: 'feature-file',
1652
+ template: 'context',
1653
+ hash,
1654
+ owner: normalizedRoute
1655
+ });
1656
+ writtenFiles.push(contextFilePath);
1657
+ }
1658
+ if (shouldCreateTests) {
1659
+ const relativeFeaturePath = `./${path.basename(featureFilePath)}`;
1660
+ const testContent = generateTestTemplate(featureComponentName, relativeFeaturePath);
1661
+ const hash = await writeFileWithSignature(testFilePath, testContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1662
+ await registerFile(testFilePath, {
1663
+ kind: 'feature-file',
1664
+ template: 'test',
1665
+ hash,
1666
+ owner: normalizedRoute
1667
+ });
1668
+ writtenFiles.push(testFilePath);
1669
+ }
1670
+ if (shouldCreateTypes) {
1671
+ const typesContent = generateTypesTemplate(featureComponentName);
1672
+ const hash = await writeFileWithSignature(typesFilePath, typesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1673
+ await registerFile(typesFilePath, {
1674
+ kind: 'feature-file',
1675
+ template: 'types',
1676
+ hash,
1677
+ owner: normalizedRoute
1678
+ });
1679
+ writtenFiles.push(typesFilePath);
1680
+ }
1681
+ if (shouldCreateReadme) {
1682
+ const readmeContent = generateReadmeTemplate(featureComponentName);
1683
+ const hash = await writeFileWithSignature(readmeFilePath, readmeContent, getSignature(config, 'astro'), config.hashing?.normalization);
1684
+ await registerFile(readmeFilePath, {
1685
+ kind: 'feature-file',
1686
+ template: 'readme',
1687
+ hash,
1688
+ owner: normalizedRoute
1689
+ });
1690
+ writtenFiles.push(readmeFilePath);
1691
+ }
1692
+ if (shouldCreateStories) {
1693
+ const relativePath = `./${path.basename(featureFilePath)}`;
1694
+ const storiesContent = generateStoriesTemplate(featureComponentName, relativePath);
1695
+ const hash = await writeFileWithSignature(storiesFilePath, storiesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1696
+ await registerFile(storiesFilePath, {
1697
+ kind: 'feature-file',
1698
+ template: 'stories',
1699
+ hash,
1700
+ owner: normalizedRoute
1701
+ });
1702
+ writtenFiles.push(storiesFilePath);
1703
+ }
1704
+ // Formatting
1705
+ if (config.formatting.tool !== 'none') {
1706
+ await formatFiles(writtenFiles, config.formatting.tool);
1707
+ }
1708
+ console.log('✓ Section created successfully:');
1709
+ console.log(` Route: ${routeFilePath}`);
1710
+ console.log(` Feature: ${featureFilePath}`);
1711
+ if (shouldCreateIndex)
1712
+ console.log(` Index: ${indexFilePath}`);
1713
+ if (shouldCreateSubComponentsDir)
1714
+ console.log(` Sub-components: ${subComponentsDir}/`);
1715
+ if (shouldCreateScriptsDir)
1716
+ console.log(` Scripts: ${scriptsIndexPath}`);
1717
+ if (shouldCreateApi)
1718
+ console.log(` Api: ${apiFilePath}`);
1719
+ if (shouldCreateServices)
1720
+ console.log(` Services: ${servicesFilePath}`);
1721
+ if (shouldCreateSchemas)
1722
+ console.log(` Schemas: ${schemasFilePath}`);
1723
+ if (shouldCreateHooks)
1724
+ console.log(` Hooks: ${hookFilePath}`);
1725
+ if (shouldCreateContext)
1726
+ console.log(` Context: ${contextFilePath}`);
1727
+ if (shouldCreateTests)
1728
+ console.log(` Tests: ${testFilePath}`);
1729
+ if (shouldCreateTypes)
1730
+ console.log(` Types: ${typesFilePath}`);
1731
+ if (shouldCreateReadme)
1732
+ console.log(` Readme: ${readmeFilePath}`);
1733
+ if (shouldCreateStories)
1734
+ console.log(` Stories: ${storiesFilePath}`);
1735
+ await addSectionToState({
1736
+ name: options.name || featureComponentName,
1737
+ route: normalizedRoute,
1738
+ featurePath: normalizedFeaturePath,
1739
+ layout: options.layout,
1740
+ extension: routeExtension
1741
+ });
1742
+ if (config.git?.stageChanges) {
1743
+ await stageFiles(writtenFiles);
1744
+ }
1745
+ }
1746
+ catch (error) {
1747
+ console.error('Error:', error.message);
1748
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
1749
+ process.exit(1);
1750
+ }
1751
+ throw error;
1752
+ }
1753
+ }
1754
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
1755
+ if (!existsSync(filePath))
1756
+ return;
1757
+ let content = await readFile(filePath, 'utf-8');
1758
+ const oldDir = path.dirname(oldFilePath);
1759
+ const newDir = path.dirname(newFilePath);
1760
+ if (oldDir === newDir)
1761
+ return;
1762
+ // Find all relative imports
1763
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
1764
+ let match;
1765
+ const replacements = [];
1766
+ while ((match = relativeImportRegex.exec(content)) !== null) {
1767
+ const relativePath = match[1];
1768
+ const absoluteTarget = path.resolve(oldDir, relativePath);
1769
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
1770
+ replacements.push({
1771
+ full: match[0],
1772
+ oldRel: relativePath,
1773
+ newRel: newRelativePath
1774
+ });
1775
+ }
1776
+ for (const repl of replacements) {
1777
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
1778
+ }
1779
+ await writeFile(filePath, content, 'utf-8');
1780
+ }
1781
+
1782
+ async function removeSectionCommand(route, featurePath, options) {
1783
+ try {
1784
+ const config = await loadConfig();
1785
+ if (config.git?.requireCleanRepo && !await isRepoClean()) {
1786
+ throw new Error('Git repository is not clean. Please commit or stash your changes before proceeding.');
1787
+ }
1788
+ const state = await loadState();
1789
+ let targetRoute = route;
1790
+ let targetFeaturePath = featurePath;
1791
+ let section = findSection(state, route);
1792
+ if (!targetFeaturePath) {
1793
+ if (section) {
1794
+ targetRoute = section.route;
1795
+ targetFeaturePath = section.featurePath;
1796
+ }
1797
+ else {
1798
+ throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
1799
+ }
1800
+ }
1801
+ const normalizedRoute = normalizeRoute(targetRoute);
1802
+ const normalizedFeaturePath = featureToDirectoryPath(targetFeaturePath);
1803
+ const routeExtension = (section && section.extension) || config.naming.routeExtension;
1804
+ const routeFileName = routeToFilePath(normalizedRoute, {
1805
+ extension: routeExtension,
1806
+ mode: config.routing.mode,
1807
+ indexFile: config.routing.indexFile
1808
+ });
1809
+ const pagesRoot = resolvePath(config, 'pages');
1810
+ const featuresRoot = resolvePath(config, 'features');
1811
+ const routeFilePath = secureJoin(pagesRoot, routeFileName);
1812
+ const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
1813
+ const deletedFiles = [];
1814
+ const skippedFiles = [];
1815
+ const deletedDirs = [];
1816
+ if (options.dryRun) {
1817
+ console.log('Dry run - would delete:');
1818
+ if (!options.keepRoute) {
1819
+ console.log(` Route: ${routeFilePath}`);
1820
+ }
1821
+ if (!options.keepFeature) {
1822
+ console.log(` Feature: ${featureDirPath}/`);
1823
+ }
1824
+ return;
1825
+ }
1826
+ if (!options.keepRoute) {
1827
+ const normalizedPath = path.relative(process.cwd(), routeFilePath).replace(/\\/g, '/');
1828
+ const fileState = state.files[normalizedPath];
1829
+ const result = await safeDelete(routeFilePath, {
1830
+ force: options.force,
1831
+ expectedHash: fileState?.hash,
1832
+ acceptChanges: options.acceptChanges,
1833
+ normalization: config.hashing?.normalization,
1834
+ owner: normalizedRoute,
1835
+ actualOwner: fileState?.owner
1836
+ });
1837
+ if (result.deleted) {
1838
+ deletedFiles.push(routeFilePath);
1839
+ delete state.files[normalizedPath];
1840
+ }
1841
+ else if (result.message) {
1842
+ skippedFiles.push({ path: routeFilePath, reason: result.message });
1843
+ }
1844
+ }
1845
+ if (!options.keepFeature) {
1846
+ const result = await safeDeleteDir(featureDirPath, {
1847
+ force: options.force,
1848
+ stateFiles: state.files,
1849
+ acceptChanges: options.acceptChanges,
1850
+ normalization: config.hashing?.normalization,
1851
+ owner: normalizedRoute
1852
+ });
1853
+ if (result.deleted) {
1854
+ deletedDirs.push(featureDirPath);
1855
+ // Unregister all files that were in this directory
1856
+ const dirPrefix = path.relative(process.cwd(), featureDirPath).replace(/\\/g, '/') + '/';
1857
+ for (const f in state.files) {
1858
+ if (f.startsWith(dirPrefix)) {
1859
+ delete state.files[f];
1860
+ }
1861
+ }
1862
+ }
1863
+ else if (result.message) {
1864
+ skippedFiles.push({ path: featureDirPath, reason: result.message });
1865
+ }
1866
+ }
1867
+ if (!options.keepRoute && deletedFiles.includes(routeFilePath)) {
1868
+ await cleanupEmptyDirs(path.dirname(routeFilePath), pagesRoot);
1869
+ }
1870
+ if (!options.keepFeature && deletedDirs.includes(featureDirPath)) {
1871
+ await cleanupEmptyDirs(path.dirname(featureDirPath), featuresRoot);
1872
+ }
1873
+ if (deletedFiles.length > 0 || deletedDirs.length > 0) {
1874
+ console.log('✓ Deleted:');
1875
+ deletedFiles.forEach(file => console.log(` ${file}`));
1876
+ deletedDirs.forEach(dir => console.log(` ${dir}/`));
1877
+ }
1878
+ if (skippedFiles.length > 0) {
1879
+ console.log('\n⚠ Skipped:');
1880
+ skippedFiles.forEach(item => {
1881
+ console.log(` ${item.path}`);
1882
+ console.log(` Reason: ${item.reason}`);
1883
+ });
1884
+ }
1885
+ if (deletedFiles.length === 0 && deletedDirs.length === 0 && skippedFiles.length === 0) {
1886
+ console.log('No files to delete.');
1887
+ }
1888
+ else {
1889
+ state.sections = state.sections.filter(s => s.route !== normalizedRoute);
1890
+ await saveState(state);
1891
+ }
1892
+ }
1893
+ catch (error) {
1894
+ console.error('Error:', error.message);
1895
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
1896
+ process.exit(1);
1897
+ }
1898
+ throw error;
1899
+ }
1900
+ }
1901
+
1902
+ /**
1903
+ * Move a section (route + feature).
1904
+ *
1905
+ * SCOPE GUARANTEES:
1906
+ * - Automatically updates imports in the moved route file.
1907
+ * - Automatically updates internal imports/references within the moved feature directory.
1908
+ * - Repo-wide scan for import updates is available via the --scan flag.
1909
+ *
1910
+ * NON-GOALS (What it won't rewrite):
1911
+ * - String literals (unless they match the component name exactly in a JSX context).
1912
+ * - Markdown documentation (except for those registered in state).
1913
+ * - Dynamic imports with complex template literals.
1914
+ */
1915
+ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, options) {
1916
+ try {
1917
+ const config = await loadConfig();
1918
+ if (config.git?.requireCleanRepo && !await isRepoClean()) {
1919
+ throw new Error('Git repository is not clean. Please commit or stash your changes before proceeding.');
1920
+ }
1921
+ const state = await loadState();
1922
+ let actualFromRoute = fromRoute;
1923
+ let actualFromFeature = fromFeature;
1924
+ let actualToRoute = toRoute;
1925
+ let actualToFeature = toFeature;
1926
+ // Shift arguments if using state
1927
+ if (!toRoute && fromRoute && fromFeature) {
1928
+ // textor move-section /old-route /new-route
1929
+ const section = findSection(state, fromRoute);
1930
+ if (section) {
1931
+ actualFromRoute = section.route;
1932
+ actualFromFeature = section.featurePath;
1933
+ actualToRoute = fromFeature; // the second argument was actually the new route
1934
+ actualToFeature = toRoute; // which is null
1935
+ // If toFeature is not provided, try to derive it from the new route
1936
+ if (!actualToFeature && actualToRoute) {
1937
+ const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
1938
+ const newRouteParts = actualToRoute.split('/').filter(Boolean);
1939
+ const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
1940
+ // If the feature path starts with the old route parts, replace them
1941
+ // We compare case-insensitively or via PascalCase to be more helpful
1942
+ let match = true;
1943
+ for (let i = 0; i < oldRouteParts.length; i++) {
1944
+ const routePart = oldRouteParts[i].toLowerCase();
1945
+ const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
1946
+ if (featurePart !== routePart) {
1947
+ match = false;
1948
+ break;
1949
+ }
1950
+ }
1951
+ if (match && oldRouteParts.length > 0) {
1952
+ actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
1953
+ }
1954
+ else {
1955
+ // Otherwise just keep it the same
1956
+ actualToFeature = actualFromFeature;
1957
+ }
1958
+ }
1959
+ }
1960
+ }
1961
+ const isRouteOnly = options.keepFeature || (!actualToFeature && actualToRoute && !actualFromFeature);
1962
+ if (isRouteOnly && !actualToRoute) {
1963
+ throw new Error('Destination route required for route-only move');
1964
+ }
1965
+ const normalizedFromRoute = normalizeRoute(actualFromRoute);
1966
+ const normalizedToRoute = normalizeRoute(actualToRoute);
1967
+ const normalizedFromFeature = actualFromFeature ? featureToDirectoryPath(actualFromFeature) : null;
1968
+ const normalizedToFeature = actualToFeature ? featureToDirectoryPath(actualToFeature) : null;
1969
+ const pagesRoot = resolvePath(config, 'pages');
1970
+ const featuresRoot = resolvePath(config, 'features');
1971
+ const fromSection = findSection(state, actualFromRoute);
1972
+ const routeExtension = (fromSection && fromSection.extension) || config.naming.routeExtension;
1973
+ const fromRouteFile = routeToFilePath(normalizedFromRoute, {
1974
+ extension: routeExtension,
1975
+ mode: config.routing.mode,
1976
+ indexFile: config.routing.indexFile
1977
+ });
1978
+ const toRouteFile = routeToFilePath(normalizedToRoute, {
1979
+ extension: routeExtension,
1980
+ mode: config.routing.mode,
1981
+ indexFile: config.routing.indexFile
1982
+ });
1983
+ const fromRoutePath = secureJoin(pagesRoot, fromRouteFile);
1984
+ const toRoutePath = secureJoin(pagesRoot, toRouteFile);
1985
+ const movedFiles = [];
1986
+ if (options.dryRun) {
1987
+ console.log('Dry run - would move:');
1988
+ console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
1989
+ if (!isRouteOnly && normalizedFromFeature && normalizedToFeature) {
1990
+ const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
1991
+ const toFeaturePath = secureJoin(featuresRoot, normalizedToFeature);
1992
+ console.log(` Feature: ${fromFeaturePath} -> ${toFeaturePath}`);
1993
+ }
1994
+ return;
1995
+ }
1996
+ const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
1997
+ const routeFileState = state.files[normalizedFromRouteRelative];
1998
+ const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
1999
+ force: options.force,
2000
+ expectedHash: routeFileState?.hash,
2001
+ acceptChanges: options.acceptChanges,
2002
+ owner: normalizedFromRoute,
2003
+ actualOwner: routeFileState?.owner
2004
+ });
2005
+ movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2006
+ // Update state for moved route file
2007
+ const normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2008
+ if (routeFileState) {
2009
+ state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2010
+ delete state.files[normalizedFromRouteRelative];
2011
+ }
2012
+ // Update imports in the moved route file
2013
+ const targetFeature = normalizedToFeature || normalizedFromFeature;
2014
+ if (targetFeature) {
2015
+ const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2016
+ const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2017
+ const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2018
+ const toFeatureComponentName = getFeatureComponentName(targetFeature);
2019
+ // Update component name in JSX
2020
+ if (fromFeatureComponentName !== toFeatureComponentName) {
2021
+ let content = await readFile(toRoutePath, 'utf-8');
2022
+ content = content.replace(new RegExp(`<${fromFeatureComponentName}`, 'g'), `<${toFeatureComponentName}`);
2023
+ content = content.replace(new RegExp(`</${fromFeatureComponentName}`, 'g'), `</${toFeatureComponentName}`);
2024
+ await writeFile(toRoutePath, content, 'utf-8');
2025
+ }
2026
+ if (config.importAliases.features) {
2027
+ if (normalizedFromFeature !== targetFeature) {
2028
+ const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2029
+ const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2030
+ // Replace both the path and the component name if they are different
2031
+ await updateSignature(toRoutePath, `import ${fromFeatureComponentName} from '${oldAliasPath}/${fromFeatureComponentName}'`, `import ${toFeatureComponentName} from '${newAliasPath}/${toFeatureComponentName}'`);
2032
+ // Fallback for prefix only replacement
2033
+ await updateSignature(toRoutePath, oldAliasPath, newAliasPath);
2034
+ }
2035
+ else if (fromFeatureComponentName !== toFeatureComponentName) {
2036
+ // Name changed but path didn't
2037
+ const aliasPath = `${config.importAliases.features}/${targetFeature}`;
2038
+ await updateSignature(toRoutePath, `import ${fromFeatureComponentName} from '${aliasPath}/${fromFeatureComponentName}'`, `import ${toFeatureComponentName} from '${aliasPath}/${toFeatureComponentName}'`);
2039
+ }
2040
+ }
2041
+ else {
2042
+ const oldRelativeDir = getRelativeImportPath(fromRoutePath, fromFeatureDirPath);
2043
+ const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2044
+ const oldImportPath = `import ${fromFeatureComponentName} from '${oldRelativeDir}/${fromFeatureComponentName}'`;
2045
+ const newImportPath = `import ${toFeatureComponentName} from '${newRelativeDir}/${toFeatureComponentName}'`;
2046
+ if (oldImportPath !== newImportPath) {
2047
+ await updateSignature(toRoutePath, oldImportPath, newImportPath);
2048
+ }
2049
+ }
2050
+ }
2051
+ if (!isRouteOnly && normalizedFromFeature && normalizedToFeature && normalizedFromFeature !== normalizedToFeature) {
2052
+ const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
2053
+ const toFeaturePath = secureJoin(featuresRoot, normalizedToFeature);
2054
+ const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2055
+ const toFeatureComponentName = getFeatureComponentName(normalizedToFeature);
2056
+ if (existsSync(fromFeaturePath)) {
2057
+ await moveDirectory(fromFeaturePath, toFeaturePath, state, config, {
2058
+ ...options,
2059
+ fromName: fromFeatureComponentName,
2060
+ toName: toFeatureComponentName,
2061
+ owner: normalizedFromRoute
2062
+ });
2063
+ movedFiles.push({ from: fromFeaturePath, to: toFeaturePath });
2064
+ await cleanupEmptyDirs(path.dirname(fromFeaturePath), featuresRoot);
2065
+ }
2066
+ }
2067
+ if (options.scan && (normalizedFromFeature || normalizedToFeature)) {
2068
+ await scanAndReplaceImports(config, state, {
2069
+ fromFeaturePath: normalizedFromFeature,
2070
+ fromComponentName: getFeatureComponentName(normalizedFromFeature)
2071
+ }, {
2072
+ toFeaturePath: normalizedToFeature || normalizedFromFeature,
2073
+ toComponentName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2074
+ }, options);
2075
+ }
2076
+ await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
2077
+ console.log('✓ Moved:');
2078
+ movedFiles.forEach(item => {
2079
+ console.log(` ${item.from}`);
2080
+ console.log(` -> ${item.to}`);
2081
+ });
2082
+ if (movedFiles.length > 0) {
2083
+ const existingSection = fromSection;
2084
+ // Update section data in state
2085
+ state.sections = state.sections.filter(s => s.route !== normalizedFromRoute);
2086
+ state.sections.push({
2087
+ name: existingSection ? existingSection.name : getFeatureComponentName(normalizedToFeature || normalizedFromFeature),
2088
+ route: normalizedToRoute,
2089
+ featurePath: normalizedToFeature || normalizedFromFeature,
2090
+ layout: existingSection ? existingSection.layout : 'Main',
2091
+ extension: routeExtension
2092
+ });
2093
+ await saveState(state);
2094
+ }
2095
+ }
2096
+ catch (error) {
2097
+ console.error('Error:', error.message);
2098
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
2099
+ process.exit(1);
2100
+ }
2101
+ throw error;
2102
+ }
2103
+ }
2104
+ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2105
+ const { fromFeaturePath, fromComponentName } = fromInfo;
2106
+ const { toFeaturePath, toComponentName } = toInfo;
2107
+ const allFiles = new Set();
2108
+ const { scanDirectory, calculateHash } = await Promise.resolve().then(function () { return filesystem; });
2109
+ await scanDirectory(process.cwd(), allFiles);
2110
+ const featuresRoot = resolvePath(config, 'features');
2111
+ for (const relPath of allFiles) {
2112
+ const fullPath = path.join(process.cwd(), relPath);
2113
+ // Skip the moved directory itself as it was already handled
2114
+ if (fullPath.startsWith(path.resolve(toFeaturePath)))
2115
+ continue;
2116
+ let content = await readFile(fullPath, 'utf-8');
2117
+ let changed = false;
2118
+ // Handle Aliases
2119
+ if (config.importAliases.features) {
2120
+ const oldAlias = `${config.importAliases.features}/${fromFeaturePath}`;
2121
+ const newAlias = `${config.importAliases.features}/${toFeaturePath}`;
2122
+ // Update component name and path if both changed
2123
+ const oldFullImport = `from '${oldAlias}/${fromComponentName}'`;
2124
+ const newFullImport = `from '${newAlias}/${toComponentName}'`;
2125
+ if (content.includes(oldFullImport)) {
2126
+ content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2127
+ changed = true;
2128
+ }
2129
+ else if (content.includes(oldAlias)) {
2130
+ content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2131
+ changed = true;
2132
+ }
2133
+ }
2134
+ else {
2135
+ // Handle Relative Imports (more complex)
2136
+ // This is best-effort: we look for imports that resolve to the old feature path
2137
+ const fromFeatureDir = secureJoin(featuresRoot, fromFeaturePath);
2138
+ const toFeatureDir = secureJoin(featuresRoot, toFeaturePath);
2139
+ const oldRelPath = getRelativeImportPath(fullPath, fromFeatureDir);
2140
+ const newRelPath = getRelativeImportPath(fullPath, toFeatureDir);
2141
+ const oldImport = `'${oldRelPath}/${fromComponentName}'`;
2142
+ const newImport = `'${newRelPath}/${toComponentName}'`;
2143
+ if (content.includes(oldImport)) {
2144
+ content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2145
+ changed = true;
2146
+ }
2147
+ }
2148
+ // Update component name in JSX and imports if it changed
2149
+ if (fromComponentName !== toComponentName && changed) {
2150
+ content = content.replace(new RegExp(`\\b${fromComponentName}\\b`, 'g'), toComponentName);
2151
+ }
2152
+ if (changed) {
2153
+ if (options.dryRun) {
2154
+ console.log(` [Scan] Would update imports in ${relPath}`);
2155
+ }
2156
+ else {
2157
+ await writeFile(fullPath, content, 'utf-8');
2158
+ console.log(` [Scan] Updated imports in ${relPath}`);
2159
+ // Update state hash if this file is managed
2160
+ if (state.files[relPath]) {
2161
+ state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2162
+ }
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2168
+ const { fromName, toName, owner = null } = options;
2169
+ if (!existsSync(fromPath)) {
2170
+ throw new Error(`Source directory not found: ${fromPath}`);
2171
+ }
2172
+ if (existsSync(toPath) && !options.force) {
2173
+ throw new Error(`Destination already exists: ${toPath}\n` +
2174
+ `Use --force to overwrite.`);
2175
+ }
2176
+ await ensureDir(toPath);
2177
+ const entries = await readdir(fromPath);
2178
+ for (const entry of entries) {
2179
+ let targetEntry = entry;
2180
+ // Rename files if they match the component name
2181
+ if (fromName && toName && fromName !== toName) {
2182
+ if (entry.includes(fromName)) {
2183
+ targetEntry = entry.replace(fromName, toName);
2184
+ }
2185
+ }
2186
+ const fromEntryPath = path.join(fromPath, entry);
2187
+ const toEntryPath = path.join(toPath, targetEntry);
2188
+ const stats = await stat(fromEntryPath);
2189
+ if (stats.isDirectory()) {
2190
+ await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2191
+ }
2192
+ else {
2193
+ const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2194
+ const fileState = state.files[normalizedFromRelative];
2195
+ const newHash = await safeMove(fromEntryPath, toEntryPath, {
2196
+ force: options.force,
2197
+ expectedHash: fileState?.hash,
2198
+ acceptChanges: options.acceptChanges,
2199
+ normalization: config.hashing?.normalization,
2200
+ owner,
2201
+ actualOwner: fileState?.owner
2202
+ });
2203
+ // Update internal content (signatures, component names) if renaming
2204
+ if (fromName && toName && fromName !== toName) {
2205
+ let content = await readFile(toEntryPath, 'utf-8');
2206
+ let hasChanged = false;
2207
+ // Simple replacement of component names
2208
+ if (content.includes(fromName)) {
2209
+ content = content.replace(new RegExp(fromName, 'g'), toName);
2210
+ hasChanged = true;
2211
+ }
2212
+ // Also handle lowercase class names if any
2213
+ const fromLower = fromName.toLowerCase();
2214
+ const toLower = toName.toLowerCase();
2215
+ if (content.includes(fromLower)) {
2216
+ content = content.replace(new RegExp(fromLower, 'g'), toLower);
2217
+ hasChanged = true;
2218
+ }
2219
+ if (hasChanged) {
2220
+ await writeFile(toEntryPath, content, 'utf-8');
2221
+ // Re-calculate hash after content update
2222
+ const { calculateHash } = await Promise.resolve().then(function () { return filesystem; });
2223
+ const updatedHash = calculateHash(content, config.hashing?.normalization);
2224
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2225
+ if (fileState) {
2226
+ state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2227
+ delete state.files[normalizedFromRelative];
2228
+ }
2229
+ }
2230
+ else {
2231
+ // Update state for each file moved normally
2232
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2233
+ if (fileState) {
2234
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2235
+ delete state.files[normalizedFromRelative];
2236
+ }
2237
+ }
2238
+ }
2239
+ else {
2240
+ // Update state for each file moved normally
2241
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2242
+ if (fileState) {
2243
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2244
+ delete state.files[normalizedFromRelative];
2245
+ }
2246
+ }
2247
+ }
2248
+ }
2249
+ const remainingFiles = await readdir(fromPath);
2250
+ if (remainingFiles.length === 0) {
2251
+ await rmdir(fromPath);
2252
+ }
2253
+ }
2254
+
2255
+ async function createComponentCommand(componentName, options) {
2256
+ try {
2257
+ const config = await loadConfig();
2258
+ const effectiveOptions = getEffectiveOptions(options, config, 'components');
2259
+ const normalizedName = normalizeComponentName(componentName);
2260
+ const componentsRoot = resolvePath(config, 'components');
2261
+ const componentDir = secureJoin(componentsRoot, normalizedName);
2262
+ const subComponentsDir = secureJoin(componentDir, 'sub-components');
2263
+ const testsDir = secureJoin(componentDir, '__tests__');
2264
+ const configDirInside = secureJoin(componentDir, 'config');
2265
+ const constantsDirInside = secureJoin(componentDir, 'constants');
2266
+ const contextDirInside = secureJoin(componentDir, 'context');
2267
+ const hooksDirInside = secureJoin(componentDir, 'hooks');
2268
+ const typesDirInside = secureJoin(componentDir, 'types');
2269
+ const apiDirInside = secureJoin(componentDir, 'api');
2270
+ const servicesDirInside = secureJoin(componentDir, 'services');
2271
+ const schemasDirInside = secureJoin(componentDir, 'schemas');
2272
+ const { framework, createContext: shouldCreateContext, createHook: shouldCreateHook, createTests: shouldCreateTests, createConfig: shouldCreateConfig, createConstants: shouldCreateConstants, createTypes: shouldCreateTypes, createSubComponentsDir: shouldCreateSubComponentsDir, createApi: shouldCreateApi, createServices: shouldCreateServices, createSchemas: shouldCreateSchemas, createReadme: shouldCreateReadme, createStories: shouldCreateStories } = effectiveOptions;
2273
+ const componentFilePath = path.join(componentDir, `${normalizedName}${config.naming.componentExtension}`);
2274
+ const indexFilePath = path.join(componentDir, 'index.ts');
2275
+ const contextFilePath = path.join(contextDirInside, `${normalizedName}Context.tsx`);
2276
+ const hookFilePath = path.join(hooksDirInside, getHookFileName(normalizedName, config.naming.hookExtension));
2277
+ const testFilePath = path.join(testsDir, `${normalizedName}${config.naming.testExtension}`);
2278
+ const configFilePath = path.join(configDirInside, 'index.ts');
2279
+ const constantsFilePath = path.join(constantsDirInside, 'index.ts');
2280
+ const typesFilePath = path.join(typesDirInside, 'index.ts');
2281
+ const apiFilePath = path.join(apiDirInside, 'index.ts');
2282
+ const servicesFilePath = path.join(servicesDirInside, 'index.ts');
2283
+ const schemasFilePath = path.join(schemasDirInside, 'index.ts');
2284
+ const readmeFilePath = path.join(componentDir, 'README.md');
2285
+ const storiesFilePath = path.join(componentDir, `${normalizedName}.stories.tsx`);
2286
+ if (options.dryRun) {
2287
+ console.log('Dry run - would create:');
2288
+ console.log(` Component: ${componentFilePath}`);
2289
+ console.log(` Index: ${indexFilePath}`);
2290
+ if (shouldCreateContext)
2291
+ console.log(` Context: ${contextFilePath}`);
2292
+ if (shouldCreateHook)
2293
+ console.log(` Hook: ${hookFilePath}`);
2294
+ if (shouldCreateTests)
2295
+ console.log(` Tests: ${testFilePath}`);
2296
+ if (shouldCreateConfig)
2297
+ console.log(` Config: ${configFilePath}`);
2298
+ if (shouldCreateConstants)
2299
+ console.log(` Constants: ${constantsFilePath}`);
2300
+ if (shouldCreateTypes)
2301
+ console.log(` Types: ${typesFilePath}`);
2302
+ if (shouldCreateApi)
2303
+ console.log(` Api: ${apiFilePath}`);
2304
+ if (shouldCreateServices)
2305
+ console.log(` Services: ${servicesFilePath}`);
2306
+ if (shouldCreateSchemas)
2307
+ console.log(` Schemas: ${schemasFilePath}`);
2308
+ if (shouldCreateReadme)
2309
+ console.log(` Readme: ${readmeFilePath}`);
2310
+ if (shouldCreateStories)
2311
+ console.log(` Stories: ${storiesFilePath}`);
2312
+ if (shouldCreateSubComponentsDir)
2313
+ console.log(` Sub-components: ${subComponentsDir}/`);
2314
+ return;
2315
+ }
2316
+ await ensureNotExists(componentFilePath, options.force);
2317
+ await ensureNotExists(indexFilePath, options.force);
2318
+ if (shouldCreateContext)
2319
+ await ensureNotExists(contextFilePath, options.force);
2320
+ if (shouldCreateHook)
2321
+ await ensureNotExists(hookFilePath, options.force);
2322
+ if (shouldCreateTests)
2323
+ await ensureNotExists(testFilePath, options.force);
2324
+ if (shouldCreateConfig)
2325
+ await ensureNotExists(configFilePath, options.force);
2326
+ if (shouldCreateConstants)
2327
+ await ensureNotExists(constantsFilePath, options.force);
2328
+ if (shouldCreateTypes)
2329
+ await ensureNotExists(typesFilePath, options.force);
2330
+ if (shouldCreateApi)
2331
+ await ensureNotExists(apiFilePath, options.force);
2332
+ if (shouldCreateServices)
2333
+ await ensureNotExists(servicesFilePath, options.force);
2334
+ if (shouldCreateSchemas)
2335
+ await ensureNotExists(schemasFilePath, options.force);
2336
+ if (shouldCreateReadme)
2337
+ await ensureNotExists(readmeFilePath, options.force);
2338
+ if (shouldCreateStories)
2339
+ await ensureNotExists(storiesFilePath, options.force);
2340
+ await ensureDir(componentDir);
2341
+ if (shouldCreateSubComponentsDir)
2342
+ await ensureDir(subComponentsDir);
2343
+ if (shouldCreateContext)
2344
+ await ensureDir(contextDirInside);
2345
+ if (shouldCreateHook)
2346
+ await ensureDir(hooksDirInside);
2347
+ if (shouldCreateTests)
2348
+ await ensureDir(testsDir);
2349
+ if (shouldCreateConfig)
2350
+ await ensureDir(configDirInside);
2351
+ if (shouldCreateConstants)
2352
+ await ensureDir(constantsDirInside);
2353
+ if (shouldCreateTypes)
2354
+ await ensureDir(typesDirInside);
2355
+ if (shouldCreateApi)
2356
+ await ensureDir(apiDirInside);
2357
+ if (shouldCreateServices)
2358
+ await ensureDir(servicesDirInside);
2359
+ if (shouldCreateSchemas)
2360
+ await ensureDir(schemasDirInside);
2361
+ const componentContent = generateComponentTemplate(normalizedName, framework);
2362
+ const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
2363
+ const componentHash = await writeFileWithSignature(componentFilePath, componentContent, signature, config.hashing?.normalization);
2364
+ await registerFile(componentFilePath, {
2365
+ kind: 'component',
2366
+ template: 'component',
2367
+ hash: componentHash,
2368
+ owner: normalizedName
2369
+ });
2370
+ const writtenFiles = [componentFilePath];
2371
+ const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
2372
+ const indexHash = await writeFileWithSignature(indexFilePath, indexContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2373
+ await registerFile(indexFilePath, {
2374
+ kind: 'component-file',
2375
+ template: 'index',
2376
+ hash: indexHash,
2377
+ owner: normalizedName
2378
+ });
2379
+ writtenFiles.push(indexFilePath);
2380
+ if (shouldCreateTypes) {
2381
+ const typesContent = generateTypesTemplate(normalizedName);
2382
+ const hash = await writeFileWithSignature(typesFilePath, typesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2383
+ await registerFile(typesFilePath, {
2384
+ kind: 'component-file',
2385
+ template: 'types',
2386
+ hash,
2387
+ owner: normalizedName
2388
+ });
2389
+ writtenFiles.push(typesFilePath);
2390
+ }
2391
+ if (shouldCreateContext) {
2392
+ const contextContent = generateContextTemplate(normalizedName);
2393
+ const hash = await writeFileWithSignature(contextFilePath, contextContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2394
+ await registerFile(contextFilePath, {
2395
+ kind: 'component-file',
2396
+ template: 'context',
2397
+ hash,
2398
+ owner: normalizedName
2399
+ });
2400
+ writtenFiles.push(contextFilePath);
2401
+ }
2402
+ if (shouldCreateHook) {
2403
+ const hookName = getHookFunctionName(normalizedName);
2404
+ const hookContent = generateHookTemplate(normalizedName, hookName);
2405
+ const hash = await writeFileWithSignature(hookFilePath, hookContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2406
+ await registerFile(hookFilePath, {
2407
+ kind: 'component-file',
2408
+ template: 'hook',
2409
+ hash,
2410
+ owner: normalizedName
2411
+ });
2412
+ writtenFiles.push(hookFilePath);
2413
+ }
2414
+ if (shouldCreateTests) {
2415
+ const relativeComponentPath = `../${normalizedName}${config.naming.componentExtension}`;
2416
+ const testContent = generateTestTemplate(normalizedName, relativeComponentPath);
2417
+ const hash = await writeFileWithSignature(testFilePath, testContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2418
+ await registerFile(testFilePath, {
2419
+ kind: 'component-file',
2420
+ template: 'test',
2421
+ hash,
2422
+ owner: normalizedName
2423
+ });
2424
+ writtenFiles.push(testFilePath);
2425
+ }
2426
+ if (shouldCreateConfig) {
2427
+ const configContent = generateConfigTemplate(normalizedName);
2428
+ const hash = await writeFileWithSignature(configFilePath, configContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2429
+ await registerFile(configFilePath, {
2430
+ kind: 'component-file',
2431
+ template: 'config',
2432
+ hash,
2433
+ owner: normalizedName
2434
+ });
2435
+ writtenFiles.push(configFilePath);
2436
+ }
2437
+ if (shouldCreateConstants) {
2438
+ const constantsContent = generateConstantsTemplate(normalizedName);
2439
+ const hash = await writeFileWithSignature(constantsFilePath, constantsContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2440
+ await registerFile(constantsFilePath, {
2441
+ kind: 'component-file',
2442
+ template: 'constants',
2443
+ hash,
2444
+ owner: normalizedName
2445
+ });
2446
+ writtenFiles.push(constantsFilePath);
2447
+ }
2448
+ if (shouldCreateApi) {
2449
+ const apiContent = generateApiTemplate(normalizedName);
2450
+ const hash = await writeFileWithSignature(apiFilePath, apiContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2451
+ await registerFile(apiFilePath, {
2452
+ kind: 'component-file',
2453
+ template: 'api',
2454
+ hash,
2455
+ owner: normalizedName
2456
+ });
2457
+ writtenFiles.push(apiFilePath);
2458
+ }
2459
+ if (shouldCreateServices) {
2460
+ const servicesContent = generateServiceTemplate(normalizedName);
2461
+ const hash = await writeFileWithSignature(servicesFilePath, servicesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2462
+ await registerFile(servicesFilePath, {
2463
+ kind: 'component-file',
2464
+ template: 'service',
2465
+ hash,
2466
+ owner: normalizedName
2467
+ });
2468
+ writtenFiles.push(servicesFilePath);
2469
+ }
2470
+ if (shouldCreateSchemas) {
2471
+ const schemasContent = generateSchemaTemplate(normalizedName);
2472
+ const hash = await writeFileWithSignature(schemasFilePath, schemasContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2473
+ await registerFile(schemasFilePath, {
2474
+ kind: 'component-file',
2475
+ template: 'schema',
2476
+ hash,
2477
+ owner: normalizedName
2478
+ });
2479
+ writtenFiles.push(schemasFilePath);
2480
+ }
2481
+ if (shouldCreateReadme) {
2482
+ const readmeContent = generateReadmeTemplate(normalizedName);
2483
+ const hash = await writeFileWithSignature(readmeFilePath, readmeContent, getSignature(config, 'astro'), config.hashing?.normalization);
2484
+ await registerFile(readmeFilePath, {
2485
+ kind: 'component-file',
2486
+ template: 'readme',
2487
+ hash,
2488
+ owner: normalizedName
2489
+ });
2490
+ writtenFiles.push(readmeFilePath);
2491
+ }
2492
+ if (shouldCreateStories) {
2493
+ const relativePath = `./${normalizedName}${config.naming.componentExtension}`;
2494
+ const storiesContent = generateStoriesTemplate(normalizedName, relativePath);
2495
+ const hash = await writeFileWithSignature(storiesFilePath, storiesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2496
+ await registerFile(storiesFilePath, {
2497
+ kind: 'component-file',
2498
+ template: 'stories',
2499
+ hash,
2500
+ owner: normalizedName
2501
+ });
2502
+ writtenFiles.push(storiesFilePath);
2503
+ }
2504
+ // Formatting
2505
+ if (config.formatting.tool !== 'none') {
2506
+ await formatFiles(writtenFiles, config.formatting.tool);
2507
+ }
2508
+ console.log('✓ Component created successfully:');
2509
+ console.log(` Component: ${componentFilePath}`);
2510
+ console.log(` Index: ${indexFilePath}`);
2511
+ if (shouldCreateContext)
2512
+ console.log(` Context: ${contextFilePath}`);
2513
+ if (shouldCreateHook)
2514
+ console.log(` Hook: ${hookFilePath}`);
2515
+ if (shouldCreateTests)
2516
+ console.log(` Tests: ${testFilePath}`);
2517
+ if (shouldCreateConfig)
2518
+ console.log(` Config: ${configFilePath}`);
2519
+ if (shouldCreateConstants)
2520
+ console.log(` Constants: ${constantsFilePath}`);
2521
+ if (shouldCreateTypes)
2522
+ console.log(` Types: ${typesFilePath}`);
2523
+ if (shouldCreateApi)
2524
+ console.log(` Api: ${apiFilePath}`);
2525
+ if (shouldCreateServices)
2526
+ console.log(` Services: ${servicesFilePath}`);
2527
+ if (shouldCreateSchemas)
2528
+ console.log(` Schemas: ${schemasFilePath}`);
2529
+ if (shouldCreateReadme)
2530
+ console.log(` Readme: ${readmeFilePath}`);
2531
+ if (shouldCreateStories)
2532
+ console.log(` Stories: ${storiesFilePath}`);
2533
+ if (shouldCreateSubComponentsDir)
2534
+ console.log(` Sub-components: ${subComponentsDir}/`);
2535
+ await addComponentToState({
2536
+ name: normalizedName,
2537
+ path: componentDir
2538
+ });
2539
+ if (config.git?.stageChanges) {
2540
+ await stageFiles(writtenFiles);
2541
+ }
2542
+ }
2543
+ catch (error) {
2544
+ console.error('Error:', error.message);
2545
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
2546
+ process.exit(1);
2547
+ }
2548
+ throw error;
2549
+ }
2550
+ }
2551
+
2552
+ async function removeComponentCommand(identifier, options) {
2553
+ try {
2554
+ const config = await loadConfig();
2555
+ if (config.git?.requireCleanRepo && !await isRepoClean()) {
2556
+ throw new Error('Git repository is not clean. Please commit or stash your changes before proceeding.');
2557
+ }
2558
+ const state = await loadState();
2559
+ const component = findComponent(state, identifier);
2560
+ let componentDir;
2561
+ if (component) {
2562
+ componentDir = component.path;
2563
+ }
2564
+ else {
2565
+ // Fallback: try to guess path if not in state
2566
+ const componentsRoot = path.resolve(process.cwd(), config.paths.components);
2567
+ componentDir = secureJoin(componentsRoot, identifier);
2568
+ }
2569
+ if (options.dryRun) {
2570
+ console.log('Dry run - would delete:');
2571
+ console.log(` Component directory: ${componentDir}/`);
2572
+ return;
2573
+ }
2574
+ const result = await safeDeleteDir(componentDir, {
2575
+ force: options.force,
2576
+ stateFiles: state.files,
2577
+ acceptChanges: options.acceptChanges,
2578
+ normalization: config.hashing?.normalization,
2579
+ owner: identifier
2580
+ });
2581
+ if (result.deleted) {
2582
+ console.log(`✓ Deleted component: ${componentDir}/`);
2583
+ await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
2584
+ // Unregister files
2585
+ const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
2586
+ for (const f in state.files) {
2587
+ if (f.startsWith(dirPrefix)) {
2588
+ delete state.files[f];
2589
+ }
2590
+ }
2591
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
2592
+ await saveState(state);
2593
+ }
2594
+ else if (result.message) {
2595
+ console.log(`⚠ Skipped: ${componentDir}`);
2596
+ console.log(` Reason: ${result.message}`);
2597
+ }
2598
+ else {
2599
+ console.log(`Component not found at ${componentDir}`);
2600
+ }
2601
+ }
2602
+ catch (error) {
2603
+ console.error('Error:', error.message);
2604
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
2605
+ process.exit(1);
2606
+ }
2607
+ throw error;
2608
+ }
2609
+ }
2610
+
2611
+ async function listSectionsCommand() {
2612
+ try {
2613
+ const config = await loadConfig();
2614
+ const state = await loadState();
2615
+ const pagesRoot = resolvePath(config, 'pages');
2616
+ console.log('Managed Sections:');
2617
+ if (!existsSync(pagesRoot)) {
2618
+ console.log(' No pages directory found.');
2619
+ }
2620
+ else {
2621
+ const extensions = [config.naming.routeExtension, '.ts', '.js'];
2622
+ const sections = await findGeneratedFiles(pagesRoot, extensions);
2623
+ if (sections.length === 0) {
2624
+ console.log(' No Textor-managed sections found.');
2625
+ }
2626
+ else {
2627
+ for (const section of sections) {
2628
+ const relativePath = path.relative(pagesRoot, section).replace(/\\/g, '/');
2629
+ const route = '/' + relativePath
2630
+ .replace(/\.[^/.]+$/, ''); // Remove extension
2631
+ const stateSection = state.sections.find(s => s.route === route);
2632
+ const name = stateSection ? stateSection.name : route;
2633
+ console.log(` - ${name} [${route}] (${relativePath})`);
2634
+ if (stateSection) {
2635
+ console.log(` Feature: ${stateSection.featurePath}`);
2636
+ console.log(` Layout: ${stateSection.layout}`);
2637
+ // Check for senior architecture folders/files
2638
+ const featuresRoot = resolvePath(config, 'features');
2639
+ const featureDir = path.join(featuresRoot, stateSection.featurePath);
2640
+ const capabilities = [];
2641
+ const checkDir = (subDir, label) => {
2642
+ if (existsSync(path.join(featureDir, subDir)))
2643
+ capabilities.push(label);
2644
+ };
2645
+ checkDir('api', 'API');
2646
+ checkDir('services', 'Services');
2647
+ checkDir('schemas', 'Schemas');
2648
+ checkDir('hooks', 'Hooks');
2649
+ checkDir('context', 'Context');
2650
+ checkDir('types', 'Types');
2651
+ checkDir('scripts', 'Scripts');
2652
+ checkDir('sub-components', 'Sub-components');
2653
+ checkDir('__tests__', 'Tests');
2654
+ if (existsSync(path.join(featureDir, 'README.md')))
2655
+ capabilities.push('Docs');
2656
+ const storiesFile = (await readdir(featureDir).catch(() => []))
2657
+ .find(f => f.endsWith('.stories.tsx') || f.endsWith('.stories.jsx'));
2658
+ if (storiesFile)
2659
+ capabilities.push('Stories');
2660
+ if (capabilities.length > 0) {
2661
+ console.log(` Architecture: ${capabilities.join(', ')}`);
2662
+ }
2663
+ }
2664
+ else {
2665
+ // Try to extract feature path from the file content
2666
+ const content = await readFile(section, 'utf-8');
2667
+ const featureImportMatch = content.match(/import\s+\w+\s+from\s+'([^']+)'/g);
2668
+ if (featureImportMatch) {
2669
+ for (const match of featureImportMatch) {
2670
+ const pathMatch = match.match(/'([^']+)'/);
2671
+ if (pathMatch) {
2672
+ console.log(` Import: ${pathMatch[1]}`);
2673
+ }
2674
+ }
2675
+ }
2676
+ }
2677
+ }
2678
+ }
2679
+ }
2680
+ if (state.components && state.components.length > 0) {
2681
+ console.log('\nManaged Components:');
2682
+ for (const component of state.components) {
2683
+ console.log(` - ${component.name} (${path.relative(process.cwd(), component.path)})`);
2684
+ const componentDir = component.path;
2685
+ const capabilities = [];
2686
+ const checkDir = (subDir, label) => {
2687
+ if (existsSync(path.join(componentDir, subDir)))
2688
+ capabilities.push(label);
2689
+ };
2690
+ checkDir('api', 'API');
2691
+ checkDir('services', 'Services');
2692
+ checkDir('schemas', 'Schemas');
2693
+ checkDir('hooks', 'Hooks');
2694
+ checkDir('context', 'Context');
2695
+ checkDir('types', 'Types');
2696
+ checkDir('sub-components', 'Sub-components');
2697
+ checkDir('__tests__', 'Tests');
2698
+ checkDir('config', 'Config');
2699
+ checkDir('constants', 'Constants');
2700
+ if (existsSync(path.join(componentDir, 'README.md')))
2701
+ capabilities.push('Docs');
2702
+ const storiesFile = (await readdir(componentDir).catch(() => []))
2703
+ .find(f => f.endsWith('.stories.tsx') || f.endsWith('.stories.jsx'));
2704
+ if (storiesFile)
2705
+ capabilities.push('Stories');
2706
+ if (capabilities.length > 0) {
2707
+ console.log(` Architecture: ${capabilities.join(', ')}`);
2708
+ }
2709
+ }
2710
+ }
2711
+ }
2712
+ catch (error) {
2713
+ console.error('Error:', error.message);
2714
+ process.exit(1);
2715
+ }
2716
+ }
2717
+ async function findGeneratedFiles(dir, extensions) {
2718
+ const results = [];
2719
+ const entries = await readdir(dir);
2720
+ const exts = Array.isArray(extensions) ? extensions : [extensions];
2721
+ for (const entry of entries) {
2722
+ const fullPath = path.join(dir, entry);
2723
+ const stats = await stat(fullPath);
2724
+ if (stats.isDirectory()) {
2725
+ results.push(...await findGeneratedFiles(fullPath, exts));
2726
+ }
2727
+ else if (exts.some(ext => entry.endsWith(ext))) {
2728
+ if (await isTextorGenerated(fullPath)) {
2729
+ results.push(fullPath);
2730
+ }
2731
+ }
2732
+ }
2733
+ return results;
2734
+ }
2735
+
2736
+ async function validateStateCommand(options) {
2737
+ try {
2738
+ const config = await loadConfig();
2739
+ const state = await loadState();
2740
+ const results = {
2741
+ missing: [],
2742
+ modified: [],
2743
+ valid: 0
2744
+ };
2745
+ const files = Object.keys(state.files);
2746
+ for (const relativePath of files) {
2747
+ const fullPath = path.join(process.cwd(), relativePath);
2748
+ const fileData = state.files[relativePath];
2749
+ if (!existsSync(fullPath)) {
2750
+ results.missing.push(relativePath);
2751
+ continue;
2752
+ }
2753
+ const content = await readFile(fullPath, 'utf-8');
2754
+ const currentHash = calculateHash(content, config.hashing?.normalization);
2755
+ if (currentHash !== fileData.hash) {
2756
+ results.modified.push({
2757
+ path: relativePath,
2758
+ newHash: currentHash
2759
+ });
2760
+ }
2761
+ else {
2762
+ results.valid++;
2763
+ }
2764
+ }
2765
+ console.log('State Validation Results:');
2766
+ console.log(` Valid files: ${results.valid}`);
2767
+ if (results.missing.length > 0) {
2768
+ console.log(`\n Missing files: ${results.missing.length}`);
2769
+ results.missing.forEach(f => console.log(` - ${f}`));
2770
+ }
2771
+ if (results.modified.length > 0) {
2772
+ console.log(`\n Modified files: ${results.modified.length}`);
2773
+ results.modified.forEach(f => console.log(` - ${f.path}`));
2774
+ }
2775
+ if (options.fix) {
2776
+ let fixedCount = 0;
2777
+ // Fix modified files if they still have the Textor signature
2778
+ for (const mod of results.modified) {
2779
+ const fullPath = path.join(process.cwd(), mod.path);
2780
+ if (await isTextorGenerated(fullPath)) {
2781
+ state.files[mod.path].hash = mod.newHash;
2782
+ fixedCount++;
2783
+ }
2784
+ }
2785
+ // Remove missing files from state
2786
+ for (const miss of results.missing) {
2787
+ delete state.files[miss];
2788
+ fixedCount++;
2789
+ }
2790
+ if (fixedCount > 0) {
2791
+ await saveState(state);
2792
+ console.log(`\n✓ Fixed ${fixedCount} entries in state.`);
2793
+ }
2794
+ else {
2795
+ console.log('\nNothing to fix or missing signatures on modified files.');
2796
+ }
2797
+ }
2798
+ else if (results.missing.length > 0 || results.modified.length > 0) {
2799
+ console.log('\nRun with --fix to synchronize state with reality (requires Textor signature to be present).');
2800
+ }
2801
+ else {
2802
+ console.log('\n✓ State is perfectly in sync.');
2803
+ }
2804
+ }
2805
+ catch (error) {
2806
+ console.error('Error:', error.message);
2807
+ process.exit(1);
2808
+ }
2809
+ }
2810
+
2811
+ async function statusCommand() {
2812
+ try {
2813
+ const config = await loadConfig();
2814
+ const state = await loadState();
2815
+ const results = {
2816
+ missing: [],
2817
+ modified: [],
2818
+ untracked: [], // Has signature, not in state
2819
+ orphaned: [], // No signature, not in state
2820
+ synced: 0
2821
+ };
2822
+ const roots = [
2823
+ resolvePath(config, 'pages'),
2824
+ resolvePath(config, 'features'),
2825
+ resolvePath(config, 'components')
2826
+ ].map(p => path.resolve(p));
2827
+ const diskFiles = new Set();
2828
+ const configSignatures = Object.values(config.signatures || {});
2829
+ for (const root of roots) {
2830
+ if (existsSync(root)) {
2831
+ await scanDirectory(root, diskFiles);
2832
+ }
2833
+ }
2834
+ // 1. Check state files against disk
2835
+ for (const relativePath in state.files) {
2836
+ const fullPath = path.join(process.cwd(), relativePath);
2837
+ if (!existsSync(fullPath)) {
2838
+ results.missing.push(relativePath);
2839
+ continue;
2840
+ }
2841
+ diskFiles.delete(relativePath);
2842
+ const content = await readFile(fullPath, 'utf-8');
2843
+ const currentHash = calculateHash(content, config.hashing?.normalization);
2844
+ const fileData = state.files[relativePath];
2845
+ if (currentHash !== fileData.hash) {
2846
+ results.modified.push(relativePath);
2847
+ }
2848
+ else {
2849
+ results.synced++;
2850
+ }
2851
+ }
2852
+ // 2. Check remaining disk files
2853
+ for (const relativePath of diskFiles) {
2854
+ const fullPath = path.join(process.cwd(), relativePath);
2855
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
2856
+ if (isGenerated) {
2857
+ results.untracked.push(relativePath);
2858
+ }
2859
+ else {
2860
+ results.orphaned.push(relativePath);
2861
+ }
2862
+ }
2863
+ // Reporting
2864
+ console.log('Textor Status Report:');
2865
+ console.log(` Synced files: ${results.synced}`);
2866
+ if (results.modified.length > 0) {
2867
+ console.log(`\n MODIFIED (In state, but content changed): ${results.modified.length}`);
2868
+ results.modified.forEach(f => console.log(` ~ ${f}`));
2869
+ }
2870
+ if (results.missing.length > 0) {
2871
+ console.log(`\n MISSING (In state, but not on disk): ${results.missing.length}`);
2872
+ results.missing.forEach(f => console.log(` - ${f}`));
2873
+ }
2874
+ if (results.untracked.length > 0) {
2875
+ console.log(`\n UNTRACKED (On disk with signature, not in state): ${results.untracked.length}`);
2876
+ results.untracked.forEach(f => console.log(` + ${f}`));
2877
+ }
2878
+ if (results.orphaned.length > 0) {
2879
+ console.log(`\n ORPHANED (On disk without signature, in managed folder): ${results.orphaned.length}`);
2880
+ results.orphaned.forEach(f => console.log(` ? ${f}`));
2881
+ }
2882
+ if (results.modified.length === 0 && results.missing.length === 0 && results.untracked.length === 0) {
2883
+ console.log('\n✓ Project is perfectly synchronized with state.');
2884
+ }
2885
+ else {
2886
+ console.log('\nUse "textor sync" to reconcile state with disk.');
2887
+ }
2888
+ }
2889
+ catch (error) {
2890
+ console.error('Error:', error.message);
2891
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
2892
+ process.exit(1);
2893
+ }
2894
+ throw error;
2895
+ }
2896
+ }
2897
+
2898
+ async function syncCommand(options) {
2899
+ try {
2900
+ const config = await loadConfig();
2901
+ const state = await loadState();
2902
+ const results = {
2903
+ added: [],
2904
+ updated: [],
2905
+ missing: [],
2906
+ untouched: 0
2907
+ };
2908
+ const roots = [
2909
+ resolvePath(config, 'pages'),
2910
+ resolvePath(config, 'features'),
2911
+ resolvePath(config, 'components')
2912
+ ].map(p => path.resolve(p));
2913
+ const managedFiles = new Set();
2914
+ const configSignatures = Object.values(config.signatures || {});
2915
+ for (const root of roots) {
2916
+ if (existsSync(root)) {
2917
+ await scanDirectory(root, managedFiles);
2918
+ }
2919
+ else {
2920
+ const relativeRoot = path.relative(process.cwd(), root).replace(/\\/g, '/');
2921
+ console.log(` Warning: Managed directory not found: ${relativeRoot}`);
2922
+ }
2923
+ }
2924
+ const stateFiles = Object.keys(state.files);
2925
+ let changed = false;
2926
+ // 1. Check files in state
2927
+ for (const relativePath of stateFiles) {
2928
+ const fullPath = path.join(process.cwd(), relativePath);
2929
+ if (!existsSync(fullPath)) {
2930
+ results.missing.push(relativePath);
2931
+ continue;
2932
+ }
2933
+ managedFiles.delete(relativePath); // Remove from managedFiles so we don't process it again in step 2
2934
+ const content = await readFile(fullPath, 'utf-8');
2935
+ const currentHash = calculateHash(content, config.hashing?.normalization);
2936
+ const fileData = state.files[relativePath];
2937
+ if (currentHash !== fileData.hash) {
2938
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
2939
+ if (isGenerated || options.force) {
2940
+ results.updated.push({ path: relativePath, newHash: currentHash });
2941
+ }
2942
+ }
2943
+ else {
2944
+ results.untouched++;
2945
+ }
2946
+ }
2947
+ // 2. Check files on disk not in state
2948
+ let ignoredCount = 0;
2949
+ for (const relativePath of managedFiles) {
2950
+ const fullPath = path.join(process.cwd(), relativePath);
2951
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
2952
+ if (isGenerated || options.includeAll) {
2953
+ const content = await readFile(fullPath, 'utf-8');
2954
+ const hash = calculateHash(content, config.hashing?.normalization);
2955
+ results.added.push({ path: relativePath, hash });
2956
+ }
2957
+ else {
2958
+ ignoredCount++;
2959
+ }
2960
+ }
2961
+ if (results.added.length > 0 || results.updated.length > 0 || results.missing.length > 0) {
2962
+ changed = true;
2963
+ }
2964
+ // Reporting
2965
+ console.log('Sync Analysis:');
2966
+ console.log(` Untouched files: ${results.untouched}`);
2967
+ if (ignoredCount > 0 && !options.includeAll) {
2968
+ console.log(` Ignored non-generated files: ${ignoredCount} (use --include-all to track them)`);
2969
+ }
2970
+ if (results.added.length > 0) {
2971
+ console.log(`\n New files to track: ${results.added.length}`);
2972
+ results.added.forEach(f => console.log(` + ${f.path}`));
2973
+ }
2974
+ if (results.updated.length > 0) {
2975
+ console.log(`\n Modified files to update: ${results.updated.length}`);
2976
+ results.updated.forEach(f => console.log(` ~ ${f.path}`));
2977
+ }
2978
+ if (results.missing.length > 0) {
2979
+ console.log(`\n Missing files to remove from state: ${results.missing.length}`);
2980
+ results.missing.forEach(f => console.log(` - ${f}`));
2981
+ }
2982
+ if (options.dryRun) {
2983
+ console.log('\nDry run: no changes applied.');
2984
+ return;
2985
+ }
2986
+ if (results.added.length > 0) {
2987
+ for (const file of results.added) {
2988
+ state.files[file.path] = {
2989
+ kind: inferKind(file.path, config),
2990
+ hash: file.hash,
2991
+ timestamp: new Date().toISOString(),
2992
+ synced: true
2993
+ };
2994
+ }
2995
+ }
2996
+ if (results.updated.length > 0) {
2997
+ for (const file of results.updated) {
2998
+ state.files[file.path].hash = file.newHash;
2999
+ state.files[file.path].timestamp = new Date().toISOString();
3000
+ state.files[file.path].synced = true;
3001
+ }
3002
+ }
3003
+ if (results.missing.length > 0) {
3004
+ for (const relPath of results.missing) {
3005
+ delete state.files[relPath];
3006
+ }
3007
+ }
3008
+ if (changed) {
3009
+ // 3. Reconstruct components and sections
3010
+ state.components = reconstructComponents(state.files, config);
3011
+ state.sections = reconstructSections(state, config);
3012
+ await saveState(state);
3013
+ console.log(`\n✓ State synchronized successfully (${results.added.length} added, ${results.updated.length} updated, ${results.missing.length} removed).`);
3014
+ }
3015
+ else {
3016
+ // Even if no files changed, check if metadata needs reconstruction
3017
+ const newComponents = reconstructComponents(state.files, config);
3018
+ const newSections = reconstructSections(state, config);
3019
+ const componentsEqual = JSON.stringify(newComponents) === JSON.stringify(state.components || []);
3020
+ const sectionsEqual = JSON.stringify(newSections) === JSON.stringify(state.sections || []);
3021
+ if (!componentsEqual || !sectionsEqual) {
3022
+ state.components = newComponents;
3023
+ state.sections = newSections;
3024
+ await saveState(state);
3025
+ console.log('\n✓ Metadata (components/sections) reconstructed.');
3026
+ }
3027
+ else {
3028
+ console.log('\n✓ Everything is already in sync.');
3029
+ }
3030
+ }
3031
+ }
3032
+ catch (error) {
3033
+ console.error('Error:', error.message);
3034
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3035
+ process.exit(1);
3036
+ }
3037
+ throw error;
3038
+ }
3039
+ }
3040
+
3041
+ async function adoptCommand(identifier, options) {
3042
+ try {
3043
+ const config = await loadConfig();
3044
+ const state = await loadState();
3045
+ const roots = {
3046
+ pages: resolvePath(config, 'pages'),
3047
+ features: resolvePath(config, 'features'),
3048
+ components: resolvePath(config, 'components')
3049
+ };
3050
+ let filesToAdopt = [];
3051
+ if (!identifier && options.all) {
3052
+ // Adopt all untracked files in all roots
3053
+ const managedFiles = new Set();
3054
+ for (const root of Object.values(roots)) {
3055
+ if (existsSync(root)) {
3056
+ await scanDirectory(root, managedFiles);
3057
+ }
3058
+ }
3059
+ filesToAdopt = Array.from(managedFiles).filter(f => !state.files[f]);
3060
+ }
3061
+ else if (identifier) {
3062
+ const untrackedFiles = new Set();
3063
+ // 1. Try as direct path
3064
+ const fullPath = path.resolve(process.cwd(), identifier);
3065
+ if (existsSync(fullPath)) {
3066
+ await scanDirectoryOrFile(fullPath, untrackedFiles, state);
3067
+ }
3068
+ // 2. Try as component name
3069
+ const compPath = path.join(roots.components, identifier);
3070
+ if (existsSync(compPath)) {
3071
+ await scanDirectoryOrFile(compPath, untrackedFiles, state);
3072
+ }
3073
+ // 3. Try as feature name
3074
+ const featPath = path.join(roots.features, identifier);
3075
+ if (existsSync(featPath)) {
3076
+ await scanDirectoryOrFile(featPath, untrackedFiles, state);
3077
+ }
3078
+ // 4. Try as route or page name
3079
+ const cleanRoute = identifier.startsWith('/') ? identifier.slice(1) : identifier;
3080
+ const pagePath = path.join(roots.pages, cleanRoute + (config.naming?.routeExtension || '.astro'));
3081
+ if (existsSync(pagePath)) {
3082
+ await scanDirectoryOrFile(pagePath, untrackedFiles, state);
3083
+ }
3084
+ const nestedPagePath = path.join(roots.pages, cleanRoute, config.routing?.indexFile || 'index.astro');
3085
+ if (existsSync(nestedPagePath)) {
3086
+ await scanDirectoryOrFile(nestedPagePath, untrackedFiles, state);
3087
+ }
3088
+ filesToAdopt = Array.from(untrackedFiles);
3089
+ if (filesToAdopt.length === 0 && !existsSync(fullPath)) {
3090
+ throw new Error(`Could not find any untracked files for identifier: ${identifier}`);
3091
+ }
3092
+ }
3093
+ else {
3094
+ throw new Error('Please provide a path/identifier or use --all');
3095
+ }
3096
+ // Filter to ensure all files are within managed roots
3097
+ const rootPaths = Object.values(roots).map(p => path.resolve(p));
3098
+ filesToAdopt = filesToAdopt.filter(relPath => {
3099
+ const fullPath = path.resolve(process.cwd(), relPath);
3100
+ return rootPaths.some(root => fullPath.startsWith(root));
3101
+ });
3102
+ if (filesToAdopt.length === 0) {
3103
+ console.log('No untracked files found to adopt.');
3104
+ return;
3105
+ }
3106
+ console.log(`Found ${filesToAdopt.length} files to adopt...`);
3107
+ let adoptedCount = 0;
3108
+ for (const relPath of filesToAdopt) {
3109
+ const success = await adoptFile(relPath, config, state, options);
3110
+ if (success)
3111
+ adoptedCount++;
3112
+ }
3113
+ if (adoptedCount > 0 && !options.dryRun) {
3114
+ state.components = reconstructComponents(state.files, config);
3115
+ state.sections = reconstructSections(state, config);
3116
+ await saveState(state);
3117
+ console.log(`\n✓ Successfully adopted ${adoptedCount} files.`);
3118
+ }
3119
+ else if (options.dryRun) {
3120
+ console.log(`\nDry run: would adopt ${adoptedCount} files.`);
3121
+ }
3122
+ }
3123
+ catch (error) {
3124
+ console.error('Error:', error.message);
3125
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3126
+ process.exit(1);
3127
+ }
3128
+ throw error;
3129
+ }
3130
+ }
3131
+ async function adoptFile(relPath, config, state, options) {
3132
+ const fullPath = path.join(process.cwd(), relPath);
3133
+ const content = await readFile(fullPath, 'utf-8');
3134
+ const ext = path.extname(relPath);
3135
+ let signature = '';
3136
+ if (ext === '.astro')
3137
+ signature = config.signatures.astro;
3138
+ else if (ext === '.ts' || ext === '.tsx')
3139
+ signature = config.signatures.typescript;
3140
+ else if (ext === '.js' || ext === '.jsx')
3141
+ signature = config.signatures.javascript;
3142
+ let finalContent = content;
3143
+ if (signature && !content.includes(signature)) {
3144
+ if (options.dryRun) {
3145
+ console.log(` ~ Would add signature to ${relPath}`);
3146
+ }
3147
+ else {
3148
+ finalContent = signature + '\n' + content;
3149
+ await writeFile(fullPath, finalContent, 'utf-8');
3150
+ console.log(` + Added signature and adopting: ${relPath}`);
3151
+ }
3152
+ }
3153
+ else {
3154
+ if (options.dryRun) {
3155
+ console.log(` + Would adopt (already has signature or no signature for ext): ${relPath}`);
3156
+ }
3157
+ else {
3158
+ console.log(` + Adopting: ${relPath}`);
3159
+ }
3160
+ }
3161
+ if (!options.dryRun) {
3162
+ const hash = calculateHash(finalContent, config.hashing?.normalization);
3163
+ state.files[relPath] = {
3164
+ kind: inferKind(relPath, config),
3165
+ hash: hash,
3166
+ timestamp: new Date().toISOString(),
3167
+ synced: true
3168
+ };
3169
+ }
3170
+ return true;
3171
+ }
3172
+ async function scanDirectoryOrFile(fullPath, fileSet, state) {
3173
+ if ((await stat(fullPath)).isDirectory()) {
3174
+ const dirFiles = new Set();
3175
+ await scanDirectory(fullPath, dirFiles);
3176
+ for (const f of dirFiles) {
3177
+ if (!state.files[f])
3178
+ fileSet.add(f);
3179
+ }
3180
+ }
3181
+ else {
3182
+ const relPath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
3183
+ if (!state.files[relPath]) {
3184
+ fileSet.add(relPath);
3185
+ }
3186
+ }
3187
+ }
3188
+
3189
+ export { addSectionCommand, adoptCommand, createComponentCommand, initCommand, listSectionsCommand, moveSectionCommand, removeComponentCommand, removeSectionCommand, statusCommand, syncCommand, validateStateCommand };
3190
+ //# sourceMappingURL=index.js.map