@tng-sh/js 0.0.8 â 0.1.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 +51 -21
- 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 +25 -1
- package/index.js +3 -2
- package/lib/generateTestsUi.js +138 -102
- package/lib/goUiSession.js +133 -68
- package/lib/jsonSession.js +49 -27
- 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
package/bin/tng.js
CHANGED
|
@@ -8,10 +8,26 @@ const { loadConfig } = require('../lib/config');
|
|
|
8
8
|
const { saveTestFile } = require('../lib/saveFile');
|
|
9
9
|
const { ping, getUserStats } = require('../index');
|
|
10
10
|
|
|
11
|
+
// Handle EPIPE errors gracefully when child process (Go UI) exits
|
|
12
|
+
process.stdout.on('error', (err) => {
|
|
13
|
+
if (err.code === 'EPIPE') {
|
|
14
|
+
// Ignore - this happens when Go UI exits and we try to write to closed pipe
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.on('uncaughtException', (err) => {
|
|
20
|
+
if (err.code === 'EPIPE') {
|
|
21
|
+
// Ignore broken pipe - child process exited
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
throw err; // Re-throw other errors
|
|
25
|
+
});
|
|
26
|
+
|
|
11
27
|
program
|
|
12
28
|
.name('tng')
|
|
13
29
|
.description('TNG - Automated Test Generation, and audit generation for JavaScript')
|
|
14
|
-
.version('0.0
|
|
30
|
+
.version('0.1.0');
|
|
15
31
|
|
|
16
32
|
/**
|
|
17
33
|
* @command init
|
|
@@ -188,7 +204,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
188
204
|
process.exit(1);
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
const {
|
|
207
|
+
const { runAudit, generateTest: nativeGenerateTest } = require('../index');
|
|
192
208
|
|
|
193
209
|
const action = auditMode ? 'Auditing' : 'Generating test for';
|
|
194
210
|
const startMessage = `đ ${action} ${methodName} in ${filePath}...`;
|
|
@@ -201,31 +217,45 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
201
217
|
} else {
|
|
202
218
|
console.log(chalk.blue(startMessage));
|
|
203
219
|
if (testType) {
|
|
204
|
-
console.log(chalk.cyan(
|
|
220
|
+
console.log(chalk.cyan(`Type hint: ${testType}`));
|
|
205
221
|
}
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
reporter.update(msg, percent);
|
|
221
|
-
} else {
|
|
222
|
-
// Simple console progress for CLI mode
|
|
223
|
-
if (percent % 10 === 0 || percent === 100) {
|
|
224
|
-
console.log(chalk.cyan(`[${percent}%] ${msg}`));
|
|
225
|
-
}
|
|
225
|
+
let resultJson;
|
|
226
|
+
|
|
227
|
+
const callback = (msg, percent) => {
|
|
228
|
+
if (jsonSession) {
|
|
229
|
+
// Emit progress events in JSON mode
|
|
230
|
+
const reporter = new (require('../lib/jsonSession').JsonProgressReporter)();
|
|
231
|
+
reporter.update(msg, percent);
|
|
232
|
+
} else {
|
|
233
|
+
// Simple console progress for CLI mode
|
|
234
|
+
if (percent % 10 === 0 || percent === 100) {
|
|
235
|
+
console.log(chalk.cyan(`[${percent}%] ${msg}`));
|
|
226
236
|
}
|
|
227
237
|
}
|
|
228
|
-
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (auditMode) {
|
|
241
|
+
resultJson = runAudit(
|
|
242
|
+
absolutePath,
|
|
243
|
+
methodName,
|
|
244
|
+
null, // class_name
|
|
245
|
+
testType || null,
|
|
246
|
+
JSON.stringify(config),
|
|
247
|
+
callback
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
resultJson = nativeGenerateTest(
|
|
251
|
+
absolutePath,
|
|
252
|
+
methodName,
|
|
253
|
+
null, // class_name
|
|
254
|
+
testType || null,
|
|
255
|
+
JSON.stringify(config),
|
|
256
|
+
callback
|
|
257
|
+
);
|
|
258
|
+
}
|
|
229
259
|
|
|
230
260
|
if (auditMode) {
|
|
231
261
|
// In audit mode, display the results
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -3,10 +3,34 @@
|
|
|
3
3
|
|
|
4
4
|
/* auto-generated by NAPI-RS */
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Analyzes a JavaScript/TypeScript file and returns its structural outline as a JSON string.
|
|
8
|
+
* Contains information about classes and methods found in the file.
|
|
9
|
+
*/
|
|
6
10
|
export declare function getFileOutline(filePath: string): string
|
|
11
|
+
/**
|
|
12
|
+
* Gathers project-wide metadata from package.json and related files.
|
|
13
|
+
* Returns a JSON string containing dependencies, frameworks, and project type.
|
|
14
|
+
*/
|
|
7
15
|
export declare function getProjectMetadata(projectRoot: string): string
|
|
16
|
+
/**
|
|
17
|
+
* Searches the project for all locations where a specific method is called.
|
|
18
|
+
* Uses ripgrep under the hood for high performance.
|
|
19
|
+
*/
|
|
8
20
|
export declare function findCallSites(projectRoot: string, methodName: string): string
|
|
21
|
+
/** Pings the TNG API to verify connectivity and API key validity. */
|
|
9
22
|
export declare function ping(baseUrl: string, apiKey?: string | undefined | null): string
|
|
23
|
+
/** Submits a test generation job to the API and returns the numeric job ID. */
|
|
10
24
|
export declare function submitJob(baseUrl: string, apiKey: string, payloadJson: string): number
|
|
25
|
+
/** Fetches usage statistics for the authenticated user from the API. */
|
|
11
26
|
export declare function getUserStats(baseUrl: string, apiKey: string): string
|
|
12
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Orchestrates the code audit process.
|
|
29
|
+
* Analyzes the source, builds context, and streams results via the provided callback.
|
|
30
|
+
*/
|
|
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
|
+
* Orchestrates the test generation process.
|
|
34
|
+
* Analyzes code, submits a job, and polls for the final generated test.
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateTest(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): 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,
|
|
313
|
+
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, generateTest } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.getFileOutline = getFileOutline
|
|
316
316
|
module.exports.getProjectMetadata = getProjectMetadata
|
|
@@ -318,4 +318,5 @@ module.exports.findCallSites = findCallSites
|
|
|
318
318
|
module.exports.ping = ping
|
|
319
319
|
module.exports.submitJob = submitJob
|
|
320
320
|
module.exports.getUserStats = getUserStats
|
|
321
|
-
module.exports.
|
|
321
|
+
module.exports.runAudit = runAudit
|
|
322
|
+
module.exports.generateTest = generateTest
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
1
|
const path = require('path');
|
|
3
2
|
const glob = require('fast-glob');
|
|
4
3
|
const chalk = require('chalk');
|
|
5
4
|
const GoUISession = require('./goUiSession');
|
|
6
|
-
const { getFileOutline,
|
|
5
|
+
const { getFileOutline, runAudit, generateTest, ping, getUserStats } = require('../index');
|
|
7
6
|
const { loadConfig } = require('./config');
|
|
8
7
|
const { saveTestFile } = require('./saveFile');
|
|
9
8
|
|
|
@@ -52,16 +51,19 @@ class GenerateTestsUI {
|
|
|
52
51
|
config_file: "tng.config.js"
|
|
53
52
|
});
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
try {
|
|
55
|
+
spawnSync(this.goUiSession._binaryPath, ['config-missing', '--data', dataJson], {
|
|
56
|
+
stdio: 'inherit'
|
|
57
|
+
});
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(chalk.red('\nFailed to show configuration error UI.\n'));
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
async _showStats() {
|
|
61
64
|
const config = loadConfig();
|
|
62
65
|
if (!config.API_KEY) {
|
|
63
66
|
console.log(chalk.red('\nNo API key configured. Run: tng init\n'));
|
|
64
|
-
console.log(chalk.yellow('Or edit tng.config.js and set your API_KEY\n'));
|
|
65
67
|
return;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -78,7 +80,6 @@ class GenerateTestsUI {
|
|
|
78
80
|
const config = loadConfig();
|
|
79
81
|
if (!config.API_KEY) {
|
|
80
82
|
console.log(chalk.red('\nNo API key configured. Run: tng init\n'));
|
|
81
|
-
console.log(chalk.yellow('Or edit tng.config.js and set your API_KEY\n'));
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -96,7 +97,7 @@ class GenerateTestsUI {
|
|
|
96
97
|
const files = await this._getUserFiles();
|
|
97
98
|
|
|
98
99
|
if (files.length === 0) {
|
|
99
|
-
console.log(chalk.yellow('
|
|
100
|
+
console.log(chalk.yellow('\nNo JavaScript or TypeScript files found in your project.\n'));
|
|
100
101
|
return 'back';
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -124,8 +125,8 @@ class GenerateTestsUI {
|
|
|
124
125
|
const result = getFileOutline(filePath);
|
|
125
126
|
outline = JSON.parse(result);
|
|
126
127
|
} catch (e) {
|
|
127
|
-
console.error(chalk.red(
|
|
128
|
-
return this._showFileSelection();
|
|
128
|
+
console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
|
|
129
|
+
return this._showFileSelection(isAudit);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
const methods = outline.methods || [];
|
|
@@ -144,8 +145,7 @@ class GenerateTestsUI {
|
|
|
144
145
|
const title = isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`;
|
|
145
146
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
146
147
|
|
|
147
|
-
if (selectedDisplay === 'back') return this._showFileSelection(isAudit);
|
|
148
|
-
if (!selectedDisplay) return this._showFileSelection(isAudit);
|
|
148
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit);
|
|
149
149
|
|
|
150
150
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
151
151
|
|
|
@@ -154,7 +154,6 @@ class GenerateTestsUI {
|
|
|
154
154
|
if (testType === 'back') return this._showFileSelection(isAudit);
|
|
155
155
|
|
|
156
156
|
const finalType = testType === 'auto' ? null : testType;
|
|
157
|
-
|
|
158
157
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
|
|
159
158
|
|
|
160
159
|
if (isAudit) {
|
|
@@ -170,88 +169,82 @@ class GenerateTestsUI {
|
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
|
|
172
|
+
if (!this._hasApiKey()) {
|
|
173
|
+
return { error: 'No API key' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isAudit) {
|
|
177
|
+
return this._handleAuditFlow(filePath, method, testType);
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
const fileName = path.basename(filePath);
|
|
174
181
|
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
175
|
-
|
|
182
|
+
return this._handleTestGenerationFlow(filePath, method, testType, displayName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_hasApiKey() {
|
|
186
|
+
const config = loadConfig();
|
|
187
|
+
if (!config.API_KEY) {
|
|
188
|
+
const msg = 'No API key configured. Run: tng init';
|
|
189
|
+
if (this.cliMode) console.log(chalk.red(msg));
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async _handleAuditFlow(filePath, method, testType) {
|
|
196
|
+
const config = loadConfig();
|
|
197
|
+
const streamingUi = await this.goUiSession.showStreamingAuditResults(
|
|
198
|
+
method.name,
|
|
199
|
+
method.class_name,
|
|
200
|
+
method.source_code || ''
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!streamingUi) return null;
|
|
204
|
+
|
|
205
|
+
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);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await streamingUi.wait;
|
|
220
|
+
return { message: 'Audit complete', resultJson };
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async _handleTestGenerationFlow(filePath, method, testType, displayName) {
|
|
228
|
+
const actionName = 'Generating test for';
|
|
176
229
|
|
|
177
230
|
const progressHandler = async (progress) => {
|
|
178
231
|
progress.update('Preparing request...');
|
|
179
232
|
|
|
180
233
|
try {
|
|
181
234
|
const config = loadConfig();
|
|
182
|
-
if (!config.API_KEY) {
|
|
183
|
-
progress.error('No API key configured. Run: tng init');
|
|
184
|
-
return { error: 'No API key' };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const agentMap = {
|
|
188
|
-
'context_agent_status': { label: 'Context Builder', step: 1 },
|
|
189
|
-
'style_agent_status': { label: 'Style Analyzer', step: 2 },
|
|
190
|
-
'logical_issue_status': { label: 'Logic Analyzer', step: 3 },
|
|
191
|
-
'behavior_expert_status': { label: 'Logic Generator', step: 4 },
|
|
192
|
-
'context_insights_status': { label: 'Context Insights', step: 5 }
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
for (const [key, config] of Object.entries(agentMap)) {
|
|
196
|
-
progress.update(`${config.label}: Pending...`, { step_increment: true });
|
|
197
|
-
}
|
|
198
235
|
|
|
199
|
-
|
|
200
|
-
const resultJson = submitAndPoll(
|
|
236
|
+
const resultJson = generateTest(
|
|
201
237
|
filePath,
|
|
202
238
|
method.name,
|
|
203
239
|
method.class_name || null,
|
|
204
240
|
testType || null,
|
|
205
|
-
isAudit, // audit_mode
|
|
206
241
|
JSON.stringify(config),
|
|
207
|
-
(msg, percent) =>
|
|
208
|
-
try {
|
|
209
|
-
if (msg.startsWith('{')) {
|
|
210
|
-
const info = JSON.parse(msg);
|
|
211
|
-
|
|
212
|
-
for (const [key, config] of Object.entries(agentMap)) {
|
|
213
|
-
const item = info[key];
|
|
214
|
-
if (!item) continue;
|
|
215
|
-
|
|
216
|
-
const agentStatus = item.status || 'pending';
|
|
217
|
-
const values = item.values || [];
|
|
218
|
-
|
|
219
|
-
let displayMsg = `${config.label}: ${agentStatus.charAt(0).toUpperCase() + agentStatus.slice(1)}...`;
|
|
220
|
-
if (agentStatus === 'completed') displayMsg = `${config.label}: Completed`;
|
|
221
|
-
|
|
222
|
-
if (values.length > 0) {
|
|
223
|
-
const vals = values.map(v => v.toString().replace(/_/g, ' ')).slice(0, 2).join(', ');
|
|
224
|
-
displayMsg += ` (${vals}${values.length > 2 ? '...' : ''})`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
progress.update(displayMsg, {
|
|
228
|
-
percent: key === 'behavior_expert_status' ? percent : undefined,
|
|
229
|
-
explicit_step: config.step,
|
|
230
|
-
step_increment: false
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
progress.update(msg, { percent });
|
|
235
|
-
}
|
|
236
|
-
} catch (e) {
|
|
237
|
-
progress.update(msg, { percent });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
242
|
+
(msg, percent) => this._updateProgress(progress, msg, percent)
|
|
240
243
|
);
|
|
241
244
|
|
|
242
|
-
|
|
243
|
-
progress.complete('Audit ready!', { auto_exit: false });
|
|
244
|
-
return {
|
|
245
|
-
message: 'Audit ready!',
|
|
246
|
-
resultJson: resultJson
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
progress.update('Tests generated successfully!');
|
|
245
|
+
progress.update('Saving generated tests...');
|
|
251
246
|
|
|
252
|
-
// Save the file inside the progress handler to match Python flow
|
|
253
247
|
const fileInfo = await saveTestFile(resultJson);
|
|
254
|
-
|
|
255
248
|
return {
|
|
256
249
|
message: 'Tests generated successfully!',
|
|
257
250
|
resultJson,
|
|
@@ -264,23 +257,54 @@ class GenerateTestsUI {
|
|
|
264
257
|
};
|
|
265
258
|
|
|
266
259
|
const uiResult = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
|
|
260
|
+
return uiResult || null;
|
|
261
|
+
}
|
|
267
262
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
_updateProgress(progress, msg, percent) {
|
|
264
|
+
const agentMap = {
|
|
265
|
+
'context_agent_status': { label: 'Context Builder', step: 1 },
|
|
266
|
+
'style_agent_status': { label: 'Style Analyzer', step: 2 },
|
|
267
|
+
'logical_issue_status': { label: 'Logic Analyzer', step: 3 },
|
|
268
|
+
'behavior_expert_status': { label: 'Logic Generator', step: 4 },
|
|
269
|
+
'context_insights_status': { label: 'Context Insights', step: 5 }
|
|
270
|
+
};
|
|
273
271
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
try {
|
|
273
|
+
if (msg.trim().startsWith('{')) {
|
|
274
|
+
const info = JSON.parse(msg);
|
|
275
|
+
|
|
276
|
+
for (const [key, config] of Object.entries(agentMap)) {
|
|
277
|
+
const item = info[key];
|
|
278
|
+
if (!item) continue;
|
|
279
|
+
|
|
280
|
+
const agentStatus = item.status || 'pending';
|
|
281
|
+
const values = item.values || [];
|
|
282
|
+
|
|
283
|
+
let displayMsg = `${config.label}: ${agentStatus.charAt(0).toUpperCase() + agentStatus.slice(1)}...`;
|
|
284
|
+
if (agentStatus === 'completed') displayMsg = `${config.label}: Completed`;
|
|
285
|
+
|
|
286
|
+
if (values.length > 0) {
|
|
287
|
+
const vals = values.map(v => v.toString().replace(/_/g, ' ')).slice(0, 2).join(', ');
|
|
288
|
+
displayMsg += ` (${vals}${values.length > 2 ? '...' : ''})`;
|
|
289
|
+
}
|
|
277
290
|
|
|
278
|
-
|
|
291
|
+
progress.update(displayMsg, {
|
|
292
|
+
percent: key === 'behavior_expert_status' ? percent : undefined,
|
|
293
|
+
explicit_step: config.step,
|
|
294
|
+
step_increment: false
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
progress.update(msg, { percent });
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
progress.update(msg, { percent });
|
|
302
|
+
}
|
|
279
303
|
}
|
|
280
304
|
|
|
281
305
|
_showPostGenerationMenu(result) {
|
|
282
306
|
const filePath = result.file_path;
|
|
283
|
-
const runCommand = `npm test ${filePath}`;
|
|
307
|
+
const runCommand = `npm test ${filePath}`;
|
|
284
308
|
|
|
285
309
|
while (true) {
|
|
286
310
|
const choice = this.goUiSession.showPostGenerationMenu(filePath, runCommand);
|
|
@@ -308,20 +332,17 @@ class GenerateTestsUI {
|
|
|
308
332
|
};
|
|
309
333
|
} catch (error) {
|
|
310
334
|
return {
|
|
311
|
-
success: true,
|
|
312
|
-
message: "Tests completed",
|
|
313
|
-
output: error.stdout + error.stderr,
|
|
314
|
-
exit_code: error.status
|
|
335
|
+
success: true,
|
|
336
|
+
message: "Tests completed with failures",
|
|
337
|
+
output: (error.stdout || '') + (error.stderr || ''),
|
|
338
|
+
exit_code: error.status || 1
|
|
315
339
|
};
|
|
316
340
|
}
|
|
317
341
|
};
|
|
318
342
|
|
|
319
343
|
const testOutput = this.goUiSession.showSpinner("Running tests...", spinnerHandler);
|
|
320
344
|
|
|
321
|
-
// Note: For simplicity in the first pass, we provide counts.
|
|
322
|
-
// We can enhance _parseTestOutput if needed.
|
|
323
345
|
const { passed, failed, errors, total } = this._parseTestOutput(testOutput.output || "", testOutput.exit_code || 0);
|
|
324
|
-
|
|
325
346
|
this.goUiSession.showTestResults("Test Results", passed, failed, errors, total, []);
|
|
326
347
|
}
|
|
327
348
|
|
|
@@ -356,11 +377,21 @@ class GenerateTestsUI {
|
|
|
356
377
|
try {
|
|
357
378
|
if (process.platform === 'darwin') {
|
|
358
379
|
execSync('pbcopy', { input: text });
|
|
380
|
+
this.goUiSession.showClipboardSuccess(text);
|
|
381
|
+
} else if (process.platform === 'linux') {
|
|
382
|
+
// Try xclip then xsel
|
|
383
|
+
try {
|
|
384
|
+
execSync('xclip -selection clipboard', { input: text });
|
|
385
|
+
} catch (e) {
|
|
386
|
+
execSync('xsel --clipboard --input', { input: text });
|
|
387
|
+
}
|
|
388
|
+
this.goUiSession.showClipboardSuccess(text);
|
|
359
389
|
} else {
|
|
360
|
-
|
|
361
|
-
console.log(`\nđ Copy this command: ${text}\n`);
|
|
390
|
+
console.log(chalk.cyan(`\nđ Please copy this command: ${text}\n`));
|
|
362
391
|
}
|
|
363
|
-
} catch (e) {
|
|
392
|
+
} catch (e) {
|
|
393
|
+
console.error(chalk.yellow(`\nâ ī¸ Failed to copy to clipboard. Command: ${text}\n`));
|
|
394
|
+
}
|
|
364
395
|
}
|
|
365
396
|
|
|
366
397
|
async _getUserFiles() {
|
|
@@ -375,11 +406,16 @@ class GenerateTestsUI {
|
|
|
375
406
|
'**/*.spec.*'
|
|
376
407
|
];
|
|
377
408
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
try {
|
|
410
|
+
return await glob(patterns, {
|
|
411
|
+
ignore,
|
|
412
|
+
absolute: true,
|
|
413
|
+
onlyFiles: true
|
|
414
|
+
});
|
|
415
|
+
} catch (e) {
|
|
416
|
+
console.error(chalk.red(`\nError searching for files: ${e.message}\n`));
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
383
419
|
}
|
|
384
420
|
}
|
|
385
421
|
|
package/lib/goUiSession.js
CHANGED
|
@@ -7,6 +7,7 @@ class GoUISession {
|
|
|
7
7
|
constructor() {
|
|
8
8
|
this._binaryPath = this._findGoUiBinary();
|
|
9
9
|
this._running = false;
|
|
10
|
+
this._tempFiles = new Set();
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
start() {
|
|
@@ -15,20 +16,30 @@ class GoUISession {
|
|
|
15
16
|
|
|
16
17
|
stop() {
|
|
17
18
|
this._running = false;
|
|
19
|
+
this.cleanup();
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
running() {
|
|
21
23
|
return this._running;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
cleanup() {
|
|
27
|
+
for (const file of this._tempFiles) {
|
|
28
|
+
this._cleanupTempFile(file);
|
|
29
|
+
}
|
|
30
|
+
this._tempFiles.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
showMenu() {
|
|
25
|
-
const outputFile = this._createTempFile('menu-output', '.txt');
|
|
34
|
+
const outputFile = this._trackTempFile(this._createTempFile('menu-output', '.txt'));
|
|
26
35
|
|
|
27
36
|
try {
|
|
28
|
-
spawnSync(this._binaryPath, ['menu', '--output', outputFile], {
|
|
37
|
+
const result = spawnSync(this._binaryPath, ['menu', '--output', outputFile], {
|
|
29
38
|
stdio: 'inherit'
|
|
30
39
|
});
|
|
31
40
|
|
|
41
|
+
if (result.error) throw result.error;
|
|
42
|
+
|
|
32
43
|
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
33
44
|
return 'exit';
|
|
34
45
|
}
|
|
@@ -36,7 +47,7 @@ class GoUISession {
|
|
|
36
47
|
const choice = fs.readFileSync(outputFile, 'utf8').trim();
|
|
37
48
|
return choice || 'exit';
|
|
38
49
|
} catch (error) {
|
|
39
|
-
console.error('Menu error:', error);
|
|
50
|
+
console.error('Menu error:', error.message);
|
|
40
51
|
return 'exit';
|
|
41
52
|
} finally {
|
|
42
53
|
this._cleanupTempFile(outputFile);
|
|
@@ -44,13 +55,15 @@ class GoUISession {
|
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
showJsTestMenu() {
|
|
47
|
-
const outputFile = this._createTempFile('js-menu-output', '.txt');
|
|
58
|
+
const outputFile = this._trackTempFile(this._createTempFile('js-menu-output', '.txt'));
|
|
48
59
|
|
|
49
60
|
try {
|
|
50
|
-
spawnSync(this._binaryPath, ['js-test-menu', '--output', outputFile], {
|
|
61
|
+
const result = spawnSync(this._binaryPath, ['js-test-menu', '--output', outputFile], {
|
|
51
62
|
stdio: 'inherit'
|
|
52
63
|
});
|
|
53
64
|
|
|
65
|
+
if (result.error) throw result.error;
|
|
66
|
+
|
|
54
67
|
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
55
68
|
return 'back';
|
|
56
69
|
}
|
|
@@ -58,7 +71,7 @@ class GoUISession {
|
|
|
58
71
|
const choice = fs.readFileSync(outputFile, 'utf8').trim();
|
|
59
72
|
return choice || 'back';
|
|
60
73
|
} catch (error) {
|
|
61
|
-
console.error('JS API Menu error:', error);
|
|
74
|
+
console.error('JS API Menu error:', error.message);
|
|
62
75
|
return 'back';
|
|
63
76
|
} finally {
|
|
64
77
|
this._cleanupTempFile(outputFile);
|
|
@@ -67,13 +80,15 @@ class GoUISession {
|
|
|
67
80
|
|
|
68
81
|
showListView(title, items) {
|
|
69
82
|
const dataJson = JSON.stringify({ title, items });
|
|
70
|
-
const outputFile = this._createTempFile('list-view-output', '.txt');
|
|
83
|
+
const outputFile = this._trackTempFile(this._createTempFile('list-view-output', '.txt'));
|
|
71
84
|
|
|
72
85
|
try {
|
|
73
|
-
spawnSync(this._binaryPath, ['list-view', '--data', dataJson, '--output', outputFile], {
|
|
86
|
+
const result = spawnSync(this._binaryPath, ['list-view', '--data', dataJson, '--output', outputFile], {
|
|
74
87
|
stdio: 'inherit'
|
|
75
88
|
});
|
|
76
89
|
|
|
90
|
+
if (result.error) throw result.error;
|
|
91
|
+
|
|
77
92
|
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
78
93
|
return 'back';
|
|
79
94
|
}
|
|
@@ -81,7 +96,7 @@ class GoUISession {
|
|
|
81
96
|
const selected = fs.readFileSync(outputFile, 'utf8').trim();
|
|
82
97
|
return selected || 'back';
|
|
83
98
|
} catch (error) {
|
|
84
|
-
console.error('List view error:', error);
|
|
99
|
+
console.error('List view error:', error.message);
|
|
85
100
|
return 'back';
|
|
86
101
|
} finally {
|
|
87
102
|
this._cleanupTempFile(outputFile);
|
|
@@ -89,10 +104,9 @@ class GoUISession {
|
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
showSpinner(message, func) {
|
|
92
|
-
const controlFile = this._createTempFile('spinner-control', '.json');
|
|
107
|
+
const controlFile = this._trackTempFile(this._createTempFile('spinner-control', '.json'));
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
const process = spawn(this._binaryPath, ['spinner', '--message', message, '--control', controlFile], {
|
|
109
|
+
const child = spawn(this._binaryPath, ['spinner', '--message', message, '--control', controlFile], {
|
|
96
110
|
stdio: ['ignore', 'inherit', 'inherit']
|
|
97
111
|
});
|
|
98
112
|
|
|
@@ -106,19 +120,20 @@ class GoUISession {
|
|
|
106
120
|
|
|
107
121
|
fs.writeFileSync(controlFile, JSON.stringify(status));
|
|
108
122
|
|
|
109
|
-
// Wait for
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// But for this simple implementation, we'll wait for the process to exit
|
|
123
|
+
// Wait for spinner to exit (it should exit after reading success/error from control file)
|
|
124
|
+
spawnSync('sleep', ['0.2']);
|
|
125
|
+
|
|
113
126
|
return result;
|
|
114
127
|
} catch (error) {
|
|
115
128
|
const status = { status: 'error', message: error.message };
|
|
116
|
-
fs.writeFileSync(controlFile, JSON.stringify(status));
|
|
129
|
+
try { fs.writeFileSync(controlFile, JSON.stringify(status)); } catch (e) { }
|
|
117
130
|
throw error;
|
|
118
131
|
} finally {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
// Give the child process a moment to exit before we delete the control file
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
this._cleanupTempFile(controlFile);
|
|
135
|
+
if (!child.killed) child.kill();
|
|
136
|
+
}, 500);
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -129,7 +144,7 @@ class GoUISession {
|
|
|
129
144
|
stdio: 'inherit'
|
|
130
145
|
});
|
|
131
146
|
} catch (error) {
|
|
132
|
-
console.error('Stats error:', error);
|
|
147
|
+
console.error('Stats error:', error.message);
|
|
133
148
|
}
|
|
134
149
|
}
|
|
135
150
|
|
|
@@ -139,26 +154,28 @@ class GoUISession {
|
|
|
139
154
|
stdio: 'inherit'
|
|
140
155
|
});
|
|
141
156
|
} catch (error) {
|
|
142
|
-
console.error('About error:', error);
|
|
157
|
+
console.error('About error:', error.message);
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
showPostGenerationMenu(filePath, runCommand) {
|
|
147
162
|
const dataJson = JSON.stringify({ file_path: filePath, run_command: runCommand });
|
|
148
|
-
const outputFile = this._createTempFile('post-gen-output', '.txt');
|
|
163
|
+
const outputFile = this._trackTempFile(this._createTempFile('post-gen-output', '.txt'));
|
|
149
164
|
|
|
150
165
|
try {
|
|
151
|
-
spawnSync(this._binaryPath, ['post-generation-menu', '--data', dataJson, '--output', outputFile], {
|
|
166
|
+
const result = spawnSync(this._binaryPath, ['post-generation-menu', '--data', dataJson, '--output', outputFile], {
|
|
152
167
|
stdio: 'inherit'
|
|
153
168
|
});
|
|
154
169
|
|
|
170
|
+
if (result.error) throw result.error;
|
|
171
|
+
|
|
155
172
|
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
156
173
|
return 'back';
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
return fs.readFileSync(outputFile, 'utf8').trim() || 'back';
|
|
160
177
|
} catch (error) {
|
|
161
|
-
console.error('Post-gen menu error:', error);
|
|
178
|
+
console.error('Post-gen menu error:', error.message);
|
|
162
179
|
return 'back';
|
|
163
180
|
} finally {
|
|
164
181
|
this._cleanupTempFile(outputFile);
|
|
@@ -166,9 +183,8 @@ class GoUISession {
|
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
async showProgress(title, handler) {
|
|
169
|
-
const controlFile = this._createTempFile('progress-control', '.json');
|
|
186
|
+
const controlFile = this._trackTempFile(this._createTempFile('progress-control', '.json'));
|
|
170
187
|
|
|
171
|
-
// Spawn progress in background
|
|
172
188
|
const child = spawn(this._binaryPath, ['progress', '--title', title, '--control', controlFile], {
|
|
173
189
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
174
190
|
env: process.env
|
|
@@ -196,20 +212,19 @@ class GoUISession {
|
|
|
196
212
|
data.percent = options.percent;
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
|
|
215
|
+
try {
|
|
216
|
+
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// Silently fail if control file is locked or missing
|
|
219
|
+
}
|
|
200
220
|
|
|
201
221
|
if (options.explicit_step === undefined && options.step_increment !== false) {
|
|
202
222
|
stepCounter++;
|
|
203
223
|
}
|
|
204
|
-
|
|
205
|
-
// Small delay to ensure Go UI (polling at 100ms) doesn't miss rapid updates
|
|
206
|
-
if (options.explicit_step !== undefined) {
|
|
207
|
-
spawnSync('sleep', ['0.05']);
|
|
208
|
-
}
|
|
209
224
|
},
|
|
210
225
|
error: (message) => {
|
|
211
226
|
const data = { type: 'error', message };
|
|
212
|
-
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
227
|
+
try { fs.writeFileSync(controlFile, JSON.stringify(data)); } catch (e) { }
|
|
213
228
|
},
|
|
214
229
|
complete: (message, options = {}) => {
|
|
215
230
|
const data = {
|
|
@@ -217,50 +232,51 @@ class GoUISession {
|
|
|
217
232
|
message: message || 'Done!',
|
|
218
233
|
auto_exit: options.auto_exit === true
|
|
219
234
|
};
|
|
220
|
-
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
235
|
+
try { fs.writeFileSync(controlFile, JSON.stringify(data)); } catch (e) { }
|
|
221
236
|
}
|
|
222
237
|
};
|
|
223
238
|
|
|
224
239
|
try {
|
|
225
240
|
const result = await handler(progress);
|
|
226
|
-
// Signal success if not already completed/errored
|
|
227
241
|
progress.complete();
|
|
228
|
-
|
|
229
|
-
// Wait for progress bar to exit CLEANLY
|
|
230
242
|
await processPromise;
|
|
231
|
-
|
|
232
243
|
return result;
|
|
233
244
|
} catch (error) {
|
|
234
245
|
progress.error(error.message);
|
|
235
|
-
await processPromise;
|
|
246
|
+
await processPromise;
|
|
236
247
|
throw error;
|
|
237
|
-
|
|
248
|
+
} finally {
|
|
249
|
+
setTimeout(() => this._cleanupTempFile(controlFile), 500);
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
252
|
|
|
241
|
-
|
|
242
253
|
showClipboardSuccess(command) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
try {
|
|
255
|
+
spawnSync(this._binaryPath, ['clipboard-success', '--command', command], {
|
|
256
|
+
stdio: 'inherit'
|
|
257
|
+
});
|
|
258
|
+
} catch (e) { }
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
showTestResults(title, passed, failed, errors, total, results = []) {
|
|
249
262
|
const dataJson = JSON.stringify({ title, passed, failed, errors, total, results });
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
263
|
+
try {
|
|
264
|
+
spawnSync(this._binaryPath, ['test-results', '--data', dataJson], {
|
|
265
|
+
stdio: 'inherit'
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('Test results display error:', error.message);
|
|
269
|
+
}
|
|
253
270
|
}
|
|
254
271
|
|
|
255
272
|
async showAuditResults(auditResult) {
|
|
256
273
|
const dataJson = JSON.stringify(auditResult);
|
|
257
|
-
const inputFile = this._createTempFile('audit-data', '.json');
|
|
258
|
-
const outputFile = this._createTempFile('audit-choice', '.txt');
|
|
274
|
+
const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
|
|
275
|
+
const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
|
|
259
276
|
|
|
260
277
|
try {
|
|
261
278
|
fs.writeFileSync(inputFile, dataJson);
|
|
262
279
|
|
|
263
|
-
// Pause stdin to allow child process to take control of TTY
|
|
264
280
|
if (process.stdin.setRawMode) {
|
|
265
281
|
process.stdin.setRawMode(false);
|
|
266
282
|
}
|
|
@@ -272,13 +288,8 @@ class GoUISession {
|
|
|
272
288
|
env: process.env
|
|
273
289
|
});
|
|
274
290
|
|
|
275
|
-
child.on('error', (err) =>
|
|
276
|
-
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
child.on('exit', (code) => {
|
|
280
|
-
resolve();
|
|
281
|
-
});
|
|
291
|
+
child.on('error', (err) => reject(err));
|
|
292
|
+
child.on('exit', () => resolve());
|
|
282
293
|
});
|
|
283
294
|
|
|
284
295
|
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
@@ -287,24 +298,75 @@ class GoUISession {
|
|
|
287
298
|
|
|
288
299
|
return fs.readFileSync(outputFile, 'utf8').trim() || 'back';
|
|
289
300
|
} catch (error) {
|
|
290
|
-
console.error('Audit results error:', error);
|
|
301
|
+
console.error('Audit results error:', error.message);
|
|
291
302
|
return 'back';
|
|
292
303
|
} finally {
|
|
293
|
-
// Resume stdin
|
|
294
304
|
process.stdin.resume();
|
|
295
|
-
|
|
296
305
|
this._cleanupTempFile(inputFile);
|
|
297
306
|
this._cleanupTempFile(outputFile);
|
|
298
307
|
}
|
|
299
308
|
}
|
|
300
309
|
|
|
310
|
+
async showStreamingAuditResults(methodName, className, sourceCode) {
|
|
311
|
+
const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
if (process.stdin.setRawMode) {
|
|
315
|
+
process.stdin.setRawMode(false);
|
|
316
|
+
}
|
|
317
|
+
process.stdin.pause();
|
|
318
|
+
|
|
319
|
+
const child = spawn(this._binaryPath, [
|
|
320
|
+
'streaming-audit-results',
|
|
321
|
+
'--method', methodName || '',
|
|
322
|
+
'--class', className || '',
|
|
323
|
+
'--source', sourceCode || '',
|
|
324
|
+
'--output', outputFile
|
|
325
|
+
], {
|
|
326
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
327
|
+
env: process.env
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Handle potential broken pipes defensively
|
|
331
|
+
child.stdin.on('error', (err) => {
|
|
332
|
+
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
|
|
333
|
+
console.error('Streaming stdin error:', err.message);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const processPromise = new Promise((resolve, reject) => {
|
|
338
|
+
child.on('error', (err) => reject(err));
|
|
339
|
+
child.on('exit', (code) => resolve(code));
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
process: child,
|
|
344
|
+
wait: processPromise,
|
|
345
|
+
outputFile: outputFile,
|
|
346
|
+
write: (msg) => {
|
|
347
|
+
if (child.stdin.writable) {
|
|
348
|
+
try {
|
|
349
|
+
child.stdin.write(msg + '\n');
|
|
350
|
+
} catch (e) {
|
|
351
|
+
// Suppress EPIPE errors during write
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('Streaming audit results error:', error.message);
|
|
358
|
+
process.stdin.resume();
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
301
363
|
showNoItems(itemType) {
|
|
302
364
|
try {
|
|
303
365
|
spawnSync(this._binaryPath, ['no-items', '--type', itemType], {
|
|
304
366
|
stdio: 'inherit'
|
|
305
367
|
});
|
|
306
368
|
} catch (error) {
|
|
307
|
-
console.error('No
|
|
369
|
+
console.error('UI No Items error:', error.message);
|
|
308
370
|
}
|
|
309
371
|
}
|
|
310
372
|
|
|
@@ -321,29 +383,32 @@ class GoUISession {
|
|
|
321
383
|
throw new Error(`Unsupported platform: ${platform}`);
|
|
322
384
|
}
|
|
323
385
|
|
|
324
|
-
// Look in binaries folder relative to this file
|
|
325
386
|
const localBinary = path.join(__dirname, '..', 'binaries', binaryName);
|
|
326
387
|
if (fs.existsSync(localBinary)) {
|
|
327
388
|
return localBinary;
|
|
328
389
|
}
|
|
329
390
|
|
|
330
|
-
throw new Error(`
|
|
391
|
+
throw new Error(`Critical: TNG UI binary not found at ${localBinary}. Please reinstall the extension.`);
|
|
331
392
|
}
|
|
332
393
|
|
|
333
394
|
_createTempFile(prefix, suffix) {
|
|
334
395
|
const tmpDir = os.tmpdir();
|
|
335
|
-
const filePath = path.join(tmpDir,
|
|
396
|
+
const filePath = path.join(tmpDir, `tng-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
|
|
336
397
|
fs.writeFileSync(filePath, '');
|
|
337
398
|
return filePath;
|
|
338
399
|
}
|
|
339
400
|
|
|
401
|
+
_trackTempFile(filePath) {
|
|
402
|
+
this._tempFiles.add(filePath);
|
|
403
|
+
return filePath;
|
|
404
|
+
}
|
|
405
|
+
|
|
340
406
|
_cleanupTempFile(filePath) {
|
|
341
407
|
if (fs.existsSync(filePath)) {
|
|
342
408
|
try {
|
|
343
409
|
fs.unlinkSync(filePath);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
410
|
+
this._tempFiles.delete(filePath);
|
|
411
|
+
} catch (e) { }
|
|
347
412
|
}
|
|
348
413
|
}
|
|
349
414
|
}
|
package/lib/jsonSession.js
CHANGED
|
@@ -14,21 +14,21 @@ class JsonSession {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
displayError(message) {
|
|
17
|
-
this.emitEvent('error', { message: this.stripColors(message) });
|
|
17
|
+
this.emitEvent('error', { message: this.stripColors(message || 'Unknown error') });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
displayWarning(message) {
|
|
21
|
-
this.emitEvent('warning', { message: this.stripColors(message) });
|
|
21
|
+
this.emitEvent('warning', { message: this.stripColors(message || 'Warning') });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
displayInfo(message) {
|
|
25
|
-
this.emitEvent('info', { message: this.stripColors(message) });
|
|
25
|
+
this.emitEvent('info', { message: this.stripColors(message || '') });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
displayList(title, items) {
|
|
29
29
|
this.emitEvent('list', {
|
|
30
|
-
title: this.stripColors(title),
|
|
31
|
-
items: items
|
|
30
|
+
title: this.stripColors(title || 'Items'),
|
|
31
|
+
items: items || []
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -37,7 +37,7 @@ class JsonSession {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
showProgress(title, callback) {
|
|
40
|
-
this.emitEvent('progress_start', { title });
|
|
40
|
+
this.emitEvent('progress_start', { title: title || 'Processing' });
|
|
41
41
|
|
|
42
42
|
const reporter = new JsonProgressReporter();
|
|
43
43
|
|
|
@@ -45,33 +45,35 @@ class JsonSession {
|
|
|
45
45
|
const result = callback(reporter);
|
|
46
46
|
|
|
47
47
|
if (result && result.error) {
|
|
48
|
-
//
|
|
48
|
+
// Keep progress state but report error
|
|
49
|
+
this.emitEvent('error', { message: result.error });
|
|
49
50
|
} else {
|
|
50
51
|
this.emitEvent('progress_complete', {
|
|
51
52
|
message: result?.message || 'Done',
|
|
52
|
-
result: result
|
|
53
|
+
result: result || {}
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
return result;
|
|
57
58
|
} catch (error) {
|
|
58
|
-
|
|
59
|
+
const errMsg = error?.message || 'Progress interrupted by unknown error';
|
|
60
|
+
this.emitEvent('error', { message: errMsg });
|
|
59
61
|
throw error;
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
showAuditResults(auditResult) {
|
|
64
|
-
this.emitEvent('result', auditResult);
|
|
66
|
+
this.emitEvent('result', auditResult || {});
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
showTestResults(title, passed, failed, errors, total, results = []) {
|
|
68
70
|
this.emitEvent('test_results', {
|
|
69
|
-
title,
|
|
70
|
-
passed,
|
|
71
|
-
failed,
|
|
72
|
-
errors,
|
|
73
|
-
total,
|
|
74
|
-
results
|
|
71
|
+
title: title || 'Test Results',
|
|
72
|
+
passed: Math.max(0, passed || 0),
|
|
73
|
+
failed: Math.max(0, failed || 0),
|
|
74
|
+
errors: Math.max(0, errors || 0),
|
|
75
|
+
total: Math.max(0, total || 0),
|
|
76
|
+
results: results || []
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -80,29 +82,35 @@ class JsonSession {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
showConfigError(missing) {
|
|
83
|
-
this.emitEvent('config_error', { missing });
|
|
85
|
+
this.emitEvent('config_error', { missing: missing || [] });
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
showConfigMissing(missingItems) {
|
|
87
|
-
this.emitEvent('config_missing', { missing: missingItems });
|
|
89
|
+
this.emitEvent('config_missing', { missing: missingItems || [] });
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
showSystemStatus(status) {
|
|
91
|
-
this.emitEvent('system_status', status);
|
|
93
|
+
this.emitEvent('system_status', status || {});
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
showNoItems(type) {
|
|
95
|
-
this.emitEvent('no_items', { type });
|
|
97
|
+
this.emitEvent('no_items', { type: type || 'items' });
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
stripColors(str) {
|
|
101
|
+
if (typeof str !== 'string') return '';
|
|
99
102
|
// Remove ANSI color codes
|
|
100
103
|
return str.replace(/\x1b\[\d+(;\d+)*m/g, '');
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
emitEvent(type, data = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
try {
|
|
108
|
+
const event = { type, ...data, timestamp: Date.now() };
|
|
109
|
+
console.log(JSON.stringify(event));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// If serialization fails (e.g. circular ref), emit minimal error
|
|
112
|
+
console.log(JSON.stringify({ type: 'error', message: 'Internal session serialization error' }));
|
|
113
|
+
}
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -115,17 +123,29 @@ class JsonProgressReporter {
|
|
|
115
123
|
const { stepIncrement = true, explicitStep = null } = options;
|
|
116
124
|
const stepIdx = explicitStep !== null ? explicitStep : this.step;
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
let payload = {
|
|
119
127
|
type: 'progress_update',
|
|
120
|
-
message,
|
|
121
128
|
step: stepIdx
|
|
122
129
|
};
|
|
123
130
|
|
|
131
|
+
if (typeof message === 'string' && message.trim().startsWith('{')) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(message);
|
|
134
|
+
payload = { ...payload, ...parsed };
|
|
135
|
+
} catch (e) {
|
|
136
|
+
payload.message = message;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
payload.message = message || '';
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
if (percent !== null) {
|
|
125
|
-
payload.percent = percent;
|
|
143
|
+
payload.percent = Math.min(100, Math.max(0, percent));
|
|
126
144
|
}
|
|
127
145
|
|
|
128
|
-
|
|
146
|
+
try {
|
|
147
|
+
console.log(JSON.stringify(payload));
|
|
148
|
+
} catch (e) { }
|
|
129
149
|
|
|
130
150
|
if (stepIncrement && explicitStep === null) {
|
|
131
151
|
this.step += 1;
|
|
@@ -133,7 +153,9 @@ class JsonProgressReporter {
|
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
error(message) {
|
|
136
|
-
|
|
156
|
+
try {
|
|
157
|
+
console.log(JSON.stringify({ type: 'error', message: message || 'Unknown error' }));
|
|
158
|
+
} catch (e) { }
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|