@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.
package/bin/tng.js CHANGED
@@ -7,11 +7,28 @@ 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');
11
+
12
+ // Handle EPIPE errors gracefully when child process (Go UI) exits
13
+ process.stdout.on('error', (err) => {
14
+ if (err.code === 'EPIPE') {
15
+ // Ignore - this happens when Go UI exits and we try to write to closed pipe
16
+ process.exit(0);
17
+ }
18
+ });
19
+
20
+ process.on('uncaughtException', (err) => {
21
+ if (err.code === 'EPIPE') {
22
+ // Ignore broken pipe - child process exited
23
+ process.exit(0);
24
+ }
25
+ throw err; // Re-throw other errors
26
+ });
10
27
 
11
28
  program
12
29
  .name('tng')
13
30
  .description('TNG - Automated Test Generation, and audit generation for JavaScript')
14
- .version('0.0.9');
31
+ .version('0.1.1');
15
32
 
16
33
  /**
17
34
  * @command init
@@ -150,6 +167,39 @@ program
150
167
  }
151
168
  });
152
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
+
153
203
 
154
204
 
155
205
  /**
@@ -188,7 +238,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
188
238
  process.exit(1);
189
239
  }
190
240
 
191
- const { submitAndPoll } = require('../index');
241
+ const { runAudit, generateTest: nativeGenerateTest } = require('../index');
192
242
 
193
243
  const action = auditMode ? 'Auditing' : 'Generating test for';
194
244
  const startMessage = `🔍 ${action} ${methodName} in ${filePath}...`;
@@ -201,31 +251,45 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
201
251
  } else {
202
252
  console.log(chalk.blue(startMessage));
203
253
  if (testType) {
204
- console.log(chalk.cyan(`ℹ️ Type hint: ${testType}`));
254
+ console.log(chalk.cyan(`Type hint: ${testType}`));
205
255
  }
206
256
  }
207
257
 
208
258
  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
- }
259
+ let resultJson;
260
+
261
+ const callback = (msg, percent) => {
262
+ if (jsonSession) {
263
+ // Emit progress events in JSON mode
264
+ const reporter = new (require('../lib/jsonSession').JsonProgressReporter)();
265
+ reporter.update(msg, percent);
266
+ } else {
267
+ // Simple console progress for CLI mode
268
+ if (percent % 10 === 0 || percent === 100) {
269
+ console.log(chalk.cyan(`[${percent}%] ${msg}`));
226
270
  }
227
271
  }
228
- );
272
+ };
273
+
274
+ if (auditMode) {
275
+ resultJson = runAudit(
276
+ absolutePath,
277
+ methodName,
278
+ null, // class_name
279
+ testType || null,
280
+ JSON.stringify(config),
281
+ callback
282
+ );
283
+ } else {
284
+ resultJson = nativeGenerateTest(
285
+ absolutePath,
286
+ methodName,
287
+ null, // class_name
288
+ testType || null,
289
+ JSON.stringify(config),
290
+ callback
291
+ );
292
+ }
229
293
 
230
294
  if (auditMode) {
231
295
  // In audit mode, display the results
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -3,10 +3,44 @@
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
+ * 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
42
+ /**
43
+ * Orchestrates the test generation process.
44
+ * Analyzes code, submits a job, and polls for the final generated test.
45
+ */
46
+ 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, applyEdit, applyEditsAtomic, generateTest } = nativeBinding
314
314
 
315
315
  module.exports.getFileOutline = getFileOutline
316
316
  module.exports.getProjectMetadata = getProjectMetadata
@@ -318,4 +318,7 @@ 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.applyEdit = applyEdit
323
+ module.exports.applyEditsAtomic = applyEditsAtomic
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 };