@tng-sh/js 0.1.3 → 0.1.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 @@
1
+ {"file_path":"/Users/claudiu/work/tng-inc/js-pie/test_duplicates.js","matches":[{"start1":1,"end1":13,"start2":16,"end2":28,"line_count":13,"node_count":12,"complexity_score":245,"algo":"structural"}]}
@@ -0,0 +1 @@
1
+ {"type":"step","step":1,"message":"Done!","percent":100}
@@ -0,0 +1,13 @@
1
+ tng_sh_js:{"kind": "interface", "name": "CloneMatch", "js_doc": "", "def": "start1: number\nend1: number\nstart2: number\nend2: number\nlineCount: number\nnodeCount: number\ncomplexityScore: number\nalgo: string", "original_name": "CloneMatch"}
2
+ tng_sh_js:{"kind": "fn", "name": "getFileOutline", "js_doc": "/**\n * Analyzes a JavaScript/TypeScript file and returns its structural outline as a JSON string.\n * Contains information about classes and methods found in the file.\n */\n", "def": "export declare function getFileOutline(filePath: string): string"}
3
+ tng_sh_js:{"kind": "fn", "name": "getProjectMetadata", "js_doc": "/**\n * Gathers project-wide metadata from package.json and related files.\n * Returns a JSON string containing dependencies, frameworks, and project type.\n */\n", "def": "export declare function getProjectMetadata(projectRoot: string): string"}
4
+ tng_sh_js:{"kind": "fn", "name": "findCallSites", "js_doc": "/**\n * Searches the project for all locations where a specific method is called.\n * Uses ripgrep under the hood for high performance.\n */\n", "def": "export declare function findCallSites(projectRoot: string, methodName: string): string"}
5
+ tng_sh_js:{"kind": "fn", "name": "ping", "js_doc": "/** Pings the TNG API to verify connectivity and API key validity. */\n", "def": "export declare function ping(baseUrl: string, apiKey?: string | undefined | null): string"}
6
+ tng_sh_js:{"kind": "fn", "name": "submitJob", "js_doc": "/** Submits a test generation job to the API and returns the numeric job ID. */\n", "def": "export declare function submitJob(baseUrl: string, apiKey: string, payloadJson: string): number"}
7
+ tng_sh_js:{"kind": "fn", "name": "getUserStats", "js_doc": "/** Fetches usage statistics for the authenticated user from the API. */\n", "def": "export declare function getUserStats(baseUrl: string, apiKey: string): string"}
8
+ tng_sh_js:{"kind": "fn", "name": "runAudit", "js_doc": "/**\n * Orchestrates the code audit process.\n * Analyzes the source, builds context, and streams results via the provided callback.\n */\n", "def": "export declare function runAudit(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string"}
9
+ tng_sh_js:{"kind": "fn", "name": "applyEdit", "js_doc": "/**\n * Applies a single edit operation to a file.\n * Uses backup and atomic write for safety.\n */\n", "def": "export declare function applyEdit(filePath: string, search: string, replace: string, lineHint?: number | undefined | null): string"}
10
+ tng_sh_js:{"kind": "fn", "name": "applyEditsAtomic", "js_doc": "/**\n * Applies multiple edit operations atomically.\n * All succeed or all fail (with rollback).\n */\n", "def": "export declare function applyEditsAtomic(operationsJson: string): string"}
11
+ tng_sh_js:{"kind": "fn", "name": "generateTest", "js_doc": "/**\n * Orchestrates the test generation process.\n * Analyzes code, submits a job, and polls for the final generated test.\n */\n", "def": "export declare function generateTest(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string"}
12
+ tng_sh_js:{"kind": "fn", "name": "getSymbolicTrace", "js_doc": "/** Analyzes a method and generates a symbolic execution trace. */\n", "def": "export declare function getSymbolicTrace(filePath: string, methodName: string, className?: string | undefined | null): string"}
13
+ tng_sh_js:{"kind": "fn", "name": "analyzeClones", "js_doc": "/**\n * Analyzes a file for code clones (duplicates).\n * Returns a JSON string containing the matches found.\n */\n", "def": "export declare function analyzeClones(projectRoot: string, filePath: string, level: string): string"}
@@ -0,0 +1,13 @@
1
+ tng_sh_js: __napi_register__CloneMatch_struct_0
2
+ tng_sh_js: __napi_register__get_file_outline_1
3
+ tng_sh_js: __napi_register__get_project_metadata_2
4
+ tng_sh_js: __napi_register__find_call_sites_3
5
+ tng_sh_js: __napi_register__ping_4
6
+ tng_sh_js: __napi_register__submit_job_5
7
+ tng_sh_js: __napi_register__get_user_stats_6
8
+ tng_sh_js: __napi_register__run_audit_7
9
+ tng_sh_js: __napi_register__apply_edit_8
10
+ tng_sh_js: __napi_register__apply_edits_atomic_9
11
+ tng_sh_js: __napi_register__generate_test_10
12
+ tng_sh_js: __napi_register__get_symbolic_trace_11
13
+ tng_sh_js: __napi_register__analyze_clones_12
package/bin/tng.js CHANGED
@@ -28,7 +28,7 @@ process.on('uncaughtException', (err) => {
28
28
  program
29
29
  .name('tng')
30
30
  .description('TNG - Automated Test Generation, and audit generation for JavaScript')
31
- .version('0.1.3');
31
+ .version('0.1.4');
32
32
 
33
33
  /**
34
34
  * @command init
@@ -211,6 +211,8 @@ program
211
211
  .option('-t, --type <type>', 'Component type (react_component, express_handler, etc) [required]')
212
212
  .option('-a, --audit', 'Run audit mode instead of test generation')
213
213
  .option('--trace', 'Run symbolic trace visualization')
214
+ .option('-c, --clones', 'Run duplicate code detection')
215
+ .option('-l, --level <level>', 'Set clone detection level (1, 2, or all)', 'all')
214
216
  .option('--json', 'Output results as JSON events (machine-readable)')
215
217
 
216
218
  .action(async (options) => {
@@ -261,12 +263,72 @@ program
261
263
  }
262
264
  generateTest(options.file, options.method, options.type, options.audit, options.json);
263
265
  } else if (options.file && !options.method) {
264
- console.log(chalk.yellow('Specify a method with -m, use --outline to see methods, or run "tng i" for full selection.'));
266
+ if (options.clones) {
267
+ runClones(options.file, options.level || 'all', options.json);
268
+ } else {
269
+ console.log(chalk.yellow('Specify a method with -m, use --outline to see methods, or run "tng i" for full selection.'));
270
+ }
265
271
  } else if (!options.file && !options.method && process.argv.length <= 2) {
266
272
  launchInteractive();
267
273
  }
268
274
  });
269
275
 
276
+ /**
277
+ * Logic to run clone detection
278
+ */
279
+ async function runClones(filePath, level, jsonMode = false) {
280
+ const { analyzeClones } = require('../index');
281
+ const absolutePath = path.resolve(filePath);
282
+ const projectRoot = process.cwd();
283
+
284
+ if (!fs.existsSync(absolutePath)) {
285
+ console.log(chalk.red(`File not found: ${filePath}`));
286
+ process.exit(1);
287
+ }
288
+
289
+ const GoUISession = require('../lib/goUiSession');
290
+ const session = new GoUISession();
291
+
292
+ if (jsonMode) {
293
+ const { JsonSession } = require('../lib/jsonSession');
294
+ const jsonSession = new JsonSession();
295
+ jsonSession.start();
296
+ try {
297
+ const matchesJson = analyzeClones(projectRoot, absolutePath, level);
298
+ const matches = JSON.parse(matchesJson);
299
+ jsonSession.showClones(absolutePath, matches);
300
+ jsonSession.stop();
301
+ } catch (e) {
302
+ jsonSession.displayError(e.message);
303
+ jsonSession.stop();
304
+ process.exit(1);
305
+ }
306
+ return;
307
+ }
308
+
309
+ console.log(chalk.blue(`🔍 Analyzing clones in ${filePath}...`));
310
+
311
+ try {
312
+ const matches = await session.showProgress('Analyzing clones...', async (progress) => {
313
+ progress.update('Scanning structures...', { percent: 50 });
314
+ const matchesJson = analyzeClones(projectRoot, absolutePath, level);
315
+ const matches = JSON.parse(matchesJson);
316
+ progress.update('Done!', { percent: 100 });
317
+
318
+ // Short delay to let the user see 100%
319
+ await new Promise(r => setTimeout(r, 100));
320
+ return matches;
321
+ });
322
+
323
+ if (matches) {
324
+ session.showClones(absolutePath, matches);
325
+ }
326
+ } catch (e) {
327
+ console.log(chalk.red(`Error: ${e.message}`));
328
+ process.exit(1);
329
+ }
330
+ }
331
+
270
332
  /**
271
333
  * @command fix
272
334
  * Apply a specific fix to a file
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -3,6 +3,16 @@
3
3
 
4
4
  /* auto-generated by NAPI-RS */
5
5
 
6
+ export interface CloneMatch {
7
+ start1: number
8
+ end1: number
9
+ start2: number
10
+ end2: number
11
+ lineCount: number
12
+ nodeCount: number
13
+ complexityScore: number
14
+ algo: string
15
+ }
6
16
  /**
7
17
  * Analyzes a JavaScript/TypeScript file and returns its structural outline as a JSON string.
8
18
  * Contains information about classes and methods found in the file.
@@ -46,3 +56,8 @@ export declare function applyEditsAtomic(operationsJson: string): string
46
56
  export declare function generateTest(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string
47
57
  /** Analyzes a method and generates a symbolic execution trace. */
48
58
  export declare function getSymbolicTrace(filePath: string, methodName: string, className?: string | undefined | null): string
59
+ /**
60
+ * Analyzes a file for code clones (duplicates).
61
+ * Returns a JSON string containing the matches found.
62
+ */
63
+ export declare function analyzeClones(projectRoot: string, filePath: string, level: string): 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 } = nativeBinding
313
+ const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace, analyzeClones } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -323,3 +323,4 @@ module.exports.applyEdit = applyEdit
323
323
  module.exports.applyEditsAtomic = applyEditsAtomic
