@tng-sh/js 0.1.9 → 0.2.1

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.
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$" />
8
+ <orderEntry type="inheritedJdk" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ </component>
11
+ </module>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/js-pie.iml" filepath="$PROJECT_DIR$/.idea/js-pie.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
package/bin/tng.js CHANGED
@@ -28,7 +28,7 @@ process.on('uncaughtException', (err) => {
28
28
  program
29
29
  .name('tng')
30
30
  .description('TNG - Advanced Code Audit, Test Generation, Visualization, Clone Detection, and Dead Code Analysis for JavaScript/TypeScript')
31
- .version('0.1.9');
31
+ .version('0.2.1');
32
32
 
33
33
  /**
34
34
  * Copy text to system clipboard
@@ -253,7 +253,9 @@ program
253
253
  .option('-c, --clones', 'Run duplicate code detection')
254
254
  .option('-l, --level <level>', 'Set clone detection level (1: token, 2: structural, 3: fuzzy, or all)', 'all')
255
255
  .option('-d, --deadcode', 'Run dead code detection (JS/TS/React)')
256
+ .option('--all', 'Run dead code detection across the entire repo (respects .gitignore)')
256
257
  .option('--xray', 'Generate X-Ray visualization (Mermaid flowchart)')
258
+ .option('--impact', 'Run impact analysis (blast radius check) on a method')
257
259
  .option('--json', 'Output results as JSON events (machine-readable)')
258
260
 
259
261
  .action(async (options) => {
@@ -284,7 +286,7 @@ program
284
286
  const binaryPath = session._binaryPath;
285
287
 
286
288
  const { spawnSync } = require('child_process');
287
- spawnSync(binaryPath, ['trace-results', '--file', tmpFile], {
289
+ spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
288
290
  stdio: 'inherit'
289
291
  });
290
292
 
@@ -344,6 +346,12 @@ program
344
346
  return;
345
347
  }
346
348
 
349
+
350
+ if (options.impact) {
351
+ await runImpact(options.file, options.method, options.json);
352
+ return;
353
+ }
354
+
347
355
  if (!options.type && !options.audit) {
348
356
  console.log(chalk.red('Error: --type <type> is required.'));
349
357
  process.exit(1);
@@ -353,11 +361,32 @@ program
353
361
  if (options.clones) {
354
362
  await runClones(options.file, options.level || 'all', options.json);
355
363
  } else if (options.deadcode) {
356
- await runDeadCode(options.file, options.json);
364
+ if (options.all) {
365
+ await runDeadCodeRepo(options.json);
366
+ } else {
367
+ await runDeadCode(options.file, options.json);
368
+ }
357
369
  } else {
358
370
  console.log(chalk.yellow('Specify a method with -m, use --outline to see methods, or run "tng i" for full selection.'));
359
371
  }
360
- } else if (!options.file && !options.method && process.argv.length <= 2) {
372
+ if (options.xray) {
373
+ // ... (existing xray logic) ...
374
+ return;
375
+ }
376
+
377
+ if (options.impact) {
378
+ await runImpact(options.file, options.method, options.json);
379
+ return;
380
+ }
381
+
382
+ if (!options.type && !options.audit) {
383
+ console.log(chalk.red('Error: --type <type> is required.'));
384
+ process.exit(1);
385
+ }
386
+ generateTest(options.file, options.method, options.type, options.audit, options.json);
387
+ } else if (options.deadcode && options.all && !options.file && !options.method) {
388
+ await runDeadCodeRepo(options.json);
389
+ } else if (options.file && !options.method) {
361
390
  launchInteractive();
362
391
  }
363
392
  });
@@ -439,7 +468,7 @@ async function runDeadCode(filePath, jsonMode = false) {
439
468
  jsonSession.start();
440
469
  try {
441
470
  const resultJson = detectDeadCode(absolutePath);
442
- jsonSession.emitEvent('dead_code', JSON.parse(resultJson));
471
+ jsonSession.showDeadCode(JSON.parse(resultJson));
443
472
  jsonSession.stop();
444
473
  } catch (e) {
445
474
  jsonSession.displayError(e.message);
@@ -469,6 +498,144 @@ async function runDeadCode(filePath, jsonMode = false) {
469
498
  }
470
499
  }
471
500
 
501
+ function listDeadCodeFiles(projectRoot) {
502
+ const { listDeadcodeFiles } = require('../index');
503
+ try {
504
+ const filesJson = listDeadcodeFiles(projectRoot);
505
+ const files = JSON.parse(filesJson);
506
+ if (!Array.isArray(files)) return [];
507
+ return files.filter((f) => !f.endsWith('.d.ts'));
508
+ } catch (e) {
509
+ return [];
510
+ }
511
+ }
512
+
513
+ async function runDeadCodeRepo(jsonMode = false) {
514
+ const { detectDeadCode } = require('../index');
515
+ const projectRoot = process.cwd();
516
+ const files = listDeadCodeFiles(projectRoot);
517
+
518
+ if (files.length === 0) {
519
+ console.log(chalk.yellow('No files found for dead code analysis.'));
520
+ return;
521
+ }
522
+
523
+ const runAnalysis = async () => {
524
+ const aggregateIssues = [];
525
+ for (let i = 0; i < files.length; i++) {
526
+ const filePath = files[i];
527
+ const rel = path.relative(projectRoot, filePath);
528
+ try {
529
+ const resultJson = detectDeadCode(filePath);
530
+ const issues = JSON.parse(resultJson);
531
+ if (Array.isArray(issues)) {
532
+ for (const issue of issues) {
533
+ aggregateIssues.push({
534
+ issue_type: issue.issue_type,
535
+ line: issue.line,
536
+ message: `[${rel}] ${issue.message}`,
537
+ code_snippet: issue.code_snippet,
538
+ file: rel
539
+ });
540
+ }
541
+ }
542
+ } catch (e) {
543
+ aggregateIssues.push({
544
+ issue_type: 'analysis_error',
545
+ line: 0,
546
+ message: `[${rel}] Failed to analyze: ${e.message}`,
547
+ code_snippet: '',
548
+ file: rel
549
+ });
550
+ }
551
+ }
552
+ return aggregateIssues;
553
+ };
554
+
555
+ if (jsonMode) {
556
+ const { JsonSession } = require('../lib/jsonSession');
557
+ const jsonSession = new JsonSession();
558
+ jsonSession.start();
559
+ const issues = await runAnalysis();
560
+ const out = {
561
+ file: projectRoot,
562
+ dead_code: issues.map(i => ({
563
+ type: i.issue_type,
564
+ line: i.line,
565
+ message: i.message,
566
+ code: i.code_snippet,
567
+ file: i.file
568
+ }))
569
+ };
570
+ jsonSession.showDeadCode(out);
571
+ jsonSession.stop();
572
+ return;
573
+ }
574
+
575
+ const total = files.length;
576
+ const aggregateIssues = [];
577
+ const start = Date.now();
578
+
579
+ const renderBar = (current, totalCount) => {
580
+ const width = 20;
581
+ const ratio = totalCount === 0 ? 1 : current / totalCount;
582
+ const filled = Math.round(ratio * width);
583
+ const bar = '#'.repeat(filled) + '-'.repeat(width - filled);
584
+ const percent = Math.round(ratio * 100);
585
+ return `Scanning project... [${bar}] ${percent}%`;
586
+ };
587
+
588
+ process.stdout.write(renderBar(0, total));
589
+ for (let i = 0; i < files.length; i++) {
590
+ const filePath = files[i];
591
+ const rel = path.relative(projectRoot, filePath);
592
+ try {
593
+ const resultJson = detectDeadCode(filePath);
594
+ const issues = JSON.parse(resultJson);
595
+ if (Array.isArray(issues)) {
596
+ for (const issue of issues) {
597
+ aggregateIssues.push({
598
+ file: rel,
599
+ line: issue.line,
600
+ message: issue.message,
601
+ code_snippet: issue.code_snippet,
602
+ issue_type: issue.issue_type
603
+ });
604
+ }
605
+ }
606
+ } catch (e) {
607
+ aggregateIssues.push({
608
+ file: rel,
609
+ line: 0,
610
+ message: `Failed to analyze: ${e.message}`,
611
+ code_snippet: '',
612
+ issue_type: 'analysis_error'
613
+ });
614
+ }
615
+
616
+ if (i % 10 === 0 || i === total - 1) {
617
+ process.stdout.write('\r' + renderBar(i + 1, total));
618
+ }
619
+ }
620
+ process.stdout.write('\n');
621
+
622
+ if (aggregateIssues.length === 0) {
623
+ console.log('No dead code detected.');
624
+ const seconds = ((Date.now() - start) / 1000).toFixed(2);
625
+ console.log(`Summary: Found 0 dead items in ${seconds}s.`);
626
+ return;
627
+ }
628
+
629
+ console.log('Found dead code:');
630
+ for (const issue of aggregateIssues) {
631
+ const snippet = issue.code_snippet ? ` (${issue.code_snippet.trim()})` : '';
632
+ console.log(`- ${issue.file}:${issue.line}${snippet}`);
633
+ }
634
+ const seconds = ((Date.now() - start) / 1000).toFixed(2);
635
+ console.log(`Summary: Found ${aggregateIssues.length} dead items in ${seconds}s.`);
636
+ console.log("[Tip] Run 'tng audit' to fix logic bugs in the remaining code.");
637
+ }
638
+
472
639
  /**
473
640
  * @command fix
474
641
  * Apply a specific fix to a file
@@ -611,17 +778,20 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
611
778
  if (saved) {
612
779
  jsonSession.emitEvent('test_saved', {
613
780
  file_path: saved.file_path,
781
+ absolute_path: saved.absolute_path,
614
782
  message: `Test saved to: ${saved.file_path}`
615
783
  });
616
784
  } else {
617
- jsonSession.displayError('Failed to save test file');
785
+ const details = saveTestFile.lastError ? `: ${saveTestFile.lastError}` : '';
786
+ jsonSession.displayError(`Failed to save test file${details}`);
618
787
  }
619
788
  jsonSession.stop();
620
789
  } else {
621
790
  if (saved) {
622
791
  console.log(chalk.green(`✓ Test saved to: ${saved.file_path}`));
623
792
  } else {
624
- console.log(chalk.yellow('Failed to save test file'));
793
+ const details = saveTestFile.lastError ? `: ${saveTestFile.lastError}` : '';
794
+ console.log(chalk.yellow(`Failed to save test file${details}`));
625
795
  }
626
796
  }
627
797
  }
@@ -651,4 +821,76 @@ program.on('--help', () => {
651
821
  console.log('');
652
822
  });
653
823
 
824
+ /**
825
+ * Logic to run impact analysis
826
+ */
827
+ async function runImpact(filePath, methodName, jsonMode = false) {
828
+ const { analyzeImpact } = require('../index');
829
+ const absolutePath = path.resolve(filePath);
830
+ const projectRoot = process.cwd();
831
+
832
+ if (!fs.existsSync(absolutePath)) {
833
+ console.log(chalk.red(`File not found: ${filePath}`));
834
+ process.exit(1);
835
+ }
836
+
837
+ if (jsonMode) {
838
+ try {
839
+ const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
840
+ console.log(resultJson);
841
+ } catch (e) {
842
+ console.error(JSON.stringify({ error: e.message }));
843
+ process.exit(1);
844
+ }
845
+ return;
846
+ }
847
+
848
+ console.log(chalk.blue(`💥 Analyzing impact for ${methodName} in ${filePath}...`));
849
+
850
+ try {
851
+ const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
852
+ const result = JSON.parse(resultJson);
853
+
854
+ if (result.status === 'error') {
855
+ console.log(chalk.red(`\nError: ${result.diffs[0].message}`));
856
+ return;
857
+ }
858
+
859
+ console.log(chalk.bold(`\nStatus: `) + (result.status === 'breaking' ? chalk.red.bold('BREAKING CHANGES DETECTED') : result.status === 'warning' ? chalk.yellow.bold('WARNINGS FOUND') : chalk.green.bold('SAFE')));
860
+
861
+ if (result.diffs.length > 0) {
862
+ console.log(chalk.bold('\nChanges:'));
863
+ result.diffs.forEach(diff => {
864
+ const color = diff.severity === 'breaking' ? chalk.red : (diff.severity === 'warning' ? chalk.yellow : chalk.white);
865
+ const icon = diff.severity === 'breaking' ? '❌' : (diff.severity === 'warning' ? '⚠️' : 'ℹ️');
866
+ // Handle optional name
867
+ const nameStr = diff.name ? ` (${diff.name})` : '';
868
+ console.log(` ${icon} ${color(diff.message)}${color(nameStr)}`);
869
+ });
870
+ } else {
871
+ console.log(chalk.green(' No changes detected in logic signature.'));
872
+ }
873
+
874
+ if (result.impacted_files.length > 0) {
875
+ console.log(chalk.bold(`\nImpacted Files (${result.impacted_files.length}):`));
876
+ result.impacted_files.forEach(f => {
877
+ // Relativize path
878
+ const relPath = path.relative(projectRoot, f.file);
879
+ console.log(chalk.dim(` • ${relPath}:${f.line}`));
880
+ // simple truncate code
881
+ const codePreview = f.code.trim().replace(/\n/g, ' ').substring(0, 60) + (f.code.length > 60 ? '...' : '');
882
+ console.log(chalk.gray(` ${codePreview}`));
883
+ });
884
+ } else if (result.status === 'breaking') {
885
+ console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
886
+ }
887
+
888
+ } catch (e) {
889
+ console.log(chalk.red(`Analysis failed: ${e.message}`));
890
+ if (e.message.includes("git")) {
891
+ console.log(chalk.yellow("Note: Impact analysis requires the file to be committed to git."));
892
+ }
893
+ }
894
+ }
895
+
654
896
  program.parse(process.argv);
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -63,3 +63,10 @@ export declare function getSymbolicTrace(filePath: string, methodName: string, c
63
63
  export declare function analyzeClones(projectRoot: string, filePath: string, level: string): string
64
64
  /** Analyzes a file for dead code. */
65
65
  export declare function detectDeadCode(filePath: string): string
66
+ /** List repo files for dead code analysis (gitignore-aware). */
67
+ export declare function listDeadcodeFiles(projectRoot: string): string
68
+ /**
69
+ * Analyzes the impact of changes in a specific method compared to git HEAD.
70
+ * Returns a JSON string containing breaking changes and impacted files.
71
+ */
72
+ export declare function analyzeImpact(projectRoot: string, filePath: string, methodName: string): string
Binary file
Binary file
package/index.js CHANGED
@@ -310,7 +310,7 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace, analyzeClones, detectDeadCode } = nativeBinding
313
+ const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace, analyzeClones, detectDeadCode, listDeadcodeFiles, analyzeImpact } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -325,3 +325,5 @@ module.exports.generateTest = generateTest
325
325
  module.exports.getSymbolicTrace = getSymbolicTrace
326
326
  module.exports.analyzeClones = analyzeClones
327
327
  module.exports.detectDeadCode = detectDeadCode
328
+ module.exports.listDeadcodeFiles = listDeadcodeFiles
329
+ module.exports.analyzeImpact = analyzeImpact
Binary file
Binary file
@@ -34,6 +34,9 @@ class GenerateTestsUI {
34
34
  } else if (choice === 'audit') {
35
35
  const result = await this._showFileSelection(true);
36
36
  if (result === 'exit') return 'exit';
37
+ } else if (choice === 'impact') {
38
+ const result = await this._showFileSelection(false, false, false, true);
39
+ if (result === 'exit') return 'exit';
37
40
  } else if (choice === 'xray') {
38
41
  const result = await this._showFileSelection(false, true);
39
42
  if (result === 'exit') return 'exit';
@@ -105,9 +108,10 @@ class GenerateTestsUI {
105
108
  return { success: false, message: e.message };
106
109
  }
107
110
  });
