@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 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.8');
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 { submitAndPoll } = require('../index');
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(`â„šī¸ Type hint: ${testType}`));
220
+ console.log(chalk.cyan(`Type hint: ${testType}`));
205
221
  }
206
222
  }
207
223
 
208
224
  try {
209
- const resultJson = submitAndPoll(
210
- absolutePath,
211
- methodName,
212
- null, // class_name
213
- testType || null,
214
- auditMode, // audit_mode
215
- JSON.stringify(config),
216
- (msg, percent) => {
217
- if (jsonSession) {
218
- // Emit progress events in JSON mode
219
- const reporter = new (require('../lib/jsonSession').JsonProgressReporter)();
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
- export declare function submitAndPoll(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, auditMode: boolean, configJson: string, callback: (...args: any[]) => any): string
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, submitAndPoll } = nativeBinding
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.submitAndPoll = submitAndPoll
321
+ module.exports.runAudit = runAudit
322
+ module.exports.generateTest = generateTest
@@ -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, submitAndPoll, ping, getUserStats } = require('../index');
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
- spawnSync(this.goUiSession._binaryPath, ['config-missing', '--data', dataJson], {
56
- stdio: 'inherit'
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('No JS/TS files found in project.'));
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(`Error parsing file: ${e.message}`));
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
- const actionName = isAudit ? 'Auditing' : 'Generating test for';
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
- // Pass a callback to the native Rust submitAndPoll
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
- if (isAudit) {
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
- if (isAudit && uiResult && uiResult.resultJson) {
269
- const auditData = JSON.parse(uiResult.resultJson);
270
- // Unwrap 'audit_results' if present (Ruby wrapper does this, but JS output might differ)
271
- return this.goUiSession.showAuditResults(auditData);
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
- if (uiResult && uiResult.resultJson) {
275
- return uiResult;
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
- return null;
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}`; // Example command
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, // We still want to show the results even if tests failed
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
- // Simplified for now
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
- return glob(patterns, {
379
- ignore,
380
- absolute: true,
381
- onlyFiles: true
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
 
@@ -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
- // Start spinner in background
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 process to exit
110
- // Wait for process to exit
111
- // In Node, we can't easily wait sync for background spawned process without busy loop or async
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
- // We don't delete controlFile immediately here if process is still running
120
- // but spawnSync/wait logic would be better.
121
- // For now, let's keep it simple.
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
- fs.writeFileSync(controlFile, JSON.stringify(data));
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; // Wait even on error
246
+ await processPromise;
236
247
  throw error;
237
- // If the process was killed or died, exitCode will be set.
248
+ } finally {
249
+ setTimeout(() => this._cleanupTempFile(controlFile), 500);
238
250
  }
239
251
  }
240
252
 
241
-
242
253
  showClipboardSuccess(command) {
243
- spawnSync(this._binaryPath, ['clipboard-success', '--command', command], {
244
- stdio: 'inherit'
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
- spawnSync(this._binaryPath, ['test-results', '--data', dataJson], {
251
- stdio: 'inherit'
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
- reject(err);
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 items error:', error);
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(`go-ui binary not found: ${localBinary}`);
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, `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
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
- } catch (e) {
345
- // Ignore cleanup errors
346
- }
410
+ this._tempFiles.delete(filePath);
411
+ } catch (e) { }
347
412
  }
348
413
  }
349
414
  }
@@ -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
- // Error handled by reporter or caller
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
- this.emitEvent('error', { message: error.message });
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
- const event = { type, ...data };
105
- console.log(JSON.stringify(event));
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
- const payload = {
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
- console.log(JSON.stringify(payload));
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
- console.log(JSON.stringify({ type: 'error', message }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
Binary file
Binary file
Binary file