@tng-sh/js 0.1.0 → 0.1.2

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
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const { loadConfig } = require('../lib/config');
8
8
  const { saveTestFile } = require('../lib/saveFile');
9
9
  const { ping, getUserStats } = require('../index');
10
+ const { applyFix } = require('../lib/fixApplier');
10
11
 
11
12
  // Handle EPIPE errors gracefully when child process (Go UI) exits
12
13
  process.stdout.on('error', (err) => {
@@ -27,7 +28,7 @@ process.on('uncaughtException', (err) => {
27
28
  program
28
29
  .name('tng')
29
30
  .description('TNG - Automated Test Generation, and audit generation for JavaScript')
30
- .version('0.1.0');
31
+ .version('0.1.2');
31
32
 
32
33
  /**
33
34
  * @command init
@@ -142,6 +143,65 @@ program
142
143
  launchInteractive();
143
144
  });
144
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
+
145
205
  /**
146
206
  * Main command handler (handles -f and -m)
147
207
  */
@@ -166,6 +226,39 @@ program
166
226
  }
167
227
  });
168
228
 
229
+ /**
230
+ * @command fix
231
+ * Apply a specific fix to a file
232
+ */
233
+ program
234
+ .command('fix')
235
+ .description('Apply a fix to a file')
236
+ .option('-f, --file <path>', 'File path to fix')
237
+ .option('-d, --data <json>', 'JSON data for the audit item containing fix info')
238
+ .action(async (options) => {
239
+ if (!options.file || !options.data) {
240
+ console.log(chalk.red('Error: Both --file and --data are required.'));
241
+ process.exit(1);
242
+ }
243
+
244
+ try {
245
+ const item = JSON.parse(options.data);
246
+ const result = await applyFix(options.file, item);
247
+ if (result.success) {
248
+ console.log(chalk.green(`āœ“ Fix applied to ${options.file}`));
249
+ if (result.backup_path) {
250
+ console.log(chalk.dim(` Backup created at ${result.backup_path}`));
251
+ }
252
+ } else {
253
+ console.log(chalk.red(`āŒ Failed to apply fix: ${result.error}`));
254
+ process.exit(1);
255
+ }
256
+ } catch (e) {
257
+ console.log(chalk.red(`Error: ${e.message}`));
258
+ process.exit(1);
259
+ }
260
+ });
261
+
169
262
 
170
263
 
171
264
  /**
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -29,6 +29,16 @@ export declare function getUserStats(baseUrl: string, apiKey: string): string
29
29
  * Analyzes the source, builds context, and streams results via the provided callback.
30
30
  */
31
31
  export declare function runAudit(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string
32
+ /**
33
+ * Applies a single edit operation to a file.
34
+ * Uses backup and atomic write for safety.
35
+ */
36
+ export declare function applyEdit(filePath: string, search: string, replace: string, lineHint?: number | undefined | null): string
37
+ /**
38
+ * Applies multiple edit operations atomically.
39
+ * All succeed or all fail (with rollback).
40
+ */
41
+ export declare function applyEditsAtomic(operationsJson: string): string
32
42
  /**
33
43
  * Orchestrates the test generation process.
34
44
  * Analyzes code, submits a job, and polls for the final generated test.
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, generateTest } = nativeBinding
313
+ const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -319,4 +319,6 @@ module.exports.ping = ping
319
319
  module.exports.submitJob = submitJob
320
320
  module.exports.getUserStats = getUserStats
321
321
  module.exports.runAudit = runAudit
322
+ module.exports.applyEdit = applyEdit
323
+ module.exports.applyEditsAtomic = applyEditsAtomic
322
324
  module.exports.generateTest = generateTest
@@ -0,0 +1,28 @@
1
+ const { parentPort, workerData } = require('worker_threads');
2
+ const { runAudit } = require('../index');
3
+ const { loadConfig } = require('./config');
4
+
5
+ const { filePath, methodName, className, testType } = workerData;
6
+ const config = loadConfig();
7
+
8
+ try {
9
+ const resultJson = runAudit(
10
+ filePath,
11
+ methodName,
12
+ className,
13
+ testType,
14
+ JSON.stringify(config),
15
+ (msg) => {
16
+ if (parentPort) {
17
+ parentPort.postMessage({ type: 'item', data: msg });
18
+ }
19
+ }
20
+ );
21
+ if (parentPort) {
22
+ parentPort.postMessage({ type: 'result', data: resultJson });
23
+ }
24
+ } catch (e) {
25
+ if (parentPort) {
26
+ parentPort.postMessage({ type: 'error', data: e.message });
27
+ }
28
+ }
package/lib/config.js CHANGED
@@ -21,6 +21,7 @@ const loadConfig = () => {
21
21
 
22
22
  return Object.keys(filtered).length > 0 ? filtered : config;
23
23
  } catch (error) {
24
+ console.error(`Failed to load config from ${configFile}:`, error.message);
24
25
  return {};
25
26
  }
26
27
  };
@@ -0,0 +1,59 @@
1
+ const { applyEdit } = require('../index');
2
+
3
+ /**
4
+ * Parses a SEARCH_AND_REPLACE block from a string
5
+ * @param {string} codeChange
6
+ * @returns {{search: string, replace: string} | null}
7
+ */
8
+ function parseSearchReplace(codeChange) {
9
+ if (!codeChange) return null;
10
+
11
+ // Try to find SEARCH: and REPLACE: blocks
12
+ const searchIdx = codeChange.indexOf('SEARCH:');
13
+ const replaceIdx = codeChange.indexOf('REPLACE:');
14
+
15
+ if (searchIdx !== -1 && replaceIdx !== -1 && searchIdx < replaceIdx) {
16
+ const search = codeChange.substring(searchIdx + 7, replaceIdx).trim();
17
+ const replace = codeChange.substring(replaceIdx + 8).trim();
18
+ return { search, replace };
19
+ }
20
+
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Appies a fix to a file
26
+ * @param {string} filePath
27
+ * @param {object} item
28
+ * @returns {object} result
29
+ */
30
+ async function applyFix(filePath, item) {
31
+ try {
32
+ if (!item) {
33
+ return { success: false, error: 'Item is null or undefined' };
34
+ }
35
+ const codeChange = item.fix_code_change || (item.fix && item.fix.how && item.fix.how.code_change);
36
+ if (!codeChange) {
37
+ return { success: false, error: 'No code change found in fix data' };
38
+ }
39
+
40
+ const parsed = parseSearchReplace(codeChange);
41
+ if (!parsed) {
42
+ return { success: false, error: 'Failed to parse SEARCH_AND_REPLACE block' };
43
+ }
44
+
45
+ const resultJson = applyEdit(
46
+ filePath,
47
+ parsed.search,
48
+ parsed.replace,
49
+ typeof item.line_number === 'number' && item.line_number > 0 ? item.line_number : null
50
+ );
51
+
52
+ const result = JSON.parse(resultJson);
53
+ return result;
54
+ } catch (e) {
55
+ return { success: false, error: e.message };
56
+ }
57
+ }
58
+
59
+ module.exports = { applyFix, parseSearchReplace };
@@ -1,10 +1,12 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
3
  const glob = require('fast-glob');
3
4
  const chalk = require('chalk');
4
5
  const GoUISession = require('./goUiSession');
5
- const { getFileOutline, runAudit, generateTest, ping, getUserStats } = require('../index');
6
+ const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
6
7
  const { loadConfig } = require('./config');
7
8
  const { saveTestFile } = require('./saveFile');
9
+ const { applyFix } = require('./fixApplier');
8
10
 
9
11
  class GenerateTestsUI {
10
12
  constructor(cliMode = false) {
@@ -32,6 +34,9 @@ class GenerateTestsUI {
32
34
  } else if (choice === 'audit') {
33
35
  const result = await this._showFileSelection(true);
34
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';
35
40
  } else if (choice === 'stats') {
36
41
  await this._showStats();
37
42
  } else if (choice === 'about') {
@@ -93,7 +98,7 @@ class GenerateTestsUI {
93
98
  });
94
99
  }
95
100
 
96
- async _showFileSelection(isAudit = false) {
101
+ async _showFileSelection(isAudit = false, isXray = false) {
97
102
  const files = await this._getUserFiles();
98
103
 
99
104
  if (files.length === 0) {
@@ -107,19 +112,19 @@ class GenerateTestsUI {
107
112
  path: path.dirname(file)
108
113
  }));
109
114
 
110
- const title = isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File';
115
+ const title = isXray ? 'Select File for X-Ray' : (isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File');
111
116
  const selectedName = this.goUiSession.showListView(title, items);
112
117
 
113
118
  if (selectedName === 'back') return 'back';
114
119
  if (!selectedName || selectedName === 'exit') return 'exit';
115
120
 
116
121
  const selectedFile = path.resolve(cwd, selectedName);
117
- const result = await this._showMethodsForFile(selectedFile, isAudit);
122
+ const result = await this._showMethodsForFile(selectedFile, isAudit, isXray);
118
123
  if (result === 'main_menu') return 'main_menu';
119
124
  return result;
120
125
  }
121
126
 
122
- async _showMethodsForFile(filePath, isAudit = false) {
127
+ async _showMethodsForFile(filePath, isAudit = false, isXray = false) {
123
128
  let outline;
124
129
  try {
125
130
  const result = getFileOutline(filePath);
@@ -142,33 +147,39 @@ class GenerateTestsUI {
142
147
  methodData: m
143
148
  }));
144
149
 
145
- const title = isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`;
150
+ const title = isXray ? `Select Method to X-Ray for ${fileName}` : (isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`);
146
151
  const selectedDisplay = this.goUiSession.showListView(title, items);
147
152
 
148
- if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit);
153
+ if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray);
149
154
 
150
155
  const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
151
156
 
152
157
  if (selectedMethod) {
158
+ if (isXray) {
159
+ const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
160
+ if (choice === 'main_menu') return 'main_menu';
161
+ return this._showFileSelection(isAudit, isXray);
162
+ }
163
+
153
164
  const testType = this.goUiSession.showJsTestMenu();
154
- if (testType === 'back') return this._showFileSelection(isAudit);
165
+ if (testType === 'back') return this._showFileSelection(isAudit, isXray);
155
166
 
156
167
  const finalType = testType === 'auto' ? null : testType;
157
168
  const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
158
169
 
159
170
  if (isAudit) {
160
171
  if (choice === 'main_menu') return 'main_menu';
161
- return this._showFileSelection(isAudit);
172
+ return this._showFileSelection(isAudit, isXray);
162
173
  }
163
174
 
164
175
  if (choice && choice.file_path && !choice.error) {
165
176
  this._showPostGenerationMenu(choice);
166
177
  }
167
178
  }
168
- return this._showFileSelection(isAudit);
179
+ return this._showFileSelection(isAudit, isXray);
169
180
  }
170
181
 
171
- async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
182
+ async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
172
183
  if (!this._hasApiKey()) {
173
184
  return { error: 'No API key' };
174
185
  }
@@ -177,6 +188,10 @@ class GenerateTestsUI {
177
188
  return this._handleAuditFlow(filePath, method, testType);
178
189
  }
179
190
 
191
+ if (isXray) {
192
+ return this._handleXrayFlow(filePath, method);
193
+ }
194
+
180
195
  const fileName = path.basename(filePath);
181
196
  const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
182
197
  return this._handleTestGenerationFlow(filePath, method, testType, displayName);
@@ -193,7 +208,7 @@ class GenerateTestsUI {
193
208
  }
194
209
 
195
210
  async _handleAuditFlow(filePath, method, testType) {
196
- const config = loadConfig();
211
+ const { Worker } = require('worker_threads');
197
212
  const streamingUi = await this.goUiSession.showStreamingAuditResults(
198
213
  method.name,
199
214
  method.class_name,
@@ -202,25 +217,158 @@ class GenerateTestsUI {
202
217
 
203
218
  if (!streamingUi) return null;
204
219
 
220
+ let results = {
221
+ issues: [],
222
+ behaviours: [],
223
+ method_name: method.name,
224
+ class_name: method.class_name,
225
+ method_source_with_lines: method.source_code
226
+ };
227
+ let auditFinished = false;
228
+
229
+ // Start audit in background worker
230
+ const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
231
+ workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
232
+ });
233
+
234
+ const itemIds = new Set();
235
+ worker.on('message', (msg) => {
236
+ if (msg.type === 'item') {
237
+ if (msg.data.startsWith('{')) {
238
+ streamingUi.write(msg.data);
239
+ try {
240
+ const data = JSON.parse(msg.data);
241
+
242
+ // Handle metadata updates from stream
243
+ if (data.method_name) results.method_name = data.method_name;
244
+ if (data.class_name) results.class_name = data.class_name;
245
+ if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
246
+ if (data.source_code) results.method_source_with_lines = data.source_code;
247
+
248
+ // Only push if it looks like an actual audit item
249
+ if (data.test_name || data.summary || data.category) {
250
+ const id = `${data.category}-${data.test_name}-${data.line_number}`;
251
+ if (!itemIds.has(id)) {
252
+ if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
253
+ else results.issues.push(data);
254
+ itemIds.add(id);
255
+ }
256
+ }
257
+ } catch (e) { }
258
+ }
259
+ } else if (msg.type === 'result') {
260
+ try {
261
+ const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
262
+
263
+ // Merge final results with our local ones, preserving 'fixed' state
264
+ const mergeList = (localList, incomingList) => {
265
+ if (!incomingList || incomingList.length === 0) return localList;
266
+
267
+ return incomingList.map(incomingItem => {
268
+ const localItem = localList.find(it =>
269
+ it.test_name === incomingItem.test_name &&
270
+ it.line_number === incomingItem.line_number
271
+ );
272
+ if (localItem && localItem.fixed) {
273
+ return { ...incomingItem, fixed: true };
274
+ }
275
+ return incomingItem;
276
+ });
277
+ };
278
+
279
+ results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
280
+ results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
281
+
282
+ if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
283
+ if (finalResults.method_name) results.method_name = finalResults.method_name;
284
+ } catch (e) { }
285
+ auditFinished = true;
286
+ } else if (msg.type === 'error') {
287
+ console.error(chalk.red(`Audit error: ${msg.data}`));
288
+ auditFinished = true;
289
+ }
290
+ });
291
+
292
+ worker.on('error', (err) => {
293
+ console.error(chalk.red(`Worker error: ${err.message}`));
294
+ auditFinished = true;
295
+ });
296
+
205
297
  try {
206
- const resultJson = runAudit(
207
- filePath,
208
- method.name,
209
- method.class_name || null,
210
- testType || null,
211
- JSON.stringify(config),
212
- (msg) => {
213
- if (msg.startsWith('{')) {
214
- streamingUi.write(msg);
298
+ let currentChoice = null;
299
+
300
+ // Wait for INITIAL streaming session to complete (user picks an action or quits)
301
+ await streamingUi.wait;
302
+
303
+ const getResponse = (outputFile) => {
304
+ if (fs.existsSync(outputFile)) {
305
+ const output = fs.readFileSync(outputFile, 'utf8').trim();
306
+ if (output) {
307
+ try {
308
+ const parsed = JSON.parse(output);
309
+ return typeof parsed === 'object' ? parsed : { action: output };
310
+ } catch (e) {
311
+ return { action: output };
312
+ }
215
313
  }
216
314
  }
217
- );
315
+ return null;
316
+ };
317
+
318
+ currentChoice = getResponse(streamingUi.outputFile);
319
+
320
+ // Action loop: handles fix/open and returns to UI
321
+ while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
322
+ const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
323
+
324
+ if (currentChoice.action === 'fix') {
325
+ console.log(chalk.cyan(`Applying fix...`));
326
+ const fixResult = await applyFix(filePath, currentChoice.item);
327
+ if (fixResult.success) {
328
+ console.log(chalk.green(`āœ“ Fix applied successfully.`));
329
+ // Mark as fixed locally
330
+ [results.issues, results.behaviours].forEach(list => {
331
+ list.forEach(it => {
332
+ if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
333
+ it.fixed = true;
334
+ }
335
+ });
336
+ });
337
+ } else {
338
+ console.log(chalk.red(`āŒ Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
339
+ }
340
+ } else if (currentChoice.action === 'open') {
341
+ this._openInEditor(filePath, currentChoice.item.line_number);
342
+ }
218
343
 
219
- await streamingUi.wait;
220
- return { message: 'Audit complete', resultJson };
344
+ // Ensure source has line numbers for static UI if it doesn't already
345
+ if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
346
+ results.method_source_with_lines = results.method_source_with_lines
347
+ .split('\n')
348
+ .map((line, i) => `${i + 1}: ${line}`)
349
+ .join('\n');
350
+ }
351
+
352
+ // Re-launch UI statically with updated results
353
+ const choiceStr = await this.goUiSession.showAuditResults(results, itemIndex);
354
+ if (choiceStr === 'back' || choiceStr === 'main_menu' || choiceStr === 'exit') {
355
+ currentChoice = null;
356
+ } else {
357
+ try {
358
+ const parsed = JSON.parse(choiceStr);
359
+ currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
360
+ } catch (e) {
361
+ currentChoice = { action: choiceStr };
362
+ }
363
+ }
364
+ }
365
+
366
+ return { message: 'Audit complete', results };
221
367
  } catch (e) {
222
- console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
368
+ console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
223
369
  return null;
370
+ } finally {
371
+ await worker.terminate();
224
372
  }
225
373
  }
226
374
 
@@ -260,6 +408,78 @@ class GenerateTestsUI {
260
408
  return uiResult || null;
261
409
  }
262
410
 
411
+ async _handleXrayFlow(filePath, method) {
412
+ const actionName = 'Generating X-Ray for';
413
+ const displayName = method.name;
414
+
415
+ const progressHandler = async (progress) => {
416
+ progress.update('Analyzing method structure...');
417
+
418
+ try {
419
+ const config = loadConfig();
420
+ // Override test_type to 'visualize' which behaves like test generation but returns Mermaid code
421
+ const resultJson = generateTest(
422
+ filePath,
423
+ method.name,
424
+ method.class_name || null,
425
+ 'visualize',
426
+ JSON.stringify(config),
427
+ (msg, percent) => {
428
+ // Simple progress updates
429
+ progress.update(msg, { percent });
430
+ }
431
+ );
432
+
433
+ // The result is expected to be a JSON string containing the mermaid definition
434
+ // But generateTest returns the full file content usually.
435
+ // We need to clarify if Backend returns just Mermaid or a file.
436
+ // Assuming Backend returns JSON with { "visualize_result": "mermaid code..." } or similar
437
+ // or if it returns raw text, we handle it.
438
+
439
+ // For 'visualize' type, existing backend generates a file or JSON.
440
+ // We should parse it.
441
+ let mermaidCode = "";
442
+ try {
443
+ const parsed = JSON.parse(resultJson);
444
+ if (parsed.mermaid_code) mermaidCode = parsed.mermaid_code;
445
+ else mermaidCode = resultJson; // Fallback
446
+ } catch (e) {
447
+ mermaidCode = resultJson;
448
+ }
449
+
450
+ return {
451
+ success: true,
452
+ mermaidCode: mermaidCode
453
+ };
454
+ } catch (e) {
455
+ progress.error(`Failed to generate X-Ray: ${e.message}`);
456
+ return { error: e.message };
457
+ }
458
+ };
459
+
460
+ const result = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
461
+
462
+ if (result && result.mermaidCode) {
463
+ console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
464
+ // Simple highlighting for terminal
465
+ console.log(chalk.blue(result.mermaidCode));
466
+ console.log(chalk.bold('\n---------------------------------\n'));
467
+
468
+ // Prompt user action
469
+ this._copyToClipboard(result.mermaidCode);
470
+ console.log(chalk.green('āœ“ Copied to clipboard! Paste into https://mermaid.live'));
471
+
472
+ // Wait for user confirmation to return
473
+ const { execSync } = require('child_process');
474
+ try {
475
+ // Just a pause hack if we want, but showListView might be better.
476
+ // For now, let's just return.
477
+ } catch (e) { }
478
+ }
479
+
480
+ return result;
481
+ }
482
+
263
483
  _updateProgress(progress, msg, percent) {
264
484
  const agentMap = {
265
485
  'context_agent_status': { label: 'Context Builder', step: 1 },
@@ -394,6 +614,26 @@ class GenerateTestsUI {
394
614
  }
395
615
  }
396
616
 
617
+ _openInEditor(filePath, lineNumber) {
618
+ const { execSync } = require('child_process');
619
+ try {
620
+ // Try to open in VS Code if available, fallback to default editor
621
+ const location = lineNumber ? `${filePath}:${lineNumber}` : filePath;
622
+ try {
623
+ execSync(`code --goto ${location}`, { stdio: 'ignore' });
624
+ console.log(chalk.blue(`\nOpening ${location} in VS Code...`));
625
+ } catch (e) {
626
+ if (process.platform === 'darwin') {
627
+ execSync(`open ${filePath}`);
628
+ } else if (process.platform === 'linux') {
629
+ execSync(`xdg-open ${filePath}`);
630
+ }
631
+ }
632
+ } catch (e) {
633
+ console.log(chalk.yellow(`\nāš ļø Failed to open editor: ${e.message}`));
634
+ }
635
+ }
636
+
397
637
  async _getUserFiles() {
398
638
  const patterns = ['**/*.{js,ts,jsx,tsx}'];
399
639
  const ignore = [
@@ -268,8 +268,7 @@ class GoUISession {
268
268
  console.error('Test results display error:', error.message);
269
269
  }
270
270
  }
271
-
272
- async showAuditResults(auditResult) {
271
+ async showAuditResults(auditResult, index = 0) {
273
272
  const dataJson = JSON.stringify(auditResult);
274
273
  const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
275
274
  const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
@@ -283,7 +282,12 @@ class GoUISession {
283
282
  process.stdin.pause();
284
283
 
285
284
  await new Promise((resolve, reject) => {
286
- const child = spawn(this._binaryPath, ['audit-results', '--file', inputFile, '--output', outputFile], {
285
+ const args = ['audit-results', '--file', inputFile, '--output', outputFile];
286
+ if (index > 0) {
287
+ args.push('--index', index.toString());
288
+ }
289
+
290
+ const child = spawn(this._binaryPath, args, {
287
291
  stdio: ['inherit', 'inherit', 'inherit'],
288
292
  env: process.env
289
293
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
Binary file
Binary file
Binary file