@tng-sh/js 0.1.1 → 0.1.3

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,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.1');
31
+ .version('0.1.3');
32
32
 
33
33
  /**
34
34
  * @command init
@@ -143,6 +143,65 @@ program
143
143
  launchInteractive();
144
144
  });
145
145
 
146
+ /**
147
+ * @command xray
148
+ * Generate X-Ray (Mermaid Visualization)
149
+ */
150
+ program
151
+ .command('xray')
152
+ .alias('x')
153
+ .description('Generate X-Ray visualization')
154
+ .option('-f, --file <path>', 'Input file path')
155
+ .option('-m, --method <name>', 'Method name to visualize')
156
+ .action(async (options) => {
157
+ if (!options.file || !options.method) {
158
+ console.log(chalk.red('Error: Both --file and --method are required for X-Ray.'));
159
+ console.log(chalk.yellow('Usage: tng xray -f <file> -m <method>'));
160
+ process.exit(1);
161
+ }
162
+
163
+ console.log(chalk.blue(`🔍 Generating X-Ray for ${options.method} in ${options.file}...`));
164
+
165
+ try {
166
+ const config = loadConfig();
167
+
168
+ // Re-use logic similar to _handleXrayFlow but tailored for CLI non-interactive output
169
+ const { generateTest } = require('../index');
170
+
171
+ // We use a custom callback to show progress
172
+ const callback = (msg, percent) => {
173
+ if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
174
+ };
175
+
176
+ // We pass 'visualize' as the test type
177
+ const resultJson = generateTest(
178
+ path.resolve(options.file),
179
+ options.method,
180
+ null,
181
+ 'visualize',
182
+ JSON.stringify(config),
183
+ callback
184
+ );
185
+
186
+ let mermaidCode = "";
187
+ try {
188
+ const parsed = JSON.parse(resultJson);
189
+ mermaidCode = parsed.mermaid_code || resultJson;
190
+ } catch (e) {
191
+ mermaidCode = resultJson;
192
+ }
193
+
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'));
198
+
199
+ } catch (e) {
200
+ console.log(chalk.red(`Error: ${e.message}`));
201
+ process.exit(1);
202
+ }
203
+ });
204
+
146
205
  /**
147
206
  * Main command handler (handles -f and -m)
148
207
  */
@@ -151,10 +210,51 @@ program
151
210
  .option('-f, --file <path>', 'JavaScript file path')
152
211
  .option('-t, --type <type>', 'Component type (react_component, express_handler, etc) [required]')
153
212
  .option('-a, --audit', 'Run audit mode instead of test generation')
213
+ .option('--trace', 'Run symbolic trace visualization')
154
214
  .option('--json', 'Output results as JSON events (machine-readable)')
155
215
 
