@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.
- package/.tmp/tng-clone-data-1770062573900-923.json +1 -0
- package/.tmp/tng-progress-control-1770062573779-154.json +1 -0
- package/.tmp/tng_sh_js-49dc3b9a.napi_type_def.tmp +13 -0
- package/.tmp/tng_sh_js-49dc3b9a.napi_wasi_register.tmp +13 -0
- package/bin/tng.js +105 -2
- 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.d.ts +17 -0
- package/index.js +3 -1
- package/lib/generateTestsUi.js +116 -6
- package/lib/goUiSession.js +93 -2
- package/lib/jsonSession.js +5 -1
- package/out.log +0 -0
- package/package.json +1 -1
- package/tng_sh_js.darwin-arm64.node +0 -0
- package/tng_sh_js.darwin-x64.node +0 -0
- package/tng_sh_js.linux-arm64-gnu.node +0 -0
- package/tng_sh_js.linux-x64-gnu.node +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/goUiSession.js
CHANGED
|
@@ -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
|
|
401
|
-
|
|
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
|
|
package/lib/jsonSession.js
CHANGED
|
@@ -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
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|