@tng-sh/js 0.1.2 → 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.2');
31
+ .version('0.1.4');
32
32
 
33
33
  /**
34
34
  * @command init
@@ -210,22 +210,125 @@ program
210
210
  .option('-f, --file <path>', 'JavaScript file path')
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
+ .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')
213
216
  .option('--json', 'Output results as JSON events (machine-readable)')
214
217
 
215
218
  .action(async (options) => {
216
219
  if (options.method && options.file) {
220
+ if (options.trace) {
221
+ const { getSymbolicTrace } = require('../index');
222
+ const GoUISession = require('../lib/goUiSession');
223
+
224
+ try {
225
+ const traceJson = getSymbolicTrace(
226
+ path.resolve(options.file),
227
+ options.method,
228
+ null
229
+ );
230
+
231
+ if (options.json) {
232
+ console.log(traceJson);
233
+ return;
234
+ }
235
+
236
+ // Create temp file for the trace
237
+ const tmpDir = require('os').tmpdir();
238
+ const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
239
+ fs.writeFileSync(tmpFile, traceJson);
240
+
241
+ // Launch Go UI
242
+ const session = new GoUISession();
243
+ const binaryPath = session._binaryPath;
244
+
245
+ const { spawnSync } = require('child_process');
246
+ spawnSync(binaryPath, ['trace-results', '--file', tmpFile], {
247
+ stdio: 'inherit'
248
+ });
249
+
250
+ // Cleanup
251
+ try { fs.unlinkSync(tmpFile); } catch (e) { }
252
+
253
+ } catch (e) {
254
+ console.log(chalk.red(`Trace failed: ${e.message}`));
255
+ process.exit(1);
256
+ }
257
+ return;
258
+ }
259
+
217
260
  if (!options.type && !options.audit) {
218
261
  console.log(chalk.red('Error: --type <type> is required.'));
219
262
  process.exit(1);
220
263
  }
221
264
  generateTest(options.file, options.method, options.type, options.audit, options.json);
222
265
  } else if (options.file && !options.method) {
223
- 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
+ }
224
271
  } else if (!options.file && !options.method && process.argv.length <= 2) {
225
272
  launchInteractive();
226
273
  }
227
274
  });
228
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
+
229
332
  /**
230
333
  * @command fix
231
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.
@@ -44,3 +54,10 @@ export declare function applyEditsAtomic(operationsJson: string): string
44
54
  * Analyzes code, submits a job, and polls for the final generated test.
45
55
  */
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
57
+ /** Analyzes a method and generates a symbolic execution trace. */
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 } = 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
@@ -322,3 +322,5 @@ module.exports.runAudit = runAudit
322
322
  module.exports.applyEdit = applyEdit
323
323
  module.exports.applyEditsAtomic = applyEditsAtomic
324
324
  module.exports.generateTest = generateTest
325
+ module.exports.getSymbolicTrace = getSymbolicTrace
326
+ module.exports.analyzeClones = analyzeClones
@@ -37,6 +37,12 @@ class GenerateTestsUI {
37
37
  } else if (choice === 'xray') {
38
38
  const result = await this._showFileSelection(false, true);
39
39
  if (result === 'exit') return 'exit';
40
+ } else if (choice === 'trace') {
41
+ const result = await this._showFileSelection(false, false, true);
42
+ if (result === 'exit') return 'exit';
43
+ } else if (choice === 'clones') {
44
+ const result = await this._showClonesSelection();
45
+ if (result === 'exit') return 'exit';
40
46
  } else if (choice === 'stats') {
41
47
  await this._showStats();
42
48
  } else if (choice === 'about') {
@@ -98,7 +104,7 @@ class GenerateTestsUI {
98
104
  });
99
105
  }
100
106
 
101
- async _showFileSelection(isAudit = false, isXray = false) {
107
+ async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
102
108
  const files = await this._getUserFiles();
103
109
 
104
110
  if (files.length === 0) {
@@ -112,19 +118,23 @@ class GenerateTestsUI {
112
118
  path: path.dirname(file)
113
119
  }));
114
120
 
115
- const title = isXray ? 'Select File for X-Ray' : (isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File');
121
+ let title = 'Select JavaScript File';
122
+ if (isAudit) title = 'Select JavaScript File to Audit';
123
+ else if (isXray) title = 'Select File for X-Ray';
124
+ else if (isTrace) title = 'Select File for Symbolic Trace';
125
+
116
126
  const selectedName = this.goUiSession.showListView(title, items);
117
127
 
118
128
  if (selectedName === 'back') return 'back';
119
129
  if (!selectedName || selectedName === 'exit') return 'exit';
120
130
 
121
131
  const selectedFile = path.resolve(cwd, selectedName);
122
- const result = await this._showMethodsForFile(selectedFile, isAudit, isXray);
132
+ const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
123
133
  if (result === 'main_menu') return 'main_menu';
124
134
  return result;
125
135
  }
126
136
 
127
- async _showMethodsForFile(filePath, isAudit = false, isXray = false) {
137
+ async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
128
138
  let outline;
129
139
  try {
130
140
  const result = getFileOutline(filePath);
@@ -147,14 +157,23 @@ class GenerateTestsUI {
147
157
  methodData: m
148
158
  }));
149
159
 
150
- const title = isXray ? `Select Method to X-Ray for ${fileName}` : (isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`);
160
+ let title = 'Select Method';
161
+ if (isAudit) title = `Select Method to Audit for ${fileName}`;
162
+ else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
163
+ else if (isTrace) title = `Select Method to Trace for ${fileName}`;
164
+
151
165
  const selectedDisplay = this.goUiSession.showListView(title, items);
152
166
 
153
- if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray);
167
+ if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
154
168
 
155
169
  const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
156
170
 
157
171
  if (selectedMethod) {
172
+ if (isTrace) {
173
+ await this._launchTrace(filePath, selectedMethod.name);
174
+ return this._showFileSelection(isAudit, isXray, isTrace);
175
+ }
176
+
158
177
  if (isXray) {
159
178
  const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
160
179
  if (choice === 'main_menu') return 'main_menu';
@@ -657,6 +676,97 @@ class GenerateTestsUI {
657
676
  return [];
658
677
  }
659
678
  }
679
+
680
+ async _launchTrace(filePath, methodName) {
681
+ const { getSymbolicTrace } = require('../index');
682
+ const fs = require('fs');
683
+ const path = require('path');
684
+ const chalk = require('chalk');
685
+
686
+ try {
687
+ // 1. Generate Trace (with Spinner)
688
+ const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
689
+ try {
690
+ const traceJson = getSymbolicTrace(filePath, methodName, null);
691
+ const tmpDir = require('os').tmpdir();
692
+ const f = path.join(tmpDir, `trace-${Date.now()}.json`);
693
+ fs.writeFileSync(f, traceJson);
694
+ return { success: true, file: f };
695
+ } catch (e) {
696
+ return { success: false, message: e.message };
697
+ }
698
+ });
699
+
700
+ if (result && result.success && result.file) {
701
+ // 2. Show Trace UI
702
+ const { spawnSync } = require('child_process');
703
+ spawnSync(this.goUiSession._binaryPath, ['trace-results', '--file', result.file], {
704
+ stdio: 'inherit'
705
+ });
706
+
707
+ try { fs.unlinkSync(result.file); } catch (e) { }
708
+ }
709
+
710
+ } catch (e) {
711
+ console.error(chalk.red(`Trace error: ${e.message}`));
712
+ }
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
+ }
660
770
  }
661
771
 
662
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.2",
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