@tng-sh/js 0.0.1
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/LICENSE.md +32 -0
- package/bin/tng.js +289 -0
- 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 +12 -0
- package/index.js +321 -0
- package/lib/config.js +31 -0
- package/lib/generateTestsUi.js +330 -0
- package/lib/goUiSession.js +349 -0
- package/lib/jsonSession.js +156 -0
- package/lib/saveFile.js +87 -0
- package/package.json +47 -0
- 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,330 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('fast-glob');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const GoUISession = require('./goUiSession');
|
|
6
|
+
const { getFileOutline, submitAndPoll, ping, getUserStats } = require('../index');
|
|
7
|
+
const { loadConfig } = require('./config');
|
|
8
|
+
const { saveTestFile } = require('./saveFile');
|
|
9
|
+
|
|
10
|
+
class GenerateTestsUI {
|
|
11
|
+
constructor(cliMode = false) {
|
|
12
|
+
this.cliMode = cliMode;
|
|
13
|
+
if (!cliMode) {
|
|
14
|
+
this.goUiSession = new GoUISession();
|
|
15
|
+
this.goUiSession.start();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async show() {
|
|
20
|
+
// Check for API key before showing menu
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
if (!config.API_KEY) {
|
|
23
|
+
this._showConfigMissing();
|
|
24
|
+
return 'exit';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
while (true) {
|
|
28
|
+
const choice = this.goUiSession.showMenu();
|
|
29
|
+
|
|
30
|
+
if (choice === 'generate' || choice === 'tests' || choice === 'select_file') {
|
|
31
|
+
const result = await this._showFileSelection(false);
|
|
32
|
+
if (result === 'exit') return 'exit';
|
|
33
|
+
} else if (choice === 'audit') {
|
|
34
|
+
const result = await this._showFileSelection(true);
|
|
35
|
+
if (result === 'exit') return 'exit';
|
|
36
|
+
} else if (choice === 'stats') {
|
|
37
|
+
await this._showStats();
|
|
38
|
+
} else if (choice === 'about') {
|
|
39
|
+
this.goUiSession.showAbout();
|
|
40
|
+
} else if (choice === 'ping') {
|
|
41
|
+
await this._pingApi();
|
|
42
|
+
} else if (choice === 'exit' || choice === null) {
|
|
43
|
+
return 'exit';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_showConfigMissing() {
|
|
49
|
+
const { spawnSync } = require('child_process');
|
|
50
|
+
const dataJson = JSON.stringify({
|
|
51
|
+
missing_items: ["API key"],
|
|
52
|
+
config_file: "tng.config.js"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
spawnSync(this.goUiSession._binaryPath, ['config-missing', '--data', dataJson], {
|
|
56
|
+
stdio: 'inherit'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async _showStats() {
|
|
61
|
+
const config = loadConfig();
|
|
62
|
+
if (!config.API_KEY) {
|
|
63
|
+
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
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const statsJson = getUserStats(config.API_URL, config.API_KEY);
|
|
70
|
+
const stats = JSON.parse(statsJson);
|
|
71
|
+
this.goUiSession.showStats(stats);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(chalk.red(`\nError fetching stats: ${e.message}\n`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _pingApi() {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
if (!config.API_KEY) {
|
|
80
|
+
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
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.goUiSession.showSpinner('Pinging TNG API...', () => {
|
|
86
|
+
try {
|
|
87
|
+
const result = ping(config.API_URL, config.API_KEY);
|
|
88
|
+
return { success: true, message: `API Response: ${result}` };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { success: false, message: e.message };
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _showFileSelection(isAudit = false) {
|
|
96
|
+
const files = await this._getUserFiles();
|
|
97
|
+
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
console.log(chalk.yellow('No JS/TS files found in project.'));
|
|
100
|
+
return 'back';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
const items = files.map(file => ({
|
|
105
|
+
name: path.relative(cwd, file),
|
|
106
|
+
path: path.dirname(file)
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const title = isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File';
|
|
110
|
+
const selectedName = this.goUiSession.showListView(title, items);
|
|
111
|
+
|
|
112
|
+
if (selectedName === 'back') return 'back';
|
|
113
|
+
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
114
|
+
|
|
115
|
+
const selectedFile = path.resolve(cwd, selectedName);
|
|
116
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit);
|
|
117
|
+
if (result === 'main_menu') return 'main_menu';
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async _showMethodsForFile(filePath, isAudit = false) {
|
|
122
|
+
let outline;
|
|
123
|
+
try {
|
|
124
|
+
const result = getFileOutline(filePath);
|
|
125
|
+
outline = JSON.parse(result);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(chalk.red(`Error parsing file: ${e.message}`));
|
|
128
|
+
return this._showFileSelection();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const methods = outline.methods || [];
|
|
132
|
+
if (methods.length === 0) {
|
|
133
|
+
this.goUiSession.showNoItems('methods');
|
|
134
|
+
return this._showFileSelection(isAudit);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fileName = path.basename(filePath);
|
|
138
|
+
const items = methods.map(m => ({
|
|
139
|
+
name: m.class_name ? `${m.class_name}.${m.name}` : m.name,
|
|
140
|
+
path: `Function in ${fileName}`,
|
|
141
|
+
methodData: m
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const title = isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`;
|
|
145
|
+
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
146
|
+
|
|
147
|
+
if (selectedDisplay === 'back') return this._showFileSelection(isAudit);
|
|
148
|
+
if (!selectedDisplay) return this._showFileSelection(isAudit);
|
|
149
|
+
|
|
150
|
+
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
151
|
+
|
|
152
|
+
if (selectedMethod) {
|
|
153
|
+
// New Step: Select Test Type
|
|
154
|
+
const testType = this.goUiSession.showJsTestMenu();
|
|
155
|
+
if (testType === 'back') return this._showFileSelection(isAudit);
|
|
156
|
+
|
|
157
|
+
// Handle auto-detect (pass null)
|
|
158
|
+
const finalType = testType === 'auto' ? null : testType;
|
|
159
|
+
|
|
160
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
|
|
161
|
+
|
|
162
|
+
if (isAudit) {
|
|
163
|
+
if (choice === 'main_menu') return 'main_menu';
|
|
164
|
+
return this._showFileSelection(isAudit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (choice && choice.file_path && !choice.error) {
|
|
168
|
+
this._showPostGenerationMenu(choice);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return this._showFileSelection(isAudit);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
|
|
175
|
+
const fileName = path.basename(filePath);
|
|
176
|
+
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
177
|
+
const actionName = isAudit ? 'Auditing' : 'Generating test for';
|
|
178
|
+
|
|
179
|
+
const progressHandler = (progress) => {
|
|
180
|
+
progress.update('Preparing request...');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
if (!config.API_KEY) {
|
|
185
|
+
progress.error('No API key configured. Run: tng init');
|
|
186
|
+
return { error: 'No API key' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const agentMap = {
|
|
190
|
+
'context_agent_status': { label: 'Context Builder', step: 1 },
|
|
191
|
+
'style_agent_status': { label: 'Style Analyzer', step: 2 },
|
|
192
|
+
'logical_issue_status': { label: 'Logic Analyzer', step: 3 },
|
|
193
|
+
'behavior_expert_status': { label: 'Logic Generator', step: 4 },
|
|
194
|
+
'context_insights_status': { label: 'Context Insights', step: 5 }
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const [key, config] of Object.entries(agentMap)) {
|
|
198
|
+
progress.update(`${config.label}: Pending...`, { step_increment: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Pass a callback to the native Rust submitAndPoll
|
|
202
|
+
const resultJson = submitAndPoll(
|
|
203
|
+
filePath,
|
|
204
|
+
method.name,
|
|
205
|
+
method.class_name || null,
|
|
206
|
+
testType || null,
|
|
207
|
+
isAudit, // audit_mode
|
|
208
|
+
JSON.stringify(config),
|
|
209
|
+
(msg, percent) => {
|
|
210
|
+
try {
|
|
211
|
+
if (msg.startsWith('{')) {
|
|
212
|
+
const info = JSON.parse(msg);
|
|
213
|
+
|
|
214
|
+
for (const [key, config] of Object.entries(agentMap)) {
|
|
215
|
+
const item = info[key];
|
|
216
|
+
if (!item) continue;
|
|
217
|
+
|
|
218
|
+
const agentStatus = item.status || 'pending';
|
|
219
|
+
const values = item.values || [];
|
|
220
|
+
|
|
221
|
+
let displayMsg = `${config.label}: ${agentStatus.charAt(0).toUpperCase() + agentStatus.slice(1)}...`;
|
|
222
|
+
if (agentStatus === 'completed') displayMsg = `${config.label}: Completed`;
|
|
223
|
+
|
|
224
|
+
if (values.length > 0) {
|
|
225
|
+
const vals = values.map(v => v.toString().replace(/_/g, ' ')).slice(0, 2).join(', ');
|
|
226
|
+
displayMsg += ` (${vals}${values.length > 2 ? '...' : ''})`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
progress.update(displayMsg, {
|
|
230
|
+
percent: key === 'behavior_expert_status' ? percent : undefined,
|
|
231
|
+
explicit_step: config.step,
|
|
232
|
+
step_increment: false
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
progress.update(msg, { percent });
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
progress.update(msg, { percent });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (isAudit) {
|
|
245
|
+
// Auto-exit the progress UI so we can immediately show the audit results
|
|
246
|
+
progress.complete('Audit complete!', { auto_exit: true });
|
|
247
|
+
return {
|
|
248
|
+
message: 'Audit complete!',
|
|
249
|
+
resultJson: resultJson
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
progress.update('Tests generated successfully!');
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
message: 'Tests generated successfully!',
|
|
257
|
+
resultJson
|
|
258
|
+
};
|
|
259
|
+
} catch (e) {
|
|
260
|
+
progress.error(`Failed to generate: ${e.message}`);
|
|
261
|
+
return { error: e.message };
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const uiResult = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
|
|
266
|
+
|
|
267
|
+
if (isAudit && uiResult && uiResult.resultJson) {
|
|
268
|
+
const auditData = JSON.parse(uiResult.resultJson);
|
|
269
|
+
// Unwrap 'audit_results' if present (Ruby wrapper does this, but JS output might differ)
|
|
270
|
+
return this.goUiSession.showAuditResults(auditData);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (uiResult && uiResult.resultJson) {
|
|
274
|
+
const fileInfo = await saveTestFile(uiResult.resultJson);
|
|
275
|
+
return {
|
|
276
|
+
...uiResult,
|
|
277
|
+
file_path: fileInfo?.file_path
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_showPostGenerationMenu(result) {
|
|
285
|
+
const filePath = result.file_path;
|
|
286
|
+
const runCommand = `npm test ${filePath}`; // Example command
|
|
287
|
+
|
|
288
|
+
while (true) {
|
|
289
|
+
const choice = this.goUiSession.showPostGenerationMenu(filePath, runCommand);
|
|
290
|
+
if (choice === 'back' || !choice) break;
|
|
291
|
+
|
|
292
|
+
if (choice === 'copy_command') {
|
|
293
|
+
this._copyToClipboard(runCommand);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_copyToClipboard(text) {
|
|
299
|
+
const { execSync } = require('child_process');
|
|
300
|
+
try {
|
|
301
|
+
if (process.platform === 'darwin') {
|
|
302
|
+
execSync('pbcopy', { input: text });
|
|
303
|
+
} else {
|
|
304
|
+
// Simplified for now
|
|
305
|
+
console.log(`\n📋 Copy this command: ${text}\n`);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) { }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async _getUserFiles() {
|
|
311
|
+
const patterns = ['**/*.{js,ts,jsx,tsx}'];
|
|
312
|
+
const ignore = [
|
|
313
|
+
'**/node_modules/**',
|
|
314
|
+
'**/dist/**',
|
|
315
|
+
'**/build/**',
|
|
316
|
+
'**/tests/**',
|
|
317
|
+
'**/test/**',
|
|
318
|
+
'**/*.test.*',
|
|
319
|
+
'**/*.spec.*'
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
return glob(patterns, {
|
|
323
|
+
ignore,
|
|
324
|
+
absolute: true,
|
|
325
|
+
onlyFiles: true
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = GenerateTestsUI;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { spawn, spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
class GoUISession {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._binaryPath = this._findGoUiBinary();
|
|
9
|
+
this._running = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
start() {
|
|
13
|
+
this._running = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
stop() {
|
|
17
|
+
this._running = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
running() {
|
|
21
|
+
return this._running;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
showMenu() {
|
|
25
|
+
const outputFile = this._createTempFile('menu-output', '.txt');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
spawnSync(this._binaryPath, ['menu', '--output', outputFile], {
|
|
29
|
+
stdio: 'inherit'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
33
|
+
return 'exit';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const choice = fs.readFileSync(outputFile, 'utf8').trim();
|
|
37
|
+
return choice || 'exit';
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Menu error:', error);
|
|
40
|
+
return 'exit';
|
|
41
|
+
} finally {
|
|
42
|
+
this._cleanupTempFile(outputFile);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
showJsTestMenu() {
|
|
47
|
+
const outputFile = this._createTempFile('js-menu-output', '.txt');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
spawnSync(this._binaryPath, ['js-test-menu', '--output', outputFile], {
|
|
51
|
+
stdio: 'inherit'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
55
|
+
return 'back';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const choice = fs.readFileSync(outputFile, 'utf8').trim();
|
|
59
|
+
return choice || 'back';
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('JS API Menu error:', error);
|
|
62
|
+
return 'back';
|
|
63
|
+
} finally {
|
|
64
|
+
this._cleanupTempFile(outputFile);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
showListView(title, items) {
|
|
69
|
+
const dataJson = JSON.stringify({ title, items });
|
|
70
|
+
const outputFile = this._createTempFile('list-view-output', '.txt');
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
spawnSync(this._binaryPath, ['list-view', '--data', dataJson, '--output', outputFile], {
|
|
74
|
+
stdio: 'inherit'
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
78
|
+
return 'back';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const selected = fs.readFileSync(outputFile, 'utf8').trim();
|
|
82
|
+
return selected || 'back';
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('List view error:', error);
|
|
85
|
+
return 'back';
|
|
86
|
+
} finally {
|
|
87
|
+
this._cleanupTempFile(outputFile);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
showSpinner(message, func) {
|
|
92
|
+
const controlFile = this._createTempFile('spinner-control', '.json');
|
|
93
|
+
|
|
94
|
+
// Start spinner in background
|
|
95
|
+
const process = spawn(this._binaryPath, ['spinner', '--message', message, '--control', controlFile], {
|
|
96
|
+
stdio: ['ignore', 'inherit', 'inherit']
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = func();
|
|
101
|
+
|
|
102
|
+
const status = {
|
|
103
|
+
status: result && result.success ? 'success' : 'error',
|
|
104
|
+
message: (result && result.message) || 'Done!'
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(controlFile, JSON.stringify(status));
|
|
108
|
+
|
|
109
|
+
// Wait for process to exit
|
|
110
|
+
// In Node, we can't easily wait sync for background spawned process without busy loop or async
|
|
111
|
+
// But for this simple implementation, we'll wait for the process to exit
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const status = { status: 'error', message: error.message };
|
|
114
|
+
fs.writeFileSync(controlFile, JSON.stringify(status));
|
|
115
|
+
throw error;
|
|
116
|
+
} finally {
|
|
117
|
+
// We don't delete controlFile immediately here if process is still running
|
|
118
|
+
// but spawnSync/wait logic would be better.
|
|
119
|
+
// For now, let's keep it simple.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
showStats(statsData) {
|
|
124
|
+
const statsJson = JSON.stringify(statsData || {});
|
|
125
|
+
try {
|
|
126
|
+
spawnSync(this._binaryPath, ['stats', '--data', statsJson], {
|
|
127
|
+
stdio: 'inherit'
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Stats error:', error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
showAbout() {
|
|
135
|
+
try {
|
|
136
|
+
spawnSync(this._binaryPath, ['about'], {
|
|
137
|
+
stdio: 'inherit'
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('About error:', error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
showPostGenerationMenu(filePath, runCommand) {
|
|
145
|
+
const dataJson = JSON.stringify({ file_path: filePath, run_command: runCommand });
|
|
146
|
+
const outputFile = this._createTempFile('post-gen-output', '.txt');
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
spawnSync(this._binaryPath, ['post-generation-menu', '--data', dataJson, '--output', outputFile], {
|
|
150
|
+
stdio: 'inherit'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
154
|
+
return 'back';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return fs.readFileSync(outputFile, 'utf8').trim() || 'back';
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Post-gen menu error:', error);
|
|
160
|
+
return 'back';
|
|
161
|
+
} finally {
|
|
162
|
+
this._cleanupTempFile(outputFile);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async showProgress(title, handler) {
|
|
167
|
+
const controlFile = this._createTempFile('progress-control', '.json');
|
|
168
|
+
|
|
169
|
+
// Spawn progress in background
|
|
170
|
+
const child = spawn(this._binaryPath, ['progress', '--title', title, '--control', controlFile], {
|
|
171
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
172
|
+
env: process.env
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const processPromise = new Promise((resolve) => {
|
|
176
|
+
child.on('exit', () => resolve());
|
|
177
|
+
child.on('error', () => resolve());
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let stepCounter = 0;
|
|
181
|
+
const progress = {
|
|
182
|
+
update: (message, options = {}) => {
|
|
183
|
+
const stepIdx = options.explicit_step !== undefined ?
|
|
184
|
+
options.explicit_step :
|
|
185
|
+
(options.step_increment === false ? (stepCounter > 0 ? stepCounter - 1 : 0) : stepCounter);
|
|
186
|
+
|
|
187
|
+
const data = {
|
|
188
|
+
type: 'step',
|
|
189
|
+
step: stepIdx,
|
|
190
|
+
message: message
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (options.percent !== undefined) {
|
|
194
|
+
data.percent = options.percent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
198
|
+
|
|
199
|
+
if (options.explicit_step === undefined && options.step_increment !== false) {
|
|
200
|
+
stepCounter++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Small delay to ensure Go UI (polling at 100ms) doesn't miss rapid updates
|
|
204
|
+
if (options.explicit_step !== undefined) {
|
|
205
|
+
spawnSync('sleep', ['0.05']);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
error: (message) => {
|
|
209
|
+
const data = { type: 'error', message };
|
|
210
|
+
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
211
|
+
},
|
|
212
|
+
complete: (message, options = {}) => {
|
|
213
|
+
const data = {
|
|
214
|
+
type: 'complete',
|
|
215
|
+
message: message || 'Done!',
|
|
216
|
+
auto_exit: options.auto_exit === true
|
|
217
|
+
};
|
|
218
|
+
fs.writeFileSync(controlFile, JSON.stringify(data));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const result = await handler(progress);
|
|
224
|
+
// Signal success if not already completed/errored
|
|
225
|
+
progress.complete();
|
|
226
|
+
|
|
227
|
+
// Wait for progress bar to exit CLEANLY
|
|
228
|
+
await processPromise;
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
progress.error(error.message);
|
|
233
|
+
await processPromise; // Wait even on error
|
|
234
|
+
throw error;
|
|
235
|
+
// If the process was killed or died, exitCode will be set.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
showClipboardSuccess(command) {
|
|
241
|
+
spawnSync(this._binaryPath, ['clipboard-success', '--command', command], {
|
|
242
|
+
stdio: 'inherit'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
showTestResults(title, passed, failed, errors, total, results = []) {
|
|
247
|
+
const dataJson = JSON.stringify({ title, passed, failed, errors, total, results });
|
|
248
|
+
spawnSync(this._binaryPath, ['test-results', '--data', dataJson], {
|
|
249
|
+
stdio: 'inherit'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async showAuditResults(auditResult) {
|
|
254
|
+
const dataJson = JSON.stringify(auditResult);
|
|
255
|
+
const inputFile = this._createTempFile('audit-data', '.json');
|
|
256
|
+
const outputFile = this._createTempFile('audit-choice', '.txt');
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
fs.writeFileSync(inputFile, dataJson);
|
|
260
|
+
|
|
261
|
+
// Pause stdin to allow child process to take control of TTY
|
|
262
|
+
if (process.stdin.setRawMode) {
|
|
263
|
+
process.stdin.setRawMode(false);
|
|
264
|
+
}
|
|
265
|
+
process.stdin.pause();
|
|
266
|
+
|
|
267
|
+
await new Promise((resolve, reject) => {
|
|
268
|
+
const child = spawn(this._binaryPath, ['audit-results', '--file', inputFile, '--output', outputFile], {
|
|
269
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
270
|
+
env: process.env
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
child.on('error', (err) => {
|
|
274
|
+
reject(err);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
child.on('exit', (code) => {
|
|
278
|
+
resolve();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
|
|
283
|
+
return 'back';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return fs.readFileSync(outputFile, 'utf8').trim() || 'back';
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error('Audit results error:', error);
|
|
289
|
+
return 'back';
|
|
290
|
+
} finally {
|
|
291
|
+
// Resume stdin
|
|
292
|
+
process.stdin.resume();
|
|
293
|
+
|
|
294
|
+
this._cleanupTempFile(inputFile);
|
|
295
|
+
this._cleanupTempFile(outputFile);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
showNoItems(itemType) {
|
|
300
|
+
try {
|
|
301
|
+
spawnSync(this._binaryPath, ['no-items', '--type', itemType], {
|
|
302
|
+
stdio: 'inherit'
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('No items error:', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_findGoUiBinary() {
|
|
310
|
+
const platform = process.platform;
|
|
311
|
+
const arch = process.arch;
|
|
312
|
+
let binaryName = '';
|
|
313
|
+
|
|
314
|
+
if (platform === 'darwin') {
|
|
315
|
+
binaryName = arch === 'arm64' ? 'go-ui-darwin-arm64' : 'go-ui-darwin-amd64';
|
|
316
|
+
} else if (platform === 'linux') {
|
|
317
|
+
binaryName = arch === 'arm64' ? 'go-ui-linux-arm64' : 'go-ui-linux-amd64';
|
|
318
|
+
} else {
|
|
319
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Look in binaries folder relative to this file
|
|
323
|
+
const localBinary = path.join(__dirname, '..', 'binaries', binaryName);
|
|
324
|
+
if (fs.existsSync(localBinary)) {
|
|
325
|
+
return localBinary;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
throw new Error(`go-ui binary not found: ${localBinary}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_createTempFile(prefix, suffix) {
|
|
332
|
+
const tmpDir = os.tmpdir();
|
|
333
|
+
const filePath = path.join(tmpDir, `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
|
|
334
|
+
fs.writeFileSync(filePath, '');
|
|
335
|
+
return filePath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_cleanupTempFile(filePath) {
|
|
339
|
+
if (fs.existsSync(filePath)) {
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(filePath);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
// Ignore cleanup errors
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = GoUISession;
|