@oamm/textor 1.0.12 → 1.0.14

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.
@@ -543,6 +543,7 @@ function normalizeRoute(route) {
543
543
 
544
544
  function routeToFilePath(route, options = {}) {
545
545
  const { extension = '.astro', mode = 'flat', indexFile = 'index.astro' } = options;
546
+ if (!route) return null;
546
547
  const normalized = normalizeRoute(route);
547
548
 
548
549
  if (normalized === '/') {
@@ -715,17 +716,6 @@ async function safeDelete(filePath, options = {}) {
715
716
  return { deleted: true };
716
717
  }
717
718
 
718
- async function ensureNotExists(filePath, force = false) {
719
- if (existsSync(filePath)) {
720
- if (!force) {
721
- throw new Error(
722
- `File already exists: ${filePath}\n` +
723
- `Use --force to overwrite.`
724
- );
725
- }
726
- }
727
- }
728
-
729
719
  async function ensureDir(dirPath) {
730
720
  await mkdir(dirPath, { recursive: true });
731
721
  }
@@ -832,6 +822,11 @@ async function safeMove(fromPath, toPath, options = {}) {
832
822
  throw new Error(`Source file not found: ${fromPath}`);
833
823
  }
834
824
 
825
+ if (path.resolve(fromPath) === path.resolve(toPath)) {
826
+ const content = await readFile(toPath, 'utf-8');
827
+ return calculateHash(content, normalization);
828
+ }
829
+
835
830
  if (existsSync(toPath) && !force) {
836
831
  throw new Error(
837
832
  `Destination already exists: ${toPath}\n` +
@@ -2076,19 +2071,25 @@ async function addSectionCommand(route, featurePath, options) {
2076
2071
  }
2077
2072
  }
2078
2073
 
2079
- await ensureNotExists(featureFilePath, options.force);
2080
-
2081
- if (shouldCreateIndex) await ensureNotExists(indexFilePath, options.force);
2082
- if (shouldCreateContext) await ensureNotExists(contextFilePath, options.force);
2083
- if (shouldCreateHooks) await ensureNotExists(hookFilePath, options.force);
2084
- if (shouldCreateTests) await ensureNotExists(testFilePath, options.force);
2085
- if (shouldCreateTypes) await ensureNotExists(typesFilePath, options.force);
2086
- if (shouldCreateApi) await ensureNotExists(apiFilePath, options.force);
2087
- if (shouldCreateServices) await ensureNotExists(servicesFilePath, options.force);
2088
- if (shouldCreateSchemas) await ensureNotExists(schemasFilePath, options.force);
2089
- if (shouldCreateReadme) await ensureNotExists(readmeFilePath, options.force);
2090
- if (shouldCreateStories) await ensureNotExists(storiesFilePath, options.force);
2091
- if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
2074
+ const featureExists = existsSync(featureFilePath);
2075
+ if (featureExists && !options.force) {
2076
+ console.log(`ℹ Feature already exists at ${featureFilePath}. Entering additive mode.`);
2077
+ }
2078
+
2079
+ // Check sub-items only if not in force mode
2080
+ if (!options.force) {
2081
+ if (shouldCreateIndex && existsSync(indexFilePath)) console.log(` - Skipping existing index: ${indexFilePath}`);
2082
+ if (shouldCreateContext && existsSync(contextFilePath)) console.log(` - Skipping existing context: ${contextFilePath}`);
2083
+ if (shouldCreateHooks && existsSync(hookFilePath)) console.log(` - Skipping existing hook: ${hookFilePath}`);
2084
+ if (shouldCreateTests && existsSync(testFilePath)) console.log(` - Skipping existing test: ${testFilePath}`);
2085
+ if (shouldCreateTypes && existsSync(typesFilePath)) console.log(` - Skipping existing types: ${typesFilePath}`);
2086
+ if (shouldCreateApi && existsSync(apiFilePath)) console.log(` - Skipping existing api: ${apiFilePath}`);
2087
+ if (shouldCreateServices && existsSync(servicesFilePath)) console.log(` - Skipping existing services: ${servicesFilePath}`);
2088
+ if (shouldCreateSchemas && existsSync(schemasFilePath)) console.log(` - Skipping existing schemas: ${schemasFilePath}`);
2089
+ if (shouldCreateReadme && existsSync(readmeFilePath)) console.log(` - Skipping existing readme: ${readmeFilePath}`);
2090
+ if (shouldCreateStories && existsSync(storiesFilePath)) console.log(` - Skipping existing stories: ${storiesFilePath}`);
2091
+ if (shouldCreateScriptsDir && existsSync(scriptsIndexPath)) console.log(` - Skipping existing scripts: ${scriptsIndexPath}`);
2092
+ }
2092
2093
 
2093
2094
  let layoutImportPath = null;
2094
2095
  const cliProps = options.prop || {};
@@ -2229,21 +2230,23 @@ async function addSectionCommand(route, featurePath, options) {
2229
2230
 
2230
2231
  const featureSignature = getSignature(config, config.naming.featureExtension === '.astro' ? 'astro' : 'tsx');
2231
2232
 
2232
- const featureHash = await writeFileWithSignature(
2233
- featureFilePath,
2234
- featureContent,
2235
- featureSignature,
2236
- config.hashing?.normalization
2237
- );
2238
- await registerFile(featureFilePath, {
2239
- kind: 'feature',
2240
- template: 'feature',
2241
- hash: featureHash,
2242
- owner: normalizedRoute
2243
- });
2244
- writtenFiles.push(featureFilePath);
2233
+ if (!featureExists || options.force) {
2234
+ const featureHash = await writeFileWithSignature(
2235
+ featureFilePath,
2236
+ featureContent,
2237
+ featureSignature,
2238
+ config.hashing?.normalization
2239
+ );
2240
+ await registerFile(featureFilePath, {
2241
+ kind: 'feature',
2242
+ template: 'feature',
2243
+ hash: featureHash,
2244
+ owner: normalizedRoute
2245
+ });
2246
+ writtenFiles.push(featureFilePath);
2247
+ }
2245
2248
 
2246
- if (shouldCreateScriptsDir) {
2249
+ if (shouldCreateScriptsDir && (!existsSync(scriptsIndexPath) || options.force)) {
2247
2250
  const hash = await writeFileWithSignature(
2248
2251
  scriptsIndexPath,
2249
2252
  generateScriptsIndexTemplate(),
@@ -2259,7 +2262,7 @@ async function addSectionCommand(route, featurePath, options) {
2259
2262
  writtenFiles.push(scriptsIndexPath);
2260
2263
  }
2261
2264
 
2262
- if (shouldCreateIndex) {
2265
+ if (shouldCreateIndex && (!existsSync(indexFilePath) || options.force)) {
2263
2266
  const indexContent = generateIndexTemplate(featureComponentName, config.naming.featureExtension);
2264
2267
  const hash = await writeFileWithSignature(
2265
2268
  indexFilePath,
@@ -2276,7 +2279,7 @@ async function addSectionCommand(route, featurePath, options) {
2276
2279
  writtenFiles.push(indexFilePath);
2277
2280
  }
2278
2281
 
2279
- if (shouldCreateApi) {
2282
+ if (shouldCreateApi && (!existsSync(apiFilePath) || options.force)) {
2280
2283
  const apiContent = generateApiTemplate(featureComponentName);
2281
2284
  const hash = await writeFileWithSignature(
2282
2285
  apiFilePath,
@@ -2293,7 +2296,7 @@ async function addSectionCommand(route, featurePath, options) {
2293
2296
  writtenFiles.push(apiFilePath);
2294
2297
  }
2295
2298
 
2296
- if (shouldCreateServices) {
2299
+ if (shouldCreateServices && (!existsSync(servicesFilePath) || options.force)) {
2297
2300
  const servicesContent = generateServiceTemplate(featureComponentName);
2298
2301
  const hash = await writeFileWithSignature(
2299
2302
  servicesFilePath,
@@ -2310,7 +2313,7 @@ async function addSectionCommand(route, featurePath, options) {
2310
2313
  writtenFiles.push(servicesFilePath);
2311
2314
  }
2312
2315
 
2313
- if (shouldCreateSchemas) {
2316
+ if (shouldCreateSchemas && (!existsSync(schemasFilePath) || options.force)) {
2314
2317
  const schemasContent = generateSchemaTemplate(featureComponentName);
2315
2318
  const hash = await writeFileWithSignature(
2316
2319
  schemasFilePath,
@@ -2327,7 +2330,7 @@ async function addSectionCommand(route, featurePath, options) {
2327
2330
  writtenFiles.push(schemasFilePath);
2328
2331
  }
2329
2332
 
2330
- if (shouldCreateHooks) {
2333
+ if (shouldCreateHooks && (!existsSync(hookFilePath) || options.force)) {
2331
2334
  const hookName = getHookFunctionName(featureComponentName);
2332
2335
  const hookContent = generateHookTemplate(featureComponentName, hookName);
2333
2336
  const hash = await writeFileWithSignature(
@@ -2345,7 +2348,7 @@ async function addSectionCommand(route, featurePath, options) {
2345
2348
  writtenFiles.push(hookFilePath);
2346
2349
  }
2347
2350
 
2348
- if (shouldCreateContext) {
2351
+ if (shouldCreateContext && (!existsSync(contextFilePath) || options.force)) {
2349
2352
  const contextContent = generateContextTemplate(featureComponentName);
2350
2353
  const hash = await writeFileWithSignature(
2351
2354
  contextFilePath,
@@ -2362,7 +2365,7 @@ async function addSectionCommand(route, featurePath, options) {
2362
2365
  writtenFiles.push(contextFilePath);
2363
2366
  }
2364
2367
 
2365
- if (shouldCreateTests) {
2368
+ if (shouldCreateTests && (!existsSync(testFilePath) || options.force)) {
2366
2369
  const relativeFeaturePath = `./${path.basename(featureFilePath)}`;
2367
2370
  const testContent = generateTestTemplate(featureComponentName, relativeFeaturePath);
2368
2371
  const hash = await writeFileWithSignature(
@@ -2380,7 +2383,7 @@ async function addSectionCommand(route, featurePath, options) {
2380
2383
  writtenFiles.push(testFilePath);
2381
2384
  }
2382
2385
 
2383
- if (shouldCreateTypes) {
2386
+ if (shouldCreateTypes && (!existsSync(typesFilePath) || options.force)) {
2384
2387
  const typesContent = generateTypesTemplate(featureComponentName);
2385
2388
  const hash = await writeFileWithSignature(
2386
2389
  typesFilePath,
@@ -2397,7 +2400,7 @@ async function addSectionCommand(route, featurePath, options) {
2397
2400
  writtenFiles.push(typesFilePath);
2398
2401
  }
2399
2402
 
2400
- if (shouldCreateReadme) {
2403
+ if (shouldCreateReadme && (!existsSync(readmeFilePath) || options.force)) {
2401
2404
  const readmeContent = generateReadmeTemplate(featureComponentName);
2402
2405
  const hash = await writeFileWithSignature(
2403
2406
  readmeFilePath,
@@ -2414,7 +2417,7 @@ async function addSectionCommand(route, featurePath, options) {
2414
2417
  writtenFiles.push(readmeFilePath);
2415
2418
  }
2416
2419
 
2417
- if (shouldCreateStories) {
2420
+ if (shouldCreateStories && (!existsSync(storiesFilePath) || options.force)) {
2418
2421
  const relativePath = `./${path.basename(featureFilePath)}`;
2419
2422
  const storiesContent = generateStoriesTemplate(featureComponentName, relativePath);
2420
2423
  const hash = await writeFileWithSignature(
@@ -2751,6 +2754,222 @@ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
2751
2754
  await writeFile(filePath, content, 'utf-8');
2752
2755
  }
2753
2756
 
2757
+ /**
2758
+ * Updates relative imports in a file after it has been moved.
2759
+ */
2760
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2761
+ if (!existsSync(filePath)) return;
2762
+
2763
+ let content = await readFile(filePath, 'utf-8');
2764
+ const oldDir = path.dirname(oldFilePath);
2765
+ const newDir = path.dirname(newFilePath);
2766
+
2767
+ if (oldDir === newDir) return;
2768
+
2769
+ // Find all relative imports
2770
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2771
+ let match;
2772
+ const replacements = [];
2773
+
2774
+ while ((match = relativeImportRegex.exec(content)) !== null) {
2775
+ const relativePath = match[1];
2776
+ const absoluteTarget = path.resolve(oldDir, relativePath);
2777
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2778
+
2779
+ replacements.push({
2780
+ full: match[0],
2781
+ oldRel: relativePath,
2782
+ newRel: newRelativePath
2783
+ });
2784
+ }
2785
+
2786
+ for (const repl of replacements) {
2787
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
2788
+ }
2789
+
2790
+ await writeFile(filePath, content, 'utf-8');
2791
+ }
2792
+
2793
+ /**
2794
+ * Moves a directory and its contents, renaming files and updating internal content/imports.
2795
+ */
2796
+ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2797
+ const { fromName, toName, owner = null, signatures = [] } = options;
2798
+
2799
+ if (!existsSync(fromPath)) {
2800
+ throw new Error(`Source directory not found: ${fromPath}`);
2801
+ }
2802
+
2803
+ if (existsSync(toPath) && !options.force) {
2804
+ throw new Error(
2805
+ `Destination already exists: ${toPath}\n` +
2806
+ `Use --force to overwrite.`
2807
+ );
2808
+ }
2809
+
2810
+ await ensureDir(toPath);
2811
+
2812
+ const entries = await readdir(fromPath);
2813
+
2814
+ for (const entry of entries) {
2815
+ let targetEntry = entry;
2816
+
2817
+ // Rename files if they match the component name
2818
+ if (fromName && toName && fromName !== toName) {
2819
+ if (entry.includes(fromName)) {
2820
+ targetEntry = entry.replace(fromName, toName);
2821
+ }
2822
+ }
2823
+
2824
+ const fromEntryPath = path.join(fromPath, entry);
2825
+ const toEntryPath = path.join(toPath, targetEntry);
2826
+
2827
+ const stats = await stat(fromEntryPath);
2828
+
2829
+ if (stats.isDirectory()) {
2830
+ await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2831
+ } else {
2832
+ const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2833
+ const fileState = state.files[normalizedFromRelative];
2834
+
2835
+ const newHash = await safeMove(fromEntryPath, toEntryPath, {
2836
+ force: options.force,
2837
+ expectedHash: fileState?.hash,
2838
+ acceptChanges: options.acceptChanges,
2839
+ normalization: config.hashing?.normalization,
2840
+ owner,
2841
+ actualOwner: fileState?.owner,
2842
+ signatures
2843
+ });
2844
+
2845
+ // Update internal content (signatures, component names) if renaming
2846
+ if (fromName && toName && fromName !== toName) {
2847
+ let content = await readFile(toEntryPath, 'utf-8');
2848
+ let hasChanged = false;
2849
+
2850
+ // Simple replacement of component names
2851
+ if (content.includes(fromName)) {
2852
+ content = content.replace(new RegExp(fromName, 'g'), toName);
2853
+ hasChanged = true;
2854
+ }
2855
+
2856
+ // Also handle lowercase class names if any
2857
+ const fromLower = fromName.toLowerCase();
2858
+ const toLower = toName.toLowerCase();
2859
+ if (content.includes(fromLower)) {
2860
+ content = content.replace(new RegExp(fromLower, 'g'), toLower);
2861
+ hasChanged = true;
2862
+ }
2863
+
2864
+ if (hasChanged) {
2865
+ await writeFile(toEntryPath, content, 'utf-8');
2866
+ // Re-calculate hash after content update
2867
+ const updatedHash = calculateHash(content, config.hashing?.normalization);
2868
+
2869
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2870
+ if (fileState) {
2871
+ state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2872
+ delete state.files[normalizedFromRelative];
2873
+ }
2874
+ } else {
2875
+ // Update state for each file moved normally
2876
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2877
+ if (fileState) {
2878
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2879
+ delete state.files[normalizedFromRelative];
2880
+ }
2881
+ }
2882
+ } else {
2883
+ // Update state for each file moved normally
2884
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2885
+ if (fileState) {
2886
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2887
+ delete state.files[normalizedFromRelative];
2888
+ }
2889
+ }
2890
+ }
2891
+ }
2892
+
2893
+ const remainingFiles = await readdir(fromPath);
2894
+ if (remainingFiles.length === 0) {
2895
+ await rmdir(fromPath);
2896
+ }
2897
+ }
2898
+
2899
+ /**
2900
+ * Scans the project and replaces imports of a moved/renamed item.
2901
+ */
2902
+ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2903
+ const { fromPath: fromItemPath, fromName, type } = fromInfo;
2904
+ const { toPath: toItemPath, toName } = toInfo;
2905
+
2906
+ const allFiles = new Set();
2907
+ await scanDirectory(process.cwd(), allFiles);
2908
+
2909
+ const rootPath = resolvePath(config, type === 'component' ? 'components' : 'features');
2910
+
2911
+ for (const relPath of allFiles) {
2912
+ const fullPath = path.resolve(process.cwd(), relPath);
2913
+
2914
+ // Skip the moved directory itself
2915
+ const toFullPath = path.resolve(toItemPath);
2916
+ if (fullPath.startsWith(toFullPath)) continue;
2917
+
2918
+ let content = await readFile(fullPath, 'utf-8');
2919
+ let changed = false;
2920
+
2921
+ const aliasBase = config.importAliases[type === 'component' ? 'components' : 'features'];
2922
+ const ext = type === 'component' ? '' : (config.naming.featureExtension === '.astro' ? '.astro' : '');
2923
+
2924
+ if (aliasBase) {
2925
+ const oldAlias = `${aliasBase}/${fromItemPath}`;
2926
+ const newAlias = `${aliasBase}/${toItemPath}`;
2927
+
2928
+ const oldFullImport = `from '${oldAlias}/${fromName}${ext}'`;
2929
+ const newFullImport = `from '${newAlias}/${toName}${ext}'`;
2930
+
2931
+ if (content.includes(oldFullImport)) {
2932
+ content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2933
+ changed = true;
2934
+ } else if (content.includes(oldAlias)) {
2935
+ content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2936
+ changed = true;
2937
+ }
2938
+ } else {
2939
+ const oldDir = path.resolve(rootPath, fromItemPath);
2940
+ const newDir = path.resolve(rootPath, toItemPath);
2941
+
2942
+ const oldRelPath = getRelativeImportPath(fullPath, oldDir);
2943
+ const newRelPath = getRelativeImportPath(fullPath, newDir);
2944
+
2945
+ const oldImport = `'${oldRelPath}/${fromName}${ext}'`;
2946
+ const newImport = `'${newRelPath}/${toName}${ext}'`;
2947
+
2948
+ if (content.includes(oldImport)) {
2949
+ content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2950
+ changed = true;
2951
+ }
2952
+ }
2953
+
2954
+ if (fromName !== toName && changed) {
2955
+ content = content.replace(new RegExp(`\\b${fromName}\\b`, 'g'), toName);
2956
+ }
2957
+
2958
+ if (changed) {
2959
+ if (options.dryRun) {
2960
+ console.log(` [Scan] Would update imports in ${relPath}`);
2961
+ } else {
2962
+ await writeFile(fullPath, content, 'utf-8');
2963
+ console.log(` [Scan] Updated imports in ${relPath}`);
2964
+
2965
+ if (state.files[relPath]) {
2966
+ state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2967
+ }
2968
+ }
2969
+ }
2970
+ }
2971
+ }
2972
+
2754
2973
  /**
2755
2974
  * Move a section (route + feature).
2756
2975
  *
@@ -2779,45 +2998,59 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2779
2998
  let actualToRoute = toRoute;
2780
2999
  let actualToFeature = toFeature;
2781
3000
 
2782
- // Shift arguments if using state
3001
+ // Shift arguments if using state or if called with fewer arguments
2783
3002
  if (!toRoute && fromRoute && fromFeature) {
2784
3003
  // textor move-section /old-route /new-route
2785
- const section = findSection(state, fromRoute);
3004
+ actualFromRoute = fromRoute;
3005
+ actualToRoute = fromFeature;
3006
+ actualFromFeature = undefined;
3007
+ actualToFeature = undefined;
3008
+ }
3009
+
3010
+ // Lookup missing info from state
3011
+ if (actualFromRoute && !actualFromFeature) {
3012
+ const section = findSection(state, actualFromRoute);
2786
3013
  if (section) {
2787
- actualFromRoute = section.route;
2788
3014
  actualFromFeature = section.featurePath;
2789
- actualToRoute = fromFeature; // the second argument was actually the new route
2790
- actualToFeature = toRoute; // which is null
3015
+ }
3016
+ } else if (!actualFromRoute && actualFromFeature) {
3017
+ const section = findSection(state, actualFromFeature);
3018
+ if (section) {
3019
+ actualFromRoute = section.route;
3020
+ }
3021
+ }
3022
+
3023
+ // If toFeature is not provided, try to derive it from the new route if route moved
3024
+ if (!actualToFeature && actualToRoute && actualFromRoute && actualFromRoute !== actualToRoute && actualFromFeature) {
3025
+ const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
3026
+ const newRouteParts = actualToRoute.split('/').filter(Boolean);
3027
+ const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
3028
+
3029
+ let match = true;
3030
+ for (let i = 0; i < oldRouteParts.length; i++) {
3031
+ const routePart = oldRouteParts[i].toLowerCase();
3032
+ const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2791
3033
 
2792
- // If toFeature is not provided, try to derive it from the new route
2793
- if (!actualToFeature && actualToRoute) {
2794
- const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
2795
- const newRouteParts = actualToRoute.split('/').filter(Boolean);
2796
- const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
2797
-
2798
- // If the feature path starts with the old route parts, replace them
2799
- // We compare case-insensitively or via PascalCase to be more helpful
2800
- let match = true;
2801
- for (let i = 0; i < oldRouteParts.length; i++) {
2802
- const routePart = oldRouteParts[i].toLowerCase();
2803
- const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2804
-
2805
- if (featurePart !== routePart) {
2806
- match = false;
2807
- break;
2808
- }
2809
- }
2810
-
2811
- if (match && oldRouteParts.length > 0) {
2812
- actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
2813
- } else {
2814
- // Otherwise just keep it the same
2815
- actualToFeature = actualFromFeature;
2816
- }
3034
+ if (featurePart !== routePart) {
3035
+ match = false;
3036
+ break;
2817
3037
  }
2818
3038
  }
3039
+
3040
+ if (match && oldRouteParts.length > 0) {
3041
+ actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
3042
+ } else {
3043
+ actualToFeature = actualFromFeature;
3044
+ }
3045
+ } else if (!actualToFeature) {
3046
+ actualToFeature = actualFromFeature;
2819
3047
  }
2820
3048
 
3049
+ if (!actualToRoute) {
3050
+ actualToRoute = actualFromRoute;
3051
+ }
3052
+
3053
+
2821
3054
  const isRouteOnly = options.keepFeature || (!actualToFeature && actualToRoute && !actualFromFeature);
2822
3055
 
2823
3056
  if (isRouteOnly && !actualToRoute) {
@@ -2847,14 +3080,16 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2847
3080
  indexFile: config.routing.indexFile
2848
3081
  });
2849
3082
 
2850
- const fromRoutePath = secureJoin(pagesRoot, fromRouteFile);
2851
- const toRoutePath = secureJoin(pagesRoot, toRouteFile);
3083
+ const fromRoutePath = fromRouteFile ? secureJoin(pagesRoot, fromRouteFile) : null;
3084
+ const toRoutePath = toRouteFile ? secureJoin(pagesRoot, toRouteFile) : null;
2852
3085
 
2853
3086
  const movedFiles = [];
2854
3087
 
2855
3088
  if (options.dryRun) {
2856
3089
  console.log('Dry run - would move:');
2857
- console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
3090
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
3091
+ console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
3092
+ }
2858
3093
 
2859
3094
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature) {
2860
3095
  const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
@@ -2865,97 +3100,107 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2865
3100
  return;
2866
3101
  }
2867
3102
 
2868
- const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
2869
- const routeFileState = state.files[normalizedFromRouteRelative];
3103
+ let normalizedToRouteRelative = null;
3104
+ if (fromRoutePath && toRoutePath) {
3105
+ const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
3106
+ const routeFileState = state.files[normalizedFromRouteRelative];
2870
3107
 
2871
- const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
2872
- force: options.force,
2873
- expectedHash: routeFileState?.hash,
2874
- acceptChanges: options.acceptChanges,
2875
- owner: normalizedFromRoute,
2876
- actualOwner: routeFileState?.owner,
2877
- signatures: configSignatures
2878
- });
2879
- movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2880
-
2881
- // Update state for moved route file
2882
- const normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2883
- if (routeFileState) {
2884
- state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2885
- delete state.files[normalizedFromRouteRelative];
2886
- }
2887
-
2888
- // Update imports in the moved route file
2889
- const targetFeature = normalizedToFeature || normalizedFromFeature;
2890
- if (targetFeature) {
2891
- const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2892
- const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2893
- const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2894
- const toFeatureComponentName = getFeatureComponentName(targetFeature);
2895
-
2896
- // First, update all relative imports in the file because it moved
2897
- await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2898
-
2899
- let content = await readFile(toRoutePath, 'utf-8');
2900
- let changed = false;
2901
-
2902
- // Update component name in JSX tags
2903
- if (fromFeatureComponentName !== toFeatureComponentName) {
2904
- content = content.replace(
2905
- new RegExp(`<${fromFeatureComponentName}`, 'g'),
2906
- `<${toFeatureComponentName}`
2907
- );
2908
- content = content.replace(
2909
- new RegExp(`</${fromFeatureComponentName}`, 'g'),
2910
- `</${toFeatureComponentName}`
2911
- );
2912
- changed = true;
3108
+ const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
3109
+ force: options.force,
3110
+ expectedHash: routeFileState?.hash,
3111
+ acceptChanges: options.acceptChanges,
3112
+ owner: normalizedFromRoute,
3113
+ actualOwner: routeFileState?.owner,
3114
+ signatures: configSignatures
3115
+ });
3116
+
3117
+ if (fromRoutePath !== toRoutePath) {
3118
+ movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2913
3119
  }
2914
-
2915
- if (config.importAliases.features) {
2916
- const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2917
- const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2918
-
2919
- // Flexible regex to match import identifier and path with alias
2920
- const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2921
-
2922
- if (importRegex.test(content)) {
2923
- content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2924
- let newSubPath = subPath || '';
2925
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2926
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2927
- }
2928
- return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2929
- });
2930
- changed = true;
2931
- } else if (content.includes(oldAliasPath)) {
2932
- // Fallback for path only replacement
2933
- content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2934
- changed = true;
3120
+
3121
+ // Update state for moved route file
3122
+ normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
3123
+ if (routeFileState) {
3124
+ state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
3125
+ if (fromRoutePath !== toRoutePath) {
3126
+ delete state.files[normalizedFromRouteRelative];
2935
3127
  }
2936
- } else {
2937
- const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2938
- const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2939
-
2940
- // Flexible regex for relative imports
2941
- const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2942
-
2943
- if (relImportRegex.test(content)) {
2944
- content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2945
- let newSubPath = subPath || '';
2946
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2947
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2948
- }
2949
- return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2950
- });
3128
+ }
3129
+
3130
+ // Update imports in the route file (even if it didn't move, as feature might have)
3131
+ const targetFeature = normalizedToFeature || normalizedFromFeature;
3132
+ if (targetFeature && existsSync(toRoutePath)) {
3133
+ const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
3134
+ const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
3135
+ const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
3136
+ const toFeatureComponentName = getFeatureComponentName(targetFeature);
3137
+
3138
+ // First, update all relative imports in the file because it moved (or stayed)
3139
+ await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
3140
+
3141
+ let content = await readFile(toRoutePath, 'utf-8');
3142
+ let changed = false;
3143
+
3144
+ // Update component name in JSX tags
3145
+ if (fromFeatureComponentName !== toFeatureComponentName) {
3146
+ content = content.replace(
3147
+ new RegExp(`<${fromFeatureComponentName}`, 'g'),
3148
+ `<${toFeatureComponentName}`
3149
+ );
3150
+ content = content.replace(
3151
+ new RegExp(`</${fromFeatureComponentName}`, 'g'),
3152
+ `</${toFeatureComponentName}`
3153
+ );
2951
3154
  changed = true;
2952
3155
  }
2953
- }
2954
3156
 
2955
- if (changed) {
2956
- await writeFile(toRoutePath, content, 'utf-8');
2957
- // Update hash in state after changes
2958
- state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
3157
+ if (config.importAliases.features) {
3158
+ const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
3159
+ const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
3160
+
3161
+ // Flexible regex to match import identifier and path with alias
3162
+ const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
3163
+
3164
+ if (importRegex.test(content)) {
3165
+ content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
3166
+ let newSubPath = subPath || '';
3167
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
3168
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
3169
+ }
3170
+ return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
3171
+ });
3172
+ changed = true;
3173
+ } else if (content.includes(oldAliasPath)) {
3174
+ // Fallback for path only replacement
3175
+ content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
3176
+ changed = true;
3177
+ }
3178
+ } else {
3179
+ const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
3180
+ const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
3181
+
3182
+ // Flexible regex for relative imports
3183
+ const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
3184
+
3185
+ if (relImportRegex.test(content)) {
3186
+ content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
3187
+ let newSubPath = subPath || '';
3188
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
3189
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
3190
+ }
3191
+ return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
3192
+ });
3193
+ changed = true;
3194
+ }
3195
+ }
3196
+
3197
+ if (changed) {
3198
+ await writeFile(toRoutePath, content, 'utf-8');
3199
+ // Update hash in state after changes
3200
+ if (state.files[normalizedToRouteRelative]) {
3201
+ state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
3202
+ }
3203
+ }
2959
3204
  }
2960
3205
  }
2961
3206
 
@@ -2982,15 +3227,18 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2982
3227
 
2983
3228
  if (options.scan && (normalizedFromFeature || normalizedToFeature)) {
2984
3229
  await scanAndReplaceImports(config, state, {
2985
- fromFeaturePath: normalizedFromFeature,
2986
- fromComponentName: getFeatureComponentName(normalizedFromFeature)
3230
+ fromPath: normalizedFromFeature,
3231
+ fromName: getFeatureComponentName(normalizedFromFeature),
3232
+ type: 'feature'
2987
3233
  }, {
2988
- toFeaturePath: normalizedToFeature || normalizedFromFeature,
2989
- toComponentName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
3234
+ toPath: normalizedToFeature || normalizedFromFeature,
3235
+ toName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2990
3236
  }, options);
2991
3237
  }
2992
3238
 
2993
- await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
3239
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
3240
+ await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
3241
+ }
2994
3242
 
2995
3243
  console.log('✓ Moved:');
2996
3244
  movedFiles.forEach(item => {
@@ -3001,6 +3249,15 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
3001
3249
  if (movedFiles.length > 0) {
3002
3250
  const existingSection = fromSection;
3003
3251
 
3252
+ // Update ownership in state if route moved
3253
+ if (normalizedFromRoute && normalizedToRoute && normalizedFromRoute !== normalizedToRoute) {
3254
+ for (const f in state.files) {
3255
+ if (state.files[f].owner === normalizedFromRoute) {
3256
+ state.files[f].owner = normalizedToRoute;
3257
+ }
3258
+ }
3259
+ }
3260
+
3004
3261
  // Update section data in state
3005
3262
  state.sections = state.sections.filter(s => s.route !== normalizedFromRoute);
3006
3263
  state.sections.push({
@@ -3023,216 +3280,6 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
3023
3280
  }
3024
3281
  }
3025
3282
 
3026
- async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
3027
- const { fromFeaturePath, fromComponentName } = fromInfo;
3028
- const { toFeaturePath, toComponentName } = toInfo;
3029
-
3030
- const allFiles = new Set();
3031
- await scanDirectory(process.cwd(), allFiles);
3032
-
3033
- const featuresRoot = resolvePath(config, 'features');
3034
-
3035
- for (const relPath of allFiles) {
3036
- const fullPath = path.join(process.cwd(), relPath);
3037
-
3038
- // Skip the moved directory itself as it was already handled
3039
- if (fullPath.startsWith(path.resolve(toFeaturePath))) continue;
3040
-
3041
- let content = await readFile(fullPath, 'utf-8');
3042
- let changed = false;
3043
-
3044
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
3045
-
3046
- // Handle Aliases
3047
- if (config.importAliases.features) {
3048
- const oldAlias = `${config.importAliases.features}/${fromFeaturePath}`;
3049
- const newAlias = `${config.importAliases.features}/${toFeaturePath}`;
3050
-
3051
- // Update component name and path if both changed
3052
- const oldFullImport = `from '${oldAlias}/${fromComponentName}${ext}'`;
3053
- const newFullImport = `from '${newAlias}/${toComponentName}${ext}'`;
3054
-
3055
- if (content.includes(oldFullImport)) {
3056
- content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
3057
- changed = true;
3058
- } else if (content.includes(oldAlias)) {
3059
- content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
3060
- changed = true;
3061
- }
3062
- } else {
3063
- // Handle Relative Imports (more complex)
3064
- // This is best-effort: we look for imports that resolve to the old feature path
3065
- const fromFeatureDir = secureJoin(featuresRoot, fromFeaturePath);
3066
- const toFeatureDir = secureJoin(featuresRoot, toFeaturePath);
3067
-
3068
- const oldRelPath = getRelativeImportPath(fullPath, fromFeatureDir);
3069
- const newRelPath = getRelativeImportPath(fullPath, toFeatureDir);
3070
-
3071
- const oldImport = `'${oldRelPath}/${fromComponentName}${ext}'`;
3072
- const newImport = `'${newRelPath}/${toComponentName}${ext}'`;
3073
-
3074
- if (content.includes(oldImport)) {
3075
- content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
3076
- changed = true;
3077
- }
3078
- }
3079
-
3080
- // Update component name in JSX and imports if it changed
3081
- if (fromComponentName !== toComponentName && changed) {
3082
- content = content.replace(new RegExp(`\\b${fromComponentName}\\b`, 'g'), toComponentName);
3083
- }
3084
-
3085
- if (changed) {
3086
- if (options.dryRun) {
3087
- console.log(` [Scan] Would update imports in ${relPath}`);
3088
- } else {
3089
- await writeFile(fullPath, content, 'utf-8');
3090
- console.log(` [Scan] Updated imports in ${relPath}`);
3091
-
3092
- // Update state hash if this file is managed
3093
- if (state.files[relPath]) {
3094
- state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
3095
- }
3096
- }
3097
- }
3098
- }
3099
- }
3100
-
3101
- async function moveDirectory(fromPath, toPath, state, config, options = {}) {
3102
- const { fromName, toName, owner = null } = options;
3103
-
3104
- if (!existsSync(fromPath)) {
3105
- throw new Error(`Source directory not found: ${fromPath}`);
3106
- }
3107
-
3108
- if (existsSync(toPath) && !options.force) {
3109
- throw new Error(
3110
- `Destination already exists: ${toPath}\n` +
3111
- `Use --force to overwrite.`
3112
- );
3113
- }
3114
-
3115
- await ensureDir(toPath);
3116
-
3117
- const entries = await readdir(fromPath);
3118
-
3119
- for (const entry of entries) {
3120
- let targetEntry = entry;
3121
-
3122
- // Rename files if they match the component name
3123
- if (fromName && toName && fromName !== toName) {
3124
- if (entry.includes(fromName)) {
3125
- targetEntry = entry.replace(fromName, toName);
3126
- }
3127
- }
3128
-
3129
- const fromEntryPath = path.join(fromPath, entry);
3130
- const toEntryPath = path.join(toPath, targetEntry);
3131
-
3132
- const stats = await stat(fromEntryPath);
3133
-
3134
- if (stats.isDirectory()) {
3135
- await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
3136
- } else {
3137
- const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
3138
- const fileState = state.files[normalizedFromRelative];
3139
-
3140
- const newHash = await safeMove(fromEntryPath, toEntryPath, {
3141
- force: options.force,
3142
- expectedHash: fileState?.hash,
3143
- acceptChanges: options.acceptChanges,
3144
- normalization: config.hashing?.normalization,
3145
- owner,
3146
- actualOwner: fileState?.owner
3147
- });
3148
-
3149
- // Update internal content (signatures, component names) if renaming
3150
- if (fromName && toName && fromName !== toName) {
3151
- let content = await readFile(toEntryPath, 'utf-8');
3152
- let hasChanged = false;
3153
-
3154
- // Simple replacement of component names
3155
- if (content.includes(fromName)) {
3156
- content = content.replace(new RegExp(fromName, 'g'), toName);
3157
- hasChanged = true;
3158
- }
3159
-
3160
- // Also handle lowercase class names if any
3161
- const fromLower = fromName.toLowerCase();
3162
- const toLower = toName.toLowerCase();
3163
- if (content.includes(fromLower)) {
3164
- content = content.replace(new RegExp(fromLower, 'g'), toLower);
3165
- hasChanged = true;
3166
- }
3167
-
3168
- if (hasChanged) {
3169
- await writeFile(toEntryPath, content, 'utf-8');
3170
- // Re-calculate hash after content update
3171
- const updatedHash = calculateHash(content, config.hashing?.normalization);
3172
-
3173
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
3174
- if (fileState) {
3175
- state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
3176
- delete state.files[normalizedFromRelative];
3177
- }
3178
- } else {
3179
- // Update state for each file moved normally
3180
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
3181
- if (fileState) {
3182
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
3183
- delete state.files[normalizedFromRelative];
3184
- }
3185
- }
3186
- } else {
3187
- // Update state for each file moved normally
3188
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
3189
- if (fileState) {
3190
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
3191
- delete state.files[normalizedFromRelative];
3192
- }
3193
- }
3194
- }
3195
- }
3196
-
3197
- const remainingFiles = await readdir(fromPath);
3198
- if (remainingFiles.length === 0) {
3199
- await rmdir(fromPath);
3200
- }
3201
- }
3202
-
3203
- async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
3204
- if (!existsSync(filePath)) return;
3205
-
3206
- let content = await readFile(filePath, 'utf-8');
3207
- const oldDir = path.dirname(oldFilePath);
3208
- const newDir = path.dirname(newFilePath);
3209
-
3210
- if (oldDir === newDir) return;
3211
-
3212
- // Find all relative imports
3213
- const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
3214
- let match;
3215
- const replacements = [];
3216
-
3217
- while ((match = relativeImportRegex.exec(content)) !== null) {
3218
- const relativePath = match[1];
3219
- const absoluteTarget = path.resolve(oldDir, relativePath);
3220
- const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
3221
-
3222
- replacements.push({
3223
- full: match[0],
3224
- oldRel: relativePath,
3225
- newRel: newRelativePath
3226
- });
3227
- }
3228
-
3229
- for (const repl of replacements) {
3230
- content = content.replace(repl.full, `from '${repl.newRel}'`);
3231
- }
3232
-
3233
- await writeFile(filePath, content, 'utf-8');
3234
- }
3235
-
3236
3283
  async function createComponentCommand(componentName, options) {
3237
3284
  try {
3238
3285
  const config = await loadConfig();
@@ -3387,20 +3434,26 @@ async function createComponentCommand(componentName, options) {
3387
3434
  return;
3388
3435
  }
3389
3436
 
3390
- await ensureNotExists(componentFilePath, options.force);
3391
- await ensureNotExists(indexFilePath, options.force);
3392
-
3393
- if (shouldCreateContext) await ensureNotExists(contextFilePath, options.force);
3394
- if (shouldCreateHook) await ensureNotExists(hookFilePath, options.force);
3395
- if (shouldCreateTests) await ensureNotExists(testFilePath, options.force);
3396
- if (shouldCreateConfig) await ensureNotExists(configFilePath, options.force);
3397
- if (shouldCreateConstants) await ensureNotExists(constantsFilePath, options.force);
3398
- if (shouldCreateTypes) await ensureNotExists(typesFilePath, options.force);
3399
- if (shouldCreateApi) await ensureNotExists(apiFilePath, options.force);
3400
- if (shouldCreateServices) await ensureNotExists(servicesFilePath, options.force);
3401
- if (shouldCreateSchemas) await ensureNotExists(schemasFilePath, options.force);
3402
- if (shouldCreateReadme) await ensureNotExists(readmeFilePath, options.force);
3403
- if (shouldCreateStories) await ensureNotExists(storiesFilePath, options.force);
3437
+ const componentExists = existsSync(componentFilePath);
3438
+ if (componentExists && !options.force) {
3439
+ console.log(`ℹ Component already exists at ${componentFilePath}. Entering additive mode.`);
3440
+ }
3441
+
3442
+ // Check sub-items only if not in force mode
3443
+ if (!options.force) {
3444
+ if (existsSync(indexFilePath)) console.log(` - Skipping existing index: ${indexFilePath}`);
3445
+ if (shouldCreateContext && existsSync(contextFilePath)) console.log(` - Skipping existing context: ${contextFilePath}`);
3446
+ if (shouldCreateHook && existsSync(hookFilePath)) console.log(` - Skipping existing hook: ${hookFilePath}`);
3447
+ if (shouldCreateTests && existsSync(testFilePath)) console.log(` - Skipping existing test: ${testFilePath}`);
3448
+ if (shouldCreateConfig && existsSync(configFilePath)) console.log(` - Skipping existing config: ${configFilePath}`);
3449
+ if (shouldCreateConstants && existsSync(constantsFilePath)) console.log(` - Skipping existing constants: ${constantsFilePath}`);
3450
+ if (shouldCreateTypes && existsSync(typesFilePath)) console.log(` - Skipping existing types: ${typesFilePath}`);
3451
+ if (shouldCreateApi && existsSync(apiFilePath)) console.log(` - Skipping existing api: ${apiFilePath}`);
3452
+ if (shouldCreateServices && existsSync(servicesFilePath)) console.log(` - Skipping existing services: ${servicesFilePath}`);
3453
+ if (shouldCreateSchemas && existsSync(schemasFilePath)) console.log(` - Skipping existing schemas: ${schemasFilePath}`);
3454
+ if (shouldCreateReadme && existsSync(readmeFilePath)) console.log(` - Skipping existing readme: ${readmeFilePath}`);
3455
+ if (shouldCreateStories && existsSync(storiesFilePath)) console.log(` - Skipping existing stories: ${storiesFilePath}`);
3456
+ }
3404
3457
 
3405
3458
  await ensureDir(componentDir);
3406
3459
 
@@ -3418,37 +3471,42 @@ async function createComponentCommand(componentName, options) {
3418
3471
  const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
3419
3472
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
3420
3473
 
3421
- const componentHash = await writeFileWithSignature(
3422
- componentFilePath,
3423
- componentContent,
3424
- signature,
3425
- config.hashing?.normalization
3426
- );
3427
- await registerFile(componentFilePath, {
3428
- kind: 'component',
3429
- template: 'component',
3430
- hash: componentHash,
3431
- owner: normalizedName
3432
- });
3474
+ const writtenFiles = [];
3475
+
3476
+ if (!componentExists || options.force) {
3477
+ const componentHash = await writeFileWithSignature(
3478
+ componentFilePath,
3479
+ componentContent,
3480
+ signature,
3481
+ config.hashing?.normalization
3482
+ );
3483
+ await registerFile(componentFilePath, {
3484
+ kind: 'component',
3485
+ template: 'component',
3486
+ hash: componentHash,
3487
+ owner: normalizedName
3488
+ });
3489
+ writtenFiles.push(componentFilePath);
3490
+ }
3433
3491
 
3434
- const writtenFiles = [componentFilePath];
3435
-
3436
- const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
3437
- const indexHash = await writeFileWithSignature(
3438
- indexFilePath,
3439
- indexContent,
3440
- getSignature(config, 'typescript'),
3441
- config.hashing?.normalization
3442
- );
3443
- await registerFile(indexFilePath, {
3444
- kind: 'component-file',
3445
- template: 'index',
3446
- hash: indexHash,
3447
- owner: normalizedName
3448
- });
3449
- writtenFiles.push(indexFilePath);
3492
+ if (!existsSync(indexFilePath) || options.force) {
3493
+ const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
3494
+ const indexHash = await writeFileWithSignature(
3495
+ indexFilePath,
3496
+ indexContent,
3497
+ getSignature(config, 'typescript'),
3498
+ config.hashing?.normalization
3499
+ );
3500
+ await registerFile(indexFilePath, {
3501
+ kind: 'component-file',
3502
+ template: 'index',
3503
+ hash: indexHash,
3504
+ owner: normalizedName
3505
+ });
3506
+ writtenFiles.push(indexFilePath);
3507
+ }
3450
3508
 
3451
- if (shouldCreateTypes) {
3509
+ if (shouldCreateTypes && (!existsSync(typesFilePath) || options.force)) {
3452
3510
  const typesContent = generateTypesTemplate(normalizedName);
3453
3511
  const hash = await writeFileWithSignature(
3454
3512
  typesFilePath,
@@ -3465,7 +3523,7 @@ async function createComponentCommand(componentName, options) {
3465
3523
  writtenFiles.push(typesFilePath);
3466
3524
  }
3467
3525
 
3468
- if (shouldCreateContext) {
3526
+ if (shouldCreateContext && (!existsSync(contextFilePath) || options.force)) {
3469
3527
  const contextContent = generateContextTemplate(normalizedName);
3470
3528
  const hash = await writeFileWithSignature(
3471
3529
  contextFilePath,
@@ -3482,7 +3540,7 @@ async function createComponentCommand(componentName, options) {
3482
3540
  writtenFiles.push(contextFilePath);
3483
3541
  }
3484
3542
 
3485
- if (shouldCreateHook) {
3543
+ if (shouldCreateHook && (!existsSync(hookFilePath) || options.force)) {
3486
3544
  const hookName = getHookFunctionName(normalizedName);
3487
3545
  const hookContent = generateHookTemplate(normalizedName, hookName);
3488
3546
  const hash = await writeFileWithSignature(
@@ -3500,7 +3558,7 @@ async function createComponentCommand(componentName, options) {
3500
3558
  writtenFiles.push(hookFilePath);
3501
3559
  }
3502
3560
 
3503
- if (shouldCreateTests) {
3561
+ if (shouldCreateTests && (!existsSync(testFilePath) || options.force)) {
3504
3562
  const relativeComponentPath = `../${normalizedName}${config.naming.componentExtension}`;
3505
3563
  const testContent = generateTestTemplate(normalizedName, relativeComponentPath);
3506
3564
  const hash = await writeFileWithSignature(
@@ -3518,7 +3576,7 @@ async function createComponentCommand(componentName, options) {
3518
3576
  writtenFiles.push(testFilePath);
3519
3577
  }
3520
3578
 
3521
- if (shouldCreateConfig) {
3579
+ if (shouldCreateConfig && (!existsSync(configFilePath) || options.force)) {
3522
3580
  const configContent = generateConfigTemplate(normalizedName);
3523
3581
  const hash = await writeFileWithSignature(
3524
3582
  configFilePath,
@@ -3535,7 +3593,7 @@ async function createComponentCommand(componentName, options) {
3535
3593
  writtenFiles.push(configFilePath);
3536
3594
  }
3537
3595
 
3538
- if (shouldCreateConstants) {
3596
+ if (shouldCreateConstants && (!existsSync(constantsFilePath) || options.force)) {
3539
3597
  const constantsContent = generateConstantsTemplate(normalizedName);
3540
3598
  const hash = await writeFileWithSignature(
3541
3599
  constantsFilePath,
@@ -3552,7 +3610,7 @@ async function createComponentCommand(componentName, options) {
3552
3610
  writtenFiles.push(constantsFilePath);
3553
3611
  }
3554
3612
 
3555
- if (shouldCreateApi) {
3613
+ if (shouldCreateApi && (!existsSync(apiFilePath) || options.force)) {
3556
3614
  const apiContent = generateApiTemplate(normalizedName);
3557
3615
  const hash = await writeFileWithSignature(
3558
3616
  apiFilePath,
@@ -3569,7 +3627,7 @@ async function createComponentCommand(componentName, options) {
3569
3627
  writtenFiles.push(apiFilePath);
3570
3628
  }
3571
3629
 
3572
- if (shouldCreateServices) {
3630
+ if (shouldCreateServices && (!existsSync(servicesFilePath) || options.force)) {
3573
3631
  const servicesContent = generateServiceTemplate(normalizedName);
3574
3632
  const hash = await writeFileWithSignature(
3575
3633
  servicesFilePath,
@@ -3586,7 +3644,7 @@ async function createComponentCommand(componentName, options) {
3586
3644
  writtenFiles.push(servicesFilePath);
3587
3645
  }
3588
3646
 
3589
- if (shouldCreateSchemas) {
3647
+ if (shouldCreateSchemas && (!existsSync(schemasFilePath) || options.force)) {
3590
3648
  const schemasContent = generateSchemaTemplate(normalizedName);
3591
3649
  const hash = await writeFileWithSignature(
3592
3650
  schemasFilePath,
@@ -3603,7 +3661,7 @@ async function createComponentCommand(componentName, options) {
3603
3661
  writtenFiles.push(schemasFilePath);
3604
3662
  }
3605
3663
 
3606
- if (shouldCreateReadme) {
3664
+ if (shouldCreateReadme && (!existsSync(readmeFilePath) || options.force)) {
3607
3665
  const readmeContent = generateReadmeTemplate(normalizedName);
3608
3666
  const hash = await writeFileWithSignature(
3609
3667
  readmeFilePath,
@@ -3620,7 +3678,7 @@ async function createComponentCommand(componentName, options) {
3620
3678
  writtenFiles.push(readmeFilePath);
3621
3679
  }
3622
3680
 
3623
- if (shouldCreateStories) {
3681
+ if (shouldCreateStories && (!existsSync(storiesFilePath) || options.force)) {
3624
3682
  const relativePath = `./${normalizedName}${config.naming.componentExtension}`;
3625
3683
  const storiesContent = generateStoriesTemplate(normalizedName, relativePath);
3626
3684
  const hash = await writeFileWithSignature(
@@ -4551,6 +4609,166 @@ async function pruneMissingCommand(options = {}) {
4551
4609
  }
4552
4610
  }
4553
4611
 
4612
+ /**
4613
+ * Add a new item (hook, api, service, etc.) to an existing feature or component.
4614
+ *
4615
+ * @param {string} itemType The type of item to add (e.g., 'api', 'hook', 'service')
4616
+ * @param {string} targetName The name of the feature or component
4617
+ * @param {Object} options Additional options from Commander
4618
+ */
4619
+ async function addItemCommand(itemType, targetName, options) {
4620
+ try {
4621
+ const state = await loadState();
4622
+
4623
+ // Normalize itemType
4624
+ let normalizedItem = itemType.toLowerCase();
4625
+ if (normalizedItem === 'test') normalizedItem = 'tests';
4626
+ if (normalizedItem === 'service') normalizedItem = 'services';
4627
+ if (normalizedItem === 'schema') normalizedItem = 'schemas';
4628
+ if (normalizedItem === 'hook') normalizedItem = 'hooks'; // for add-section
4629
+
4630
+ // Try to find as section (feature) first
4631
+ let section = findSection(state, targetName);
4632
+ let component = findComponent(state, targetName);
4633
+
4634
+ // If not found by exact name, try to find by featurePath or part of it
4635
+ if (!section && !component) {
4636
+ section = state.sections.find(s => s.featurePath === targetName || s.featurePath.endsWith('/' + targetName));
4637
+ }
4638
+
4639
+ if (!section && !component) {
4640
+ throw new Error(`Target not found in state: "${targetName}". Please use "add-section" or "create-component" directly if it's not managed by Textor.`);
4641
+ }
4642
+
4643
+ const flags = { [normalizedItem]: true };
4644
+ // Also set singular for create-component which uses 'hook'
4645
+ if (normalizedItem === 'hooks') flags.hook = true;
4646
+
4647
+ if (section) {
4648
+ console.log(`ℹ Adding ${normalizedItem} to feature: ${section.featurePath}`);
4649
+ return await addSectionCommand(undefined, section.featurePath, { ...options, ...flags });
4650
+ }
4651
+
4652
+ if (component) {
4653
+ console.log(`ℹ Adding ${normalizedItem} to component: ${component.name}`);
4654
+ // For create-component, we might need to be careful with flags that are on by default
4655
+ // but getEffectiveOptions should handle it if we pass them explicitly as true.
4656
+ return await createComponentCommand(component.name, { ...options, ...flags });
4657
+ }
4658
+ } catch (error) {
4659
+ console.error('Error:', error.message);
4660
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4661
+ process.exit(1);
4662
+ }
4663
+ throw error;
4664
+ }
4665
+ }
4666
+
4667
+ /**
4668
+ * Dispatcher for rename commands.
4669
+ */
4670
+ async function renameCommand(type, oldName, newName, options) {
4671
+ try {
4672
+ if (!type || !oldName || !newName) {
4673
+ throw new Error('Usage: textor rename <route|feature|component> <oldName> <newName>');
4674
+ }
4675
+
4676
+ if (type === 'route' || type === 'path') {
4677
+ const normalizedOld = normalizeRoute(oldName);
4678
+ const normalizedNew = normalizeRoute(newName);
4679
+ // By default, move-section will try to move the feature if it matches the route.
4680
+ // For a simple "rename route", we might want to keep that behavior or not.
4681
+ // Usually "rename route" means just the URL/file.
4682
+ return await moveSectionCommand(normalizedOld, undefined, normalizedNew, undefined, options);
4683
+ }
4684
+
4685
+ if (type === 'feature') {
4686
+ const state = await loadState();
4687
+ const normalizedOld = featureToDirectoryPath(oldName);
4688
+ const normalizedNew = featureToDirectoryPath(newName);
4689
+
4690
+ const section = findSection(state, normalizedOld);
4691
+
4692
+ if (section) {
4693
+ // If it's a managed section, move it using section logic
4694
+ return await moveSectionCommand(section.route, section.featurePath, section.route, normalizedNew, options);
4695
+ } else {
4696
+ // Standalone feature move
4697
+ return await moveSectionCommand(undefined, normalizedOld, undefined, normalizedNew, options);
4698
+ }
4699
+ }
4700
+
4701
+ if (type === 'component') {
4702
+ return await renameComponent(oldName, newName, options);
4703
+ }
4704
+
4705
+ throw new Error(`Unknown rename type: ${type}. Supported types: route, feature, component.`);
4706
+ } catch (error) {
4707
+ console.error('Error:', error.message);
4708
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4709
+ process.exit(1);
4710
+ }
4711
+ throw error;
4712
+ }
4713
+ }
4714
+
4715
+ /**
4716
+ * Specialized logic for renaming shared components.
4717
+ */
4718
+ async function renameComponent(oldName, newName, options) {
4719
+ const config = await loadConfig();
4720
+ const state = await loadState();
4721
+
4722
+ const normalizedOldName = normalizeComponentName(oldName);
4723
+ const normalizedNewName = normalizeComponentName(newName);
4724
+
4725
+ const component = findComponent(state, normalizedOldName);
4726
+
4727
+ const componentsRoot = resolvePath(config, 'components');
4728
+ const fromPath = component
4729
+ ? path.resolve(process.cwd(), component.path)
4730
+ : path.join(componentsRoot, normalizedOldName);
4731
+
4732
+ const toPath = path.join(componentsRoot, normalizedNewName);
4733
+
4734
+ if (options.dryRun) {
4735
+ console.log(`Dry run - would rename component: ${normalizedOldName} -> ${normalizedNewName}`);
4736
+ console.log(` Path: ${fromPath} -> ${toPath}`);
4737
+ return;
4738
+ }
4739
+
4740
+ const signatures = Object.values(config.signatures || {});
4741
+
4742
+ await moveDirectory(fromPath, toPath, state, config, {
4743
+ ...options,
4744
+ fromName: normalizedOldName,
4745
+ toName: normalizedNewName,
4746
+ signatures
4747
+ });
4748
+
4749
+ if (options.scan) {
4750
+ await scanAndReplaceImports(config, state, {
4751
+ fromPath: normalizedOldName,
4752
+ fromName: normalizedOldName,
4753
+ type: 'component'
4754
+ }, {
4755
+ toPath: normalizedNewName,
4756
+ toName: normalizedNewName
4757
+ }, options);
4758
+ }
4759
+
4760
+ await cleanupEmptyDirs(path.dirname(fromPath), componentsRoot);
4761
+
4762
+ // Update state metadata
4763
+ if (component) {
4764
+ component.name = normalizedNewName;
4765
+ component.path = path.relative(process.cwd(), toPath).replace(/\\/g, '/');
4766
+ }
4767
+
4768
+ await saveState(state);
4769
+ console.log(`✓ Renamed component ${normalizedOldName} to ${normalizedNewName}`);
4770
+ }
4771
+
4554
4772
  const program = new Command();
4555
4773
 
4556
4774
  program
@@ -4696,5 +4914,21 @@ program
4696
4914
  .option('--no-interactive', 'Disable interactive prompts')
4697
4915
  .action(pruneMissingCommand);
4698
4916
 
4917
+ program
4918
+ .command('rename <type> <oldName> <newName>')
4919
+ .description('Rename a route, feature, or component')
4920
+ .option('--scan', 'Enable repo-wide import updates')
4921
+ .option('--force', 'Rename even if files are modified or not generated by Textor')
4922
+ .option('--accept-changes', 'Allow renaming of modified files')
4923
+ .option('--dry-run', 'Show what would be renamed without applying')
4924
+ .action(renameCommand);
4925
+
4926
+ program
4927
+ .command('add <item> <target>')
4928
+ .description('Add a sub-item (api, hook, test, etc.) to an existing feature or component')
4929
+ .option('--force', 'Overwrite existing files')
4930
+ .option('--dry-run', 'Show what would be created without creating')
4931
+ .action(addItemCommand);
4932
+
4699
4933
  program.parse();
4700
4934
  //# sourceMappingURL=textor.js.map