324
324
  module.exports.generateTest = generateTest
325
325
  module.exports.getSymbolicTrace = getSymbolicTrace
326
+ module.exports.analyzeClones = analyzeClones
@@ -40,6 +40,9 @@ class GenerateTestsUI {
40
40
  } else if (choice === 'trace') {
41
41
  const result = await this._showFileSelection(false, false, true);
42
42
  if (result === 'exit') return 'exit';
43
+ } else if (choice === 'clones') {
44
+ const result = await this._showClonesSelection();
45
+ if (result === 'exit') return 'exit';
43
46
  } else if (choice === 'stats') {
44
47
  await this._showStats();
45
48
  } else if (choice === 'about') {
@@ -708,6 +711,62 @@ class GenerateTestsUI {
708
711
  console.error(chalk.red(`Trace error: ${e.message}`));
709
712
  }
710
713
  }
714
+
715
+ async _showClonesSelection() {
716
+ const files = await this._getUserFiles();
717
+
718
+ if (files.length === 0) {
719
+ console.log(chalk.yellow('\nNo JavaScript or TypeScript files found in your project.\n'));
720
+ return 'back';
721
+ }
722
+
723
+ const cwd = process.cwd();
724
+ const items = files.map(file => ({
725
+ name: path.relative(cwd, file),
726
+ path: path.dirname(file)
727
+ }));
728
+
729
+ const selectedName = this.goUiSession.showListView('Select File for Duplicate Detection', items);
730
+
731
+ if (selectedName === 'back') return 'back';
732
+ if (!selectedName || selectedName === 'exit') return 'exit';
733
+
734
+ const selectedFile = path.resolve(cwd, selectedName);
735
+
736
+ // Show level selection
737
+ const level = this.goUiSession.showCloneMenu();
738
+ if (level === 'back') return this._showClonesSelection();
739
+
740
+ await this._runClones(selectedFile, level);
741
+ return 'main_menu';
742
+ }
743
+
744
+ async _runClones(filePath, level) {
745
+ const { analyzeClones } = require('../index');
746
+ const absolutePath = path.resolve(filePath);
747
+ const projectRoot = process.cwd();
748
+
749
+ console.log(chalk.blue(`🔍 Analyzing clones in ${path.relative(projectRoot, filePath)}...`));
750
+
751
+ try {
752
+ const matches = await this.goUiSession.showProgress('Analyzing clones...', async (progress) => {
753
+ progress.update('Scanning structures...', { percent: 50 });
754
+ const matchesJson = analyzeClones(projectRoot, absolutePath, level);
755
+ const matches = JSON.parse(matchesJson);
756
+ progress.update('Done!', { percent: 100 });
757
+
758
+ // Short delay to let the user see 100%
759
+ await new Promise(r => setTimeout(r, 100));
760
+ return matches;
761
+ });
762
+
763
+ if (matches) {
764
+ this.goUiSession.showClones(absolutePath, matches);
765
+ }
766
+ } catch (e) {
767
+ console.log(chalk.red(`Error: ${e.message}`));
768
+ }
769
+ }
711
770
  }
