@tng-sh/js 0.2.0 → 0.2.4

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.4');
32
32
 
33
33
  /**
34
34
  * Copy text to system clipboard
@@ -253,113 +253,175 @@ 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')
259
+ .option('--callsites', 'Find in-repo call sites for a method')
260
+ .option('--class <name>', 'Class/module name to disambiguate methods when multiple classes define the same method in a file')
257
261
  .option('--json', 'Output results as JSON events (machine-readable)')
258
262
 
259
263
  .action(async (options) => {
260
- if (options.method && options.file) {
261
- if (options.trace) {
262
- const { getSymbolicTrace } = require('../index');
263
- const GoUISession = require('../lib/goUiSession');
264
-
265
- try {
266
- const traceJson = getSymbolicTrace(
267
- path.resolve(options.file),
268
- options.method,
269
- null
270
- );
271
-
272
- if (options.json) {
273
- console.log(traceJson);
274
- return;
275
- }
276
-
277
- // Create temp file for the trace
278
- const tmpDir = require('os').tmpdir();
279
- const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
280
- fs.writeFileSync(tmpFile, traceJson);
264
+ if (options.callsites) {
265
+ if (!options.method) {
266
+ console.log(chalk.red('Error: --method is required for call site analysis.'));
267
+ process.exit(1);
268
+ }
281
269
 
282
- // Launch Go UI
283
- const session = new GoUISession();
284
- const binaryPath = session._binaryPath;
270
+ try {
271
+ const { findCallSites } = require('../index');
272
+ const projectRoot = process.cwd();
273
+ const resultJson = findCallSites(projectRoot, options.method);
285
274
 
286
- const { spawnSync } = require('child_process');
287
- spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
288
- stdio: 'inherit'
289
- });
275
+ if (options.json) {
276
+ console.log(resultJson);
277
+ return;
278
+ }
290
279
 
291
- // Cleanup
292
- try { fs.unlinkSync(tmpFile); } catch (e) { }
280
+ const parsed = JSON.parse(resultJson);
281
+ const sites = Array.isArray(parsed) ? parsed : (parsed.call_sites || parsed.sites || parsed);
282
+ if (!sites || sites.length === 0) {
283
+ console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
284
+ return;
285
+ }
293
286
 
294
- } catch (e) {
295
- console.log(chalk.red(`Trace failed: ${e.message}`));
296
- process.exit(1);
287
+ console.log(chalk.blue(`\nFound ${sites.length} call sites:\n`));
288
+ sites.slice(0, 200).forEach((site) => {
289
+ const file = site.file || 'unknown';
290
+ const line = site.line || 0;
291
+ const content = site.content || '';
292
+ console.log(`${chalk.yellow(file)}:${chalk.cyan(line)} ${content}`);
293
+ });
294
+ if (sites.length > 200) {
295
+ console.log(chalk.dim(`\n... ${sites.length - 200} more`));
297
296
  }
298
297
  return;
298
+ } catch (e) {
299
+ console.log(chalk.red(`Error: ${e.message}`));
300
+ process.exit(1);
299
301
  }
302
+ }
303
+ // 1. Symbolic Trace
304
+ if (options.method && options.file && options.trace) {
305
+ const { getSymbolicTrace } = require('../index');
306
+ const GoUISession = require('../lib/goUiSession');
300
307
 
301
- if (options.xray) {
302
- const config = loadConfig();
303
- const { generateTest: nativeGenerateTest } = require('../index');
308
+ try {
309
+ const traceJson = getSymbolicTrace(
310
+ path.resolve(options.file),
311
+ options.method,
312
+ options.class || null
313
+ );
304
314
 
305
- try {
306
- const resultJson = nativeGenerateTest(
307
- path.resolve(options.file),
308
- options.method,
309
- null,
310
- 'visualize',
311
- JSON.stringify(config),
312
- (msg, percent) => {
313
- if (options.json) return;
314
- if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
315
- }
316
- );
317
-
318
- if (options.json) {
319
- console.log(resultJson);
320
- return;
321
- }
315
+ if (options.json) {
316
+ console.log(traceJson);
317
+ return;
318
+ }
319
+
320
+ // Create temp file for the trace
321
+ const tmpDir = require('os').tmpdir();
322
+ const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
323
+ fs.writeFileSync(tmpFile, traceJson);
324
+
325
+ // Launch Go UI
326
+ const session = new GoUISession();
327
+ const binaryPath = session._binaryPath;
328
+
329
+ const { spawnSync } = require('child_process');
330
+ spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
331
+ stdio: 'inherit'
332
+ });
333
+
334
+ // Cleanup
335
+ try { fs.unlinkSync(tmpFile); } catch (e) { }
336
+
337
+ } catch (e) {
338
+ console.log(chalk.red(`Trace failed: ${e.message}`));
339
+ process.exit(1);
340
+ }
341
+ return;
342
+ }
343
+
344
+ // 2. X-Ray
345
+ if (options.method && options.file && options.xray) {
346
+ const config = loadConfig();
347
+ const { generateTest: nativeGenerateTest } = require('../index');
322
348
 
323
- // For global --xray, we just launch the Premium UI directly if not JSON
324
- const GoUISession = require('../lib/goUiSession');
325
- const session = new GoUISession();
326
- let mermaidCode = "";
327
- let explanation = "";
328
- try {
329
- const parsed = JSON.parse(resultJson);
330
- mermaidCode = parsed.mermaid_code || resultJson;
331
- explanation = parsed.explanation || "";
332
- } catch (e) {
333
- mermaidCode = resultJson;
349
+ try {
350
+ const resultJson = nativeGenerateTest(
351
+ path.resolve(options.file),
352
+ options.method,
353
+ options.class || null,
354
+ 'visualize',
355
+ JSON.stringify(config),
356
+ (msg, percent) => {
357
+ if (options.json) return;
358
+ if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
334
359
  }
360
+ );
335
361
 
336
- // Copy to clipboard
337
- copyToClipboard(mermaidCode);
362
+ if (options.json) {
363
+ console.log(resultJson);
364
+ return;
365
+ }
338
366
 
339
- await session.showXrayResults(options.method, '', mermaidCode, explanation);
367
+ const GoUISession = require('../lib/goUiSession');
368
+ const session = new GoUISession();
369
+ let mermaidCode = "";
370
+ let explanation = "";
371
+ try {
372
+ const parsed = JSON.parse(resultJson);
373
+ mermaidCode = parsed.mermaid_code || resultJson;
374
+ explanation = parsed.explanation || "";
340
375
  } catch (e) {
341
- console.log(chalk.red(`X-Ray failed: ${e.message}`));
342
- process.exit(1);
376
+ mermaidCode = resultJson;
343
377
  }
344
- return;
345
- }
346
378
 
347
- if (!options.type && !options.audit) {
348
- console.log(chalk.red('Error: --type <type> is required.'));
379
+ copyToClipboard(mermaidCode);
380
+ await session.showXrayResults(options.method, '', mermaidCode, explanation);
381
+ } catch (e) {
382
+ console.log(chalk.red(`X-Ray failed: ${e.message}`));
349
383
  process.exit(1);
350
384
  }
351
- generateTest(options.file, options.method, options.type, options.audit, options.json);
352
- } else if (options.file && !options.method) {
353
- if (options.clones) {
354
- await runClones(options.file, options.level || 'all', options.json);
355
- } else if (options.deadcode) {
385
+ return;
386
+ }
387
+
388
+ // 3. Impact Analysis
389
+ if (options.method && options.file && options.impact) {
390
+ await runImpact(options.file, options.method, options.json, options.class || null);
391
+ return;
392
+ }
393
+
394
+ // 4. Clone Detection
395
+ if (options.file && options.clones) {
396
+ await runClones(options.file, options.level || 'all', options.json);
397
+ return;
398
+ }
399
+
400
+ // 5. Dead Code Analysis
401
+ if (options.deadcode) {
402
+ if (options.all) {
403
+ await runDeadCodeRepo(options.json);
404
+ } else if (options.file) {
356
405
  await runDeadCode(options.file, options.json);
357
406
  } else {
358
- console.log(chalk.yellow('Specify a method with -m, use --outline to see methods, or run "tng i" for full selection.'));
407
+ console.log(chalk.red('Error: --file <path> or --all is required for deadcode analysis.'));
408
+ process.exit(1);
409
+ }
410
+ return;
411
+ }
412
+
413
+ // 6. Test Generation / Audit (Method + File required)
414
+ if (options.method && options.file) {
415
+ if (!options.type && !options.audit) {
416
+ console.log(chalk.red('Error: --type <type> is required.'));
417
+ process.exit(1);
359
418
  }
360
- } else if (!options.file && !options.method && process.argv.length <= 2) {
361
- launchInteractive();
419
+ generateTest(options.file, options.method, options.type, options.audit, options.json, options.class || null);
420
+ return;
362
421
  }
422
+
423
+ // 7. Interactive Fallback
424
+ launchInteractive();
363
425
  });
364
426
 
365
427
  /**
@@ -439,7 +501,7 @@ async function runDeadCode(filePath, jsonMode = false) {
439
501
  jsonSession.start();
440
502
  try {
441
503
  const resultJson = detectDeadCode(absolutePath);
442
- jsonSession.emitEvent('dead_code', JSON.parse(resultJson));
504
+ jsonSession.showDeadCode(JSON.parse(resultJson));
443
505
  jsonSession.stop();
444
506
  } catch (e) {
445
507
  jsonSession.displayError(e.message);
@@ -469,6 +531,144 @@ async function runDeadCode(filePath, jsonMode = false) {
469
531
  }
470
532
  }
471
533
 
534
+ function listDeadCodeFiles(projectRoot) {
535
+ const { listDeadcodeFiles } = require('../index');
536
+ try {
537
+ const filesJson = listDeadcodeFiles(projectRoot);
538
+ const files = JSON.parse(filesJson);
539
+ if (!Array.isArray(files)) return [];
540
+ return files.filter((f) => !f.endsWith('.d.ts'));
541
+ } catch (e) {
542
+ return [];
543
+ }
544
+ }
545
+
546
+ async function runDeadCodeRepo(jsonMode = false) {
547
+ const { detectDeadCode } = require('../index');
548
+ const projectRoot = process.cwd();
549
+ const files = listDeadCodeFiles(projectRoot);
550
+
551
+ if (files.length === 0) {
552
+ console.log(chalk.yellow('No files found for dead code analysis.'));
553
+ return;
554
+ }
555
+
556
+ const runAnalysis = async () => {
557
+ const aggregateIssues = [];
558
+ for (let i = 0; i < files.length; i++) {
559
+ const filePath = files[i];
560
+ const rel = path.relative(projectRoot, filePath);
561
+ try {
562
+ const resultJson = detectDeadCode(filePath);
563
+ const issues = JSON.parse(resultJson);
564
+ if (Array.isArray(issues)) {
565
+ for (const issue of issues) {
566
+ aggregateIssues.push({
567
+ issue_type: issue.issue_type,
568
+ line: issue.line,
569
+ message: `[${rel}] ${issue.message}`,
570
+ code_snippet: issue.code_snippet,
571
+ file: rel
572
+ });
573
+ }
574
+ }
575
+ } catch (e) {
576
+ aggregateIssues.push({
577
+ issue_type: 'analysis_error',
578
+ line: 0,
579
+ message: `[${rel}] Failed to analyze: ${e.message}`,
580
+ code_snippet: '',
581
+ file: rel
582
+ });
583
+ }
584
+ }
585
+ return aggregateIssues;
586
+ };
587
+
588
+ if (jsonMode) {
589
+ const { JsonSession } = require('../lib/jsonSession');
590
+ const jsonSession = new JsonSession();
591
+ jsonSession.start();
592
+ const issues = await runAnalysis();
593
+ const out = {
594
+ file: projectRoot,
595
+ dead_code: issues.map(i => ({
596
+ type: i.issue_type,
597
+ line: i.line,
598
+ message: i.message,
599
+ code: i.code_snippet,
600
+ file: i.file
601
+ }))
602
+ };
603
+ jsonSession.showDeadCode(out);
604
+ jsonSession.stop();
605
+ return;
606
+ }
607
+
608
+ const total = files.length;
609
+ const aggregateIssues = [];
610
+ const start = Date.now();
611
+
612
+ const renderBar = (current, totalCount) => {
613
+ const width = 20;
614
+ const ratio = totalCount === 0 ? 1 : current / totalCount;
615
+ const filled = Math.round(ratio * width);
616
+ const bar = '#'.repeat(filled) + '-'.repeat(width - filled);
617
+ const percent = Math.round(ratio * 100);
618
+ return `Scanning project... [${bar}] ${percent}%`;
619
+ };
620
+
621
+ process.stdout.write(renderBar(0, total));
622
+ for (let i = 0; i < files.length; i++) {
623
+ const filePath = files[i];
624
+ const rel = path.relative(projectRoot, filePath);
625
+ try {
626
+ const resultJson = detectDeadCode(filePath);
627
+ const issues = JSON.parse(resultJson);
628
+ if (Array.isArray(issues)) {
629
+ for (const issue of issues) {
630
+ aggregateIssues.push({
631
+ file: rel,
632
+ line: issue.line,
633
+ message: issue.message,
634
+ code_snippet: issue.code_snippet,
635
+ issue_type: issue.issue_type
636
+ });
637
+ }
638
+ }
639
+ } catch (e) {
640
+ aggregateIssues.push({
641
+ file: rel,
642
+ line: 0,
643
+ message: `Failed to analyze: ${e.message}`,
644
+ code_snippet: '',
645
+ issue_type: 'analysis_error'
646
+ });
647
+ }
648
+
649
+ if (i % 10 === 0 || i === total - 1) {
650
+ process.stdout.write('\r' + renderBar(i + 1, total));
651
+ }
652
+ }
653
+ process.stdout.write('\n');
654
+
655
+ if (aggregateIssues.length === 0) {
656
+ console.log('No dead code detected.');
657
+ const seconds = ((Date.now() - start) / 1000).toFixed(2);
658
+ console.log(`Summary: Found 0 dead items in ${seconds}s.`);
659
+ return;
660
+ }
661
+
662
+ console.log('Found dead code:');
663
+ for (const issue of aggregateIssues) {
664
+ const snippet = issue.code_snippet ? ` (${issue.code_snippet.trim()})` : '';
665
+ console.log(`- ${issue.file}:${issue.line}${snippet}`);
666
+ }
667
+ const seconds = ((Date.now() - start) / 1000).toFixed(2);
668
+ console.log(`Summary: Found ${aggregateIssues.length} dead items in ${seconds}s.`);
669
+ console.log("[Tip] Run 'tng audit' to fix logic bugs in the remaining code.");
670
+ }
671
+
472
672
  /**
473
673
  * @command fix
474
674
  * Apply a specific fix to a file
@@ -507,7 +707,7 @@ program
507
707
  /**
508
708
  * Logic to generate test or run audit (delegates to native binary)
509
709
  */