111
+ return false
108
112
  }
109
113
 
110
- async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
114
+ async _showFileSelection(isAudit = false, isXray = false, isTrace = false, isImpact = false) {
111
115
  const files = await this._getUserFiles();
112
116
 
113
117
  if (files.length === 0) {
@@ -123,6 +127,7 @@ class GenerateTestsUI {
123
127
 
124
128
  let title = 'Select JavaScript File';
125
129
  if (isAudit) title = 'Select JavaScript File to Audit';
130
+ else if (isImpact) title = 'Select JavaScript File to Impact Audit';
126
131
  else if (isXray) title = 'Select File for X-Ray';
127
132
  else if (isTrace) title = 'Select File for Symbolic Trace';
128
133
 
@@ -132,25 +137,25 @@ class GenerateTestsUI {
132
137
  if (!selectedName || selectedName === 'exit') return 'exit';
133
138
 
134
139
  const selectedFile = path.resolve(cwd, selectedName);
135
- const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
140
+ const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace, isImpact);
136
141
  if (result === 'main_menu') return 'main_menu';
137
142
  return result;
138
143
  }
139
144
 
140
- async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
145
+ async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false, isImpact = false) {
141
146
  let outline;
142
147
  try {
143
148
  const result = getFileOutline(filePath);
144
149
  outline = JSON.parse(result);
145
150
  } catch (e) {
146
151
  console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
147
- return this._showFileSelection(isAudit);
152
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
148
153
  }
149
154
 
150
155
  const methods = outline.methods || [];
151
156
  if (methods.length === 0) {
152
157
  this.goUiSession.showNoItems('methods');
153
- return this._showFileSelection(isAudit);
158
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
154
159
  }
155
160
 
156
161
  const fileName = path.basename(filePath);
@@ -162,46 +167,58 @@ class GenerateTestsUI {
162
167
 
163
168
  let title = 'Select Method';
164
169
  if (isAudit) title = `Select Method to Audit for ${fileName}`;
170
+ else if (isImpact) title = `Select Method to Impact Audit for ${fileName}`;
165
171
  else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
166
172
  else if (isTrace) title = `Select Method to Trace for ${fileName}`;
167
173
 
168
174
  const selectedDisplay = this.goUiSession.showListView(title, items);
169
175
 
170
- if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
176
+ if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
171
177
 
172
178
  const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
173
179
 
174
180
  if (selectedMethod) {
175
181
  if (isTrace) {
176
182
  await this._launchTrace(filePath, selectedMethod.name);
177
- return this._showFileSelection(isAudit, isXray, isTrace);
183
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
178
184
  }
179
185
 
180
186
  if (isXray) {
181
187
  const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
182
188
  if (choice === 'main_menu') return 'main_menu';
183
- return this._showFileSelection(isAudit, isXray);
189
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
184
190
  }
185
191
 
186
- const testType = this.goUiSession.showJsTestMenu();
187
- if (testType === 'back') return this._showFileSelection(isAudit, isXray);
188
-
189
- const finalType = testType === 'auto' ? null : testType;
190
- const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
192
+ if (isImpact) {
193
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, false, false, true);
194
+ if (choice === 'main_menu') return 'main_menu';
195
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
196
+ }
191
197
 
192
198
  if (isAudit) {
199
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, true);
193
200
  if (choice === 'main_menu') return 'main_menu';
194
- return this._showFileSelection(isAudit, isXray);
201
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
195
202
  }
196
203
 
204
+ const testType = this.goUiSession.showJsTestMenu();
205
+ if (testType === 'back') return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
206
+
207
+ const finalType = testType === 'auto' ? null : testType;
208
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, false);
209
+
197
210
  if (choice && choice.file_path && !choice.error) {
198
211
  this._showPostGenerationMenu(choice);
199
212
  }
200
213
  }
