@tng-sh/js 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/tng.js CHANGED
@@ -28,7 +28,35 @@ 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.8');
31
+ .version('0.1.9');
32
+
33
+ /**
34
+ * Copy text to system clipboard
35
+ */
36
+ function copyToClipboard(text) {
37
+ const { execSync } = require('child_process');
38
+ try {
39
+ if (process.platform === 'darwin') {
40
+ execSync('pbcopy', { input: text });
41
+ return true;
42
+ } else if (process.platform === 'linux') {
43
+ try {
44
+ execSync('xclip -selection clipboard', { input: text });
45
+ return true;
46
+ } catch (e) {
47
+ try {
48
+ execSync('xsel --clipboard --input', { input: text });
49
+ return true;
50
+ } catch (e2) {
51
+ return false;
52
+ }
53
+ }
54
+ }
55
+ } catch (e) {
56
+ return false;
57
+ }
58
+ return false;
59
+ }
32
60
 
33
61
  /**
34
62
  * @command init
@@ -153,6 +181,7 @@ program
153
181
  .description('Generate X-Ray visualization')
154
182
  .option('-f, --file <path>', 'Input file path')
155
183
  .option('-m, --method <name>', 'Method name to visualize')
184
+ .option('-j, --json', 'Output results in JSON format')
156
185
  .action(async (options) => {
157
186
  if (!options.file || !options.method) {
158
187
  console.log(chalk.red('Error: Both --file and --method are required for X-Ray.'));
@@ -183,18 +212,28 @@ program
183
212
  callback
184
213
  );
185
214
 
215
+ if (options.json) {
216
+ console.log(resultJson);
217
+ return;
218
+ }
219
+
186
220
  let mermaidCode = "";
221
+ let explanation = "";
187
222
  try {
188
223
  const parsed = JSON.parse(resultJson);
189
224
  mermaidCode = parsed.mermaid_code || resultJson;
225
+ explanation = parsed.explanation || "";
190
226
  } catch (e) {
191
227
  mermaidCode = resultJson;
192
228
  }
193
229
 
194
- console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
195
- console.log(chalk.blue(mermaidCode));
196
- console.log(chalk.bold('\n---------------------------------\n'));
197
- console.log(chalk.green('✓ Copy the code above and paste into https://mermaid.live'));
230
+ // Copy to clipboard
231
+ copyToClipboard(mermaidCode);
232
+
233
+ // Launch Premium UI
234
+ const GoUISession = require('../lib/goUiSession');
235
+ const session = new GoUISession();
236
+ await session.showXrayResults(options.method, '', mermaidCode, explanation);
198
237
 
199
238
  } catch (e) {
200
239
  console.log(chalk.red(`Error: ${e.message}`));
@@ -214,6 +253,7 @@ program
214
253
  .option('-c, --clones', 'Run duplicate code detection')
215
254
  .option('-l, --level <level>', 'Set clone detection level (1: token, 2: structural, 3: fuzzy, or all)', 'all')
216
255
  .option('-d, --deadcode', 'Run dead code detection (JS/TS/React)')
256
+ .option('--xray', 'Generate X-Ray visualization (Mermaid flowchart)')
217
257
  .option('--json', 'Output results as JSON events (machine-readable)')
218
258
 
219
259
  .action(async (options) => {
@@ -244,7 +284,7 @@ program
244
284
  const binaryPath = session._binaryPath;
245
285
 
246
286
  const { spawnSync } = require('child_process');
247
- spawnSync(binaryPath, ['trace-results', '--file', tmpFile], {
287
+ spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
248
288
  stdio: 'inherit'
249
289
  });
250
290
 
@@ -258,6 +298,52 @@ program
258
298
  return;
259
299
  }
260
300
 
301
+ if (options.xray) {
302
+ const config = loadConfig();
303
+ const { generateTest: nativeGenerateTest } = require('../index');
304
+
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
+ }
322
+
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;
334
+ }
335
+
336
+ // Copy to clipboard
337
+ copyToClipboard(mermaidCode);
338
+
339
+ await session.showXrayResults(options.method, '', mermaidCode, explanation);
340
+ } catch (e) {
341
+ console.log(chalk.red(`X-Ray failed: ${e.message}`));
342
+ process.exit(1);
343
+ }
344
+ return;
345
+ }
346
+
261
347
  if (!options.type && !options.audit) {
262
348
  console.log(chalk.red('Error: --type <type> is required.'));
263
349
  process.exit(1);
@@ -525,17 +611,20 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
525
611
  if (saved) {
526
612
  jsonSession.emitEvent('test_saved', {
527
613
  file_path: saved.file_path,
614
+ absolute_path: saved.absolute_path,
528
615
  message: `Test saved to: ${saved.file_path}`
529
616
  });
530
617
  } else {
531
- jsonSession.displayError('Failed to save test file');
618
+ const details = saveTestFile.lastError ? `: ${saveTestFile.lastError}` : '';
619
+ jsonSession.displayError(`Failed to save test file${details}`);
532
620
  }
533
621
  jsonSession.stop();
534
622
  } else {
535
623
  if (saved) {
536
624
  console.log(chalk.green(`✓ Test saved to: ${saved.file_path}`));
537
625
  } else {
538
- console.log(chalk.yellow('Failed to save test file'));
626
+ const details = saveTestFile.lastError ? `: ${saveTestFile.lastError}` : '';
627
+ console.log(chalk.yellow(`Failed to save test file${details}`));
539
628
  }
540
629
  }
541
630
  }
@@ -558,6 +647,11 @@ program.on('--help', () => {
558
647
  console.log(' 3: Fuzzy (Fuzzy structural, catches patterns with small variations)');
559
648
  console.log(' all: Runs all detection levels (default)');
560
649
  console.log('');
650
+ console.log('X-Ray Visualization:');
651
+ console.log(' tng src/components/MyComponent.js render --xray');
652
+ console.log(' tng --file=api/handler.js --method=post --xray --json');
653
+ console.log(' tng xray -f src/utils.js -m processData');
654
+ console.log('');
561
655
  });
562
656
 
563
657
  program.parse(process.argv);
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -461,17 +461,19 @@ class GenerateTestsUI {
461
461
  // For 'visualize' type, existing backend generates a file or JSON.
462
462
  // We should parse it.
463
463
  let mermaidCode = "";
464
+ let explanation = "";
464
465
  try {
465
466
  const parsed = JSON.parse(resultJson);
466
- if (parsed.mermaid_code) mermaidCode = parsed.mermaid_code;
467
- else mermaidCode = resultJson; // Fallback
467
+ mermaidCode = parsed.mermaid_code || resultJson;
468
+ explanation = parsed.explanation || "";
468
469
  } catch (e) {
469
470
  mermaidCode = resultJson;
470
471
  }
471
472
 
472
473
  return {
473
474
  success: true,
474
- mermaidCode: mermaidCode
475
+ mermaidCode: mermaidCode,
476
+ explanation: explanation
475
477
  };
476
478
  } catch (e) {
477
479
  progress.error(`Failed to generate X-Ray: ${e.message}`);
@@ -482,21 +484,16 @@ class GenerateTestsUI {
482
484
  const result = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
483
485
 
484
486
  if (result && result.mermaidCode) {
485
- console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
486
- // Simple highlighting for terminal
487
- console.log(chalk.blue(result.mermaidCode));
488
- console.log(chalk.bold('\n---------------------------------\n'));
489
-
490
- // Prompt user action
491
- this._copyToClipboard(result.mermaidCode);
492
- console.log(chalk.green('✓ Copied to clipboard! Paste into https://mermaid.live'));
493
-
494
- // Wait for user confirmation to return
495
- const { execSync } = require('child_process');
496
- try {
497
- // Just a pause hack if we want, but showListView might be better.
498
- // For now, let's just return.
499
- } catch (e) { }
487
+ // Copy to clipboard first so it's ready for the user
488
+ this._copyToClipboard(result.mermaidCode, true);
489
+
490
+ // Show Premium X-Ray UI
491
+ await this.goUiSession.showXrayResults(
492
+ method.name,
493
+ method.class_name || '',
494
+ result.mermaidCode,
495
+ result.explanation || 'Logic flow visualization generated by TNG.'
496
+ );
500
497
  }
501
498
 
502
499
  return result;
@@ -614,12 +611,12 @@ class GenerateTestsUI {
614
611
  return { passed, failed, errors, total };
615
612
  }
616
613
 
617
- _copyToClipboard(text) {
614
+ _copyToClipboard(text, silent = false) {
618
615
  const { execSync } = require('child_process');
619
616
  try {
620
617
  if (process.platform === 'darwin') {
621
618
  execSync('pbcopy', { input: text });
622
- this.goUiSession.showClipboardSuccess(text);
619
+ if (!silent) this.goUiSession.showClipboardSuccess(text);
623
620
  } else if (process.platform === 'linux') {
624
621
  // Try xclip then xsel
625
622
  try {
@@ -627,12 +624,12 @@ class GenerateTestsUI {
627
624
  } catch (e) {
628
625
  execSync('xsel --clipboard --input', { input: text });
629
626
  }
630
- this.goUiSession.showClipboardSuccess(text);
627
+ if (!silent) this.goUiSession.showClipboardSuccess(text);
631
628
  } else {
632
- console.log(chalk.cyan(`\n📋 Please copy this command: ${text}\n`));
629
+ if (!silent) console.log(chalk.cyan(`\n📋 Please copy this command: ${text}\n`));
633
630
  }
634
631
  } catch (e) {
635
- console.error(chalk.yellow(`\n⚠️ Failed to copy to clipboard. Command: ${text}\n`));
632
+ if (!silent) console.error(chalk.yellow(`\n⚠️ Failed to copy to clipboard. Command: ${text}\n`));
636
633
  }
637
634
  }
638
635
 
@@ -703,7 +700,7 @@ class GenerateTestsUI {
703
700
  if (result && result.success && result.file) {
704
701
  // 2. Show Trace UI
705
702
  const { spawnSync } = require('child_process');
706
- spawnSync(this.goUiSession._binaryPath, ['trace-results', '--file', result.file], {
703
+ spawnSync(this.goUiSession._binaryPath, ['js-trace-results', '--file', result.file], {
707
704
  stdio: 'inherit'
708
705
  });
709
706
 
@@ -369,6 +369,29 @@ class GoUISession {
369
369
  }
370
370
  }
371
371
 
372
+ async showXrayResults(methodName, className, mermaidCode, explanation) {
373
+ const data = {
374
+ method_name: methodName,
375
+ class_name: className,
376
+ mermaid_code: mermaidCode,
377
+ explanation: explanation
378
+ };
379
+ const inputFile = this._trackTempFile(this._createTempFile('xray-data', '.json'));
380
+
381
+ try {
382
+ fs.writeFileSync(inputFile, JSON.stringify(data));
383
+
384
+ spawnSync(this._binaryPath, ['xray-results', '--file', inputFile], {
385
+ stdio: 'inherit',
386
+ env: process.env
387
+ });
388
+ } catch (error) {
389
+ console.error('X-Ray results display error:', error.message);
390
+ } finally {
391
+ this._cleanupTempFile(inputFile);
392
+ }
393
+ }
394
+
372
395
  async showDeadCodeResults(filePath, resultsJson) {
373
396
  const issues = JSON.parse(resultsJson);
374
397
  const data = {
@@ -66,6 +66,10 @@ class JsonSession {
66
66
  this.emitEvent('result', auditResult || {});
67
67
  }
68
68
 
69
+ showXrayResults(xrayResult) {
70
+ this.emitEvent('result', xrayResult || {});
71
+ }
72
+
69
73
  showClones(filePath, results) {
70
74
  this.emitEvent('clones', { file_path: filePath, matches: results });
71
75
  }
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.8",
3
+ "version": "0.2.0",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",