510
- async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false) {
710
+ async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false, className = null) {
511
711
  const config = loadConfig();
512
712
 
513
713
  // Initialize JSON session if requested
@@ -577,7 +777,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
577
777
  resultJson = runAudit(
578
778
  absolutePath,
579
779
  methodName,
580
- null, // class_name
780
+ className,
581
781
  testType || null,
582
782
  JSON.stringify(config),
583
783
  callback
@@ -586,7 +786,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
586
786
  resultJson = nativeGenerateTest(
587
787
  absolutePath,
588
788
  methodName,
589
- null, // class_name
789
+ className,
590
790
  testType || null,
591
791
  JSON.stringify(config),
592
792
  callback
@@ -654,4 +854,76 @@ program.on('--help', () => {
654
854
  console.log('');
655
855
  });
656
856
 
857
+ /**
858
+ * Logic to run impact analysis
859
+ */
860
+ async function runImpact(filePath, methodName, jsonMode = false, className = null) {
861
+ const { analyzeImpact } = require('../index');
862
+ const absolutePath = path.resolve(filePath);
863
+ const projectRoot = process.cwd();
864
+
865
+ if (!fs.existsSync(absolutePath)) {
866
+ console.log(chalk.red(`File not found: ${filePath}`));
867
+ process.exit(1);
868
+ }
869
+
870
+ if (jsonMode) {
871
+ try {
872
+ const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
873
+ console.log(resultJson);
874
+ } catch (e) {
875
+ console.error(JSON.stringify({ error: e.message }));
876
+ process.exit(1);
877
+ }
878
+ return;
879
+ }
880
+
881
+ console.log(chalk.blue(`💥 Analyzing impact for ${methodName} in ${filePath}...`));
882
+
883
+ try {
884
+ const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
885
+ const result = JSON.parse(resultJson);
886
+
887
+ if (result.status === 'error') {
888
+ console.log(chalk.red(`\nError: ${result.diffs[0].message}`));
889
+ return;
890
+ }
891
+
892
+ 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')));
893
+
894
+ if (result.diffs.length > 0) {
895
+ console.log(chalk.bold('\nChanges:'));
896
+ result.diffs.forEach(diff => {
897
+ const color = diff.severity === 'breaking' ? chalk.red : (diff.severity === 'warning' ? chalk.yellow : chalk.white);
898
+ const icon = diff.severity === 'breaking' ? '❌' : (diff.severity === 'warning' ? '⚠️' : 'ℹ️');
899
+ // Handle optional name
900
+ const nameStr = diff.name ? ` (${diff.name})` : '';
901
+ console.log(` ${icon} ${color(diff.message)}${color(nameStr)}`);
902
+ });
903
+ } else {
904
+ console.log(chalk.green(' No changes detected in logic signature.'));
905
+ }
906
+
907
+ if (result.impacted_files.length > 0) {
908
+ console.log(chalk.bold(`\nImpacted Files (${result.impacted_files.length}):`));
909
+ result.impacted_files.forEach(f => {
910
+ // Relativize path
911
+ const relPath = path.relative(projectRoot, f.file);
912
+ console.log(chalk.dim(` • ${relPath}:${f.line}`));
913
+ // simple truncate code
914
+ const codePreview = f.code.trim().replace(/\n/g, ' ').substring(0, 60) + (f.code.length > 60 ? '...' : '');
915
+ console.log(chalk.gray(` ${codePreview}`));
916
+ });
917
+ } else if (result.status === 'breaking') {
918
+ console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
919
+ }
920
+
921
+ } catch (e) {
922
+ console.log(chalk.red(`Analysis failed: ${e.message}`));
923
+ if (e.message.includes("git")) {
924
+ console.log(chalk.yellow("Note: Impact analysis requires the file to be committed to git."));
925
+ }
926
+ }
927
+ }
928
+
657
929
  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, className?: string | undefined | null): string
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
@@ -6,7 +6,6 @@ const GoUISession = require('./goUiSession');
6
6
  const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
7
7
  const { loadConfig } = require('./config');
8
8
  const { saveTestFile } = require('./saveFile');
9
- const { applyFix } = require('./fixApplier');
10
9
 
11
10
  class GenerateTestsUI {
12
11
  constructor(cliMode = false) {
@@ -34,6 +33,9 @@ class GenerateTestsUI {
34
33
  } else if (choice === 'audit') {
35
34
  const result = await this._showFileSelection(true);
36
35
  if (result === 'exit') return 'exit';
36
+ } else if (choice === 'impact') {
37
+ const result = await this._showFileSelection(false, false, false, true);
38
+ if (result === 'exit') return 'exit';
37
39
  } else if (choice === 'xray') {
38
40
  const result = await this._showFileSelection(false, true);
39
41
  if (result === 'exit') return 'exit';
@@ -105,9 +107,10 @@ class GenerateTestsUI {
105
107
  return { success: false, message: e.message };
106
108
  }
107
109
  });
110
+ return false
108
111
  }
109
112
 
110
- async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
113
+ async _showFileSelection(isAudit = false, isXray = false, isTrace = false, isImpact = false) {
111
114
  const files = await this._getUserFiles();
112
115
 
113
116
  if (files.length === 0) {
@@ -123,6 +126,7 @@ class GenerateTestsUI {
123
126
 
124
127
  let title = 'Select JavaScript File';
125
128
  if (isAudit) title = 'Select JavaScript File to Audit';
129
+ else if (isImpact) title = 'Select JavaScript File for Regression Check';
126
130
  else if (isXray) title = 'Select File for X-Ray';
127
131
  else if (isTrace) title = 'Select File for Symbolic Trace';
128
132
 
@@ -132,76 +136,88 @@ class GenerateTestsUI {
132
136
  if (!selectedName || selectedName === 'exit') return 'exit';
133
137
 
134
138
  const selectedFile = path.resolve(cwd, selectedName);
135
- const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
139
+ const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace, isImpact);
136
140
  if (result === 'main_menu') return 'main_menu';
137
141
  return result;
138
142
  }
139
143
 
140
- async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
144
+ async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false, isImpact = false) {
141
145
  let outline;
142
146
  try {
143
147
  const result = getFileOutline(filePath);
144
148
  outline = JSON.parse(result);
145
149
  } catch (e) {
146
150
  console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
147
- return this._showFileSelection(isAudit);
151
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
148
152
  }
149
153
 
150
154
  const methods = outline.methods || [];
151
155
  if (methods.length === 0) {
152
156
  this.goUiSession.showNoItems('methods');
153
- return this._showFileSelection(isAudit);
157
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
154
158
  }
155
159
 
156
160
  const fileName = path.basename(filePath);
157
161
  const items = methods.map(m => ({
158
- name: m.class_name ? `${m.class_name}.${m.name}` : m.name,
162
+ name: m.class_name ? `${m.class_name}#${m.name}` : m.name,
159
163
  path: `Function in ${fileName}`,
160
164
  methodData: m
161
165
  }));
162
166
 
163
167
  let title = 'Select Method';
164
168
  if (isAudit) title = `Select Method to Audit for ${fileName}`;
169
+ else if (isImpact) title = `Select Method for Regression Check for ${fileName}`;
165
170
  else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
166
171
  else if (isTrace) title = `Select Method to Trace for ${fileName}`;
167
172
 
168
173
  const selectedDisplay = this.goUiSession.showListView(title, items);
169
174
 
170
- if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
175
+ if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
171
176
 
172
177
  const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
173
178
 
174
179
  if (selectedMethod) {
175
180
  if (isTrace) {
176
- await this._launchTrace(filePath, selectedMethod.name);
177
- return this._showFileSelection(isAudit, isXray, isTrace);
181
+ await this._launchTrace(filePath, selectedMethod.name, selectedMethod.class_name || null);
182
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
178
183
  }
179
184
 
180
185
  if (isXray) {
181
186
  const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
182
187
  if (choice === 'main_menu') return 'main_menu';
183
- return this._showFileSelection(isAudit, isXray);
188
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
184
189
  }
185
190
 
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);
191
+ if (isImpact) {
192
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, false, false, true);
193
+ if (choice === 'main_menu') return 'main_menu';
194
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
195
+ }
191
196
 
192
197
  if (isAudit) {
198
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, true);
193
199
  if (choice === 'main_menu') return 'main_menu';
194
- return this._showFileSelection(isAudit, isXray);
200
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
195
201
  }
196
202
 
203
+ const testType = this.goUiSession.showJsTestMenu();
204
+ if (testType === 'back') return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
205
+
206
+ const finalType = testType === 'auto' ? null : testType;
207
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, false);
208
+
197
209
  if (choice && choice.file_path && !choice.error) {
198
210
  this._showPostGenerationMenu(choice);
199
211
  }
200
212
  }