712
771
 
713
772
  module.exports = GenerateTestsUI;
@@ -78,6 +78,30 @@ class GoUISession {
78
78
  }
79
79
  }
80
80
 
81
+ showCloneMenu() {
82
+ const outputFile = this._trackTempFile(this._createTempFile('clone-menu-output', '.txt'));
83
+
84
+ try {
85
+ const result = spawnSync(this._binaryPath, ['clone-menu', '--output', outputFile], {
86
+ stdio: 'inherit'
87
+ });
88
+
89
+ if (result.error) throw result.error;
90
+
91
+ if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
92
+ return 'back';
93
+ }
94
+
95
+ const choice = fs.readFileSync(outputFile, 'utf8').trim();
96
+ return choice || 'back';
97
+ } catch (error) {
98
+ console.error('Clone Menu error:', error.message);
99
+ return 'back';
100
+ } finally {
101
+ this._cleanupTempFile(outputFile);
102
+ }
103
+ }
104
+
81
105
  showListView(title, items) {
82
106
  const dataJson = JSON.stringify({ title, items });
83
107
  const outputFile = this._trackTempFile(this._createTempFile('list-view-output', '.txt'));
@@ -148,6 +172,22 @@ class GoUISession {
148
172
  }
149
173
  }
150
174
 
175
+ showSystemStatus(statusData) {
176
+ const dataJson = JSON.stringify(statusData);
177
+ const inputFile = this._trackTempFile(this._createTempFile('system-status', '.json'));
178
+
179
+ try {
180
+ fs.writeFileSync(inputFile, dataJson);
181
+ spawnSync(this._binaryPath, ['system-status', '--file', inputFile], {
182
+ stdio: 'inherit'
183
+ });
184
+ } catch (error) {
185
+ console.error('System status error:', error.message);
186
+ } finally {
187
+ this._cleanupTempFile(inputFile);
188
+ }
189
+ }
190
+
151
191
  showAbout() {
152
192
  try {
153
193
  spawnSync(this._binaryPath, ['about'], {
@@ -311,6 +351,24 @@ class GoUISession {
311
351
  }
312
352
  }
313
353
 
354
+ async showClones(filePath, results) {
355
+ const dataJson = JSON.stringify({ file_path: filePath, matches: results });
356
+ const inputFile = this._trackTempFile(this._createTempFile('clone-data', '.json'));
357
+
358
+ try {
359
+ fs.writeFileSync(inputFile, dataJson);
360
+
361
+ spawnSync(this._binaryPath, ['clones', '--file', inputFile], {
362
+ stdio: 'inherit',
363
+ env: process.env
364
+ });
365
+ } catch (error) {
366
+ console.error('Clone results display error:', error.message);
367
+ } finally {
368
+ this._cleanupTempFile(inputFile);
369
+ }
370
+ }
371
+
314
372
  async showStreamingAuditResults(methodName, className, sourceCode) {
315
373
  const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
316
374
 
@@ -374,6 +432,27 @@ class GoUISession {
374
432
  }
375
433
  }
376
434
 
435
+ showAuthError(message = 'Authentication failed') {
436
+ try {
437
+ spawnSync(this._binaryPath, ['auth-error', '--message', message], {
438
+ stdio: 'inherit'
439
+ });
440
+ } catch (error) {
441
+ console.error('Auth error:', error.message);
442
+ }
443
+ }
444
+
445
+ showConfigError(missing = []) {
446
+ try {
447
+ const dataJson = JSON.stringify({ missing });
448
+ spawnSync(this._binaryPath, ['config-error', '--data', dataJson], {
449
+ stdio: 'inherit'
450
+ });
451
+ } catch (error) {
452
+ console.error('Config error:', error.message);
453
+ }
454
+ }
455
+
377
456
  _findGoUiBinary() {
378
457
  const platform = process.platform;
379
458
  const arch = process.arch;
@@ -397,8 +476,20 @@ class GoUISession {
397
476
 
398
477
  _createTempFile(prefix, suffix) {
399
478
  const tmpDir = os.tmpdir();
400
- const filePath = path.join(tmpDir, `tng-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
401
- fs.writeFileSync(filePath, '');
479
+ const fileName = `tng-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`;
480
+ let filePath = path.join(tmpDir, fileName);
481
+
482
+ try {
483
+ fs.writeFileSync(filePath, '');
484
+ } catch (e) {
485
+ // Fallback to local directory if system tmp is not writable
486
+ const localTmp = path.join(process.cwd(), '.tng-tmp');
487
+ if (!fs.existsSync(localTmp)) {
488
+ fs.mkdirSync(localTmp, { recursive: true });
489
+ }
490
+ filePath = path.join(localTmp, fileName);
491
+ fs.writeFileSync(filePath, '');
492
+ }
402
493
  return filePath;
403
494
  }
404
495
 
@@ -66,6 +66,10 @@ class JsonSession {
66
66
  this.emitEvent('result', auditResult || {});
67
67
  }
68
68
 
69
+ showClones(filePath, results) {
70
+ this.emitEvent('clones', { file_path: filePath, matches: results });
71
+ }
72
+
69
73
  showTestResults(title, passed, failed, errors, total, results = []) {
70
74
  this.emitEvent('test_results', {
71
75
  title: title || 'Test Results',
@@ -105,7 +109,7 @@ class JsonSession {
105
109
 
106
110
  emitEvent(type, data = {}) {
107
111
  try {
108
- const event = { type, ...data, timestamp: Date.now() };
112
+ const event = { type, ...data };
109
113
  console.log(JSON.stringify(event));
110
114
  } catch (e) {
111
115
  // If serialization fails (e.g. circular ref), emit minimal error
package/out.log ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
Binary file
Binary file
Binary file