@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 +102 -8
- package/binaries/go-ui-darwin-amd64 +0 -0
- package/binaries/go-ui-darwin-arm64 +0 -0
- package/binaries/go-ui-linux-amd64 +0 -0
- package/binaries/go-ui-linux-arm64 +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/lib/generateTestsUi.js +21 -24
- package/lib/goUiSession.js +23 -0
- package/lib/jsonSession.js +4 -0
- package/lib/saveFile.js +70 -6
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/index.darwin-arm64.node
CHANGED
|
Binary file
|
package/index.darwin-x64.node
CHANGED
|
Binary file
|
|
Binary file
|
package/index.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
package/lib/goUiSession.js
CHANGED
|
@@ -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 = {
|
package/lib/jsonSession.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
70
|
-
|
|
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
|
-
|
|
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 };
|