156
216
  .action(async (options) => {
157
217
  if (options.method && options.file) {
218
+ if (options.trace) {
219
+ const { getSymbolicTrace } = require('../index');
220
+ const GoUISession = require('../lib/goUiSession');
221
+
222
+ try {
223
+ const traceJson = getSymbolicTrace(
224
+ path.resolve(options.file),
225
+ options.method,
226
+ null
227
+ );
228
+
229
+ if (options.json) {
230
+ console.log(traceJson);
231
+ return;
232
+ }
233
+
234
+ // Create temp file for the trace
235
+ const tmpDir = require('os').tmpdir();
236
+ const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
237
+ fs.writeFileSync(tmpFile, traceJson);
238
+
239
+ // Launch Go UI
240
+ const session = new GoUISession();
241
+ const binaryPath = session._binaryPath;
242
+
243
+ const { spawnSync } = require('child_process');
244
+ spawnSync(binaryPath, ['trace-results', '--file', tmpFile], {
245
+ stdio: 'inherit'
246
+ });
247
+
248
+ // Cleanup
249
+ try { fs.unlinkSync(tmpFile); } catch (e) { }
250
+
251
+ } catch (e) {
252
+ console.log(chalk.red(`Trace failed: ${e.message}`));
253
+ process.exit(1);
254
+ }
255
+ return;
256
+ }
257
+
158
258
  if (!options.type && !options.audit) {
159
259
  console.log(chalk.red('Error: --type <type> is required.'));
160
260
  process.exit(1);
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -44,3 +44,5 @@ export declare function applyEditsAtomic(operationsJson: string): string
44
44
  * Analyzes code, submits a job, and polls for the final generated test.
45
45
  */
46
46
  export declare function generateTest(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string
47
+ /** Analyzes a method and generates a symbolic execution trace. */
48
+ export declare function getSymbolicTrace(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 } = nativeBinding
313
+ const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -322,3 +322,4 @@ 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
@@ -34,6 +34,12 @@ class GenerateTestsUI {
34
34
  } else if (choice === 'audit') {
35
35
  const result = await this._showFileSelection(true);
36
36
  if (result === 'exit') return 'exit';
37
+ } else if (choice === 'xray') {
38
+ const result = await this._showFileSelection(false, true);
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';
37
43
  } else if (choice === 'stats') {
38
44
  await this._showStats();
39
45
  } else if (choice === 'about') {
@@ -95,7 +101,7 @@ class GenerateTestsUI {
95
101
  });
96
102
  }
97
103
 
98
- async _showFileSelection(isAudit = false) {
104
+ async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
99
105
  const files = await this._getUserFiles();
100
106
 
101
107
  if (files.length === 0) {
@@ -109,19 +115,23 @@ class GenerateTestsUI {
109
115
  path: path.dirname(file)
110
116
  }));
111
117
 
112
- const title = isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File';
118
+ let title = 'Select JavaScript File';
119
+ if (isAudit) title = 'Select JavaScript File to Audit';
120
+ else if (isXray) title = 'Select File for X-Ray';
121
+ else if (isTrace) title = 'Select File for Symbolic Trace';
122
+
113
123
  const selectedName = this.goUiSession.showListView(title, items);
114
124
 
115
125
  if (selectedName === 'back') return 'back';
116
126
  if (!selectedName || selectedName === 'exit') return 'exit';
117
127
 
118
128
  const selectedFile = path.resolve(cwd, selectedName);
119
- const result = await this._showMethodsForFile(selectedFile, isAudit);
129
+ const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
120
130
  if (result === 'main_menu') return 'main_menu';
121
131
  return result;
122
132
  }
123
133
 
124
- async _showMethodsForFile(filePath, isAudit = false) {
134
+ async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
125
135
  let outline;
126
136
  try {
127
137
  const result = getFileOutline(filePath);
@@ -144,33 +154,48 @@ class GenerateTestsUI {
144
154
  methodData: m
145
155
  }));
146
156
 
147
- const title = isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`;
157
+ let title = 'Select Method';
158
+ if (isAudit) title = `Select Method to Audit for ${fileName}`;
159
+ else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
160
+ else if (isTrace) title = `Select Method to Trace for ${fileName}`;
161
+
148
162
  const selectedDisplay = this.goUiSession.showListView(title, items);
149
163
 
150
- if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit);
164
+ if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
151
165
 
152
166
  const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
153
167
 
154
168
  if (selectedMethod) {
169
+ if (isTrace) {
170
+ await this._launchTrace(filePath, selectedMethod.name);
171
+ return this._showFileSelection(isAudit, isXray, isTrace);
172
+ }
173
+
174
+ if (isXray) {
175
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
176
+ if (choice === 'main_menu') return 'main_menu';
177
+ return this._showFileSelection(isAudit, isXray);
178
+ }
179
+
155
180
  const testType = this.goUiSession.showJsTestMenu();
156
- if (testType === 'back') return this._showFileSelection(isAudit);
181
+ if (testType === 'back') return this._showFileSelection(isAudit, isXray);
157
182
 
158
183
  const finalType = testType === 'auto' ? null : testType;
159
184
  const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
160
185
 
161
186
  if (isAudit) {
162
187
  if (choice === 'main_menu') return 'main_menu';
163
- return this._showFileSelection(isAudit);
188
+ return this._showFileSelection(isAudit, isXray);
164
189
  }
165
190
 
166
191
  if (choice && choice.file_path && !choice.error) {
167
192
  this._showPostGenerationMenu(choice);
168
193
  }
169
194
  }
170
- return this._showFileSelection(isAudit);
195
+ return this._showFileSelection(isAudit, isXray);
171
196
  }
172
197
 
173
- async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
198
+ async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
174
199
  if (!this._hasApiKey()) {
175
200
  return { error: 'No API key' };
176
201
  }
@@ -179,6 +204,10 @@ class GenerateTestsUI {
179
204
  return this._handleAuditFlow(filePath, method, testType);
180
205
  }
181
206
 
207
+ if (isXray) {
208
+ return this._handleXrayFlow(filePath, method);
209
+ }
210
+
182
211
  const fileName = path.basename(filePath);
183
212
  const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
184
213
  return this._handleTestGenerationFlow(filePath, method, testType, displayName);
@@ -395,6 +424,78 @@ class GenerateTestsUI {
395
424
  return uiResult || null;
396
425
  }
397
426
 
427
+ async _handleXrayFlow(filePath, method) {
428
+ const actionName = 'Generating X-Ray for';
429
+ const displayName = method.name;
430
+
431
+ const progressHandler = async (progress) => {
432
+ progress.update('Analyzing method structure...');
433
+
434
+ try {
435
+ const config = loadConfig();
436
+ // Override test_type to 'visualize' which behaves like test generation but returns Mermaid code
437
+ const resultJson = generateTest(
438
+ filePath,
439
+ method.name,
440
+ method.class_name || null,
441
+ 'visualize',
442
+ JSON.stringify(config),
443
+ (msg, percent) => {
444
+ // Simple progress updates
445
+ progress.update(msg, { percent });
446
+ }
447
+ );
448
+
449
+ // The result is expected to be a JSON string containing the mermaid definition
450
+ // But generateTest returns the full file content usually.
451
+ // We need to clarify if Backend returns just Mermaid or a file.
452
+ // Assuming Backend returns JSON with { "visualize_result": "mermaid code..." } or similar
453
+ // or if it returns raw text, we handle it.
454
+
455
+ // For 'visualize' type, existing backend generates a file or JSON.
456
+ // We should parse it.
457
+ let mermaidCode = "";
458
+ try {
459
+ const parsed = JSON.parse(resultJson);
460
+ if (parsed.mermaid_code) mermaidCode = parsed.mermaid_code;
461
+ else mermaidCode = resultJson; // Fallback
462
+ } catch (e) {
463
+ mermaidCode = resultJson;
464
+ }
465
+
466
+ return {
467
+ success: true,
468
+ mermaidCode: mermaidCode
469
+ };
470
+ } catch (e) {
471
+ progress.error(`Failed to generate X-Ray: ${e.message}`);
472
+ return { error: e.message };
473
+ }
474
+ };
475
+
476
+ const result = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
477
+
478
+ if (result && result.mermaidCode) {
479
+ console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
480
+ // Simple highlighting for terminal
481
+ console.log(chalk.blue(result.mermaidCode));
482
+ console.log(chalk.bold('\n---------------------------------\n'));
483
+
484
+ // Prompt user action
485
+ this._copyToClipboard(result.mermaidCode);
486
+ console.log(chalk.green('✓ Copied to clipboard! Paste into https://mermaid.live'));
487
+
488
+ // Wait for user confirmation to return
489
+ const { execSync } = require('child_process');
490
+ try {
491
+ // Just a pause hack if we want, but showListView might be better.
492
+ // For now, let's just return.
493
+ } catch (e) { }
494
+ }
495
+
496
+ return result;
497
+ }
498
+
398
499
  _updateProgress(progress, msg, percent) {
399
500
  const agentMap = {
400
501
  'context_agent_status': { label: 'Context Builder', step: 1 },
@@ -572,6 +673,41 @@ class GenerateTestsUI {
572
673
  return [];
573
674
  }
574
675
  }
676
+
677
+ async _launchTrace(filePath, methodName) {
678
+ const { getSymbolicTrace } = require('../index');
679
+ const fs = require('fs');
680
+ const path = require('path');
681
+ const chalk = require('chalk');
682
+
683
+ try {
684
+ // 1. Generate Trace (with Spinner)
685
+ const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
686
+ try {
687
+ const traceJson = getSymbolicTrace(filePath, methodName, null);
688
+ const tmpDir = require('os').tmpdir();
689
+ const f = path.join(tmpDir, `trace-${Date.now()}.json`);
690
+ fs.writeFileSync(f, traceJson);
691
+ return { success: true, file: f };
692
+ } catch (e) {
693
+ return { success: false, message: e.message };
694
+ }
695
+ });
696
+
697
+ if (result && result.success && result.file) {
698
+ // 2. Show Trace UI
699
+ const { spawnSync } = require('child_process');
700
+ spawnSync(this.goUiSession._binaryPath, ['trace-results', '--file', result.file], {
701
+ stdio: 'inherit'
702
+ });
703
+
704
+ try { fs.unlinkSync(result.file); } catch (e) { }
705
+ }
706
+
707
+ } catch (e) {
708
+ console.error(chalk.red(`Trace error: ${e.message}`));
709
+ }
710
+ }
575
711
  }
576
712
 
577
713
  module.exports = GenerateTestsUI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
Binary file
Binary file
Binary file