@oamm/textor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -269,6 +269,16 @@ pnpm textor upgrade-config
269
269
  **Options:**
270
270
  - `--dry-run`: Print the upgraded config without writing it.
271
271
 
272
+ ### normalize-state
273
+ Normalize `.textor/state.json` to use project-relative paths (helpful when moving between machines).
274
+
275
+ ```bash
276
+ pnpm textor normalize-state
277
+ ```
278
+
279
+ **Options:**
280
+ - `--dry-run`: Print the normalized state without writing it.
281
+
272
282
  ## 🏗️ Technical Architecture
273
283
 
274
284
  Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.
@@ -456,7 +466,7 @@ You can customize the code generated by Textor by providing your own templates.
456
466
 
457
467
  1. Create the `.textor/templates/` directory if it doesn't exist.
458
468
  2. Create a file named according to the table below (e.g., `feature.astro` or `component.tsx`).
459
- 3. Use `{{variable}}` placeholders in your template. Textor will automatically replace them when generating files.
469
+ 3. Use `{{variable}}` or `__variable__` placeholders in your template. Textor will automatically replace them when generating files. Using `__variable__` (e.g., `__componentName__`) is recommended for TypeScript/JavaScript templates as it is a valid identifier and avoids "broken code" warnings in your IDE.
460
470
 
461
471
  ### Supported Templates
462
472
 
@@ -485,15 +495,38 @@ You can customize the code generated by Textor by providing your own templates.
485
495
  ### Variables Description
486
496
 
487
497
  - `{{componentName}}`: The PascalCase name of the feature or component (e.g., `UserCatalog`).
498
+ - `{{componentNameCamel}}`: camelCase version of the name (e.g., `userCatalog`).
499
+ - `{{componentNameKebab}}`: kebab-case version of the name (e.g., `user-catalog`).
500
+ - `{{componentNameSnake}}`: snake_case version of the name (e.g., `user_catalog`).
501
+ - `{{componentNameUpper}}`: SCREAMING_SNAKE_CASE version of the name (e.g., `USER_CATALOG`).
488
502
  - `{{hookName}}`: The camelCase name of the generated hook (e.g., `useUserCatalog`).
489
503
  - `{{componentPath}}`: Relative path to the component file (useful for imports in tests or stories).
490
504
  - `{{featureComponentName}}`: The name of the feature component as imported in a route.
491
- - `{{featureImportPath}}`: The import path for the feature component.
505
+ - `{{featureComponentNameCamel}}`, `{{featureComponentNameKebab}}`, `{{featureComponentNameSnake}}`, `{{featureComponentNameUpper}}`, `{{featureComponentNamePascal}}`: Case variations for the feature component name.
492
506
  - `{{layoutName}}`: The name of the layout component being used.
507
+ - `{{layoutNameCamel}}`, `{{layoutNameKebab}}`, `{{layoutNameSnake}}`, `{{layoutNameUpper}}`, `{{layoutNamePascal}}`: Case variations for the layout name.
493
508
  - `{{layoutImportPath}}`: The import path for the layout component.
494
509
  - `{{scriptImportPath}}`: Relative path to the client-side script entry point.
495
510
  - `{{componentExtension}}`: The file extension of the component (e.g., `.astro` or `.tsx`).
496
511
 
512
+ ### Example: Custom Route Template (`.textor/templates/route.ts`)
513
+
514
+ If you are using a custom routing library and want your templates to be valid TypeScript:
515
+
516
+ ```typescript
517
+ /**
518
+ * @generated by Textor
519
+ * Route: {{featureComponentName}}
520
+ */
521
+ import { defineRoute } from "my-router";
522
+ import __featureComponentName__ from "{{featureImportPath}}";
523
+
524
+ export const __featureComponentName__Route = defineRoute({
525
+ path: "/__featureComponentNameKebab__",
526
+ component: __featureComponentName__
527
+ });
528
+ ```
529
+
497
530
  ### Example: Custom Feature Template (`.textor/templates/feature.astro`)
