@tng-sh/js 0.0.9 → 0.1.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.
@@ -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,68 +232,68 @@ 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
-
255
- async showAuditResults(auditResult) {
271
+ async showAuditResults(auditResult, index = 0) {
256
272
  const dataJson = JSON.stringify(auditResult);
257
- const inputFile = this._createTempFile('audit-data', '.json');
258
- const outputFile = this._createTempFile('audit-choice', '.txt');
273
+ const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
274
+ const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
259
275
 
260
276
  try {
261
277
  fs.writeFileSync(inputFile, dataJson);
262
278
 
263
- // Pause stdin to allow child process to take control of TTY
264
279
  if (process.stdin.setRawMode) {
265
280
  process.stdin.setRawMode(false);
266
281
  }
267
282
  process.stdin.pause();
268
283
 
269
284
  await new Promise((resolve, reject) => {
270
- const child = spawn(this._binaryPath, ['audit-results', '--file', inputFile, '--output', outputFile], {
285
+ const args = ['audit-results', '--file', inputFile, '--output', outputFile];
286
+ if (index > 0) {
287
+ args.push('--index', index.toString());
288
+ }
289
+
290
+ const child = spawn(this._binaryPath, args, {
271
291
  stdio: ['inherit', 'inherit', 'inherit'],
272
292
  env: process.env
273
293
  });
274
294
 
275
- child.on('error', (err) => {
276
- reject(err);
277
- });
278
-
279
- child.on('exit', (code) => {
280
- resolve();
281
- });
295
+ child.on('error', (err) => reject(err));
296
+ child.on('exit', () => resolve());
282
297
  });
283
298
 
284
299
  if (!fs.existsSync(outputFile) || fs.statSync(outputFile).size === 0) {
@@ -287,24 +302,75 @@ class GoUISession {
287
302
 
288
303
  return fs.readFileSync(outputFile, 'utf8').trim() || 'back';
289
304
  } catch (error) {
290
- console.error('Audit results error:', error);
305
+ console.error('Audit results error:', error.message);
291
306
  return 'back';
292
307
  } finally {
293
- // Resume stdin
294
308
  process.stdin.resume();
295
-
296
309
  this._cleanupTempFile(inputFile);
297
310
  this._cleanupTempFile(outputFile);
298
311
  }
299
312
  }
300
313
 
314
+ async showStreamingAuditResults(methodName, className, sourceCode) {
315
+ const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
316
+
317
+ try {
318
+ if (process.stdin.setRawMode) {
319
+ process.stdin.setRawMode(false);
320
+ }
321
+ process.stdin.pause();
322
+
323
+ const child = spawn(this._binaryPath, [
324
+ 'streaming-audit-results',
325
+ '--method', methodName || '',
326
+ '--class', className || '',
327
+ '--source', sourceCode || '',
328
+ '--output', outputFile
329
+ ], {
330
+ stdio: ['pipe', 'inherit', 'inherit'],
331
+ env: process.env
332
+ });
333
+
334
+ // Handle potential broken pipes defensively
335
+ child.stdin.on('error', (err) => {
336
+ if (err.code !== 'EPIPE' && err.code !== 'EOF') {
337
+ console.error('Streaming stdin error:', err.message);
338
+ }
339
+ });
340
+
341
+ const processPromise = new Promise((resolve, reject) => {
342
+ child.on('error', (err) => reject(err));
343
+ child.on('exit', (code) => resolve(code));
344
+ });
345
+
346
+ return {
347
+ process: child,
348
+ wait: processPromise,
349
+ outputFile: outputFile,
350
+ write: (msg) => {
351
+ if (child.stdin.writable) {
352
+ try {
353
+ child.stdin.write(msg + '\n');
354
+ } catch (e) {
355
+ // Suppress EPIPE errors during write
356
+ }
357
+ }
358
+ }
359
+ };
360
+ } catch (error) {
361
+ console.error('Streaming audit results error:', error.message);
362
+ process.stdin.resume();
363
+ return null;
364
+ }
365
+ }
366
+
301
367
  showNoItems(itemType) {
302
368
  try {
303
369
  spawnSync(this._binaryPath, ['no-items', '--type', itemType], {
304
370
  stdio: 'inherit'
305
371
  });
306
372
  } catch (error) {
307
- console.error('No items error:', error);
373
+ console.error('UI No Items error:', error.message);
308
374
  }
309
375
  }
310
376
 
@@ -321,29 +387,32 @@ class GoUISession {
321
387
  throw new Error(`Unsupported platform: ${platform}`);
322
388
  }
323
389
 
324
- // Look in binaries folder relative to this file
325
390
  const localBinary = path.join(__dirname, '..', 'binaries', binaryName);
326
391
  if (fs.existsSync(localBinary)) {
327
392
  return localBinary;
328
393
  }
329
394
 
330
- throw new Error(`go-ui binary not found: ${localBinary}`);
395
+ throw new Error(`Critical: TNG UI binary not found at ${localBinary}. Please reinstall the extension.`);
331
396
  }
332
397
 
333
398
  _createTempFile(prefix, suffix) {
334
399
  const tmpDir = os.tmpdir();
335
- const filePath = path.join(tmpDir, `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
400
+ const filePath = path.join(tmpDir, `tng-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}${suffix}`);
336
401
  fs.writeFileSync(filePath, '');
337
402
  return filePath;
338
403
  }
339
404
 
405
+ _trackTempFile(filePath) {
406
+ this._tempFiles.add(filePath);
407
+ return filePath;
408
+ }
409
+
340
410
  _cleanupTempFile(filePath) {
341
411
  if (fs.existsSync(filePath)) {
342
412
  try {
343
413
  fs.unlinkSync(filePath);
344
- } catch (e) {
345
- // Ignore cleanup errors
346
- }
414
+ this._tempFiles.delete(filePath);
415
+ } catch (e) { }
347
416
  }
348
417
  }
349
418
  }
@@ -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.9",
3
+ "version": "0.1.1",
4
4
  "description": "TNG JavaScript CLI",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
Binary file
Binary file
Binary file