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