@oamm/textor 1.0.4 → 1.0.7

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
@@ -370,7 +370,8 @@ The .textor/config.json file allows full control over the tool's behavior.
370
370
  "createTypes": false,
371
371
  "createReadme": false,
372
372
  "createStories": false,
373
- "createIndex": false
373
+ "createIndex": false,
374
+ "layout": "Main"
374
375
  },
375
376
  "components": {
376
377
  "framework": "react",
@@ -466,7 +467,7 @@ You can customize the code generated by Textor by providing your own templates.
466
467
 
467
468
  1. Create the `.textor/templates/` directory if it doesn't exist.
468
469
  2. Create a file named according to the table below (e.g., `feature.astro` or `component.tsx`).
469
- 3. Use `{{variable}}` placeholders in your template. Textor will automatically replace them when generating files.
470
+ 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.
470
471
 
471
472
  ### Supported Templates
472
473
 
@@ -495,15 +496,38 @@ You can customize the code generated by Textor by providing your own templates.
495
496
  ### Variables Description
496
497
 
497
498
  - `{{componentName}}`: The PascalCase name of the feature or component (e.g., `UserCatalog`).
499
+ - `{{componentNameCamel}}`: camelCase version of the name (e.g., `userCatalog`).
500
+ - `{{componentNameKebab}}`: kebab-case version of the name (e.g., `user-catalog`).
501
+ - `{{componentNameSnake}}`: snake_case version of the name (e.g., `user_catalog`).
502
+ - `{{componentNameUpper}}`: SCREAMING_SNAKE_CASE version of the name (e.g., `USER_CATALOG`).
498
503
  - `{{hookName}}`: The camelCase name of the generated hook (e.g., `useUserCatalog`).
499
504
  - `{{componentPath}}`: Relative path to the component file (useful for imports in tests or stories).
500
505
  - `{{featureComponentName}}`: The name of the feature component as imported in a route.
501
- - `{{featureImportPath}}`: The import path for the feature component.
506
+ - `{{featureComponentNameCamel}}`, `{{featureComponentNameKebab}}`, `{{featureComponentNameSnake}}`, `{{featureComponentNameUpper}}`, `{{featureComponentNamePascal}}`: Case variations for the feature component name.
502
507
  - `{{layoutName}}`: The name of the layout component being used.
508
+ - `{{layoutNameCamel}}`, `{{layoutNameKebab}}`, `{{layoutNameSnake}}`, `{{layoutNameUpper}}`, `{{layoutNamePascal}}`: Case variations for the layout name.
503
509
  - `{{layoutImportPath}}`: The import path for the layout component.
504
510
  - `{{scriptImportPath}}`: Relative path to the client-side script entry point.
505
511
  - `{{componentExtension}}`: The file extension of the component (e.g., `.astro` or `.tsx`).
506
512
 
513
+ ### Example: Custom Route Template (`.textor/templates/route.ts`)
514
+
515
+ If you are using a custom routing library and want your templates to be valid TypeScript:
516
+
517
+ ```typescript
518
+ /**
519
+ * @generated by Textor
520
+ * Route: {{featureComponentName}}
521
+ */
522
+ import { defineRoute } from "my-router";
523
+ import __featureComponentName__ from "{{featureImportPath}}";
524
+
525
+ export const __featureComponentName__Route = defineRoute({
526
+ path: "/__featureComponentNameKebab__",
527
+ component: __featureComponentName__
528
+ });
529
+ ```
530
+
507
531
  ### Example: Custom Feature Template (`.textor/templates/feature.astro`)
508
532
 
509
533
  ```astro
@@ -36,10 +36,12 @@ const CURRENT_CONFIG_VERSION = 2;
36
36
  * @property {string} signatures.typescript
37
37
  * @property {string} signatures.javascript
38
38
  * @property {Object} features
39
+ * @property {string} features.framework
39
40
  * @property {string} features.entry
40
41
  * @property {boolean} features.createSubComponentsDir
41
42
  * @property {boolean} features.createScriptsDir
42
43
  * @property {string} features.scriptsIndexFile
44
+ * @property {string} features.layout
43
45
  * @property {Object} components
44
46
  * @property {boolean} components.createSubComponentsDir
45
47
  * @property {boolean} components.createContext
@@ -105,7 +107,8 @@ const DEFAULT_CONFIG = {
105
107
  createTypes: false,
106
108
  createReadme: false,
107
109
  createStories: false,
108
- createIndex: false
110
+ createIndex: false,
111
+ layout: 'Main'
109
112
  },
110
113
  components: {
111
114
  framework: 'react',
@@ -454,12 +457,15 @@ function getEffectiveOptions(cmdOptions, config, type) {
454
457
  async function initCommand(options) {
455
458
  try {
456
459
  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');
460
+ const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
461
+
462
+ if (!quiet) {
463
+ console.log('Textor configuration created at:', configPath);
464
+ console.log('\nDefault configuration:');
465
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
466
+ console.log('\nYou can now use Textor commands like:');
467
+ console.log(' textor add-section /users users/catalog --layout Main');
468
+ }
463
469
  } catch (error) {
464
470
  console.error('Error:', error.message);
465
471
  process.exit(1);
@@ -479,6 +485,28 @@ function toPascalCase(input) {
479
485
  .join('');
480
486
  }
481
487
 
488
+ function toCamelCase(input) {
489
+ const pascal = toPascalCase(input);
490
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
491
+ }
492
+
493
+ function toKebabCase(input) {
494
+ return input
495
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
496
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
497
+ .replace(/[_\s/\\-]+/g, '-')
498
+ .toLowerCase()
499
+ .replace(/^-+|-+$/g, '');
500
+ }
501
+
502
+ function toSnakeCase(input) {
503
+ return toKebabCase(input).replace(/-/g, '_');
504
+ }
505
+
506
+ function toScreamingSnakeCase(input) {
507
+ return toSnakeCase(input).toUpperCase();
508
+ }
509
+
482
510
  function getFeatureComponentName(featurePath) {
483
511
  return toPascalCase(featurePath);
484
512
  }
@@ -1044,12 +1072,30 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
1044
1072
  return secureJoin(baseDir, fileName);
1045
1073
  }
1046
1074
 
1075
+ function enrichData(data) {
1076
+ const enriched = { ...data };
1077
+ const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
1078
+ for (const key of nameKeys) {
1079
+ if (data[key] && typeof data[key] === 'string') {
1080
+ enriched[`${key}Camel`] = toCamelCase(data[key]);
1081
+ enriched[`${key}Kebab`] = toKebabCase(data[key]);
1082
+ enriched[`${key}Snake`] = toSnakeCase(data[key]);
1083
+ enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
1084
+ enriched[`${key}Pascal`] = toPascalCase(data[key]);
1085
+ }
1086
+ }
1087
+ return enriched;
1088
+ }
1089
+
1047
1090
  function getTemplateOverride(templateName, extension, data = {}) {
1048
1091
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
1049
1092
  if (existsSync(overridePath)) {
1050
1093
  let content = readFileSync(overridePath, 'utf-8');
1051
- for (const [key, value] of Object.entries(data)) {
1052
- content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
1094
+ const finalData = enrichData(data);
1095
+ for (const [key, value] of Object.entries(finalData)) {
1096
+ const replacement = () => value || '';
1097
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
1098
+ content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
1053
1099
  }
1054
1100
  return content;
1055
1101
  }
@@ -1063,8 +1109,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
1063
1109
  * - featureImportPath: Path to import the feature component
1064
1110
  * - featureComponentName: Name of the feature component
1065
1111
  */
1066
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
1067
- const override = getTemplateOverride('route', '.astro', {
1112
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
1113
+ const override = getTemplateOverride('route', extension, {
1068
1114
  layoutName,
1069
1115
  layoutImportPath,
1070
1116
  featureImportPath,
@@ -1097,9 +1143,9 @@ import ${featureComponentName} from '${featureImportPath}';
1097
1143
  * - componentName: Name of the feature component
1098
1144
  * - scriptImportPath: Path to the feature's client-side script
1099
1145
  */
1100
- function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
1101
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1102
- const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
1146
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
1147
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1148
+ const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
1103
1149
  if (override) return override;
1104
1150
 
1105
1151
  if (framework === 'react') {
@@ -1144,9 +1190,9 @@ function generateScriptsIndexTemplate() {
1144
1190
  * Component Template Variables:
1145
1191
  * - componentName: Name of the component
1146
1192
  */
1147
- function generateComponentTemplate(componentName, framework = 'react') {
1148
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1149
- const override = getTemplateOverride('component', extension, { componentName });
1193
+ function generateComponentTemplate(componentName, framework = 'react', extension) {
1194
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1195
+ const override = getTemplateOverride('component', templateExtension, { componentName });
1150
1196
  if (override) return override;
1151
1197
 
1152
1198
  if (framework === 'react') {
@@ -1751,6 +1797,7 @@ async function addSectionCommand(route, featurePath, options) {
1751
1797
 
1752
1798
  const {
1753
1799
  framework,
1800
+ layout,
1754
1801
  createSubComponentsDir: shouldCreateSubComponentsDir,
1755
1802
  createScriptsDir: shouldCreateScriptsDir,
1756
1803
  createApi: shouldCreateApi,
@@ -1922,7 +1969,13 @@ async function addSectionCommand(route, featurePath, options) {
1922
1969
  }
1923
1970
 
1924
1971
  // Update imports in the moved file
1925
- await updateImportsInFile(reorg.to, reorg.from, reorg.to);
1972
+ await updateImportsInFile$1(reorg.to, reorg.from, reorg.to);
1973
+
1974
+ // Update hash in state after import updates
1975
+ if (state.files[newRelative]) {
1976
+ const content = await readFile(reorg.to, 'utf-8');
1977
+ state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
1978
+ }
1926
1979
 
1927
1980
  console.log(`✓ Reorganized ${oldRelative} to ${newRelative}`);
1928
1981
  }
@@ -1945,11 +1998,11 @@ async function addSectionCommand(route, featurePath, options) {
1945
1998
  if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
1946
1999
 
1947
2000
  let layoutImportPath = null;
1948
- if (routeFilePath && options.layout !== 'none') {
2001
+ if (routeFilePath && layout !== 'none') {
1949
2002
  if (config.importAliases.layouts) {
1950
- layoutImportPath = `${config.importAliases.layouts}/${options.layout}.astro`;
2003
+ layoutImportPath = `${config.importAliases.layouts}/${layout}.astro`;
1951
2004
  } else {
1952
- const layoutFilePath = secureJoin(layoutsRoot, `${options.layout}.astro`);
2005
+ const layoutFilePath = secureJoin(layoutsRoot, `${layout}.astro`);
1953
2006
  layoutImportPath = getRelativeImportPath(routeFilePath, layoutFilePath);
1954
2007
  }
1955
2008
  }
@@ -1986,16 +2039,17 @@ async function addSectionCommand(route, featurePath, options) {
1986
2039
  routeSignature = getSignature(config, 'typescript');
1987
2040
  } else {
1988
2041
  routeContent = generateRouteTemplate(
1989
- options.layout,
2042
+ layout,
1990
2043
  layoutImportPath,
1991
2044
  featureImportPath,
1992
- featureComponentName
2045
+ featureComponentName,
2046
+ routeExtension
1993
2047
  );
1994
2048
  routeSignature = getSignature(config, 'astro');
1995
2049
  }
1996
2050
  }
1997
2051
 
1998
- const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
2052
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
1999
2053
 
2000
2054
  const writtenFiles = [];
2001
2055
 
@@ -2257,7 +2311,7 @@ async function addSectionCommand(route, featurePath, options) {
2257
2311
  name: options.name || featureComponentName,
2258
2312
  route: normalizedRoute,
2259
2313
  featurePath: normalizedFeaturePath,
2260
- layout: options.layout,
2314
+ layout: layout,
2261
2315
  extension: routeExtension
2262
2316
  });
2263
2317
 
@@ -2274,7 +2328,7 @@ async function addSectionCommand(route, featurePath, options) {
2274
2328
  }
2275
2329
  }
2276
2330
 
2277
- async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2331
+ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
2278
2332
  if (!existsSync(filePath)) return;
2279
2333
 
2280
2334
  let content = await readFile(filePath, 'utf-8');
@@ -2317,33 +2371,43 @@ async function removeSectionCommand(route, featurePath, options) {
2317
2371
 
2318
2372
  const state = await loadState();
2319
2373
 
2320
- let targetRoute = route;
2321
- let targetFeaturePath = featurePath;
2322
2374
  let section = findSection(state, route);
2375
+ if (!section && featurePath) {
2376
+ section = findSection(state, featurePath);
2377
+ }
2378
+
2379
+ const targetRoute = section ? section.route : route;
2380
+ const targetFeaturePath = section ? section.featurePath : featurePath;
2323
2381
 
2324
2382
  if (!targetFeaturePath) {
2325
- if (section) {
2326
- targetRoute = section.route;
2327
- targetFeaturePath = section.featurePath;
2328
- } else {
2329
- throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
2330
- }
2383
+ throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
2331
2384
  }
2332
2385
 
2333
2386
  const normalizedRoute = normalizeRoute(targetRoute);
2334
2387
  const normalizedFeaturePath = featureToDirectoryPath(targetFeaturePath);
2335
2388
 
2336
- const routeExtension = (section && section.extension) || config.naming.routeExtension;
2337
- const routeFileName = routeToFilePath(normalizedRoute, {
2338
- extension: routeExtension,
2339
- mode: config.routing.mode,
2340
- indexFile: config.routing.indexFile
2341
- });
2342
-
2343
2389
  const pagesRoot = resolvePath(config, 'pages');
2344
2390
  const featuresRoot = resolvePath(config, 'features');
2391
+
2392
+ // Find route file in state if possible
2393
+ let routeFilePath = null;
2394
+ const routeRelPath = Object.keys(state.files).find(f => {
2395
+ const data = state.files[f];
2396
+ return data.kind === 'route' && data.owner === normalizedRoute;
2397
+ });
2398
+
2399
+ if (routeRelPath) {
2400
+ routeFilePath = path.resolve(process.cwd(), routeRelPath);
2401
+ } else {
2402
+ const routeExtension = (section && section.extension) || config.naming.routeExtension;
2403
+ const routeFileName = routeToFilePath(normalizedRoute, {
2404
+ extension: routeExtension,
2405
+ mode: config.routing.mode,
2406
+ indexFile: config.routing.indexFile
2407
+ });
2408
+ routeFilePath = secureJoin(pagesRoot, routeFileName);
2409
+ }
2345
2410
 
2346
- const routeFilePath = secureJoin(pagesRoot, routeFileName);
2347
2411
  const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
2348
2412
 
2349
2413
  const deletedFiles = [];
@@ -2430,8 +2494,67 @@ async function removeSectionCommand(route, featurePath, options) {
2430
2494
  }
2431
2495
 
2432
2496
  if (deletedFiles.length === 0 && deletedDirs.length === 0 && skippedFiles.length === 0) {
2433
- console.log('No files to delete.');
2497
+ if (section) {
2498
+ console.log(`✓ Section ${normalizedRoute} removed from state (files were already missing on disk).`);
2499
+ state.sections = state.sections.filter(s => s.route !== normalizedRoute);
2500
+ await saveState(state);
2501
+ } else {
2502
+ console.log('No files to delete.');
2503
+ }
2434
2504
  } else {
2505
+ // Reorganization (Flattening)
2506
+ if (!options.keepRoute && deletedFiles.length > 0 && config.routing.mode === 'flat') {
2507
+ const routeParts = normalizedRoute.split('/').filter(Boolean);
2508
+ if (routeParts.length > 1) {
2509
+ for (let i = routeParts.length - 1; i >= 1; i--) {
2510
+ const parentRoute = '/' + routeParts.slice(0, i).join('/');
2511
+ const parentDirName = routeParts.slice(0, i).join('/');
2512
+ const parentDirPath = secureJoin(pagesRoot, parentDirName);
2513
+
2514
+ if (existsSync(parentDirPath)) {
2515
+ const filesInDir = await readdir(parentDirPath);
2516
+
2517
+ if (filesInDir.length === 1) {
2518
+ const loneFile = filesInDir[0];
2519
+ const ext = path.extname(loneFile);
2520
+ const indexFile = ext === '.astro' ? config.routing.indexFile : `index${ext}`;
2521
+
2522
+ if (loneFile === indexFile) {
2523
+ const loneFilePath = path.join(parentDirPath, loneFile);
2524
+ const oldRelative = path.relative(process.cwd(), loneFilePath).replace(/\\/g, '/');
2525
+
2526
+ if (state.files[oldRelative] && state.files[oldRelative].kind === 'route') {
2527
+ const flatFileName = routeToFilePath(parentRoute, {
2528
+ extension: ext,
2529
+ mode: 'flat'
2530
+ });
2531
+ const flatFilePath = secureJoin(pagesRoot, flatFileName);
2532
+
2533
+ if (!existsSync(flatFilePath)) {
2534
+ await rename(loneFilePath, flatFilePath);
2535
+
2536
+ const newRelative = path.relative(process.cwd(), flatFilePath).replace(/\\/g, '/');
2537
+
2538
+ state.files[newRelative] = { ...state.files[oldRelative] };
2539
+ delete state.files[oldRelative];
2540
+
2541
+ await updateImportsInFile(flatFilePath, loneFilePath, flatFilePath);
2542
+
2543
+ // Update hash in state after import updates
2544
+ const content = await readFile(flatFilePath, 'utf-8');
2545
+ state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
2546
+
2547
+ console.log(`✓ Reorganized ${oldRelative} to ${newRelative} (flattened)`);
2548
+ await cleanupEmptyDirs(parentDirPath, pagesRoot);
2549
+ }
2550
+ }
2551
+ }
2552
+ }
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+
2435
2558
  state.sections = state.sections.filter(s => s.route !== normalizedRoute);
2436
2559
  await saveState(state);
2437
2560
  }
@@ -2445,6 +2568,39 @@ async function removeSectionCommand(route, featurePath, options) {
2445
2568
  }
2446
2569
  }
2447
2570
 
2571
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2572
+ if (!existsSync(filePath)) return;
2573
+
2574
+ let content = await readFile(filePath, 'utf-8');
2575
+ const oldDir = path.dirname(oldFilePath);
2576
+ const newDir = path.dirname(newFilePath);
2577
+
2578
+ if (oldDir === newDir) return;
2579
+
2580
+ // Find all relative imports
2581
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2582
+ let match;
2583
+ const replacements = [];
2584
+
2585
+ while ((match = relativeImportRegex.exec(content)) !== null) {
2586
+ const relativePath = match[1];
2587
+ const absoluteTarget = path.resolve(oldDir, relativePath);
2588
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2589
+
2590
+ replacements.push({
2591
+ full: match[0],
2592
+ oldRel: relativePath,
2593
+ newRel: newRelativePath
2594
+ });
2595
+ }
2596
+
2597
+ for (const repl of replacements) {
2598
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
2599
+ }
2600
+
2601
+ await writeFile(filePath, content, 'utf-8');
2602
+ }
2603
+
2448
2604
  /**
2449
2605
  * Move a section (route + feature).
2450
2606
  *
@@ -3060,7 +3216,7 @@ async function createComponentCommand(componentName, options) {
3060
3216
  if (shouldCreateServices) await ensureDir(servicesDirInside);
3061
3217
  if (shouldCreateSchemas) await ensureDir(schemasDirInside);
3062
3218
 
3063
- const componentContent = generateComponentTemplate(normalizedName, framework);
3219
+ const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
3064
3220
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
3065
3221
 
3066
3222
  const componentHash = await writeFileWithSignature(
@@ -3358,18 +3514,23 @@ async function removeComponentCommand(identifier, options) {
3358
3514
  owner: identifier
3359
3515
  });
3360
3516
 
3361
- if (result.deleted) {
3362
- console.log(`✓ Deleted component: ${componentDir}/`);
3363
- await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3517
+ if (result.deleted || (result.reason === 'not-found' && component)) {
3518
+ if (result.deleted) {
3519
+ console.log(`✓ Deleted component: ${componentDir}/`);
3520
+ await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3521
+ } else {
3522
+ console.log(`✓ Component ${identifier} removed from state (directory was already missing on disk).`);
3523
+ }
3364
3524
 
3365
3525
  // Unregister files
3366
- const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
3526
+ const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
3527
+ const dirPrefix = relComponentPath + '/';
3367
3528
  for (const f in state.files) {
3368
3529
  if (f.startsWith(dirPrefix)) {
3369
3530
  delete state.files[f];
3370
3531
  }
3371
3532
  }
3372
- state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
3533
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
3373
3534
  await saveState(state);
3374
3535
  } else if (result.message) {
3375
3536
  console.log(`⚠ Skipped: ${componentDir}`);
@@ -4103,13 +4264,14 @@ program
4103
4264
  .command('init')
4104
4265
  .description('Initialize Textor configuration')
4105
4266
  .option('--force', 'Overwrite existing configuration')
4267
+ .option('--quiet', 'Skip printing full default configuration')
4106
4268
  .action(initCommand);
4107
4269
 
4108
4270
  program
4109
4271
  .command('add-section [route] [featurePath]')
4110
4272
  .description('Create a route + feature binding (route optional for standalone features)')
4111
4273
  .option('--preset <name>', 'Scaffolding preset (minimal, standard, senior)')
4112
- .option('--layout <name>', 'Layout component name (use "none" for no layout)', 'Main')
4274
+ .option('--layout <name>', 'Layout component name (use "none" for no layout)')
4113
4275
  .option('--name <name>', 'Section name for state tracking')
4114
4276
  .option('--endpoint', 'Create an API endpoint (.ts) instead of an Astro page')
4115
4277
  .option('--api', 'Create api directory')
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}