201
- return this._showFileSelection(isAudit, isXray);
214
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
202
215
  }
203
216
 
204
- async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
217
+ async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false, isImpact = false) {
218
+ if (isImpact) {
219
+ return this._handleImpactFlow(filePath, method);
220
+ }
221
+
205
222
  if (!this._hasApiKey()) {
206
223
  return { error: 'No API key' };
207
224
  }
@@ -229,169 +246,75 @@ class GenerateTestsUI {
229
246
  return true;
230
247
  }
231
248
 
232
- async _handleAuditFlow(filePath, method, testType) {
233
- const { Worker } = require('worker_threads');
234
- const streamingUi = await this.goUiSession.showStreamingAuditResults(
235
- method.name,
236
- method.class_name,
237
- method.source_code || ''
238
- );
239
-
240
- if (!streamingUi) return null;
241
-
242
- let results = {
243
- issues: [],
244
- behaviours: [],
245
- method_name: method.name,
246
- class_name: method.class_name,
247
- method_source_with_lines: method.source_code
248
- };
249
- let auditFinished = false;
249
+ async _handleImpactFlow(filePath, method) {
250
+ const { analyzeImpact } = require('../index');
251
+ const projectRoot = process.cwd();
250
252
 
251
- // Start audit in background worker
252
- const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
253
- workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
254
- });
253
+ console.log(chalk.blue(`\n🔍 Analyzing impact for ${method.name}...`));
255
254
 
256
- const itemIds = new Set();
257
- worker.on('message', (msg) => {
258
- if (msg.type === 'item') {
259
- if (msg.data.startsWith('{')) {
260
- streamingUi.write(msg.data);
261
- try {
262
- const data = JSON.parse(msg.data);
263
-
264
- // Handle metadata updates from stream
265
- if (data.method_name) results.method_name = data.method_name;
266
- if (data.class_name) results.class_name = data.class_name;
267
- if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
268
- if (data.source_code) results.method_source_with_lines = data.source_code;
269
-
270
- // Only push if it looks like an actual audit item
271
- if (data.test_name || data.summary || data.category) {
272
- const id = `${data.category}-${data.test_name}-${data.line_number}`;
273
- if (!itemIds.has(id)) {
274
- if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
275
- else results.issues.push(data);
276
- itemIds.add(id);
277
- }
278
- }
279
- } catch (e) { }
280
- }
281
- } else if (msg.type === 'result') {
282
- try {
283
- const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
284
-
285
- // Merge final results with our local ones, preserving 'fixed' state
286
- const mergeList = (localList, incomingList) => {
287
- if (!incomingList || incomingList.length === 0) return localList;
288
-
289
- return incomingList.map(incomingItem => {
290
- const localItem = localList.find(it =>
291
- it.test_name === incomingItem.test_name &&
292
- it.line_number === incomingItem.line_number
293
- );
294
- if (localItem && localItem.fixed) {
295
- return { ...incomingItem, fixed: true };
296
- }
297
- return incomingItem;
298
- });
299
- };
300
-
301
- results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
302
- results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
303
-
304
- if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
305
- if (finalResults.method_name) results.method_name = finalResults.method_name;
306
- } catch (e) { }
307
- auditFinished = true;
308
- } else if (msg.type === 'error') {
309
- console.error(chalk.red(`Audit error: ${msg.data}`));
310
- auditFinished = true;
311
- }
312
- });
255
+ let impactResult;
256
+ try {
257
+ const resultJson = await this.goUiSession.showProgress('Analyzing impact...', async (progress) => {
258
+ progress.update('Tracing method versions...', { percent: 30 });
259
+ // Artificial delay for UX
260
+ await new Promise(r => setTimeout(r, 300));
313
261
 
314
- worker.on('error', (err) => {
315
- console.error(chalk.red(`Worker error: ${err.message}`));
316
- auditFinished = true;
317
- });
262
+ const json = analyzeImpact(projectRoot, filePath, method.name);
263
+ progress.update('Comparing logic...', { percent: 80 });
264
+
265
+ return json;
266
+ });
318
267
 
268
+ impactResult = JSON.parse(resultJson);
269
+ } catch (e) {
270
+ console.error(chalk.red(`\nImpact Analysis failed: ${e.message}\n`));
271
+ return null;
272
+ }
273
+
274
+ await this.goUiSession.showImpactResults(impactResult);
275
+ return { message: 'Impact audit complete' };
276
+ }
277
+
278
+ async _handleAuditFlow(filePath, method, testType) {
279
+ const { runAudit } = require('../index');
280
+ const config = loadConfig();
281
+ const fileName = path.basename(filePath);
282
+ const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
283
+
284
+ console.log(chalk.blue(`\n🔍 Auditing ${displayName}...`));
285
+
286
+ let auditResult;
319
287
  try {
320
- let currentChoice = null;
321
-
322
- // Wait for INITIAL streaming session to complete (user picks an action or quits)
323
- await streamingUi.wait;
324
-
325
- const getResponse = (outputFile) => {
326
- if (fs.existsSync(outputFile)) {
327
- const output = fs.readFileSync(outputFile, 'utf8').trim();
328
- if (output) {
329
- try {
330
- const parsed = JSON.parse(output);
331
- return typeof parsed === 'object' ? parsed : { action: output };
332
- } catch (e) {
333
- return { action: output };
288
+ const resultJson = await this.goUiSession.showProgress(`Auditing ${displayName}...`, async (progress) => {
289
+ progress.update('Preparing audit...', { percent: 10 });
290
+
291
+ const json = runAudit(
292
+ filePath,
293
+ method.name,
294
+ method.class_name || null,
295
+ testType || null,
296
+ JSON.stringify(config),
297
+ (msg, percent) => {
298
+ if (typeof percent === 'number') {
299
+ progress.update(msg, { percent });
300
+ } else {
301
+ progress.update(msg, { step_increment: false });
334
302
  }
335
303
  }
336
- }
337
- return null;
338
- };
339
-
340
- currentChoice = getResponse(streamingUi.outputFile);
341
-
342
- // Action loop: handles fix/open and returns to UI
343
- while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
344
- const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
345
-
346
- if (currentChoice.action === 'fix') {
347
- console.log(chalk.cyan(`Applying fix...`));
348
- const fixResult = await applyFix(filePath, currentChoice.item);
349
- if (fixResult.success) {
350
- console.log(chalk.green(`✓ Fix applied successfully.`));
351
- // Mark as fixed locally
352
- [results.issues, results.behaviours].forEach(list => {
353
- list.forEach(it => {
354
- if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
355
- it.fixed = true;
356
- }
357
- });
358
- });
359
- } else {
360
- console.log(chalk.red(`❌ Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
361
- }
362
- } else if (currentChoice.action === 'open') {
363
- this._openInEditor(filePath, currentChoice.item.line_number);
364
- }
365
-
366
- // Ensure source has line numbers for static UI if it doesn't already
367
- if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
368
- results.method_source_with_lines = results.method_source_with_lines
369
- .split('\n')
370
- .map((line, i) => `${i + 1}: ${line}`)
371
- .join('\n');
372
- }
304
+ );
373
305
 
374
- // Re-launch UI statically with updated results
375
- const choiceStr = await this.goUiSession.showAuditResults(results, itemIndex);
376
- if (choiceStr === 'back' || choiceStr === 'main_menu' || choiceStr === 'exit') {
377
- currentChoice = null;
378
- } else {
379
- try {
380
- const parsed = JSON.parse(choiceStr);
381
- currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
382
- } catch (e) {
383
- currentChoice = { action: choiceStr };
384
- }
385
- }
386
- }
306
+ progress.update('Rendering results...', { percent: 95 });
307
+ return json;
308
+ });
387
309
 
388
- return { message: 'Audit complete', results };
310
+ auditResult = JSON.parse(resultJson);
389
311
  } catch (e) {
390
- console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
312
+ console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
391
313
  return null;
392
- } finally {
393
- await worker.terminate();
394
314
  }
315
+
316
+ await this.goUiSession.showAuditResults(auditResult);
317
+ return { message: 'Audit complete' };
395
318
  }
396
319
 
397
320
  async _handleTestGenerationFlow(filePath, method, testType, displayName) {
@@ -700,7 +623,7 @@ class GenerateTestsUI {
700
623
  if (result && result.success && result.file) {
701
624
  // 2. Show Trace UI
702
625
  const { spawnSync } = require('child_process');
703
- spawnSync(this.goUiSession._binaryPath, ['trace-results', '--file', result.file], {
626
+ spawnSync(this.goUiSession._binaryPath, ['js-trace-results', '--file', result.file], {
704
627
  stdio: 'inherit'
705
628
  });
706
629
 
@@ -103,7 +103,18 @@ class GoUISession {
103
103
  }
104
104
 
105
105
  showListView(title, items) {
106
- const dataJson = JSON.stringify({ title, items });
106
+ // Limit items to prevent triggering command line length limits
107
+ // optimized items (relative paths) allow for more items
108
+ let displayItems = items;
109
+ if (items.length > 500) {
110
+ displayItems = items.slice(0, 500);
111
+ displayItems.push({
112
+ name: `... and ${items.length - 500} more files (use CLI for full list)`,
113
+ path: 'INFO'
114
+ });
115
+ }
116
+
117
+ const dataJson = JSON.stringify({ title, items: displayItems });
107
118
  const outputFile = this._trackTempFile(this._createTempFile('list-view-output', '.txt'));
108
119
 
109
120
  try {
@@ -308,6 +319,25 @@ class GoUISession {
308
319
  console.error('Test results display error:', error.message);
309
320
  }
310
321
  }
322
+
323
+ async showImpactResults(impactResult) {
324
+ const dataJson = JSON.stringify(impactResult);
325
+ const inputFile = this._trackTempFile(this._createTempFile('impact-data', '.json'));
326
+
327
+ try {
328
+ fs.writeFileSync(inputFile, dataJson);
329
+
330
+ spawnSync(this._binaryPath, ['js-impact-results', '--file', inputFile], {
331
+ stdio: 'inherit',
332
+ env: process.env
333
+ });
334
+ } catch (error) {
335
+ console.error('Impact results display error:', error.message);
336
+ } finally {
337
+ this._cleanupTempFile(inputFile);
338
+ }
339
+ }
340
+
311
341
  async showAuditResults(auditResult, index = 0) {
312
342
  const dataJson = JSON.stringify(auditResult);
313
343
  const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
@@ -351,6 +381,7 @@ class GoUISession {
351
381
  }
352
382
  }
353
383
 
384
+
354
385
  async showClones(filePath, results) {
355
386
  const dataJson = JSON.stringify({ file_path: filePath, matches: results });
356
387
  const inputFile = this._trackTempFile(this._createTempFile('clone-data', '.json'));
@@ -74,6 +74,10 @@ class JsonSession {
74
74
  this.emitEvent('clones', { file_path: filePath, matches: results });
75
75
  }
76
76
 
77
+ showDeadCode(result) {
78
+ this.emitEvent('dead_code', result || {});
79
+ }
80
+
77
81
  showTestResults(title, passed, failed, errors, total, results = []) {
78
82
  this.emitEvent('test_results', {
79
83
  title: title || 'Test Results',
package/lib/saveFile.js CHANGED
@@ -4,6 +4,7 @@ const chalk = require('chalk');
4
4
  const prettier = require('prettier');
5
5
 
6
6
  const saveTestFile = async (testContent) => {
7
+ saveTestFile.lastError = null;
7
8
  try {
8
9
  const parsed = typeof testContent === 'string' ? JSON.parse(testContent) : testContent;
9
10
 
@@ -31,6 +32,14 @@ const saveTestFile = async (testContent) => {
31
32
  return null;
32
33
  }
33
34
 
35
+ // Quick sanity check for likely truncation (common when API cuts the response)
36
+ const trimmedContent = contentStr.trim();
37
+ if (trimmedContent.includes('describe(') && !/}\);\s*$/.test(trimmedContent)) {
38
+ const msg = 'Generated test content appears truncated (missing closing `});`)';
39
+ console.log(chalk.red.bold(`❌ ${msg}`));
40
+ return null;
41
+ }
42
+
34
43
  // Resolve file path
35
44
  let filePath = metaSource.test_file_path || metaSource.file_path || metaSource.file_name || metaSource.file ||
36
45
  parsed.test_file_path || parsed.file_path || 'generated_test.js';
@@ -60,14 +69,66 @@ const saveTestFile = async (testContent) => {
60
69
 
61
70
  // Format with Prettier
62
71
  let formattedContent = contentStr;
63
- try {
72
+ const tryFormat = async (input) => {
64
73
  const prettierOptions = (await prettier.resolveConfig(absolutePath)) || { semi: true, singleQuote: true, parser: 'babel' };
65
- // Fallback parser if filepath doesn't help
66
74
  if (!prettierOptions.parser) prettierOptions.parser = 'babel';
67
- formattedContent = prettier.format(contentStr, { ...prettierOptions, filepath: absolutePath });
75
+ return prettier.format(input, { ...prettierOptions, filepath: absolutePath });
76
+ };
77
+
78
+ const sanitizeExpectStringHelpers = (input) => {
79
+ const targets = ['expect.stringContaining("', 'expect.stringMatching("'];
80
+ let output = '';
81
+ let idx = 0;
82
+
83
+ while (idx < input.length) {
84
+ let nextIndex = -1;
85
+ let matched = '';
86
+ for (const t of targets) {
87
+ const i = input.indexOf(t, idx);
88
+ if (i !== -1 && (nextIndex === -1 || i < nextIndex)) {
89
+ nextIndex = i;
90
+ matched = t;
91
+ }
92
+ }
93
+ if (nextIndex === -1) {
94
+ output += input.slice(idx);
95
+ break;
96
+ }
97
+
98
+ output += input.slice(idx, nextIndex);
99
+ const start = nextIndex + matched.length;
100
+ const end = input.indexOf('")', start);
101
+ if (end === -1) {
102
+ output += input.slice(nextIndex);
103
+ break;
104
+ }
105
+
106
+ const inner = input.slice(start, end);
107
+ const escaped = inner.replace(/`/g, '\\`');
108
+ const helper = matched.startsWith('expect.stringMatching')
109
+ ? 'expect.stringMatching'
110
+ : 'expect.stringContaining';
111
+ output += `${helper}(String.raw\`${escaped}\`)`;
112
+ idx = end + 2;
113
+ }
114
+
115
+ return output;
116
+ };
117
+
118
+ try {
119
+ formattedContent = await tryFormat(contentStr);
68
120
  } catch (prettierError) {
69
- // Best effort formatting
70
- console.log(chalk.yellow(`⚠ Prettier formatting failed, saving raw: ${prettierError.message}`));
121
+ // Try a targeted sanitize pass for malformed string literals, then re-format
122
+ const sanitized = sanitizeExpectStringHelpers(contentStr);
123
+ if (sanitized !== contentStr) {
124
+ try {
125
+ formattedContent = await tryFormat(sanitized);
126
+ } catch (secondError) {
127
+ console.log(chalk.yellow(`⚠ Prettier formatting failed after sanitize, saving raw: ${secondError.message}`));
128
+ }
129
+ } else {
130
+ console.log(chalk.yellow(`⚠ Prettier formatting failed, saving raw: ${prettierError.message}`));
131
+ }
71
132
  }
72
133
 
73
134
  fs.writeFileSync(absolutePath, formattedContent);
@@ -78,9 +139,12 @@ const saveTestFile = async (testContent) => {
78
139
  framework: metaSource.framework || parsed.framework || 'jest'
79
140
  };
80
141
  } catch (error) {
81
- console.log(chalk.red.bold(`❌ Failed to save test file: ${error.message}`));
142
+ saveTestFile.lastError = error?.message || 'Unknown error';
143
+ console.log(chalk.red.bold(`❌ Failed to save test file: ${saveTestFile.lastError}`));
82
144
  return null;
83
145
  }
84
146
  };
85
147
 
148
+ saveTestFile.lastError = null;
149
+
86
150
  module.exports = { saveTestFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,8 @@
33
33
  "test": "node test.js"
34
34
  },
35
35
  "devDependencies": {
36
- "@napi-rs/cli": "^2.18.0"
36
+ "@napi-rs/cli": "^2.18.0",
37
+ "@tng-sh/js": "^0.2.0"
37
38
  },
38
39
  "engines": {
39
40
  "node": ">= 10"
@@ -44,4 +45,4 @@
44
45
  "fast-glob": "^3.3.3",
45
46
  "prettier": "^2.8.8"
46
47
  }
47
- }
48
+ }
Binary file
Binary file