@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.
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var path = require('path');
6
6
  var crypto = require('crypto');
7
7
  var child_process = require('child_process');
8
8
  var util = require('util');
9
+ var readline = require('readline');
9
10
 
10
11
  const CONFIG_DIR$1 = '.textor';
11
12
  const CONFIG_FILE = 'config.json';
@@ -497,6 +498,8 @@ function normalizeRoute(route) {
497
498
  }
498
499
  function routeToFilePath(route, options = {}) {
499
500
  const { extension = '.astro', mode = 'flat', indexFile = 'index.astro' } = options;
501
+ if (!route)
502
+ return null;
500
503
  const normalized = normalizeRoute(route);
501
504
  if (normalized === '/') {
502
505
  return indexFile;
@@ -634,14 +637,6 @@ async function safeDelete(filePath, options = {}) {
634
637
  await promises.unlink(filePath);
635
638
  return { deleted: true };
636
639
  }
637
- async function ensureNotExists(filePath, force = false) {
638
- if (fs.existsSync(filePath)) {
639
- if (!force) {
640
- throw new Error(`File already exists: ${filePath}\n` +
641
- `Use --force to overwrite.`);
642
- }
643
- }
644
- }
645
640
  async function ensureDir(dirPath) {
646
641
  await promises.mkdir(dirPath, { recursive: true });
647
642
  }
@@ -715,6 +710,10 @@ async function safeMove(fromPath, toPath, options = {}) {
715
710
  if (!fs.existsSync(fromPath)) {
716
711
  throw new Error(`Source file not found: ${fromPath}`);
717
712
  }
713
+ if (path.resolve(fromPath) === path.resolve(toPath)) {
714
+ const content = await promises.readFile(toPath, 'utf-8');
715
+ return calculateHash(content, normalization);
716
+ }
718
717
  if (fs.existsSync(toPath) && !force) {
719
718
  throw new Error(`Destination already exists: ${toPath}\n` +
720
719
  `Use --force to overwrite.`);
@@ -1789,29 +1788,35 @@ async function addSectionCommand(route, featurePath, options) {
1789
1788
  }
1790
1789
  }
1791
1790
  }
1792
- await ensureNotExists(featureFilePath, options.force);
1793
- if (shouldCreateIndex)
1794
- await ensureNotExists(indexFilePath, options.force);
1795
- if (shouldCreateContext)
1796
- await ensureNotExists(contextFilePath, options.force);
1797
- if (shouldCreateHooks)
1798
- await ensureNotExists(hookFilePath, options.force);
1799
- if (shouldCreateTests)
1800
- await ensureNotExists(testFilePath, options.force);
1801
- if (shouldCreateTypes)
1802
- await ensureNotExists(typesFilePath, options.force);
1803
- if (shouldCreateApi)
1804
- await ensureNotExists(apiFilePath, options.force);
1805
- if (shouldCreateServices)
1806
- await ensureNotExists(servicesFilePath, options.force);
1807
- if (shouldCreateSchemas)
1808
- await ensureNotExists(schemasFilePath, options.force);
1809
- if (shouldCreateReadme)
1810
- await ensureNotExists(readmeFilePath, options.force);
1811
- if (shouldCreateStories)
1812
- await ensureNotExists(storiesFilePath, options.force);
1813
- if (shouldCreateScriptsDir)
1814
- await ensureNotExists(scriptsIndexPath, options.force);
1791
+ const featureExists = fs.existsSync(featureFilePath);
1792
+ if (featureExists && !options.force) {
1793
+ console.log(`ℹ Feature already exists at ${featureFilePath}. Entering additive mode.`);
1794
+ }
1795
+ // Check sub-items only if not in force mode
1796
+ if (!options.force) {
1797
+ if (shouldCreateIndex && fs.existsSync(indexFilePath))
1798
+ console.log(` - Skipping existing index: ${indexFilePath}`);
1799
+ if (shouldCreateContext && fs.existsSync(contextFilePath))
1800
+ console.log(` - Skipping existing context: ${contextFilePath}`);
1801
+ if (shouldCreateHooks && fs.existsSync(hookFilePath))
1802
+ console.log(` - Skipping existing hook: ${hookFilePath}`);
1803
+ if (shouldCreateTests && fs.existsSync(testFilePath))
1804
+ console.log(` - Skipping existing test: ${testFilePath}`);
1805
+ if (shouldCreateTypes && fs.existsSync(typesFilePath))
1806
+ console.log(` - Skipping existing types: ${typesFilePath}`);
1807
+ if (shouldCreateApi && fs.existsSync(apiFilePath))
1808
+ console.log(` - Skipping existing api: ${apiFilePath}`);
1809
+ if (shouldCreateServices && fs.existsSync(servicesFilePath))
1810
+ console.log(` - Skipping existing services: ${servicesFilePath}`);
1811
+ if (shouldCreateSchemas && fs.existsSync(schemasFilePath))
1812
+ console.log(` - Skipping existing schemas: ${schemasFilePath}`);
1813
+ if (shouldCreateReadme && fs.existsSync(readmeFilePath))
1814
+ console.log(` - Skipping existing readme: ${readmeFilePath}`);
1815
+ if (shouldCreateStories && fs.existsSync(storiesFilePath))
1816
+ console.log(` - Skipping existing stories: ${storiesFilePath}`);
1817
+ if (shouldCreateScriptsDir && fs.existsSync(scriptsIndexPath))
1818
+ console.log(` - Skipping existing scripts: ${scriptsIndexPath}`);
1819
+ }
1815
1820
  let layoutImportPath = null;
1816
1821
  const cliProps = options.prop || {};
1817
1822
  const rawLayoutProps = { ...configLayoutProps, ...cliProps };
@@ -1933,15 +1938,17 @@ async function addSectionCommand(route, featurePath, options) {
1933
1938
  if (shouldCreateTypes)
1934
1939
  await ensureDir(typesDirInside);
1935
1940
  const featureSignature = getSignature(config, config.naming.featureExtension === '.astro' ? 'astro' : 'tsx');
1936
- const featureHash = await writeFileWithSignature(featureFilePath, featureContent, featureSignature, config.hashing?.normalization);
1937
- await registerFile(featureFilePath, {
1938
- kind: 'feature',
1939
- template: 'feature',
1940
- hash: featureHash,
1941
- owner: normalizedRoute
1942
- });
1943
- writtenFiles.push(featureFilePath);
1944
- if (shouldCreateScriptsDir) {
1941
+ if (!featureExists || options.force) {
1942
+ const featureHash = await writeFileWithSignature(featureFilePath, featureContent, featureSignature, config.hashing?.normalization);
1943
+ await registerFile(featureFilePath, {
1944
+ kind: 'feature',
1945
+ template: 'feature',
1946
+ hash: featureHash,
1947
+ owner: normalizedRoute
1948
+ });
1949
+ writtenFiles.push(featureFilePath);
1950
+ }
1951
+ if (shouldCreateScriptsDir && (!fs.existsSync(scriptsIndexPath) || options.force)) {
1945
1952
  const hash = await writeFileWithSignature(scriptsIndexPath, generateScriptsIndexTemplate(), getSignature(config, 'typescript'), config.hashing?.normalization);
1946
1953
  await registerFile(scriptsIndexPath, {
1947
1954
  kind: 'feature-file',
@@ -1951,7 +1958,7 @@ async function addSectionCommand(route, featurePath, options) {
1951
1958
  });
1952
1959
  writtenFiles.push(scriptsIndexPath);
1953
1960
  }
1954
- if (shouldCreateIndex) {
1961
+ if (shouldCreateIndex && (!fs.existsSync(indexFilePath) || options.force)) {
1955
1962
  const indexContent = generateIndexTemplate(featureComponentName, config.naming.featureExtension);
1956
1963
  const hash = await writeFileWithSignature(indexFilePath, indexContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1957
1964
  await registerFile(indexFilePath, {
@@ -1962,7 +1969,7 @@ async function addSectionCommand(route, featurePath, options) {
1962
1969
  });
1963
1970
  writtenFiles.push(indexFilePath);
1964
1971
  }
1965
- if (shouldCreateApi) {
1972
+ if (shouldCreateApi && (!fs.existsSync(apiFilePath) || options.force)) {
1966
1973
  const apiContent = generateApiTemplate(featureComponentName);
1967
1974
  const hash = await writeFileWithSignature(apiFilePath, apiContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1968
1975
  await registerFile(apiFilePath, {
@@ -1973,7 +1980,7 @@ async function addSectionCommand(route, featurePath, options) {
1973
1980
  });
1974
1981
  writtenFiles.push(apiFilePath);
1975
1982
  }
1976
- if (shouldCreateServices) {
1983
+ if (shouldCreateServices && (!fs.existsSync(servicesFilePath) || options.force)) {
1977
1984
  const servicesContent = generateServiceTemplate(featureComponentName);
1978
1985
  const hash = await writeFileWithSignature(servicesFilePath, servicesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1979
1986
  await registerFile(servicesFilePath, {
@@ -1984,7 +1991,7 @@ async function addSectionCommand(route, featurePath, options) {
1984
1991
  });
1985
1992
  writtenFiles.push(servicesFilePath);
1986
1993
  }
1987
- if (shouldCreateSchemas) {
1994
+ if (shouldCreateSchemas && (!fs.existsSync(schemasFilePath) || options.force)) {
1988
1995
  const schemasContent = generateSchemaTemplate(featureComponentName);
1989
1996
  const hash = await writeFileWithSignature(schemasFilePath, schemasContent, getSignature(config, 'typescript'), config.hashing?.normalization);
1990
1997
  await registerFile(schemasFilePath, {
@@ -1995,7 +2002,7 @@ async function addSectionCommand(route, featurePath, options) {
1995
2002
  });
1996
2003
  writtenFiles.push(schemasFilePath);
1997
2004
  }
1998
- if (shouldCreateHooks) {
2005
+ if (shouldCreateHooks && (!fs.existsSync(hookFilePath) || options.force)) {
1999
2006
  const hookName = getHookFunctionName(featureComponentName);
2000
2007
  const hookContent = generateHookTemplate(featureComponentName, hookName);
2001
2008
  const hash = await writeFileWithSignature(hookFilePath, hookContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -2007,7 +2014,7 @@ async function addSectionCommand(route, featurePath, options) {
2007
2014
  });
2008
2015
  writtenFiles.push(hookFilePath);
2009
2016
  }
2010
- if (shouldCreateContext) {
2017
+ if (shouldCreateContext && (!fs.existsSync(contextFilePath) || options.force)) {
2011
2018
  const contextContent = generateContextTemplate(featureComponentName);
2012
2019
  const hash = await writeFileWithSignature(contextFilePath, contextContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2013
2020
  await registerFile(contextFilePath, {
@@ -2018,7 +2025,7 @@ async function addSectionCommand(route, featurePath, options) {
2018
2025
  });
2019
2026
  writtenFiles.push(contextFilePath);
2020
2027
  }
2021
- if (shouldCreateTests) {
2028
+ if (shouldCreateTests && (!fs.existsSync(testFilePath) || options.force)) {
2022
2029
  const relativeFeaturePath = `./${path.basename(featureFilePath)}`;
2023
2030
  const testContent = generateTestTemplate(featureComponentName, relativeFeaturePath);
2024
2031
  const hash = await writeFileWithSignature(testFilePath, testContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -2030,7 +2037,7 @@ async function addSectionCommand(route, featurePath, options) {
2030
2037
  });
2031
2038
  writtenFiles.push(testFilePath);
2032
2039
  }
2033
- if (shouldCreateTypes) {
2040
+ if (shouldCreateTypes && (!fs.existsSync(typesFilePath) || options.force)) {
2034
2041
  const typesContent = generateTypesTemplate(featureComponentName);
2035
2042
  const hash = await writeFileWithSignature(typesFilePath, typesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2036
2043
  await registerFile(typesFilePath, {
@@ -2041,7 +2048,7 @@ async function addSectionCommand(route, featurePath, options) {
2041
2048
  });
2042
2049
  writtenFiles.push(typesFilePath);
2043
2050
  }
2044
- if (shouldCreateReadme) {
2051
+ if (shouldCreateReadme && (!fs.existsSync(readmeFilePath) || options.force)) {
2045
2052
  const readmeContent = generateReadmeTemplate(featureComponentName);
2046
2053
  const hash = await writeFileWithSignature(readmeFilePath, readmeContent, getSignature(config, 'astro'), config.hashing?.normalization);
2047
2054
  await registerFile(readmeFilePath, {
@@ -2052,7 +2059,7 @@ async function addSectionCommand(route, featurePath, options) {
2052
2059
  });
2053
2060
  writtenFiles.push(readmeFilePath);
2054
2061
  }
2055
- if (shouldCreateStories) {
2062
+ if (shouldCreateStories && (!fs.existsSync(storiesFilePath) || options.force)) {
2056
2063
  const relativePath = `./${path.basename(featureFilePath)}`;
2057
2064
  const storiesContent = generateStoriesTemplate(featureComponentName, relativePath);
2058
2065
  const hash = await writeFileWithSignature(storiesFilePath, storiesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -2350,6 +2357,189 @@ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
2350
2357
  await promises.writeFile(filePath, content, 'utf-8');
2351
2358
  }
2352
2359
 
2360
+ /**
2361
+ * Updates relative imports in a file after it has been moved.
2362
+ */
2363
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2364
+ if (!fs.existsSync(filePath))
2365
+ return;
2366
+ let content = await promises.readFile(filePath, 'utf-8');
2367
+ const oldDir = path.dirname(oldFilePath);
2368
+ const newDir = path.dirname(newFilePath);
2369
+ if (oldDir === newDir)
2370
+ return;
2371
+ // Find all relative imports
2372
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2373
+ let match;
2374
+ const replacements = [];
2375
+ while ((match = relativeImportRegex.exec(content)) !== null) {
2376
+ const relativePath = match[1];
2377
+ const absoluteTarget = path.resolve(oldDir, relativePath);
2378
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2379
+ replacements.push({
2380
+ full: match[0],
2381
+ oldRel: relativePath,
2382
+ newRel: newRelativePath
2383
+ });
2384
+ }
2385
+ for (const repl of replacements) {
2386
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
2387
+ }
2388
+ await promises.writeFile(filePath, content, 'utf-8');
2389
+ }
2390
+ /**
2391
+ * Moves a directory and its contents, renaming files and updating internal content/imports.
2392
+ */
2393
+ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2394
+ const { fromName, toName, owner = null, signatures = [] } = options;
2395
+ if (!fs.existsSync(fromPath)) {
2396
+ throw new Error(`Source directory not found: ${fromPath}`);
2397
+ }
2398
+ if (fs.existsSync(toPath) && !options.force) {
2399
+ throw new Error(`Destination already exists: ${toPath}\n` +
2400
+ `Use --force to overwrite.`);
2401
+ }
2402
+ await ensureDir(toPath);
2403
+ const entries = await promises.readdir(fromPath);
2404
+ for (const entry of entries) {
2405
+ let targetEntry = entry;
2406
+ // Rename files if they match the component name
2407
+ if (fromName && toName && fromName !== toName) {
2408
+ if (entry.includes(fromName)) {
2409
+ targetEntry = entry.replace(fromName, toName);
2410
+ }
2411
+ }
2412
+ const fromEntryPath = path.join(fromPath, entry);
2413
+ const toEntryPath = path.join(toPath, targetEntry);
2414
+ const stats = await promises.stat(fromEntryPath);
2415
+ if (stats.isDirectory()) {
2416
+ await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2417
+ }
2418
+ else {
2419
+ const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2420
+ const fileState = state.files[normalizedFromRelative];
2421
+ const newHash = await safeMove(fromEntryPath, toEntryPath, {
2422
+ force: options.force,
2423
+ expectedHash: fileState?.hash,
2424
+ acceptChanges: options.acceptChanges,
2425
+ normalization: config.hashing?.normalization,
2426
+ owner,
2427
+ actualOwner: fileState?.owner,
2428
+ signatures
2429
+ });
2430
+ // Update internal content (signatures, component names) if renaming
2431
+ if (fromName && toName && fromName !== toName) {
2432
+ let content = await promises.readFile(toEntryPath, 'utf-8');
2433
+ let hasChanged = false;
2434
+ // Simple replacement of component names
2435
+ if (content.includes(fromName)) {
2436
+ content = content.replace(new RegExp(fromName, 'g'), toName);
2437
+ hasChanged = true;
2438
+ }
2439
+ // Also handle lowercase class names if any
2440
+ const fromLower = fromName.toLowerCase();
2441
+ const toLower = toName.toLowerCase();
2442
+ if (content.includes(fromLower)) {
2443
+ content = content.replace(new RegExp(fromLower, 'g'), toLower);
2444
+ hasChanged = true;
2445
+ }
2446
+ if (hasChanged) {
2447
+ await promises.writeFile(toEntryPath, content, 'utf-8');
2448
+ // Re-calculate hash after content update
2449
+ const updatedHash = calculateHash(content, config.hashing?.normalization);
2450
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2451
+ if (fileState) {
2452
+ state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2453
+ delete state.files[normalizedFromRelative];
2454
+ }
2455
+ }
2456
+ else {
2457
+ // Update state for each file moved normally
2458
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2459
+ if (fileState) {
2460
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2461
+ delete state.files[normalizedFromRelative];
2462
+ }
2463
+ }
2464
+ }
2465
+ else {
2466
+ // Update state for each file moved normally
2467
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2468
+ if (fileState) {
2469
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2470
+ delete state.files[normalizedFromRelative];
2471
+ }
2472
+ }
2473
+ }
2474
+ }
2475
+ const remainingFiles = await promises.readdir(fromPath);
2476
+ if (remainingFiles.length === 0) {
2477
+ await promises.rmdir(fromPath);
2478
+ }
2479
+ }
2480
+ /**
2481
+ * Scans the project and replaces imports of a moved/renamed item.
2482
+ */
2483
+ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2484
+ const { fromPath: fromItemPath, fromName, type } = fromInfo;
2485
+ const { toPath: toItemPath, toName } = toInfo;
2486
+ const allFiles = new Set();
2487
+ await scanDirectory(process.cwd(), allFiles);
2488
+ const rootPath = resolvePath(config, type === 'component' ? 'components' : 'features');
2489
+ for (const relPath of allFiles) {
2490
+ const fullPath = path.resolve(process.cwd(), relPath);
2491
+ // Skip the moved directory itself
2492
+ const toFullPath = path.resolve(toItemPath);
2493
+ if (fullPath.startsWith(toFullPath))
2494
+ continue;
2495
+ let content = await promises.readFile(fullPath, 'utf-8');
2496
+ let changed = false;
2497
+ const aliasBase = config.importAliases[type === 'component' ? 'components' : 'features'];
2498
+ const ext = type === 'component' ? '' : (config.naming.featureExtension === '.astro' ? '.astro' : '');
2499
+ if (aliasBase) {
2500
+ const oldAlias = `${aliasBase}/${fromItemPath}`;
2501
+ const newAlias = `${aliasBase}/${toItemPath}`;
2502
+ const oldFullImport = `from '${oldAlias}/${fromName}${ext}'`;
2503
+ const newFullImport = `from '${newAlias}/${toName}${ext}'`;
2504
+ if (content.includes(oldFullImport)) {
2505
+ content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2506
+ changed = true;
2507
+ }
2508
+ else if (content.includes(oldAlias)) {
2509
+ content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2510
+ changed = true;
2511
+ }
2512
+ }
2513
+ else {
2514
+ const oldDir = path.resolve(rootPath, fromItemPath);
2515
+ const newDir = path.resolve(rootPath, toItemPath);
2516
+ const oldRelPath = getRelativeImportPath(fullPath, oldDir);
2517
+ const newRelPath = getRelativeImportPath(fullPath, newDir);
2518
+ const oldImport = `'${oldRelPath}/${fromName}${ext}'`;
2519
+ const newImport = `'${newRelPath}/${toName}${ext}'`;
2520
+ if (content.includes(oldImport)) {
2521
+ content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2522
+ changed = true;
2523
+ }
2524
+ }
2525
+ if (fromName !== toName && changed) {
2526
+ content = content.replace(new RegExp(`\\b${fromName}\\b`, 'g'), toName);
2527
+ }
2528
+ if (changed) {
2529
+ if (options.dryRun) {
2530
+ console.log(` [Scan] Would update imports in ${relPath}`);
2531
+ }
2532
+ else {
2533
+ await promises.writeFile(fullPath, content, 'utf-8');
2534
+ console.log(` [Scan] Updated imports in ${relPath}`);
2535
+ if (state.files[relPath]) {
2536
+ state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2537
+ }
2538
+ }
2539
+ }
2540
+ }
2541
+ }
2542
+
2353
2543
  /**
2354
2544
  * Move a section (route + feature).
2355
2545
  *
@@ -2374,40 +2564,53 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2374
2564
  let actualFromFeature = fromFeature;
2375
2565
  let actualToRoute = toRoute;
2376
2566
  let actualToFeature = toFeature;
2377
- // Shift arguments if using state
2567
+ // Shift arguments if using state or if called with fewer arguments
2378
2568
  if (!toRoute && fromRoute && fromFeature) {
2379
2569
  // textor move-section /old-route /new-route
2380
- const section = findSection(state, fromRoute);
2570
+ actualFromRoute = fromRoute;
2571
+ actualToRoute = fromFeature;
2572
+ actualFromFeature = undefined;
2573
+ actualToFeature = undefined;
2574
+ }
2575
+ // Lookup missing info from state
2576
+ if (actualFromRoute && !actualFromFeature) {
2577
+ const section = findSection(state, actualFromRoute);
2381
2578
  if (section) {
2382
- actualFromRoute = section.route;
2383
2579
  actualFromFeature = section.featurePath;
2384
- actualToRoute = fromFeature; // the second argument was actually the new route
2385
- actualToFeature = toRoute; // which is null
2386
- // If toFeature is not provided, try to derive it from the new route
2387
- if (!actualToFeature && actualToRoute) {
2388
- const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
2389
- const newRouteParts = actualToRoute.split('/').filter(Boolean);
2390
- const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
2391
- // If the feature path starts with the old route parts, replace them
2392
- // We compare case-insensitively or via PascalCase to be more helpful
2393
- let match = true;
2394
- for (let i = 0; i < oldRouteParts.length; i++) {
2395
- const routePart = oldRouteParts[i].toLowerCase();
2396
- const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2397
- if (featurePart !== routePart) {
2398
- match = false;
2399
- break;
2400
- }
2401
- }
2402
- if (match && oldRouteParts.length > 0) {
2403
- actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
2404
- }
2405
- else {
2406
- // Otherwise just keep it the same
2407
- actualToFeature = actualFromFeature;
2408
- }
2580
+ }
2581
+ }
2582
+ else if (!actualFromRoute && actualFromFeature) {
2583
+ const section = findSection(state, actualFromFeature);
2584
+ if (section) {
2585
+ actualFromRoute = section.route;
2586
+ }
2587
+ }
2588
+ // If toFeature is not provided, try to derive it from the new route if route moved
2589
+ if (!actualToFeature && actualToRoute && actualFromRoute && actualFromRoute !== actualToRoute && actualFromFeature) {
2590
+ const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
2591
+ const newRouteParts = actualToRoute.split('/').filter(Boolean);
2592
+ const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
2593
+ let match = true;
2594
+ for (let i = 0; i < oldRouteParts.length; i++) {
2595
+ const routePart = oldRouteParts[i].toLowerCase();
2596
+ const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2597
+ if (featurePart !== routePart) {
2598
+ match = false;
2599
+ break;
2409
2600
  }
2410
2601
  }
2602
+ if (match && oldRouteParts.length > 0) {
2603
+ actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
2604
+ }
2605
+ else {
2606
+ actualToFeature = actualFromFeature;
2607
+ }
2608
+ }
2609
+ else if (!actualToFeature) {
2610
+ actualToFeature = actualFromFeature;
2611
+ }
2612
+ if (!actualToRoute) {
2613
+ actualToRoute = actualFromRoute;
2411
2614
  }
2412
2615
  const isRouteOnly = options.keepFeature || (!actualToFeature && actualToRoute && !actualFromFeature);
2413
2616
  if (isRouteOnly && !actualToRoute) {
@@ -2432,12 +2635,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2432
2635
  mode: config.routing.mode,
2433
2636
  indexFile: config.routing.indexFile
2434
2637
  });
2435
- const fromRoutePath = secureJoin(pagesRoot, fromRouteFile);
2436
- const toRoutePath = secureJoin(pagesRoot, toRouteFile);
2638
+ const fromRoutePath = fromRouteFile ? secureJoin(pagesRoot, fromRouteFile) : null;
2639
+ const toRoutePath = toRouteFile ? secureJoin(pagesRoot, toRouteFile) : null;
2437
2640
  const movedFiles = [];
2438
2641
  if (options.dryRun) {
2439
2642
  console.log('Dry run - would move:');
2440
- console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
2643
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
2644
+ console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
2645
+ }
2441
2646
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature) {
2442
2647
  const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
2443
2648
  const toFeaturePath = secureJoin(featuresRoot, normalizedToFeature);
@@ -2445,81 +2650,90 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2445
2650
  }
2446
2651
  return;
2447
2652
  }
2448
- const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
2449
- const routeFileState = state.files[normalizedFromRouteRelative];
2450
- const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
2451
- force: options.force,
2452
- expectedHash: routeFileState?.hash,
2453
- acceptChanges: options.acceptChanges,
2454
- owner: normalizedFromRoute,
2455
- actualOwner: routeFileState?.owner,
2456
- signatures: configSignatures
2457
- });
2458
- movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2459
- // Update state for moved route file
2460
- const normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2461
- if (routeFileState) {
2462
- state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2463
- delete state.files[normalizedFromRouteRelative];
2464
- }
2465
- // Update imports in the moved route file
2466
- const targetFeature = normalizedToFeature || normalizedFromFeature;
2467
- if (targetFeature) {
2468
- const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2469
- const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2470
- const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2471
- const toFeatureComponentName = getFeatureComponentName(targetFeature);
2472
- // First, update all relative imports in the file because it moved
2473
- await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2474
- let content = await promises.readFile(toRoutePath, 'utf-8');
2475
- let changed = false;
2476
- // Update component name in JSX tags
2477
- if (fromFeatureComponentName !== toFeatureComponentName) {
2478
- content = content.replace(new RegExp(`<${fromFeatureComponentName}`, 'g'), `<${toFeatureComponentName}`);
2479
- content = content.replace(new RegExp(`</${fromFeatureComponentName}`, 'g'), `</${toFeatureComponentName}`);
2480
- changed = true;
2653
+ let normalizedToRouteRelative = null;
2654
+ if (fromRoutePath && toRoutePath) {
2655
+ const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
2656
+ const routeFileState = state.files[normalizedFromRouteRelative];
2657
+ const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
2658
+ force: options.force,
2659
+ expectedHash: routeFileState?.hash,
2660
+ acceptChanges: options.acceptChanges,
2661
+ owner: normalizedFromRoute,
2662
+ actualOwner: routeFileState?.owner,
2663
+ signatures: configSignatures
2664
+ });
2665
+ if (fromRoutePath !== toRoutePath) {
2666
+ movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2667
+ }
2668
+ // Update state for moved route file
2669
+ normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2670
+ if (routeFileState) {
2671
+ state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2672
+ if (fromRoutePath !== toRoutePath) {
2673
+ delete state.files[normalizedFromRouteRelative];
2674
+ }
2481
2675
  }
2482
- if (config.importAliases.features) {
2483
- const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2484
- const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2485
- // Flexible regex to match import identifier and path with alias
2486
- const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2487
- if (importRegex.test(content)) {
2488
- content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2489
- let newSubPath = subPath || '';
2490
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2491
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2492
- }
2493
- return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2494
- });
2676
+ // Update imports in the route file (even if it didn't move, as feature might have)
2677
+ const targetFeature = normalizedToFeature || normalizedFromFeature;
2678
+ if (targetFeature && fs.existsSync(toRoutePath)) {
2679
+ const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2680
+ const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2681
+ const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2682
+ const toFeatureComponentName = getFeatureComponentName(targetFeature);
2683
+ // First, update all relative imports in the file because it moved (or stayed)
2684
+ await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2685
+ let content = await promises.readFile(toRoutePath, 'utf-8');
2686
+ let changed = false;
2687
+ // Update component name in JSX tags
2688
+ if (fromFeatureComponentName !== toFeatureComponentName) {
2689
+ content = content.replace(new RegExp(`<${fromFeatureComponentName}`, 'g'), `<${toFeatureComponentName}`);
2690
+ content = content.replace(new RegExp(`</${fromFeatureComponentName}`, 'g'), `</${toFeatureComponentName}`);
2495
2691
  changed = true;
2496
2692
  }
2497
- else if (content.includes(oldAliasPath)) {
2498
- // Fallback for path only replacement
2499
- content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2500
- changed = true;
2693
+ if (config.importAliases.features) {
2694
+ const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2695
+ const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2696
+ // Flexible regex to match import identifier and path with alias
2697
+ const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2698
+ if (importRegex.test(content)) {
2699
+ content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2700
+ let newSubPath = subPath || '';
2701
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2702
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2703
+ }
2704
+ return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2705
+ });
2706
+ changed = true;
2707
+ }
2708
+ else if (content.includes(oldAliasPath)) {
2709
+ // Fallback for path only replacement
2710
+ content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2711
+ changed = true;
2712
+ }
2501
2713
  }
2502
- }
2503
- else {
2504
- const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2505
- const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2506
- // Flexible regex for relative imports
2507
- const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2508
- if (relImportRegex.test(content)) {
2509
- content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2510
- let newSubPath = subPath || '';
2511
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2512
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2513
- }
2514
- return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2515
- });
2516
- changed = true;
2714
+ else {
2715
+ const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2716
+ const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2717
+ // Flexible regex for relative imports
2718
+ const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2719
+ if (relImportRegex.test(content)) {
2720
+ content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2721
+ let newSubPath = subPath || '';
2722
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2723
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2724
+ }
2725
+ return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2726
+ });
2727
+ changed = true;
2728
+ }
2729
+ }
2730
+ if (changed) {
2731
+ await promises.writeFile(toRoutePath, content, 'utf-8');
2732
+ // Update hash in state after changes
2733
+ if (state.files[normalizedToRouteRelative]) {
2734
+ state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
2735
+ }
2517
2736
  }
2518
- }
2519
- if (changed) {
2520
- await promises.writeFile(toRoutePath, content, 'utf-8');
2521
- // Update hash in state after changes
2522
- state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
2523
2737
  }
2524
2738
  }
2525
2739
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature && normalizedFromFeature !== normalizedToFeature) {
@@ -2541,14 +2755,17 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2541
2755
  }
2542
2756
  if (options.scan && (normalizedFromFeature || normalizedToFeature)) {
2543
2757
  await scanAndReplaceImports(config, state, {
2544
- fromFeaturePath: normalizedFromFeature,
2545
- fromComponentName: getFeatureComponentName(normalizedFromFeature)
2758
+ fromPath: normalizedFromFeature,
2759
+ fromName: getFeatureComponentName(normalizedFromFeature),
2760
+ type: 'feature'
2546
2761
  }, {
2547
- toFeaturePath: normalizedToFeature || normalizedFromFeature,
2548
- toComponentName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2762
+ toPath: normalizedToFeature || normalizedFromFeature,
2763
+ toName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2549
2764
  }, options);
2550
2765
  }
2551
- await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
2766
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
2767
+ await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
2768
+ }
2552
2769
  console.log('✓ Moved:');
2553
2770
  movedFiles.forEach(item => {
2554
2771
  console.log(` ${item.from}`);
@@ -2556,6 +2773,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2556
2773
  });
2557
2774
  if (movedFiles.length > 0) {
2558
2775
  const existingSection = fromSection;
2776
+ // Update ownership in state if route moved
2777
+ if (normalizedFromRoute && normalizedToRoute && normalizedFromRoute !== normalizedToRoute) {
2778
+ for (const f in state.files) {
2779
+ if (state.files[f].owner === normalizedFromRoute) {
2780
+ state.files[f].owner = normalizedToRoute;
2781
+ }
2782
+ }
2783
+ }
2559
2784
  // Update section data in state
2560
2785
  state.sections = state.sections.filter(s => s.route !== normalizedFromRoute);
2561
2786
  state.sections.push({
@@ -2576,182 +2801,6 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2576
2801
  throw error;
2577
2802
  }
2578
2803
  }
2579
- async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2580
- const { fromFeaturePath, fromComponentName } = fromInfo;
2581
- const { toFeaturePath, toComponentName } = toInfo;
2582
- const allFiles = new Set();
2583
- await scanDirectory(process.cwd(), allFiles);
2584
- const featuresRoot = resolvePath(config, 'features');
2585
- for (const relPath of allFiles) {
2586
- const fullPath = path.join(process.cwd(), relPath);
2587
- // Skip the moved directory itself as it was already handled
2588
- if (fullPath.startsWith(path.resolve(toFeaturePath)))
2589
- continue;
2590
- let content = await promises.readFile(fullPath, 'utf-8');
2591
- let changed = false;
2592
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
2593
- // Handle Aliases
2594
- if (config.importAliases.features) {
2595
- const oldAlias = `${config.importAliases.features}/${fromFeaturePath}`;
2596
- const newAlias = `${config.importAliases.features}/${toFeaturePath}`;
2597
- // Update component name and path if both changed
2598
- const oldFullImport = `from '${oldAlias}/${fromComponentName}${ext}'`;
2599
- const newFullImport = `from '${newAlias}/${toComponentName}${ext}'`;
2600
- if (content.includes(oldFullImport)) {
2601
- content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2602
- changed = true;
2603
- }
2604
- else if (content.includes(oldAlias)) {
2605
- content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2606
- changed = true;
2607
- }
2608
- }
2609
- else {
2610
- // Handle Relative Imports (more complex)
2611
- // This is best-effort: we look for imports that resolve to the old feature path
2612
- const fromFeatureDir = secureJoin(featuresRoot, fromFeaturePath);
2613
- const toFeatureDir = secureJoin(featuresRoot, toFeaturePath);
2614
- const oldRelPath = getRelativeImportPath(fullPath, fromFeatureDir);
2615
- const newRelPath = getRelativeImportPath(fullPath, toFeatureDir);
2616
- const oldImport = `'${oldRelPath}/${fromComponentName}${ext}'`;
2617
- const newImport = `'${newRelPath}/${toComponentName}${ext}'`;
2618
- if (content.includes(oldImport)) {
2619
- content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2620
- changed = true;
2621
- }
2622
- }
2623
- // Update component name in JSX and imports if it changed
2624
- if (fromComponentName !== toComponentName && changed) {
2625
- content = content.replace(new RegExp(`\\b${fromComponentName}\\b`, 'g'), toComponentName);
2626
- }
2627
- if (changed) {
2628
- if (options.dryRun) {
2629
- console.log(` [Scan] Would update imports in ${relPath}`);
2630
- }
2631
- else {
2632
- await promises.writeFile(fullPath, content, 'utf-8');
2633
- console.log(` [Scan] Updated imports in ${relPath}`);
2634
- // Update state hash if this file is managed
2635
- if (state.files[relPath]) {
2636
- state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2637
- }
2638
- }
2639
- }
2640
- }
2641
- }
2642
- async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2643
- const { fromName, toName, owner = null } = options;
2644
- if (!fs.existsSync(fromPath)) {
2645
- throw new Error(`Source directory not found: ${fromPath}`);
2646
- }
2647
- if (fs.existsSync(toPath) && !options.force) {
2648
- throw new Error(`Destination already exists: ${toPath}\n` +
2649
- `Use --force to overwrite.`);
2650
- }
2651
- await ensureDir(toPath);
2652
- const entries = await promises.readdir(fromPath);
2653
- for (const entry of entries) {
2654
- let targetEntry = entry;
2655
- // Rename files if they match the component name
2656
- if (fromName && toName && fromName !== toName) {
2657
- if (entry.includes(fromName)) {
2658
- targetEntry = entry.replace(fromName, toName);
2659
- }
2660
- }
2661
- const fromEntryPath = path.join(fromPath, entry);
2662
- const toEntryPath = path.join(toPath, targetEntry);
2663
- const stats = await promises.stat(fromEntryPath);
2664
- if (stats.isDirectory()) {
2665
- await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2666
- }
2667
- else {
2668
- const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2669
- const fileState = state.files[normalizedFromRelative];
2670
- const newHash = await safeMove(fromEntryPath, toEntryPath, {
2671
- force: options.force,
2672
- expectedHash: fileState?.hash,
2673
- acceptChanges: options.acceptChanges,
2674
- normalization: config.hashing?.normalization,
2675
- owner,
2676
- actualOwner: fileState?.owner
2677
- });
2678
- // Update internal content (signatures, component names) if renaming
2679
- if (fromName && toName && fromName !== toName) {
2680
- let content = await promises.readFile(toEntryPath, 'utf-8');
2681
- let hasChanged = false;
2682
- // Simple replacement of component names
2683
- if (content.includes(fromName)) {
2684
- content = content.replace(new RegExp(fromName, 'g'), toName);
2685
- hasChanged = true;
2686
- }
2687
- // Also handle lowercase class names if any
2688
- const fromLower = fromName.toLowerCase();
2689
- const toLower = toName.toLowerCase();
2690
- if (content.includes(fromLower)) {
2691
- content = content.replace(new RegExp(fromLower, 'g'), toLower);
2692
- hasChanged = true;
2693
- }
2694
- if (hasChanged) {
2695
- await promises.writeFile(toEntryPath, content, 'utf-8');
2696
- // Re-calculate hash after content update
2697
- const updatedHash = calculateHash(content, config.hashing?.normalization);
2698
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2699
- if (fileState) {
2700
- state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2701
- delete state.files[normalizedFromRelative];
2702
- }
2703
- }
2704
- else {
2705
- // Update state for each file moved normally
2706
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2707
- if (fileState) {
2708
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2709
- delete state.files[normalizedFromRelative];
2710
- }
2711
- }
2712
- }
2713
- else {
2714
- // Update state for each file moved normally
2715
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2716
- if (fileState) {
2717
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2718
- delete state.files[normalizedFromRelative];
2719
- }
2720
- }
2721
- }
2722
- }
2723
- const remainingFiles = await promises.readdir(fromPath);
2724
- if (remainingFiles.length === 0) {
2725
- await promises.rmdir(fromPath);
2726
- }
2727
- }
2728
- async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2729
- if (!fs.existsSync(filePath))
2730
- return;
2731
- let content = await promises.readFile(filePath, 'utf-8');
2732
- const oldDir = path.dirname(oldFilePath);
2733
- const newDir = path.dirname(newFilePath);
2734
- if (oldDir === newDir)
2735
- return;
2736
- // Find all relative imports
2737
- const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2738
- let match;
2739
- const replacements = [];
2740
- while ((match = relativeImportRegex.exec(content)) !== null) {
2741
- const relativePath = match[1];
2742
- const absoluteTarget = path.resolve(oldDir, relativePath);
2743
- const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2744
- replacements.push({
2745
- full: match[0],
2746
- oldRel: relativePath,
2747
- newRel: newRelativePath
2748
- });
2749
- }
2750
- for (const repl of replacements) {
2751
- content = content.replace(repl.full, `from '${repl.newRel}'`);
2752
- }
2753
- await promises.writeFile(filePath, content, 'utf-8');
2754
- }
2755
2804
 
2756
2805
  async function createComponentCommand(componentName, options) {
2757
2806
  try {
@@ -2823,30 +2872,37 @@ async function createComponentCommand(componentName, options) {
2823
2872
  console.log(` Sub-components: ${subComponentsDir}/`);
2824
2873
  return;
2825
2874
  }
2826
- await ensureNotExists(componentFilePath, options.force);
2827
- await ensureNotExists(indexFilePath, options.force);
2828
- if (shouldCreateContext)
2829
- await ensureNotExists(contextFilePath, options.force);
2830
- if (shouldCreateHook)
2831
- await ensureNotExists(hookFilePath, options.force);
2832
- if (shouldCreateTests)
2833
- await ensureNotExists(testFilePath, options.force);
2834
- if (shouldCreateConfig)
2835
- await ensureNotExists(configFilePath, options.force);
2836
- if (shouldCreateConstants)
2837
- await ensureNotExists(constantsFilePath, options.force);
2838
- if (shouldCreateTypes)
2839
- await ensureNotExists(typesFilePath, options.force);
2840
- if (shouldCreateApi)
2841
- await ensureNotExists(apiFilePath, options.force);
2842
- if (shouldCreateServices)
2843
- await ensureNotExists(servicesFilePath, options.force);
2844
- if (shouldCreateSchemas)
2845
- await ensureNotExists(schemasFilePath, options.force);
2846
- if (shouldCreateReadme)
2847
- await ensureNotExists(readmeFilePath, options.force);
2848
- if (shouldCreateStories)
2849
- await ensureNotExists(storiesFilePath, options.force);
2875
+ const componentExists = fs.existsSync(componentFilePath);
2876
+ if (componentExists && !options.force) {
2877
+ console.log(`ℹ Component already exists at ${componentFilePath}. Entering additive mode.`);
2878
+ }
2879
+ // Check sub-items only if not in force mode
2880
+ if (!options.force) {
2881
+ if (fs.existsSync(indexFilePath))
2882
+ console.log(` - Skipping existing index: ${indexFilePath}`);
2883
+ if (shouldCreateContext && fs.existsSync(contextFilePath))
2884
+ console.log(` - Skipping existing context: ${contextFilePath}`);
2885
+ if (shouldCreateHook && fs.existsSync(hookFilePath))
2886
+ console.log(` - Skipping existing hook: ${hookFilePath}`);
2887
+ if (shouldCreateTests && fs.existsSync(testFilePath))
2888
+ console.log(` - Skipping existing test: ${testFilePath}`);
2889
+ if (shouldCreateConfig && fs.existsSync(configFilePath))
2890
+ console.log(` - Skipping existing config: ${configFilePath}`);
2891
+ if (shouldCreateConstants && fs.existsSync(constantsFilePath))
2892
+ console.log(` - Skipping existing constants: ${constantsFilePath}`);
2893
+ if (shouldCreateTypes && fs.existsSync(typesFilePath))
2894
+ console.log(` - Skipping existing types: ${typesFilePath}`);
2895
+ if (shouldCreateApi && fs.existsSync(apiFilePath))
2896
+ console.log(` - Skipping existing api: ${apiFilePath}`);
2897
+ if (shouldCreateServices && fs.existsSync(servicesFilePath))
2898
+ console.log(` - Skipping existing services: ${servicesFilePath}`);
2899
+ if (shouldCreateSchemas && fs.existsSync(schemasFilePath))
2900
+ console.log(` - Skipping existing schemas: ${schemasFilePath}`);
2901
+ if (shouldCreateReadme && fs.existsSync(readmeFilePath))
2902
+ console.log(` - Skipping existing readme: ${readmeFilePath}`);
2903
+ if (shouldCreateStories && fs.existsSync(storiesFilePath))
2904
+ console.log(` - Skipping existing stories: ${storiesFilePath}`);
2905
+ }
2850
2906
  await ensureDir(componentDir);
2851
2907
  if (shouldCreateSubComponentsDir)
2852
2908
  await ensureDir(subComponentsDir);
@@ -2870,24 +2926,29 @@ async function createComponentCommand(componentName, options) {
2870
2926
  await ensureDir(schemasDirInside);
2871
2927
  const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
2872
2928
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
2873
- const componentHash = await writeFileWithSignature(componentFilePath, componentContent, signature, config.hashing?.normalization);
2874
- await registerFile(componentFilePath, {
2875
- kind: 'component',
2876
- template: 'component',
2877
- hash: componentHash,
2878
- owner: normalizedName
2879
- });
2880
- const writtenFiles = [componentFilePath];
2881
- const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
2882
- const indexHash = await writeFileWithSignature(indexFilePath, indexContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2883
- await registerFile(indexFilePath, {
2884
- kind: 'component-file',
2885
- template: 'index',
2886
- hash: indexHash,
2887
- owner: normalizedName
2888
- });
2889
- writtenFiles.push(indexFilePath);
2890
- if (shouldCreateTypes) {
2929
+ const writtenFiles = [];
2930
+ if (!componentExists || options.force) {
2931
+ const componentHash = await writeFileWithSignature(componentFilePath, componentContent, signature, config.hashing?.normalization);
2932
+ await registerFile(componentFilePath, {
2933
+ kind: 'component',
2934
+ template: 'component',
2935
+ hash: componentHash,
2936
+ owner: normalizedName
2937
+ });
2938
+ writtenFiles.push(componentFilePath);
2939
+ }
2940
+ if (!fs.existsSync(indexFilePath) || options.force) {
2941
+ const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
2942
+ const indexHash = await writeFileWithSignature(indexFilePath, indexContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2943
+ await registerFile(indexFilePath, {
2944
+ kind: 'component-file',
2945
+ template: 'index',
2946
+ hash: indexHash,
2947
+ owner: normalizedName
2948
+ });
2949
+ writtenFiles.push(indexFilePath);
2950
+ }
2951
+ if (shouldCreateTypes && (!fs.existsSync(typesFilePath) || options.force)) {
2891
2952
  const typesContent = generateTypesTemplate(normalizedName);
2892
2953
  const hash = await writeFileWithSignature(typesFilePath, typesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2893
2954
  await registerFile(typesFilePath, {
@@ -2898,7 +2959,7 @@ async function createComponentCommand(componentName, options) {
2898
2959
  });
2899
2960
  writtenFiles.push(typesFilePath);
2900
2961
  }
2901
- if (shouldCreateContext) {
2962
+ if (shouldCreateContext && (!fs.existsSync(contextFilePath) || options.force)) {
2902
2963
  const contextContent = generateContextTemplate(normalizedName);
2903
2964
  const hash = await writeFileWithSignature(contextFilePath, contextContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2904
2965
  await registerFile(contextFilePath, {
@@ -2909,7 +2970,7 @@ async function createComponentCommand(componentName, options) {
2909
2970
  });
2910
2971
  writtenFiles.push(contextFilePath);
2911
2972
  }
2912
- if (shouldCreateHook) {
2973
+ if (shouldCreateHook && (!fs.existsSync(hookFilePath) || options.force)) {
2913
2974
  const hookName = getHookFunctionName(normalizedName);
2914
2975
  const hookContent = generateHookTemplate(normalizedName, hookName);
2915
2976
  const hash = await writeFileWithSignature(hookFilePath, hookContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -2921,7 +2982,7 @@ async function createComponentCommand(componentName, options) {
2921
2982
  });
2922
2983
  writtenFiles.push(hookFilePath);
2923
2984
  }
2924
- if (shouldCreateTests) {
2985
+ if (shouldCreateTests && (!fs.existsSync(testFilePath) || options.force)) {
2925
2986
  const relativeComponentPath = `../${normalizedName}${config.naming.componentExtension}`;
2926
2987
  const testContent = generateTestTemplate(normalizedName, relativeComponentPath);
2927
2988
  const hash = await writeFileWithSignature(testFilePath, testContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -2933,7 +2994,7 @@ async function createComponentCommand(componentName, options) {
2933
2994
  });
2934
2995
  writtenFiles.push(testFilePath);
2935
2996
  }
2936
- if (shouldCreateConfig) {
2997
+ if (shouldCreateConfig && (!fs.existsSync(configFilePath) || options.force)) {
2937
2998
  const configContent = generateConfigTemplate(normalizedName);
2938
2999
  const hash = await writeFileWithSignature(configFilePath, configContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2939
3000
  await registerFile(configFilePath, {
@@ -2944,7 +3005,7 @@ async function createComponentCommand(componentName, options) {
2944
3005
  });
2945
3006
  writtenFiles.push(configFilePath);
2946
3007
  }
2947
- if (shouldCreateConstants) {
3008
+ if (shouldCreateConstants && (!fs.existsSync(constantsFilePath) || options.force)) {
2948
3009
  const constantsContent = generateConstantsTemplate(normalizedName);
2949
3010
  const hash = await writeFileWithSignature(constantsFilePath, constantsContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2950
3011
  await registerFile(constantsFilePath, {
@@ -2955,7 +3016,7 @@ async function createComponentCommand(componentName, options) {
2955
3016
  });
2956
3017
  writtenFiles.push(constantsFilePath);
2957
3018
  }
2958
- if (shouldCreateApi) {
3019
+ if (shouldCreateApi && (!fs.existsSync(apiFilePath) || options.force)) {
2959
3020
  const apiContent = generateApiTemplate(normalizedName);
2960
3021
  const hash = await writeFileWithSignature(apiFilePath, apiContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2961
3022
  await registerFile(apiFilePath, {
@@ -2966,7 +3027,7 @@ async function createComponentCommand(componentName, options) {
2966
3027
  });
2967
3028
  writtenFiles.push(apiFilePath);
2968
3029
  }
2969
- if (shouldCreateServices) {
3030
+ if (shouldCreateServices && (!fs.existsSync(servicesFilePath) || options.force)) {
2970
3031
  const servicesContent = generateServiceTemplate(normalizedName);
2971
3032
  const hash = await writeFileWithSignature(servicesFilePath, servicesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2972
3033
  await registerFile(servicesFilePath, {
@@ -2977,7 +3038,7 @@ async function createComponentCommand(componentName, options) {
2977
3038
  });
2978
3039
  writtenFiles.push(servicesFilePath);
2979
3040
  }
2980
- if (shouldCreateSchemas) {
3041
+ if (shouldCreateSchemas && (!fs.existsSync(schemasFilePath) || options.force)) {
2981
3042
  const schemasContent = generateSchemaTemplate(normalizedName);
2982
3043
  const hash = await writeFileWithSignature(schemasFilePath, schemasContent, getSignature(config, 'typescript'), config.hashing?.normalization);
2983
3044
  await registerFile(schemasFilePath, {
@@ -2988,7 +3049,7 @@ async function createComponentCommand(componentName, options) {
2988
3049
  });
2989
3050
  writtenFiles.push(schemasFilePath);
2990
3051
  }
2991
- if (shouldCreateReadme) {
3052
+ if (shouldCreateReadme && (!fs.existsSync(readmeFilePath) || options.force)) {
2992
3053
  const readmeContent = generateReadmeTemplate(normalizedName);
2993
3054
  const hash = await writeFileWithSignature(readmeFilePath, readmeContent, getSignature(config, 'astro'), config.hashing?.normalization);
2994
3055
  await registerFile(readmeFilePath, {
@@ -2999,7 +3060,7 @@ async function createComponentCommand(componentName, options) {
2999
3060
  });
3000
3061
  writtenFiles.push(readmeFilePath);
3001
3062
  }
3002
- if (shouldCreateStories) {
3063
+ if (shouldCreateStories && (!fs.existsSync(storiesFilePath) || options.force)) {
3003
3064
  const relativePath = `./${normalizedName}${config.naming.componentExtension}`;
3004
3065
  const storiesContent = generateStoriesTemplate(normalizedName, relativePath);
3005
3066
  const hash = await writeFileWithSignature(storiesFilePath, storiesContent, getSignature(config, 'typescript'), config.hashing?.normalization);
@@ -3794,6 +3855,205 @@ async function normalizeStateCommand(options) {
3794
3855
  }
3795
3856
  }
3796
3857
 
3858
+ /**
3859
+ * Removes missing references from Textor state.
3860
+ * @param {Object} options
3861
+ * @param {boolean} options.dryRun
3862
+ * @param {boolean} options.yes
3863
+ */
3864
+ async function pruneMissingCommand(options = {}) {
3865
+ try {
3866
+ const config = await loadConfig();
3867
+ const state = await loadState();
3868
+ const results = await getProjectStatus(config, state);
3869
+ if (results.missing.length === 0) {
3870
+ console.log('No missing references found.');
3871
+ return;
3872
+ }
3873
+ console.log(`Found ${results.missing.length} missing references:`);
3874
+ results.missing.forEach(f => console.log(` - ${f}`));
3875
+ if (options.dryRun) {
3876
+ console.log('\nDry run: no changes applied to state.');
3877
+ return;
3878
+ }
3879
+ if (!options.yes && options.interactive !== false && process.stdin.isTTY && process.env.NODE_ENV !== 'test') {
3880
+ const rl = readline.createInterface({
3881
+ input: process.stdin,
3882
+ output: process.stdout
3883
+ });
3884
+ const confirmed = await new Promise(resolve => {
3885
+ rl.question('\nDo you want to proceed with pruning? (y/N) ', (answer) => {
3886
+ rl.close();
3887
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
3888
+ });
3889
+ });
3890
+ if (!confirmed) {
3891
+ console.log('Aborted.');
3892
+ return;
3893
+ }
3894
+ }
3895
+ for (const relPath of results.missing) {
3896
+ delete state.files[relPath];
3897
+ }
3898
+ // Reconstruct metadata
3899
+ state.components = reconstructComponents(state.files, config);
3900
+ state.sections = reconstructSections(state, config);
3901
+ await saveState(state);
3902
+ console.log(`\n✓ Successfully removed ${results.missing.length} missing references from state.`);
3903
+ }
3904
+ catch (error) {
3905
+ console.error('Error:', error.message);
3906
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3907
+ process.exit(1);
3908
+ }
3909
+ throw error;
3910
+ }
3911
+ }
3912
+
3913
+ /**
3914
+ * Dispatcher for rename commands.
3915
+ */
3916
+ async function renameCommand(type, oldName, newName, options) {
3917
+ try {
3918
+ if (!type || !oldName || !newName) {
3919
+ throw new Error('Usage: textor rename <route|feature|component> <oldName> <newName>');
3920
+ }
3921
+ if (type === 'route' || type === 'path') {
3922
+ const normalizedOld = normalizeRoute(oldName);
3923
+ const normalizedNew = normalizeRoute(newName);
3924
+ // By default, move-section will try to move the feature if it matches the route.
3925
+ // For a simple "rename route", we might want to keep that behavior or not.
3926
+ // Usually "rename route" means just the URL/file.
3927
+ return await moveSectionCommand(normalizedOld, undefined, normalizedNew, undefined, options);
3928
+ }
3929
+ if (type === 'feature') {
3930
+ const state = await loadState();
3931
+ const normalizedOld = featureToDirectoryPath(oldName);
3932
+ const normalizedNew = featureToDirectoryPath(newName);
3933
+ const section = findSection(state, normalizedOld);
3934
+ if (section) {
3935
+ // If it's a managed section, move it using section logic
3936
+ return await moveSectionCommand(section.route, section.featurePath, section.route, normalizedNew, options);
3937
+ }
3938
+ else {
3939
+ // Standalone feature move
3940
+ return await moveSectionCommand(undefined, normalizedOld, undefined, normalizedNew, options);
3941
+ }
3942
+ }
3943
+ if (type === 'component') {
3944
+ return await renameComponent(oldName, newName, options);
3945
+ }
3946
+ throw new Error(`Unknown rename type: ${type}. Supported types: route, feature, component.`);
3947
+ }
3948
+ catch (error) {
3949
+ console.error('Error:', error.message);
3950
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3951
+ process.exit(1);
3952
+ }
3953
+ throw error;
3954
+ }
3955
+ }
3956
+ /**
3957
+ * Specialized logic for renaming shared components.
3958
+ */
3959
+ async function renameComponent(oldName, newName, options) {
3960
+ const config = await loadConfig();
3961
+ const state = await loadState();
3962
+ const normalizedOldName = normalizeComponentName(oldName);
3963
+ const normalizedNewName = normalizeComponentName(newName);
3964
+ const component = findComponent(state, normalizedOldName);
3965
+ const componentsRoot = resolvePath(config, 'components');
3966
+ const fromPath = component
3967
+ ? path.resolve(process.cwd(), component.path)
3968
+ : path.join(componentsRoot, normalizedOldName);
3969
+ const toPath = path.join(componentsRoot, normalizedNewName);
3970
+ if (options.dryRun) {
3971
+ console.log(`Dry run - would rename component: ${normalizedOldName} -> ${normalizedNewName}`);
3972
+ console.log(` Path: ${fromPath} -> ${toPath}`);
3973
+ return;
3974
+ }
3975
+ const signatures = Object.values(config.signatures || {});
3976
+ await moveDirectory(fromPath, toPath, state, config, {
3977
+ ...options,
3978
+ fromName: normalizedOldName,
3979
+ toName: normalizedNewName,
3980
+ signatures
3981
+ });
3982
+ if (options.scan) {
3983
+ await scanAndReplaceImports(config, state, {
3984
+ fromPath: normalizedOldName,
3985
+ fromName: normalizedOldName,
3986
+ type: 'component'
3987
+ }, {
3988
+ toPath: normalizedNewName,
3989
+ toName: normalizedNewName
3990
+ }, options);
3991
+ }
3992
+ await cleanupEmptyDirs(path.dirname(fromPath), componentsRoot);
3993
+ // Update state metadata
3994
+ if (component) {
3995
+ component.name = normalizedNewName;
3996
+ component.path = path.relative(process.cwd(), toPath).replace(/\\/g, '/');
3997
+ }
3998
+ await saveState(state);
3999
+ console.log(`✓ Renamed component ${normalizedOldName} to ${normalizedNewName}`);
4000
+ }
4001
+
4002
+ /**
4003
+ * Add a new item (hook, api, service, etc.) to an existing feature or component.
4004
+ *
4005
+ * @param {string} itemType The type of item to add (e.g., 'api', 'hook', 'service')
4006
+ * @param {string} targetName The name of the feature or component
4007
+ * @param {Object} options Additional options from Commander
4008
+ */
4009
+ async function addItemCommand(itemType, targetName, options) {
4010
+ try {
4011
+ const state = await loadState();
4012
+ // Normalize itemType
4013
+ let normalizedItem = itemType.toLowerCase();
4014
+ if (normalizedItem === 'test')
4015
+ normalizedItem = 'tests';
4016
+ if (normalizedItem === 'service')
4017
+ normalizedItem = 'services';
4018
+ if (normalizedItem === 'schema')
4019
+ normalizedItem = 'schemas';
4020
+ if (normalizedItem === 'hook')
4021
+ normalizedItem = 'hooks'; // for add-section
4022
+ // Try to find as section (feature) first
4023
+ let section = findSection(state, targetName);
4024
+ let component = findComponent(state, targetName);
4025
+ // If not found by exact name, try to find by featurePath or part of it
4026
+ if (!section && !component) {
4027
+ section = state.sections.find(s => s.featurePath === targetName || s.featurePath.endsWith('/' + targetName));
4028
+ }
4029
+ if (!section && !component) {
4030
+ throw new Error(`Target not found in state: "${targetName}". Please use "add-section" or "create-component" directly if it's not managed by Textor.`);
4031
+ }
4032
+ const flags = { [normalizedItem]: true };
4033
+ // Also set singular for create-component which uses 'hook'
4034
+ if (normalizedItem === 'hooks')
4035
+ flags.hook = true;
4036
+ if (section) {
4037
+ console.log(`ℹ Adding ${normalizedItem} to feature: ${section.featurePath}`);
4038
+ return await addSectionCommand(undefined, section.featurePath, { ...options, ...flags });
4039
+ }
4040
+ if (component) {
4041
+ console.log(`ℹ Adding ${normalizedItem} to component: ${component.name}`);
4042
+ // For create-component, we might need to be careful with flags that are on by default
4043
+ // but getEffectiveOptions should handle it if we pass them explicitly as true.
4044
+ return await createComponentCommand(component.name, { ...options, ...flags });
4045
+ }
4046
+ }
4047
+ catch (error) {
4048
+ console.error('Error:', error.message);
4049
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4050
+ process.exit(1);
4051
+ }
4052
+ throw error;
4053
+ }
4054
+ }
4055
+
4056
+ exports.addItemCommand = addItemCommand;
3797
4057
  exports.addSectionCommand = addSectionCommand;
3798
4058
  exports.adoptCommand = adoptCommand;
3799
4059
  exports.createComponentCommand = createComponentCommand;
@@ -3801,8 +4061,10 @@ exports.initCommand = initCommand;
3801
4061
  exports.listSectionsCommand = listSectionsCommand;
3802
4062
  exports.moveSectionCommand = moveSectionCommand;
3803
4063
  exports.normalizeStateCommand = normalizeStateCommand;
4064
+ exports.pruneMissingCommand = pruneMissingCommand;
3804
4065
  exports.removeComponentCommand = removeComponentCommand;
3805
4066
  exports.removeSectionCommand = removeSectionCommand;
4067
+ exports.renameCommand = renameCommand;
3806
4068
  exports.statusCommand = statusCommand;
3807
4069
  exports.syncCommand = syncCommand;
3808
4070
  exports.upgradeConfigCommand = upgradeConfigCommand;