@oamm/textor 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -279,6 +279,20 @@ pnpm textor normalize-state
279
279
  **Options:**
280
280
  - `--dry-run`: Print the normalized state without writing it.
281
281
 
282
+ ### prune-missing
283
+ Remove missing references from Textor state. This command identifies files that are tracked in the state but are no longer present on disk and removes them from `.textor/state.json`.
284
+
285
+ **Safe-by-default**: This command never deletes any files from your disk; it only updates the state tracker.
286
+
287
+ ```bash
288
+ pnpm textor prune-missing
289
+ ```
290
+
291
+ **Options:**
292
+ - `--dry-run`: Preview what would be removed from the state without making changes.
293
+ - `--yes`: Skip confirmation prompt.
294
+ - `--no-interactive`: Disable interactive prompts (useful for CI).
295
+
282
296
  ## 🏗️ Technical Architecture
283
297
 
284
298
  Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import { createHash } from 'crypto';
7
7
  import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
+ import readline from 'readline';
9
10
 
10
11
  const CONFIG_DIR$1 = '.textor';
11
12
  const CONFIG_FILE = 'config.json';
@@ -3844,67 +3845,87 @@ async function validateStateCommand(options) {
3844
3845
  }
3845
3846
  }
3846
3847
 
3847
- async function statusCommand() {
3848
- try {
3849
- const config = await loadConfig();
3850
- const state = await loadState();
3851
-
3852
- const results = {
3853
- missing: [],
3854
- modified: [],
3855
- untracked: [], // Has signature, not in state
3856
- orphaned: [], // No signature, not in state
3857
- synced: 0
3858
- };
3848
+ /**
3849
+ * Computes the drift between the state and the actual files on disk.
3850
+ *
3851
+ * @param {import('./config.js').TextorConfig} config
3852
+ * @param {Object} state
3853
+ * @returns {Promise<{
3854
+ * missing: string[],
3855
+ * modified: string[],
3856
+ * untracked: string[],
3857
+ * orphaned: string[],
3858
+ * synced: number
3859
+ * }>}
3860
+ */
3861
+ async function getProjectStatus(config, state) {
3862
+ const results = {
3863
+ missing: [],
3864
+ modified: [],
3865
+ untracked: [], // Has signature, not in state
3866
+ orphaned: [], // No signature, not in state
3867
+ synced: 0
3868
+ };
3859
3869
 
3860
- const roots = [
3861
- resolvePath(config, 'pages'),
3862
- resolvePath(config, 'features'),
3863
- resolvePath(config, 'components')
3864
- ].map(p => path.resolve(p));
3870
+ const roots = [
3871
+ resolvePath(config, 'pages'),
3872
+ resolvePath(config, 'features'),
3873
+ resolvePath(config, 'components')
3874
+ ].map(p => path.resolve(p));
3865
3875
 
3866
- const diskFiles = new Set();
3867
- const configSignatures = Object.values(config.signatures || {});
3876
+ const diskFiles = new Set();
3877
+ const configSignatures = Object.values(config.signatures || {});
3868
3878
 
3869
- for (const root of roots) {
3870
- if (existsSync(root)) {
3871
- await scanDirectory(root, diskFiles);
3872
- }
3879
+ for (const root of roots) {
3880
+ if (existsSync(root)) {
3881
+ await scanDirectory(root, diskFiles);
3873
3882
  }
3883
+ }
3874
3884
 
3875
- // 1. Check state files against disk
3876
- for (const relativePath in state.files) {
3877
- const fullPath = path.join(process.cwd(), relativePath);
3878
-
3879
- if (!existsSync(fullPath)) {
3880
- results.missing.push(relativePath);
3881
- continue;
3882
- }
3885
+ // 1. Check state files against disk
3886
+ for (const relativePath in state.files) {
3887
+ const fullPath = path.join(process.cwd(), relativePath);
3888
+
3889
+ if (!existsSync(fullPath)) {
3890
+ results.missing.push(relativePath);
3891
+ continue;
3892
+ }
3883
3893
 
3884
- diskFiles.delete(relativePath);
3894
+ // It exists on disk, so it's not untracked/orphaned
3895
+ diskFiles.delete(relativePath);
3885
3896
 
3886
- const content = await readFile(fullPath, 'utf-8');
3887
- const currentHash = calculateHash(content, config.hashing?.normalization);
3888
- const fileData = state.files[relativePath];
3897
+ const content = await readFile(fullPath, 'utf-8');
3898
+ const currentHash = calculateHash(content, config.hashing?.normalization);
3899
+ const fileData = state.files[relativePath];
3889
3900
 
3890
- if (currentHash !== fileData.hash) {
3891
- results.modified.push(relativePath);
3892
- } else {
3893
- results.synced++;
3894
- }
3901
+ if (currentHash !== fileData.hash) {
3902
+ results.modified.push(relativePath);
3903
+ } else {
3904
+ results.synced++;
3895
3905
  }
3906
+ }
3896
3907
 
3897
- // 2. Check remaining disk files
3898
- for (const relativePath of diskFiles) {
3899
- const fullPath = path.join(process.cwd(), relativePath);
3900
- const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3901
-
3902
- if (isGenerated) {
3903
- results.untracked.push(relativePath);
3904
- } else {
3905
- results.orphaned.push(relativePath);
3906
- }
3908
+ // 2. Check remaining disk files
3909
+ for (const relativePath of diskFiles) {
3910
+ const fullPath = path.join(process.cwd(), relativePath);
3911
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3912
+
3913
+ if (isGenerated) {
3914
+ results.untracked.push(relativePath);
3915
+ } else {
3916
+ results.orphaned.push(relativePath);
3907
3917
  }
3918
+ }
3919
+
3920
+ return results;
3921
+ }
3922
+
3923
+ async function statusCommand() {
3924
+ try {
3925
+ const config = await loadConfig();
3926
+ const state = await loadState();
3927
+
3928
+ const results = await getProjectStatus(config, state);
3908
3929
 
3909
3930
  // Reporting
3910
3931
  console.log('Textor Status Report:');
@@ -4329,6 +4350,71 @@ async function normalizeStateCommand(options) {
4329
4350
  }
4330
4351
  }
4331
4352
 
4353
+ /**
4354
+ * Removes missing references from Textor state.
4355
+ * @param {Object} options
4356
+ * @param {boolean} options.dryRun
4357
+ * @param {boolean} options.yes
4358
+ */
4359
+ async function pruneMissingCommand(options = {}) {
4360
+ try {
4361
+ const config = await loadConfig();
4362
+ const state = await loadState();
4363
+
4364
+ const results = await getProjectStatus(config, state);
4365
+
4366
+ if (results.missing.length === 0) {
4367
+ console.log('No missing references found.');
4368
+ return;
4369
+ }
4370
+
4371
+ console.log(`Found ${results.missing.length} missing references:`);
4372
+ results.missing.forEach(f => console.log(` - ${f}`));
4373
+
4374
+ if (options.dryRun) {
4375
+ console.log('\nDry run: no changes applied to state.');
4376
+ return;
4377
+ }
4378
+
4379
+ if (!options.yes && options.interactive !== false && process.stdin.isTTY && process.env.NODE_ENV !== 'test') {
4380
+ const rl = readline.createInterface({
4381
+ input: process.stdin,
4382
+ output: process.stdout
4383
+ });
4384
+
4385
+ const confirmed = await new Promise(resolve => {
4386
+ rl.question('\nDo you want to proceed with pruning? (y/N) ', (answer) => {
4387
+ rl.close();
4388
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
4389
+ });
4390
+ });
4391
+
4392
+ if (!confirmed) {
4393
+ console.log('Aborted.');
4394
+ return;
4395
+ }
4396
+ }
4397
+
4398
+ for (const relPath of results.missing) {
4399
+ delete state.files[relPath];
4400
+ }
4401
+
4402
+ // Reconstruct metadata
4403
+ state.components = reconstructComponents(state.files, config);
4404
+ state.sections = reconstructSections(state, config);
4405
+
4406
+ await saveState(state);
4407
+ console.log(`\n✓ Successfully removed ${results.missing.length} missing references from state.`);
4408
+
4409
+ } catch (error) {
4410
+ console.error('Error:', error.message);
4411
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4412
+ process.exit(1);
4413
+ }
4414
+ throw error;
4415
+ }
4416
+ }
4417
+
4332
4418
  const program = new Command();
4333
4419
 
4334
4420
  program
@@ -4466,5 +4552,13 @@ program
4466
4552
  .option('--dry-run', 'Show the normalized state without writing to disk')
4467
4553
  .action(normalizeStateCommand);
4468
4554
 
4555
+ program
4556
+ .command('prune-missing')
4557
+ .description('Remove missing files from state (files that are in state but not on disk)')
4558
+ .option('--dry-run', 'Show what would be removed without applying')
4559
+ .option('--yes', 'Skip confirmation')
4560
+ .option('--no-interactive', 'Disable interactive prompts')
4561
+ .action(pruneMissingCommand);
4562
+
4469
4563
  program.parse();
4470
4564
  //# sourceMappingURL=textor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.cjs CHANGED
@@ -3208,58 +3208,77 @@ async function validateStateCommand(options) {
3208
3208
  }
3209
3209
  }
3210
3210
 
3211
+ /**
3212
+ * Computes the drift between the state and the actual files on disk.
3213
+ *
3214
+ * @param {import('./config.js').TextorConfig} config
3215
+ * @param {Object} state
3216
+ * @returns {Promise<{
3217
+ * missing: string[],
3218
+ * modified: string[],
3219
+ * untracked: string[],
3220
+ * orphaned: string[],
3221
+ * synced: number
3222
+ * }>}
3223
+ */
3224
+ async function getProjectStatus(config, state) {
3225
+ const results = {
3226
+ missing: [],
3227
+ modified: [],
3228
+ untracked: [], // Has signature, not in state
3229
+ orphaned: [], // No signature, not in state
3230
+ synced: 0
3231
+ };
3232
+ const roots = [
3233
+ resolvePath(config, 'pages'),
3234
+ resolvePath(config, 'features'),
3235
+ resolvePath(config, 'components')
3236
+ ].map(p => path.resolve(p));
3237
+ const diskFiles = new Set();
3238
+ const configSignatures = Object.values(config.signatures || {});
3239
+ for (const root of roots) {
3240
+ if (fs.existsSync(root)) {
3241
+ await scanDirectory(root, diskFiles);
3242
+ }
3243
+ }
3244
+ // 1. Check state files against disk
3245
+ for (const relativePath in state.files) {
3246
+ const fullPath = path.join(process.cwd(), relativePath);
3247
+ if (!fs.existsSync(fullPath)) {
3248
+ results.missing.push(relativePath);
3249
+ continue;
3250
+ }
3251
+ // It exists on disk, so it's not untracked/orphaned
3252
+ diskFiles.delete(relativePath);
3253
+ const content = await promises.readFile(fullPath, 'utf-8');
3254
+ const currentHash = calculateHash(content, config.hashing?.normalization);
3255
+ const fileData = state.files[relativePath];
3256
+ if (currentHash !== fileData.hash) {
3257
+ results.modified.push(relativePath);
3258
+ }
3259
+ else {
3260
+ results.synced++;
3261
+ }
3262
+ }
3263
+ // 2. Check remaining disk files
3264
+ for (const relativePath of diskFiles) {
3265
+ const fullPath = path.join(process.cwd(), relativePath);
3266
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3267
+ if (isGenerated) {
3268
+ results.untracked.push(relativePath);
3269
+ }
3270
+ else {
3271
+ results.orphaned.push(relativePath);
3272
+ }
3273
+ }
3274
+ return results;
3275
+ }
3276
+
3211
3277
  async function statusCommand() {
3212
3278
  try {
3213
3279
  const config = await loadConfig();
3214
3280
  const state = await loadState();
3215
- const results = {
3216
- missing: [],
3217
- modified: [],
3218
- untracked: [], // Has signature, not in state
3219
- orphaned: [], // No signature, not in state
3220
- synced: 0
3221
- };
3222
- const roots = [
3223
- resolvePath(config, 'pages'),
3224
- resolvePath(config, 'features'),
3225
- resolvePath(config, 'components')
3226
- ].map(p => path.resolve(p));
3227
- const diskFiles = new Set();
3228
- const configSignatures = Object.values(config.signatures || {});
3229
- for (const root of roots) {
3230
- if (fs.existsSync(root)) {
3231
- await scanDirectory(root, diskFiles);
3232
- }
3233
- }
3234
- // 1. Check state files against disk
3235
- for (const relativePath in state.files) {
3236
- const fullPath = path.join(process.cwd(), relativePath);
3237
- if (!fs.existsSync(fullPath)) {
3238
- results.missing.push(relativePath);
3239
- continue;
3240
- }
3241
- diskFiles.delete(relativePath);
3242
- const content = await promises.readFile(fullPath, 'utf-8');
3243
- const currentHash = calculateHash(content, config.hashing?.normalization);
3244
- const fileData = state.files[relativePath];
3245
- if (currentHash !== fileData.hash) {
3246
- results.modified.push(relativePath);
3247
- }
3248
- else {
3249
- results.synced++;
3250
- }
3251
- }
3252
- // 2. Check remaining disk files
3253
- for (const relativePath of diskFiles) {
3254
- const fullPath = path.join(process.cwd(), relativePath);
3255
- const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3256
- if (isGenerated) {
3257
- results.untracked.push(relativePath);
3258
- }
3259
- else {
3260
- results.orphaned.push(relativePath);
3261
- }
3262
- }
3281
+ const results = await getProjectStatus(config, state);
3263
3282
  // Reporting
3264
3283
  console.log('Textor Status Report:');
3265
3284
  console.log(` Synced files: ${results.synced}`);