201
- return this._showFileSelection(isAudit, isXray);
213
+ return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
202
214
  }
203
215
 
204
- async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
216
+ async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false, isImpact = false) {
217
+ if (isImpact) {
218
+ return this._handleImpactFlow(filePath, method);
219
+ }
220
+
205
221
  if (!this._hasApiKey()) {
206
222
  return { error: 'No API key' };
207
223
  }
@@ -229,169 +245,75 @@ class GenerateTestsUI {
229
245
  return true;
230
246
  }
231
247
 
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;
248
+ async _handleImpactFlow(filePath, method) {
249
+ const { analyzeImpact } = require('../index');
250
+ const projectRoot = process.cwd();
250
251
 
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
- });
252
+ console.log(chalk.blue(`\n🔍 Analyzing impact for ${method.name}...`));
255
253
 
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
- });
254
+ let impactResult;
255
+ try {
256
+ const resultJson = await this.goUiSession.showProgress('Analyzing impact...', async (progress) => {
257
+ progress.update('Tracing method versions...', { percent: 30 });
258
+ // Artificial delay for UX
259
+ await new Promise(r => setTimeout(r, 300));
313
260
 
314
- worker.on('error', (err) => {
315
- console.error(chalk.red(`Worker error: ${err.message}`));
316
- auditFinished = true;
317
- });
261
+ const json = analyzeImpact(projectRoot, filePath, method.name, method.class_name || null);
262
+ progress.update('Comparing logic...', { percent: 80 });
263
+
264
+ return json;
265
+ });
318
266
 
267
+ impactResult = JSON.parse(resultJson);
268
+ } catch (e) {
269
+ console.error(chalk.red(`\nImpact Analysis failed: ${e.message}\n`));
270
+ return null;
271
+ }
272
+
273
+ await this.goUiSession.showImpactResults(impactResult);
274
+ return { message: 'Regression check complete' };
275
+ }
276
+
277
+ async _handleAuditFlow(filePath, method, testType) {
278
+ const { runAudit } = require('../index');
279
+ const config = loadConfig();
280
+ const fileName = path.basename(filePath);
281
+ const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
282
+
283
+ console.log(chalk.blue(`\n🔍 Auditing ${displayName}...`));
284
+
285
+ let auditResult;
319
286
  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 };