498
531
 
499
532
  ```astro
@@ -454,12 +454,15 @@ function getEffectiveOptions(cmdOptions, config, type) {
454
454
  async function initCommand(options) {
455
455
  try {
456
456
  const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
457
-
458
- console.log('✓ Textor configuration created at:', configPath);
459
- console.log('\nDefault configuration:');
460
- console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
461
- console.log('\nYou can now use Textor commands like:');
462
- console.log(' textor add-section /users users/catalog --layout Main');
457
+ const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
458
+
459
+ if (!quiet) {
460
+ console.log('Textor configuration created at:', configPath);
461
+ console.log('\nDefault configuration:');
462
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
463
+ console.log('\nYou can now use Textor commands like:');
464
+ console.log(' textor add-section /users users/catalog --layout Main');
465
+ }
463
466
  } catch (error) {
464
467
  console.error('Error:', error.message);
465
468
  process.exit(1);
@@ -479,6 +482,28 @@ function toPascalCase(input) {
479
482
  .join('');
480
483
  }
481
484
 
485
+ function toCamelCase(input) {
486
+ const pascal = toPascalCase(input);
487
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
488
+ }
489
+
490
+ function toKebabCase(input) {
491
+ return input
492
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
493
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
494
+ .replace(/[_\s/\\-]+/g, '-')
495
+ .toLowerCase()
496
+ .replace(/^-+|-+$/g, '');
497
+ }
498
+
499
+ function toSnakeCase(input) {
500
+ return toKebabCase(input).replace(/-/g, '_');
501
+ }
502
+
503
+ function toScreamingSnakeCase(input) {
504
+ return toSnakeCase(input).toUpperCase();
505
+ }
506
+
482
507
  function getFeatureComponentName(featurePath) {
483
508
  return toPascalCase(featurePath);
484
509
  }
@@ -1044,12 +1069,30 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
1044
1069
  return secureJoin(baseDir, fileName);
1045
1070
  }
1046
1071
 
1072
+ function enrichData(data) {
1073
+ const enriched = { ...data };
1074
+ const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
1075
+ for (const key of nameKeys) {
1076
+ if (data[key] && typeof data[key] === 'string') {
1077
+ enriched[`${key}Camel`] = toCamelCase(data[key]);
1078
+ enriched[`${key}Kebab`] = toKebabCase(data[key]);
1079
+ enriched[`${key}Snake`] = toSnakeCase(data[key]);
1080
+ enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
1081
+ enriched[`${key}Pascal`] = toPascalCase(data[key]);
1082
+ }
1083
+ }
1084
+ return enriched;
1085
+ }
1086
+
1047
1087
  function getTemplateOverride(templateName, extension, data = {}) {
1048
1088
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
1049
1089
  if (existsSync(overridePath)) {
1050
1090
  let content = readFileSync(overridePath, 'utf-8');
1051
- for (const [key, value] of Object.entries(data)) {
1052
- content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
1091
+ const finalData = enrichData(data);
1092
+ for (const [key, value] of Object.entries(finalData)) {
1093
+ const replacement = () => value || '';
1094
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
1095
+ content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
1053
1096
  }
1054
1097
  return content;
1055
1098
  }
@@ -1063,8 +1106,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
1063
1106
  * - featureImportPath: Path to import the feature component
1064
1107
  * - featureComponentName: Name of the feature component
1065
1108
  */
1066
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
1067
- const override = getTemplateOverride('route', '.astro', {
1109
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
1110
+ const override = getTemplateOverride('route', extension, {
1068
1111
  layoutName,
1069
1112
  layoutImportPath,
1070
1113
  featureImportPath,
@@ -1097,9 +1140,9 @@ import ${featureComponentName} from '${featureImportPath}';
1097
1140
  * - componentName: Name of the feature component
1098
1141
  * - scriptImportPath: Path to the feature's client-side script
1099
1142
  */
1100
- function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
1101
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1102
- const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
1143
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
1144
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1145
+ const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
1103
1146
  if (override) return override;
1104
1147
 
1105
1148
  if (framework === 'react') {
@@ -1144,9 +1187,9 @@ function generateScriptsIndexTemplate() {
1144
1187
  * Component Template Variables:
1145
1188
  * - componentName: Name of the component
1146
1189
  */
1147
- function generateComponentTemplate(componentName, framework = 'react') {
1148
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1149
- const override = getTemplateOverride('component', extension, { componentName });
1190
+ function generateComponentTemplate(componentName, framework = 'react', extension) {
1191
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1192
+ const override = getTemplateOverride('component', templateExtension, { componentName });
1150
1193
  if (override) return override;
1151
1194
 
1152
1195
  if (framework === 'react') {
@@ -1458,6 +1501,7 @@ async function loadState() {
1458
1501
  const content = await readFile(statePath, 'utf-8');
1459
1502
  const state = JSON.parse(content);
1460
1503
  if (!state.files) state.files = {};
1504
+ normalizeStatePaths(state);
1461
1505
  return state;
1462
1506
  } catch (error) {
1463
1507
  return { sections: [], components: [], files: {} };
@@ -1507,24 +1551,62 @@ async function registerFile(filePath, { kind, template, hash, templateVersion =
1507
1551
 
1508
1552
  async function addSectionToState(section) {
1509
1553
  const state = await loadState();
1554
+ const normalizedSection = { ...section };
1555
+ if (normalizedSection.featurePath) {
1556
+ normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1557
+ }
1510
1558
  // Avoid duplicates by route OR by featurePath if route is null
1511
- if (section.route) {
1512
- state.sections = state.sections.filter(s => s.route !== section.route);
1559
+ if (normalizedSection.route) {
1560
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1513
1561
  } else {
1514
- state.sections = state.sections.filter(s => s.featurePath !== section.featurePath || s.route);
1562
+ state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
1515
1563
  }
1516
- state.sections.push(section);
1564
+ state.sections.push(normalizedSection);
1517
1565
  await saveState(state);
1518
1566
  }
1519
1567
 
1520
1568
  async function addComponentToState(component) {
1521
1569
  const state = await loadState();
1570
+ const normalizedComponent = { ...component };
1571
+ if (normalizedComponent.path) {
1572
+ normalizedComponent.path = normalizeStatePath(normalizedComponent.path);
1573
+ }
1522
1574
  // Avoid duplicates by name
1523
- state.components = state.components.filter(c => c.name !== component.name);
1524
- state.components.push(component);
1575
+ state.components = state.components.filter(c => c.name !== normalizedComponent.name);
1576
+ state.components.push(normalizedComponent);
1525
1577
  await saveState(state);
1526
1578
  }
1527
1579
 
1580
+ function normalizeStatePath(filePath) {
1581
+ if (!filePath || typeof filePath !== 'string') return filePath;
1582
+ const relative = path.isAbsolute(filePath)
1583
+ ? path.relative(process.cwd(), filePath)
1584
+ : filePath;
1585
+ return relative.replace(/\\/g, '/');
1586
+ }
1587
+
1588
+ function normalizeStatePaths(state) {
1589
+ if (!state || typeof state !== 'object') return;
1590
+ if (Array.isArray(state.sections)) {
1591
+ state.sections = state.sections.map(section => {
1592
+ if (!section || typeof section !== 'object') return section;
1593
+ if (!section.featurePath) return section;
1594
+ const normalized = normalizeStatePath(section.featurePath);
1595
+ if (normalized === section.featurePath) return section;
1596
+ return { ...section, featurePath: normalized };
1597
+ });
1598
+ }
1599
+ if (Array.isArray(state.components)) {
1600
+ state.components = state.components.map(component => {
1601
+ if (!component || typeof component !== 'object') return component;
1602
+ if (!component.path) return component;
1603
+ const normalized = normalizeStatePath(component.path);
1604
+ if (normalized === component.path) return component;
1605
+ return { ...component, path: normalized };
1606
+ });
1607
+ }
1608
+ }
1609
+
1528
1610
  function findSection(state, identifier) {
1529
1611
  return state.sections.find(s => s.route === identifier || s.name === identifier || s.featurePath === identifier);
1530
1612
  }
@@ -1950,13 +2032,14 @@ async function addSectionCommand(route, featurePath, options) {
1950
2032
  options.layout,
1951
2033
  layoutImportPath,
1952
2034
  featureImportPath,
1953
- featureComponentName
2035
+ featureComponentName,
2036
+ routeExtension
1954
2037
  );
1955
2038
  routeSignature = getSignature(config, 'astro');
1956
2039
  }
1957
2040
  }
1958
2041
 
1959
- const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
2042
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
1960
2043
 
1961
2044
  const writtenFiles = [];
1962
2045
 
@@ -3021,7 +3104,7 @@ async function createComponentCommand(componentName, options) {
3021
3104
  if (shouldCreateServices) await ensureDir(servicesDirInside);
3022
3105
  if (shouldCreateSchemas) await ensureDir(schemasDirInside);
3023
3106
 
3024
- const componentContent = generateComponentTemplate(normalizedName, framework);
3107
+ const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
3025
3108
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
3026
3109
 
3027
3110
  const componentHash = await writeFileWithSignature(
@@ -3298,7 +3381,7 @@ async function removeComponentCommand(identifier, options) {
3298
3381
  let componentDir;
3299
3382
 
3300
3383
  if (component) {
3301
- componentDir = component.path;
3384
+ componentDir = path.resolve(process.cwd(), component.path);
3302
3385
  } else {
3303
3386
  // Fallback: try to guess path if not in state
3304
3387
  const componentsRoot = path.resolve(process.cwd(), config.paths.components);
@@ -3324,13 +3407,14 @@ async function removeComponentCommand(identifier, options) {
3324
3407
  await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3325
3408
 
3326
3409
  // Unregister files
3327
- const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
3410
+ const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
3411
+ const dirPrefix = relComponentPath + '/';
3328
3412
  for (const f in state.files) {
3329
3413
  if (f.startsWith(dirPrefix)) {
3330
3414
  delete state.files[f];
3331
3415
  }
3332
3416
  }
3333
- state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
3417
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
3334
3418
  await saveState(state);
3335
3419
  } else if (result.message) {
3336
3420
  console.log(`⚠ Skipped: ${componentDir}`);
@@ -4032,6 +4116,27 @@ async function upgradeConfigCommand(options) {
4032
4116
  }
4033
4117
  }
4034
4118
 
4119
+ async function normalizeStateCommand(options) {
4120
+ try {
4121
+ const state = await loadState();
4122
+
4123
+ if (options.dryRun) {
4124
+ console.log('Dry run - normalized state:');
4125
+ console.log(JSON.stringify(state, null, 2));
4126
+ return;
4127
+ }
4128
+
4129
+ await saveState(state);
4130
+ console.log('State normalized successfully.');
4131
+ } catch (error) {
4132
+ console.error('Error:', error.message);
4133
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4134
+ process.exit(1);
4135
+ }
4136
+ throw error;
4137
+ }
4138
+ }
4139
+
4035
4140
  const program = new Command();
4036
4141
 
4037
4142
  program
@@ -4043,6 +4148,7 @@ program
4043
4148
  .command('init')
4044
4149
  .description('Initialize Textor configuration')
4045
4150
  .option('--force', 'Overwrite existing configuration')
4151
+ .option('--quiet', 'Skip printing full default configuration')
4046
4152
  .action(initCommand);
4047
4153
 
4048
4154
  program
@@ -4157,5 +4263,11 @@ program
4157
4263
  .option('--dry-run', 'Show the upgraded config without writing to disk')
4158
4264
  .action(upgradeConfigCommand);
4159
4265
 
4266
+ program
4267
+ .command('normalize-state')
4268
+ .description('Normalize state paths to be project-relative')
4269
+ .option('--dry-run', 'Show the normalized state without writing to disk')
4270
+ .action(normalizeStateCommand);
4271
+
4160
4272
  program.parse();
4161
4273
  //# sourceMappingURL=textor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.cjs CHANGED
@@ -421,11 +421,14 @@ function getEffectiveOptions(cmdOptions, config, type) {
421
421
  async function initCommand(options) {
422
422
  try {
423
423
  const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
424
- console.log(' Textor configuration created at:', configPath);
425
- console.log('\nDefault configuration:');
426
- console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
427
- console.log('\nYou can now use Textor commands like:');
428
- console.log(' textor add-section /users users/catalog --layout Main');
424
+ const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
425
+ if (!quiet) {
426
+ console.log('Textor configuration created at:', configPath);
427
+ console.log('\nDefault configuration:');
428
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
429
+ console.log('\nYou can now use Textor commands like:');
430
+ console.log(' textor add-section /users users/catalog --layout Main');
431
+ }
429
432
  }
430
433
  catch (error) {
431
434
  console.error('Error:', error.message);
@@ -445,6 +448,24 @@ function toPascalCase(input) {
445
448
  })
446
449
  .join('');
447
450
  }
451
+ function toCamelCase(input) {
452
+ const pascal = toPascalCase(input);
453
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
454
+ }
455
+ function toKebabCase(input) {
456
+ return input
457
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
458
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
459
+ .replace(/[_\s/\\-]+/g, '-')
460
+ .toLowerCase()
461
+ .replace(/^-+|-+$/g, '');
462
+ }
463
+ function toSnakeCase(input) {
464
+ return toKebabCase(input).replace(/-/g, '_');
465
+ }
466
+ function toScreamingSnakeCase(input) {
467
+ return toSnakeCase(input).toUpperCase();
468
+ }
448
469
  function getFeatureComponentName(featurePath) {
449
470
  return toPascalCase(featurePath);
450
471
  }
@@ -900,12 +921,29 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
900
921
  return secureJoin(baseDir, fileName);
901
922
  }
902
923
 
924
+ function enrichData(data) {
925
+ const enriched = { ...data };
926
+ const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
927
+ for (const key of nameKeys) {
928
+ if (data[key] && typeof data[key] === 'string') {
929
+ enriched[`${key}Camel`] = toCamelCase(data[key]);
930
+ enriched[`${key}Kebab`] = toKebabCase(data[key]);
931
+ enriched[`${key}Snake`] = toSnakeCase(data[key]);
932
+ enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
933
+ enriched[`${key}Pascal`] = toPascalCase(data[key]);
934
+ }
935
+ }
936
+ return enriched;
937
+ }
903
938
  function getTemplateOverride(templateName, extension, data = {}) {
904
939
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
905
940
  if (fs.existsSync(overridePath)) {
906
941
  let content = fs.readFileSync(overridePath, 'utf-8');
907
- for (const [key, value] of Object.entries(data)) {
908
- content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
942
+ const finalData = enrichData(data);
943
+ for (const [key, value] of Object.entries(finalData)) {
944
+ const replacement = () => value || '';
945
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
946
+ content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
909
947
  }
910
948
  return content;
911
949
  }
@@ -918,8 +956,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
918
956
  * - featureImportPath: Path to import the feature component
919
957
  * - featureComponentName: Name of the feature component
920
958
  */
921
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
922
- const override = getTemplateOverride('route', '.astro', {
959
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
960
+ const override = getTemplateOverride('route', extension, {
923
961
  layoutName,
924
962
  layoutImportPath,
925
963
  featureImportPath,
@@ -950,9 +988,9 @@ import ${featureComponentName} from '${featureImportPath}';
950
988
  * - componentName: Name of the feature component
951
989
  * - scriptImportPath: Path to the feature's client-side script
952
990
  */
953
- function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
954
- const extension = framework === 'astro' ? '.astro' : '.tsx';
955
- const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
991
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
992
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
993
+ const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
956
994
  if (override)
957
995
  return override;
958
996
  if (framework === 'react') {
@@ -993,9 +1031,9 @@ function generateScriptsIndexTemplate() {
993
1031
  * Component Template Variables:
994
1032
  * - componentName: Name of the component
995
1033
  */
996
- function generateComponentTemplate(componentName, framework = 'react') {
997
- const extension = framework === 'astro' ? '.astro' : '.tsx';
998
- const override = getTemplateOverride('component', extension, { componentName });
1034
+ function generateComponentTemplate(componentName, framework = 'react', extension) {
1035
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1036
+ const override = getTemplateOverride('component', templateExtension, { componentName });
999
1037
  if (override)
1000
1038
  return override;
1001
1039
  if (framework === 'react') {
@@ -1291,6 +1329,7 @@ async function loadState() {
1291
1329
  const state = JSON.parse(content);
1292
1330
  if (!state.files)
1293
1331
  state.files = {};
1332
+ normalizeStatePaths(state);
1294
1333
  return state;
1295
1334
  }
1296
1335
  catch (error) {
@@ -1331,23 +1370,67 @@ async function registerFile(filePath, { kind, template, hash, templateVersion =
1331
1370
  }
1332
1371
  async function addSectionToState(section) {
1333
1372
  const state = await loadState();
1373
+ const normalizedSection = { ...section };
1374
+ if (normalizedSection.featurePath) {
1375
+ normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1376
+ }
1334
1377
  // Avoid duplicates by route OR by featurePath if route is null
1335
- if (section.route) {
1336
- state.sections = state.sections.filter(s => s.route !== section.route);
1378
+ if (normalizedSection.route) {
1379
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1337
1380
  }
1338
1381
  else {
1339
- state.sections = state.sections.filter(s => s.featurePath !== section.featurePath || s.route);
1382
+ state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
1340
1383
  }
1341
- state.sections.push(section);
1384
+ state.sections.push(normalizedSection);
1342
1385
  await saveState(state);
1343
1386
  }
1344
1387
  async function addComponentToState(component) {
1345
1388
  const state = await loadState();
1389
+ const normalizedComponent = { ...component };
1390
+ if (normalizedComponent.path) {
1391
+ normalizedComponent.path = normalizeStatePath(normalizedComponent.path);
1392
+ }
1346
1393
  // Avoid duplicates by name
1347
- state.components = state.components.filter(c => c.name !== component.name);
1348
- state.components.push(component);
1394
+ state.components = state.components.filter(c => c.name !== normalizedComponent.name);
1395
+ state.components.push(normalizedComponent);
1349
1396
  await saveState(state);
1350
1397
  }
1398
+ function normalizeStatePath(filePath) {
1399
+ if (!filePath || typeof filePath !== 'string')
1400
+ return filePath;
1401
+ const relative = path.isAbsolute(filePath)
1402
+ ? path.relative(process.cwd(), filePath)
1403
+ : filePath;
1404
+ return relative.replace(/\\/g, '/');
1405
+ }
1406
+ function normalizeStatePaths(state) {
1407
+ if (!state || typeof state !== 'object')
1408
+ return;
1409
+ if (Array.isArray(state.sections)) {
1410
+ state.sections = state.sections.map(section => {
1411
+ if (!section || typeof section !== 'object')
1412
+ return section;
1413
+ if (!section.featurePath)
1414
+ return section;
1415
+ const normalized = normalizeStatePath(section.featurePath);
1416
+ if (normalized === section.featurePath)
1417
+ return section;
1418
+ return { ...section, featurePath: normalized };
1419
+ });
1420
+ }
1421
+ if (Array.isArray(state.components)) {
1422
+ state.components = state.components.map(component => {
1423
+ if (!component || typeof component !== 'object')
1424
+ return component;
1425
+ if (!component.path)
1426
+ return component;
1427
+ const normalized = normalizeStatePath(component.path);
1428
+ if (normalized === component.path)
1429
+ return component;
1430
+ return { ...component, path: normalized };
1431
+ });
1432
+ }
1433
+ }
1351
1434
  function findSection(state, identifier) {
1352
1435
  return state.sections.find(s => s.route === identifier || s.name === identifier || s.featurePath === identifier);
1353
1436
  }
@@ -1675,11 +1758,11 @@ async function addSectionCommand(route, featurePath, options) {
1675
1758
  routeSignature = getSignature(config, 'typescript');
1676
1759
  }
1677
1760
  else {
1678
- routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName);
1761
+ routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName, routeExtension);
1679
1762
  routeSignature = getSignature(config, 'astro');
1680
1763
  }
1681
1764
  }
1682
- const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
1765
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
1683
1766
  const writtenFiles = [];
1684
1767
  if (routeFilePath) {
1685
1768
  const routeHash = await writeFileWithSignature(routeFilePath, routeContent, routeSignature, config.hashing?.normalization);
@@ -2511,7 +2594,7 @@ async function createComponentCommand(componentName, options) {
2511
2594
  await ensureDir(servicesDirInside);
2512
2595
  if (shouldCreateSchemas)
2513
2596
  await ensureDir(schemasDirInside);
2514
- const componentContent = generateComponentTemplate(normalizedName, framework);
2597
+ const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
2515
2598
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
2516
2599
  const componentHash = await writeFileWithSignature(componentFilePath, componentContent, signature, config.hashing?.normalization);
2517
2600
  await registerFile(componentFilePath, {
@@ -2712,7 +2795,7 @@ async function removeComponentCommand(identifier, options) {
2712
2795
  const component = findComponent(state, identifier);
2713
2796
  let componentDir;
2714
2797
  if (component) {
2715
- componentDir = component.path;
2798
+ componentDir = path.resolve(process.cwd(), component.path);
2716
2799
  }
2717
2800
  else {
2718
2801
  // Fallback: try to guess path if not in state
@@ -2735,13 +2818,14 @@ async function removeComponentCommand(identifier, options) {
2735
2818
  console.log(`✓ Deleted component: ${componentDir}/`);
2736
2819
  await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
2737
2820
  // Unregister files
2738
- const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
2821
+ const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
2822
+ const dirPrefix = relComponentPath + '/';
2739
2823
  for (const f in state.files) {
2740
2824
  if (f.startsWith(dirPrefix)) {
2741
2825
  delete state.files[f];
2742
2826
  }
2743
2827
  }
2744
- state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
2828
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
2745
2829
  await saveState(state);
2746
2830
  }
2747
2831
  else if (result.message) {
@@ -3376,12 +3460,33 @@ async function upgradeConfigCommand(options) {
3376
3460
  }
3377
3461
  }
3378
3462
 
3463
+ async function normalizeStateCommand(options) {
3464
+ try {
3465
+ const state = await loadState();
3466
+ if (options.dryRun) {
3467
+ console.log('Dry run - normalized state:');
3468
+ console.log(JSON.stringify(state, null, 2));
3469
+ return;
3470
+ }
3471
+ await saveState(state);
3472
+ console.log('State normalized successfully.');
3473
+ }
3474
+ catch (error) {
3475
+ console.error('Error:', error.message);
3476
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3477
+ process.exit(1);
3478
+ }
3479
+ throw error;
3480
+ }
3481
+ }
3482
+
3379
3483
  exports.addSectionCommand = addSectionCommand;
3380
3484
  exports.adoptCommand = adoptCommand;
3381
3485
  exports.createComponentCommand = createComponentCommand;
3382
3486
  exports.initCommand = initCommand;
3383
3487
  exports.listSectionsCommand = listSectionsCommand;
3384
3488
  exports.moveSectionCommand = moveSectionCommand;
3489
+ exports.normalizeStateCommand = normalizeStateCommand;
3385
3490
  exports.removeComponentCommand = removeComponentCommand;
3386
3491
  exports.removeSectionCommand = removeSectionCommand;
3387
3492
  exports.statusCommand = statusCommand;