@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 +35 -2
- package/dist/bin/textor.js +140 -28
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +132 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +132 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
- `{{
|
|
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
|
package/dist/bin/textor.js
CHANGED
|
@@ -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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
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',
|
|
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
|
|
1102
|
-
const override = getTemplateOverride('feature',
|
|
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
|
|
1149
|
-
const override = getTemplateOverride('component',
|
|
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 (
|
|
1512
|
-
state.sections = state.sections.filter(s => s.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 !==
|
|
1562
|
+
state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
|
|
1515
1563
|
}
|
|
1516
|
-
state.sections.push(
|
|
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 !==
|
|
1524
|
-
state.components.push(
|
|
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
|
|
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 !==
|
|
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
|
package/dist/bin/textor.js.map
CHANGED
|
@@ -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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
908
|
-
|
|
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',
|
|
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
|
|
955
|
-
const override = getTemplateOverride('feature',
|
|
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
|
|
998
|
-
const override = getTemplateOverride('component',
|
|
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 (
|
|
1336
|
-
state.sections = state.sections.filter(s => s.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 !==
|
|
1382
|
+
state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
|
|
1340
1383
|
}
|
|
1341
|
-
state.sections.push(
|
|
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 !==
|
|
1348
|
-
state.components.push(
|
|
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
|
|
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 !==
|
|
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;
|