287
+ const resultJson = await this.goUiSession.showProgress(`Auditing ${displayName}...`, async (progress) => {
288
+ progress.update('Preparing audit...', { percent: 10 });
289
+
290
+ const json = runAudit(
291
+ filePath,
292
+ method.name,
293
+ method.class_name || null,
294
+ testType || null,
295
+ JSON.stringify(config),
296
+ (msg, percent) => {
297
+ if (typeof percent === 'number') {
298
+ progress.update(msg, { percent });
299
+ } else {
300
+ progress.update(msg, { step_increment: false });
334
301
  }
335
302
  }
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
- }
303
+ );
373
304
 
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
- }
305
+ progress.update('Rendering results...', { percent: 95 });
306
+ return json;
307
+ });
387
308
 
388
- return { message: 'Audit complete', results };
309
+ auditResult = JSON.parse(resultJson);
389
310
  } catch (e) {
390
- console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
311
+ console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
391
312
  return null;
392
- } finally {
393
- await worker.terminate();
394
313
  }
314
+
315
+ await this.goUiSession.showAuditResults(auditResult);
316
+ return { message: 'Audit complete' };
395
317
  }
396
318
 
397
319
  async _handleTestGenerationFlow(filePath, method, testType, displayName) {
@@ -677,7 +599,7 @@ class GenerateTestsUI {
677
599
  }
678
600
  }
679
601
 
680
- async _launchTrace(filePath, methodName) {
602
+ async _launchTrace(filePath, methodName, className) {
681
603
  const { getSymbolicTrace } = require('../index');
682
604
  const fs = require('fs');
683
605
  const path = require('path');
@@ -687,7 +609,7 @@ class GenerateTestsUI {
687
609
  // 1. Generate Trace (with Spinner)
688
610
  const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
689
611
  try {
690
- const traceJson = getSymbolicTrace(filePath, methodName, null);
612
+ const traceJson = getSymbolicTrace(filePath, methodName, className || null);
691
613
  const tmpDir = require('os').tmpdir();
692
614
  const f = path.join(tmpDir, `trace-${Date.now()}.json`);
693
615
  fs.writeFileSync(f, traceJson);
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.2.0",
3
+ "version": "0.2.4",
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