@tng-sh/js 0.1.0 → 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.
package/bin/tng.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const { loadConfig } = require('../lib/config');
8
8
  const { saveTestFile } = require('../lib/saveFile');
9
9
  const { ping, getUserStats } = require('../index');
10
+ const { applyFix } = require('../lib/fixApplier');
10
11
 
11
12
  // Handle EPIPE errors gracefully when child process (Go UI) exits
12
13
  process.stdout.on('error', (err) => {
@@ -27,7 +28,7 @@ process.on('uncaughtException', (err) => {
27
28
  program
28
29
  .name('tng')
29
30
  .description('TNG - Automated Test Generation, and audit generation for JavaScript')
30
- .version('0.1.0');
31
+ .version('0.1.1');
31
32
 
32
33
  /**
33
34
  * @command init
@@ -166,6 +167,39 @@ program
166
167
  }
167
168
  });
168
169
 
170
+ /**
171
+ * @command fix
172
+ * Apply a specific fix to a file
173
+ */
174
+ program
175
+ .command('fix')
176
+ .description('Apply a fix to a file')
177
+ .option('-f, --file <path>', 'File path to fix')
178
+ .option('-d, --data <json>', 'JSON data for the audit item containing fix info')
179
+ .action(async (options) => {
180
+ if (!options.file || !options.data) {
181
+ console.log(chalk.red('Error: Both --file and --data are required.'));
182
+ process.exit(1);
183
+ }
184
+
185
+ try {
186
+ const item = JSON.parse(options.data);
187
+ const result = await applyFix(options.file, item);
188
+ if (result.success) {
189
+ console.log(chalk.green(`✓ Fix applied to ${options.file}`));
190
+ if (result.backup_path) {
191
+ console.log(chalk.dim(` Backup created at ${result.backup_path}`));
192
+ }
193
+ } else {
194
+ console.log(chalk.red(`❌ Failed to apply fix: ${result.error}`));
195
+ process.exit(1);
196
+ }
197
+ } catch (e) {
198
+ console.log(chalk.red(`Error: ${e.message}`));
199
+ process.exit(1);
200
+ }
201
+ });
202
+
169
203
 
170
204
 
171
205
  /**
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -29,6 +29,16 @@ export declare function getUserStats(baseUrl: string, apiKey: string): string
29
29
  * Analyzes the source, builds context, and streams results via the provided callback.
30
30
  */
31
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
+ * Applies a single edit operation to a file.
34
+ * Uses backup and atomic write for safety.
35
+ */
36
+ export declare function applyEdit(filePath: string, search: string, replace: string, lineHint?: number | undefined | null): string
37
+ /**
38
+ * Applies multiple edit operations atomically.
39
+ * All succeed or all fail (with rollback).
40
+ */
41
+ export declare function applyEditsAtomic(operationsJson: string): string
32
42
  /**
33
43
  * Orchestrates the test generation process.
34
44
  * Analyzes code, submits a job, and polls for the final generated test.
package/index.js CHANGED
@@ -310,7 +310,7 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, generateTest } = nativeBinding
313
+ const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -319,4 +319,6 @@ module.exports.ping = ping
319
319
  module.exports.submitJob = submitJob
320
320
  module.exports.getUserStats = getUserStats
321
321
  module.exports.runAudit = runAudit
322
+ module.exports.applyEdit = applyEdit
323
+ module.exports.applyEditsAtomic = applyEditsAtomic
322
324
  module.exports.generateTest = generateTest
@@ -0,0 +1,28 @@
1
+ const { parentPort, workerData } = require('worker_threads');
2
+ const { runAudit } = require('../index');
3
+ const { loadConfig } = require('./config');
4
+
5
+ const { filePath, methodName, className, testType } = workerData;
6
+ const config = loadConfig();
7
+
8
+ try {
9
+ const resultJson = runAudit(
10
+ filePath,
11
+ methodName,
12
+ className,
13
+ testType,
14
+ JSON.stringify(config),
15
+ (msg) => {
16
+ if (parentPort) {
17
+ parentPort.postMessage({ type: 'item', data: msg });
18
+ }
19
+ }
20
+ );
21
+ if (parentPort) {
22
+ parentPort.postMessage({ type: 'result', data: resultJson });
23
+ }
24
+ } catch (e) {
25
+ if (parentPort) {
26
+ parentPort.postMessage({ type: 'error', data: e.message });
27
+ }
28
+ }
package/lib/config.js CHANGED
@@ -21,6 +21,7 @@ const loadConfig = () => {
21
21
 
22
22
  return Object.keys(filtered).length > 0 ? filtered : config;
23
23
  } catch (error) {
24
+ console.error(`Failed to load config from ${configFile}:`, error.message);
24
25
  return {};
25
26
  }
26
27
  };
@@ -0,0 +1,59 @@
1
+ const { applyEdit } = require('../index');
2
+
3
+ /**
4
+ * Parses a SEARCH_AND_REPLACE block from a string
5
+ * @param {string} codeChange
6
+ * @returns {{search: string, replace: string} | null}
7
+ */
8
+ function parseSearchReplace(codeChange) {
9
+ if (!codeChange) return null;
10
+
11
+ // Try to find SEARCH: and REPLACE: blocks
12
+ const searchIdx = codeChange.indexOf('SEARCH:');
13
+ const replaceIdx = codeChange.indexOf('REPLACE:');
14
+
15
+ if (searchIdx !== -1 && replaceIdx !== -1 && searchIdx < replaceIdx) {
16
+ const search = codeChange.substring(searchIdx + 7, replaceIdx).trim();
17
+ const replace = codeChange.substring(replaceIdx + 8).trim();
18
+ return { search, replace };
19
+ }
20
+
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Appies a fix to a file
26
+ * @param {string} filePath
27
+ * @param {object} item
28
+ * @returns {object} result
29
+ */
30
+ async function applyFix(filePath, item) {
31
+ try {
32
+ if (!item) {
33
+ return { success: false, error: 'Item is null or undefined' };
34
+ }
35
+ const codeChange = item.fix_code_change || (item.fix && item.fix.how && item.fix.how.code_change);
36
+ if (!codeChange) {
37
+ return { success: false, error: 'No code change found in fix data' };
38
+ }
39
+
40
+ const parsed = parseSearchReplace(codeChange);
41
+ if (!parsed) {
42
+ return { success: false, error: 'Failed to parse SEARCH_AND_REPLACE block' };
43
+ }
44
+
45
+ const resultJson = applyEdit(
46
+ filePath,
47
+ parsed.search,
48
+ parsed.replace,
49
+ typeof item.line_number === 'number' && item.line_number > 0 ? item.line_number : null
50
+ );
51
+
52
+ const result = JSON.parse(resultJson);
53
+ return result;
54
+ } catch (e) {
55
+ return { success: false, error: e.message };
56
+ }
57
+ }
58
+
59
+ module.exports = { applyFix, parseSearchReplace };
@@ -1,10 +1,12 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
3
  const glob = require('fast-glob');
3
4
  const chalk = require('chalk');
4
5
  const GoUISession = require('./goUiSession');
5
- const { getFileOutline, runAudit, generateTest, ping, getUserStats } = require('../index');
6
+ const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
6
7
  const { loadConfig } = require('./config');
7
8
  const { saveTestFile } = require('./saveFile');
9
+ const { applyFix } = require('./fixApplier');
8
10
 
9
11
  class GenerateTestsUI {
10
12
  constructor(cliMode = false) {
@@ -193,7 +195,7 @@ class GenerateTestsUI {
193
195
  }
194
196
 
195
197
  async _handleAuditFlow(filePath, method, testType) {
196
- const config = loadConfig();
198
+ const { Worker } = require('worker_threads');
197
199
  const streamingUi = await this.goUiSession.showStreamingAuditResults(
198
200
  method.name,
199
201
  method.class_name,
@@ -202,25 +204,158 @@ class GenerateTestsUI {
202
204
 
203
205
  if (!streamingUi) return null;
204
206
 
207
+ let results = {
208
+ issues: [],
209
+ behaviours: [],
210
+ method_name: method.name,
211
+ class_name: method.class_name,
212
+ method_source_with_lines: method.source_code
213
+ };
214
+ let auditFinished = false;
215
+
216
+ // Start audit in background worker
217
+ const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
218
+ workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
219
+ });
220
+
221
+ const itemIds = new Set();
222
+ worker.on('message', (msg) => {
223
+ if (msg.type === 'item') {
224
+ if (msg.data.startsWith('{')) {
225
+ streamingUi.write(msg.data);
226
+ try {
227
+ const data = JSON.parse(msg.data);
228
+
229
+ // Handle metadata updates from stream
230
+ if (data.method_name) results.method_name = data.method_name;
231
+ if (data.class_name) results.class_name = data.class_name;
232
+ if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
233
+ if (data.source_code) results.method_source_with_lines = data.source_code;
234
+
235
+ // Only push if it looks like an actual audit item
236
+ if (data.test_name || data.summary || data.category) {
237
+ const id = `${data.category}-${data.test_name}-${data.line_number}`;
238
+ if (!itemIds.has(id)) {
239
+ if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
240
+ else results.issues.push(data);
241
+ itemIds.add(id);
242
+ }
243
+ }
244
+ } catch (e) { }
245
+ }
246
+ } else if (msg.type === 'result') {
247
+ try {
248
+ const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
249
+
250
+ // Merge final results with our local ones, preserving 'fixed' state
251
+ const mergeList = (localList, incomingList) => {
252
+ if (!incomingList || incomingList.length === 0) return localList;
253
+
254
+ return incomingList.map(incomingItem => {
255
+ const localItem = localList.find(it =>
256
+ it.test_name === incomingItem.test_name &&
257
+ it.line_number === incomingItem.line_number
258
+ );
259
+ if (localItem && localItem.fixed) {
260
+ return { ...incomingItem, fixed: true };
261
+ }
262
+ return incomingItem;
263
+ });
264
+ };
265
+
266
+ results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
267
+ results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
268
+
269
+ if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
270
+ if (finalResults.method_name) results.method_name = finalResults.method_name;
271
+ } catch (e) { }
272
+ auditFinished = true;
273
+ } else if (msg.type === 'error') {
274
+ console.error(chalk.red(`Audit error: ${msg.data}`));
275
+ auditFinished = true;
276
+ }
277
+ });
278
+
279
+ worker.on('error', (err) => {
280
+ console.error(chalk.red(`Worker error: ${err.message}`));
281
+ auditFinished = true;
282
+ });
283
+
205
284
  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);
285
+ let currentChoice = null;
286
+
287
+ // Wait for INITIAL streaming session to complete (user picks an action or quits)
288
+ await streamingUi.wait;
289
+
290
+ const getResponse = (outputFile) => {
291
+ if (fs.existsSync(outputFile)) {
292
+ const output = fs.readFileSync(outputFile, 'utf8').trim();
293
+ if (output) {
294
+ try {
295
+ const parsed = JSON.parse(output);
296
+ return typeof parsed === 'object' ? parsed : { action: output };
297
+ } catch (e) {
298
+ return { action: output };
299
+ }
300
+ }
301
+ }
302
+ return null;
303
+ };
304
+
305
+ currentChoice = getResponse(streamingUi.outputFile);
306
+
307
+ // Action loop: handles fix/open and returns to UI
308
+ while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
309
+ const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
310
+
311
+ if (currentChoice.action === 'fix') {
312
+ console.log(chalk.cyan(`Applying fix...`));
313
+ const fixResult = await applyFix(filePath, currentChoice.item);
314
+ if (fixResult.success) {
315
+ console.log(chalk.green(`✓ Fix applied successfully.`));
316
+ // Mark as fixed locally
317
+ [results.issues, results.behaviours].forEach(list => {
318
+ list.forEach(it => {
319
+ if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
320
+ it.fixed = true;
321
+ }
322
+ });
323
+ });
324
+ } else {
325
+ console.log(chalk.red(`❌ Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
215
326
  }
327
+ } else if (currentChoice.action === 'open') {
328
+ this._openInEditor(filePath, currentChoice.item.line_number);
216
329
  }
217
- );
218
330
 
219
- await streamingUi.wait;
220
- return { message: 'Audit complete', resultJson };
331
+ // Ensure source has line numbers for static UI if it doesn't already
332
+ if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
333
+ results.method_source_with_lines = results.method_source_with_lines
334
+ .split('\n')
335
+ .map((line, i) => `${i + 1}: ${line}`)
336
+ .join('\n');
337
+ }
338
+
339
+ // Re-launch UI statically with updated results
340
+ const choiceStr = await this.goUiSession.showAuditResults(results, itemIndex);
341
+ if (choiceStr === 'back' || choiceStr === 'main_menu' || choiceStr === 'exit') {
342
+ currentChoice = null;
343
+ } else {
344
+ try {
345
+ const parsed = JSON.parse(choiceStr);
346
+ currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
347
+ } catch (e) {
348
+ currentChoice = { action: choiceStr };
349
+ }
350
+ }
351
+ }
352
+
353
+ return { message: 'Audit complete', results };
221
354
  } catch (e) {
222
- console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
355
+ console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
223
356
  return null;
357
+ } finally {
358
+ await worker.terminate();
224
359
  }
225
360
  }
226
361
 
@@ -394,6 +529,26 @@ class GenerateTestsUI {
394
529
  }
395
530
  }
396
531
 
532
+ _openInEditor(filePath, lineNumber) {
533
+ const { execSync } = require('child_process');
534
+ try {
535
+ // Try to open in VS Code if available, fallback to default editor
536
+ const location = lineNumber ? `${filePath}:${lineNumber}` : filePath;
537
+ try {
538
+ execSync(`code --goto ${location}`, { stdio: 'ignore' });
539
+ console.log(chalk.blue(`\nOpening ${location} in VS Code...`));
540
+ } catch (e) {
541
+ if (process.platform === 'darwin') {
542
+ execSync(`open ${filePath}`);
543
+ } else if (process.platform === 'linux') {
544
+ execSync(`xdg-open ${filePath}`);
545
+ }
546
+ }
547
+ } catch (e) {
548
+ console.log(chalk.yellow(`\n⚠️ Failed to open editor: ${e.message}`));
549
+ }
550
+ }
551
+
397
552
  async _getUserFiles() {
398
553
  const patterns = ['**/*.{js,ts,jsx,tsx}'];
399
554
  const ignore = [
@@ -268,8 +268,7 @@ class GoUISession {
268
268
  console.error('Test results display error:', error.message);
269
269
  }
270
270
  }
271
-
272
- async showAuditResults(auditResult) {
271
+ async showAuditResults(auditResult, index = 0) {
273
272
  const dataJson = JSON.stringify(auditResult);
274
273
  const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
275
274
  const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
@@ -283,7 +282,12 @@ class GoUISession {
283
282
  process.stdin.pause();
284
283
 
285
284
  await new Promise((resolve, reject) => {
286
- 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, {
287
291
  stdio: ['inherit', 'inherit', 'inherit'],
288
292
  env: process.env
289
293
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tng-sh/js",
3
- "version": "0.1.0",
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