@eldrforge/ai-service 0.1.1 → 0.1.3
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/dist/index.d.ts +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/ai.d.ts +55 -0
- package/{src/index.ts → dist/src/index.d.ts} +1 -2
- package/dist/src/interactive.d.ts +122 -0
- package/dist/src/logger.d.ts +19 -0
- package/dist/src/prompts/commit.d.ts +29 -0
- package/dist/src/prompts/index.d.ts +10 -0
- package/dist/src/prompts/release.d.ts +25 -0
- package/dist/src/prompts/review.d.ts +21 -0
- package/dist/src/types.d.ts +99 -0
- package/package.json +11 -8
- package/.github/dependabot.yml +0 -12
- package/.github/workflows/npm-publish.yml +0 -48
- package/.github/workflows/test.yml +0 -33
- package/eslint.config.mjs +0 -84
- package/src/ai.ts +0 -421
- package/src/interactive.ts +0 -562
- package/src/logger.ts +0 -69
- package/src/prompts/commit.ts +0 -85
- package/src/prompts/index.ts +0 -28
- package/src/prompts/instructions/commit.md +0 -133
- package/src/prompts/instructions/release.md +0 -188
- package/src/prompts/instructions/review.md +0 -169
- package/src/prompts/personas/releaser.md +0 -24
- package/src/prompts/personas/you.md +0 -55
- package/src/prompts/release.ts +0 -118
- package/src/prompts/review.ts +0 -72
- package/src/types.ts +0 -112
- package/tests/ai-complete-coverage.test.ts +0 -241
- package/tests/ai-create-completion.test.ts +0 -288
- package/tests/ai-edge-cases.test.ts +0 -221
- package/tests/ai-openai-error.test.ts +0 -35
- package/tests/ai-transcribe.test.ts +0 -169
- package/tests/ai.test.ts +0 -139
- package/tests/interactive-editor.test.ts +0 -253
- package/tests/interactive-secure-temp.test.ts +0 -264
- package/tests/interactive-user-choice.test.ts +0 -173
- package/tests/interactive-user-text.test.ts +0 -174
- package/tests/interactive.test.ts +0 -94
- package/tests/logger-noop.test.ts +0 -40
- package/tests/logger.test.ts +0 -122
- package/tests/prompts.test.ts +0 -179
- package/tsconfig.json +0 -35
- package/vite.config.ts +0 -69
- package/vitest.config.ts +0 -25
package/src/interactive.ts
DELETED
|
@@ -1,562 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { getLogger } from './logger';
|
|
3
|
-
import type { Logger, Choice, InteractiveOptions } from './types';
|
|
4
|
-
import { spawnSync } from 'child_process';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as fs from 'fs/promises';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Get user choice interactively from terminal input
|
|
11
|
-
* @param prompt The prompt message to display
|
|
12
|
-
* @param choices Array of available choices
|
|
13
|
-
* @param options Additional options for customizing behavior
|
|
14
|
-
* @returns Promise resolving to the selected choice key
|
|
15
|
-
*/
|
|
16
|
-
export async function getUserChoice(
|
|
17
|
-
prompt: string,
|
|
18
|
-
choices: Choice[],
|
|
19
|
-
options: InteractiveOptions = {}
|
|
20
|
-
): Promise<string> {
|
|
21
|
-
const logger = options.logger || getLogger();
|
|
22
|
-
|
|
23
|
-
logger.info(prompt);
|
|
24
|
-
choices.forEach(choice => {
|
|
25
|
-
logger.info(` [${choice.key}] ${choice.label}`);
|
|
26
|
-
});
|
|
27
|
-
logger.info('');
|
|
28
|
-
|
|
29
|
-
// Check if stdin is a TTY (terminal) or piped
|
|
30
|
-
if (!process.stdin.isTTY) {
|
|
31
|
-
logger.error('⚠️ STDIN is piped but interactive mode is enabled');
|
|
32
|
-
logger.error(' Interactive prompts cannot be used when input is piped');
|
|
33
|
-
logger.error(' Solutions:');
|
|
34
|
-
logger.error(' • Use terminal input instead of piping');
|
|
35
|
-
|
|
36
|
-
// Add any additional suggestions
|
|
37
|
-
if (options.nonTtyErrorSuggestions) {
|
|
38
|
-
options.nonTtyErrorSuggestions.forEach(suggestion => {
|
|
39
|
-
logger.error(` • ${suggestion}`);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return 's'; // Default to skip
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
let isResolved = false;
|
|
48
|
-
let dataHandler: ((key: Buffer) => void) | null = null;
|
|
49
|
-
let errorHandler: ((error: Error) => void) | null = null;
|
|
50
|
-
|
|
51
|
-
const cleanup = () => {
|
|
52
|
-
if (dataHandler) {
|
|
53
|
-
process.stdin.removeListener('data', dataHandler);
|
|
54
|
-
}
|
|
55
|
-
if (errorHandler) {
|
|
56
|
-
process.stdin.removeListener('error', errorHandler);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
if (process.stdin.setRawMode) {
|
|
61
|
-
process.stdin.setRawMode(false);
|
|
62
|
-
}
|
|
63
|
-
process.stdin.pause();
|
|
64
|
-
// Detach stdin again now that we're done
|
|
65
|
-
if (typeof process.stdin.unref === 'function') {
|
|
66
|
-
process.stdin.unref();
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// Ignore cleanup errors
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const safeResolve = (value: string) => {
|
|
74
|
-
if (!isResolved) {
|
|
75
|
-
isResolved = true;
|
|
76
|
-
cleanup();
|
|
77
|
-
resolve(value);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const safeReject = (error: Error) => {
|
|
82
|
-
if (!isResolved) {
|
|
83
|
-
isResolved = true;
|
|
84
|
-
cleanup();
|
|
85
|
-
reject(error);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// Ensure stdin is referenced so the process doesn't exit while waiting for input
|
|
91
|
-
if (typeof process.stdin.ref === 'function') {
|
|
92
|
-
process.stdin.ref();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
process.stdin.setRawMode(true);
|
|
96
|
-
process.stdin.resume();
|
|
97
|
-
|
|
98
|
-
dataHandler = (key: Buffer) => {
|
|
99
|
-
try {
|
|
100
|
-
const keyStr = key.toString().toLowerCase();
|
|
101
|
-
const choice = choices.find(c => c.key === keyStr);
|
|
102
|
-
if (choice) {
|
|
103
|
-
logger.info(`Selected: ${choice.label}\n`);
|
|
104
|
-
safeResolve(choice.key);
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
errorHandler = (error: Error) => {
|
|
112
|
-
safeReject(error);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
process.stdin.on('data', dataHandler);
|
|
116
|
-
process.stdin.on('error', errorHandler);
|
|
117
|
-
|
|
118
|
-
} catch (error) {
|
|
119
|
-
safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Secure temporary file handle that prevents TOCTOU vulnerabilities
|
|
126
|
-
*/
|
|
127
|
-
export class SecureTempFile {
|
|
128
|
-
private fd: fs.FileHandle | null = null;
|
|
129
|
-
private filePath: string;
|
|
130
|
-
private isCleanedUp = false;
|
|
131
|
-
private logger: Logger;
|
|
132
|
-
|
|
133
|
-
private constructor(filePath: string, fd: fs.FileHandle, logger?: Logger) {
|
|
134
|
-
this.filePath = filePath;
|
|
135
|
-
this.fd = fd;
|
|
136
|
-
this.logger = logger || getLogger();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Create a secure temporary file with proper permissions and atomic operations
|
|
141
|
-
* @param prefix Prefix for the temporary filename
|
|
142
|
-
* @param extension File extension (e.g., '.txt', '.md')
|
|
143
|
-
* @param logger Optional logger instance
|
|
144
|
-
* @returns Promise resolving to SecureTempFile instance
|
|
145
|
-
*/
|
|
146
|
-
static async create(prefix: string = 'ai-service', extension: string = '.txt', logger?: Logger): Promise<SecureTempFile> {
|
|
147
|
-
const tmpDir = os.tmpdir();
|
|
148
|
-
const log = logger || getLogger();
|
|
149
|
-
|
|
150
|
-
// Ensure temp directory exists and is writable (skip check in test environments)
|
|
151
|
-
if (!process.env.VITEST) {
|
|
152
|
-
try {
|
|
153
|
-
await fs.access(tmpDir, fs.constants.W_OK);
|
|
154
|
-
} catch (error: any) {
|
|
155
|
-
// Try to create the directory if it doesn't exist
|
|
156
|
-
try {
|
|
157
|
-
await fs.mkdir(tmpDir, { recursive: true, mode: 0o700 });
|
|
158
|
-
} catch (mkdirError: any) {
|
|
159
|
-
throw new Error(`Temp directory not writable: ${tmpDir} - ${error.message}. Failed to create: ${mkdirError.message}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const tmpFilePath = path.join(tmpDir, `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`);
|
|
165
|
-
|
|
166
|
-
// Create file with exclusive access and restrictive permissions (owner read/write only)
|
|
167
|
-
// Using 'wx' flag ensures exclusive creation (fails if file exists)
|
|
168
|
-
let fd: fs.FileHandle;
|
|
169
|
-
try {
|
|
170
|
-
fd = await fs.open(tmpFilePath, 'wx', 0o600);
|
|
171
|
-
} catch (error: any) {
|
|
172
|
-
if (error.code === 'EEXIST') {
|
|
173
|
-
// Highly unlikely with timestamp + random suffix, but handle it
|
|
174
|
-
throw new Error(`Temporary file already exists: ${tmpFilePath}`);
|
|
175
|
-
}
|
|
176
|
-
throw new Error(`Failed to create temporary file: ${error.message}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return new SecureTempFile(tmpFilePath, fd, log);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Get the file path (use with caution in external commands)
|
|
184
|
-
*/
|
|
185
|
-
get path(): string {
|
|
186
|
-
if (this.isCleanedUp) {
|
|
187
|
-
throw new Error('Temp file has been cleaned up');
|
|
188
|
-
}
|
|
189
|
-
return this.filePath;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Write content to the temporary file
|
|
194
|
-
*/
|
|
195
|
-
async writeContent(content: string): Promise<void> {
|
|
196
|
-
if (!this.fd || this.isCleanedUp) {
|
|
197
|
-
throw new Error('Temp file is not available for writing');
|
|
198
|
-
}
|
|
199
|
-
await this.fd.writeFile(content, 'utf8');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Read content from the temporary file
|
|
204
|
-
*/
|
|
205
|
-
async readContent(): Promise<string> {
|
|
206
|
-
if (!this.fd || this.isCleanedUp) {
|
|
207
|
-
throw new Error('Temp file is not available for reading');
|
|
208
|
-
}
|
|
209
|
-
const content = await this.fd.readFile('utf8');
|
|
210
|
-
return content;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Close the file handle
|
|
215
|
-
*/
|
|
216
|
-
async close(): Promise<void> {
|
|
217
|
-
if (this.fd && !this.isCleanedUp) {
|
|
218
|
-
await this.fd.close();
|
|
219
|
-
this.fd = null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Securely cleanup the temporary file - prevents TOCTOU by using file descriptor
|
|
225
|
-
*/
|
|
226
|
-
async cleanup(): Promise<void> {
|
|
227
|
-
if (this.isCleanedUp) {
|
|
228
|
-
return; // Already cleaned up
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
// Close file descriptor first if still open
|
|
233
|
-
if (this.fd) {
|
|
234
|
-
await this.fd.close();
|
|
235
|
-
this.fd = null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Now safely remove the file
|
|
239
|
-
// Use fs.unlink which is safer than checking existence first
|
|
240
|
-
await fs.unlink(this.filePath);
|
|
241
|
-
} catch (error: any) {
|
|
242
|
-
// Only ignore ENOENT (file not found) errors
|
|
243
|
-
if (error.code !== 'ENOENT') {
|
|
244
|
-
this.logger.warn(`Failed to cleanup temp file ${this.filePath}: ${error.message}`);
|
|
245
|
-
// Don't throw here to avoid masking main operations
|
|
246
|
-
}
|
|
247
|
-
} finally {
|
|
248
|
-
this.isCleanedUp = true;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Create a secure temporary file for editing with proper permissions
|
|
255
|
-
* @param prefix Prefix for the temporary filename
|
|
256
|
-
* @param extension File extension (e.g., '.txt', '.md')
|
|
257
|
-
* @param logger Optional logger instance
|
|
258
|
-
* @returns Promise resolving to the temporary file path
|
|
259
|
-
* @deprecated Use SecureTempFile.create() for better security
|
|
260
|
-
*/
|
|
261
|
-
export async function createSecureTempFile(prefix: string = 'ai-service', extension: string = '.txt', logger?: Logger): Promise<string> {
|
|
262
|
-
const secureTempFile = await SecureTempFile.create(prefix, extension, logger);
|
|
263
|
-
await secureTempFile.close();
|
|
264
|
-
return secureTempFile.path;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Clean up a temporary file
|
|
269
|
-
* @param filePath Path to the temporary file to clean up
|
|
270
|
-
* @param logger Optional logger instance
|
|
271
|
-
* @deprecated Use SecureTempFile.cleanup() for better security
|
|
272
|
-
*/
|
|
273
|
-
export async function cleanupTempFile(filePath: string, logger?: Logger): Promise<void> {
|
|
274
|
-
const log = logger || getLogger();
|
|
275
|
-
try {
|
|
276
|
-
await fs.unlink(filePath);
|
|
277
|
-
} catch (error: any) {
|
|
278
|
-
// Only ignore ENOENT (file not found) errors
|
|
279
|
-
if (error.code !== 'ENOENT') {
|
|
280
|
-
log.warn(`Failed to cleanup temp file ${filePath}: ${error.message}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export interface EditorResult {
|
|
286
|
-
content: string;
|
|
287
|
-
wasEdited: boolean;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Open content in user's editor for editing
|
|
292
|
-
* @param content Initial content to edit
|
|
293
|
-
* @param templateLines Additional template lines to include (will be filtered out)
|
|
294
|
-
* @param fileExtension File extension for syntax highlighting
|
|
295
|
-
* @param editor Editor command to use (defaults to EDITOR/VISUAL env var or 'vi')
|
|
296
|
-
* @param logger Optional logger instance
|
|
297
|
-
* @returns Promise resolving to the edited content
|
|
298
|
-
*/
|
|
299
|
-
export async function editContentInEditor(
|
|
300
|
-
content: string,
|
|
301
|
-
templateLines: string[] = [],
|
|
302
|
-
fileExtension: string = '.txt',
|
|
303
|
-
editor?: string,
|
|
304
|
-
logger?: Logger
|
|
305
|
-
): Promise<EditorResult> {
|
|
306
|
-
const log = logger || getLogger();
|
|
307
|
-
const editorCmd = editor || process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
308
|
-
|
|
309
|
-
const secureTempFile = await SecureTempFile.create('ai-service_edit', fileExtension, log);
|
|
310
|
-
try {
|
|
311
|
-
// Build template content
|
|
312
|
-
const templateContent = [
|
|
313
|
-
...templateLines,
|
|
314
|
-
...(templateLines.length > 0 ? [''] : []), // Add separator if we have template lines
|
|
315
|
-
content,
|
|
316
|
-
'',
|
|
317
|
-
].join('\n');
|
|
318
|
-
|
|
319
|
-
await secureTempFile.writeContent(templateContent);
|
|
320
|
-
await secureTempFile.close(); // Close before external editor access
|
|
321
|
-
|
|
322
|
-
log.info(`📝 Opening ${editorCmd} to edit content...`);
|
|
323
|
-
|
|
324
|
-
// Open the editor synchronously
|
|
325
|
-
const result = spawnSync(editorCmd, [secureTempFile.path], { stdio: 'inherit' });
|
|
326
|
-
|
|
327
|
-
if (result.error) {
|
|
328
|
-
throw new Error(`Failed to launch editor '${editorCmd}': ${result.error.message}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Read the file back in, stripping comment lines
|
|
332
|
-
const fileContent = (await fs.readFile(secureTempFile.path, 'utf8'))
|
|
333
|
-
.split('\n')
|
|
334
|
-
.filter(line => !line.trim().startsWith('#'))
|
|
335
|
-
.join('\n')
|
|
336
|
-
.trim();
|
|
337
|
-
|
|
338
|
-
if (!fileContent) {
|
|
339
|
-
throw new Error('Content is empty after editing');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
log.info('✅ Content updated successfully');
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
content: fileContent,
|
|
346
|
-
wasEdited: fileContent !== content.trim()
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
} finally {
|
|
350
|
-
// Always clean up the temp file securely
|
|
351
|
-
await secureTempFile.cleanup();
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Standard choices for interactive feedback loops
|
|
357
|
-
*/
|
|
358
|
-
export const STANDARD_CHOICES = {
|
|
359
|
-
CONFIRM: { key: 'c', label: 'Confirm and proceed' },
|
|
360
|
-
EDIT: { key: 'e', label: 'Edit in editor' },
|
|
361
|
-
SKIP: { key: 's', label: 'Skip and abort' },
|
|
362
|
-
IMPROVE: { key: 'i', label: 'Improve with LLM feedback' }
|
|
363
|
-
} as const;
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Get text input from the user
|
|
367
|
-
* @param prompt The prompt message to display
|
|
368
|
-
* @param options Additional options for customizing behavior
|
|
369
|
-
* @returns Promise resolving to the user's text input
|
|
370
|
-
*/
|
|
371
|
-
export async function getUserTextInput(
|
|
372
|
-
prompt: string,
|
|
373
|
-
options: InteractiveOptions = {}
|
|
374
|
-
): Promise<string> {
|
|
375
|
-
const logger = options.logger || getLogger();
|
|
376
|
-
|
|
377
|
-
// Check if stdin is a TTY (terminal) or piped
|
|
378
|
-
if (!process.stdin.isTTY) {
|
|
379
|
-
logger.error('⚠️ STDIN is piped but interactive text input is required');
|
|
380
|
-
logger.error(' Interactive text input cannot be used when input is piped');
|
|
381
|
-
logger.error(' Solutions:');
|
|
382
|
-
logger.error(' • Use terminal input instead of piping');
|
|
383
|
-
|
|
384
|
-
// Add any additional suggestions
|
|
385
|
-
if (options.nonTtyErrorSuggestions) {
|
|
386
|
-
options.nonTtyErrorSuggestions.forEach(suggestion => {
|
|
387
|
-
logger.error(` • ${suggestion}`);
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
throw new Error('Interactive text input requires a terminal');
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
logger.info(prompt);
|
|
395
|
-
logger.info('(Press Enter when done, or type Ctrl+C to cancel)');
|
|
396
|
-
logger.info('');
|
|
397
|
-
|
|
398
|
-
return new Promise((resolve, reject) => {
|
|
399
|
-
let inputBuffer = '';
|
|
400
|
-
let isResolved = false;
|
|
401
|
-
let dataHandler: ((chunk: string) => void) | null = null;
|
|
402
|
-
let errorHandler: ((error: Error) => void) | null = null;
|
|
403
|
-
|
|
404
|
-
const cleanup = () => {
|
|
405
|
-
if (dataHandler) {
|
|
406
|
-
process.stdin.removeListener('data', dataHandler);
|
|
407
|
-
}
|
|
408
|
-
if (errorHandler) {
|
|
409
|
-
process.stdin.removeListener('error', errorHandler);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
process.stdin.pause();
|
|
414
|
-
// Detach stdin again now that we're done
|
|
415
|
-
if (typeof process.stdin.unref === 'function') {
|
|
416
|
-
process.stdin.unref();
|
|
417
|
-
}
|
|
418
|
-
} catch {
|
|
419
|
-
// Ignore cleanup errors
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
const safeResolve = (value: string) => {
|
|
424
|
-
if (!isResolved) {
|
|
425
|
-
isResolved = true;
|
|
426
|
-
cleanup();
|
|
427
|
-
resolve(value);
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const safeReject = (error: Error) => {
|
|
432
|
-
if (!isResolved) {
|
|
433
|
-
isResolved = true;
|
|
434
|
-
cleanup();
|
|
435
|
-
reject(error);
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
// Ensure stdin is referenced so the process doesn't exit while waiting for input
|
|
441
|
-
if (typeof process.stdin.ref === 'function') {
|
|
442
|
-
process.stdin.ref();
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
process.stdin.setEncoding('utf8');
|
|
446
|
-
process.stdin.resume();
|
|
447
|
-
|
|
448
|
-
dataHandler = (chunk: string) => {
|
|
449
|
-
try {
|
|
450
|
-
inputBuffer += chunk;
|
|
451
|
-
|
|
452
|
-
// Check if user pressed Enter (newline character)
|
|
453
|
-
if (inputBuffer.includes('\n')) {
|
|
454
|
-
const userInput = inputBuffer.replace(/\n$/, '').trim();
|
|
455
|
-
|
|
456
|
-
if (userInput === '') {
|
|
457
|
-
logger.warn('Empty input received. Please provide feedback text.');
|
|
458
|
-
safeReject(new Error('Empty input received'));
|
|
459
|
-
} else {
|
|
460
|
-
logger.info(`✅ Received feedback: "${userInput}"\n`);
|
|
461
|
-
safeResolve(userInput);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
} catch (error) {
|
|
465
|
-
safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
errorHandler = (error: Error) => {
|
|
470
|
-
safeReject(error);
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
process.stdin.on('data', dataHandler);
|
|
474
|
-
process.stdin.on('error', errorHandler);
|
|
475
|
-
|
|
476
|
-
} catch (error) {
|
|
477
|
-
safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Get LLM improvement feedback from the user using the editor
|
|
484
|
-
* @param contentType Type of content being improved (e.g., 'commit message', 'release notes')
|
|
485
|
-
* @param currentContent The current content to be improved
|
|
486
|
-
* @param editor Optional editor command
|
|
487
|
-
* @param logger Optional logger instance
|
|
488
|
-
* @returns Promise resolving to the user's feedback text
|
|
489
|
-
*/
|
|
490
|
-
export async function getLLMFeedbackInEditor(
|
|
491
|
-
contentType: string,
|
|
492
|
-
currentContent: string,
|
|
493
|
-
editor?: string,
|
|
494
|
-
logger?: Logger
|
|
495
|
-
): Promise<string> {
|
|
496
|
-
const templateLines = [
|
|
497
|
-
'# Provide Your Instructions and Guidance for a Revision Here',
|
|
498
|
-
'#',
|
|
499
|
-
'# Type your guidance above this line. Be specific about what you want changed,',
|
|
500
|
-
'# added, or improved. You can also edit the original content below directly',
|
|
501
|
-
'# to provide examples or show desired changes.',
|
|
502
|
-
'#',
|
|
503
|
-
'# Lines starting with "#" will be ignored.',
|
|
504
|
-
'',
|
|
505
|
-
'### YOUR FEEDBACK AND GUIDANCE:',
|
|
506
|
-
'',
|
|
507
|
-
'# (Type your improvement instructions here)',
|
|
508
|
-
'',
|
|
509
|
-
`### ORIGINAL ${contentType.toUpperCase()}:`,
|
|
510
|
-
''
|
|
511
|
-
];
|
|
512
|
-
|
|
513
|
-
const result = await editContentInEditor(
|
|
514
|
-
currentContent,
|
|
515
|
-
templateLines,
|
|
516
|
-
'.md',
|
|
517
|
-
editor,
|
|
518
|
-
logger
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
// Extract just the feedback section (everything before the original content)
|
|
522
|
-
const lines = result.content.split('\n');
|
|
523
|
-
const originalSectionIndex = lines.findIndex(line =>
|
|
524
|
-
line.trim().toLowerCase().startsWith('### original')
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
let feedback: string;
|
|
528
|
-
if (originalSectionIndex >= 0) {
|
|
529
|
-
// Take everything before the "### ORIGINAL" section
|
|
530
|
-
feedback = lines.slice(0, originalSectionIndex).join('\n').trim();
|
|
531
|
-
} else {
|
|
532
|
-
// If no original section found, take everything
|
|
533
|
-
feedback = result.content.trim();
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Remove the feedback header if it exists
|
|
537
|
-
feedback = feedback.replace(/^### YOUR FEEDBACK AND GUIDANCE:\s*/i, '').trim();
|
|
538
|
-
|
|
539
|
-
if (!feedback) {
|
|
540
|
-
throw new Error('No feedback provided. Please provide improvement instructions.');
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return feedback;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Check if interactive mode is available (TTY check)
|
|
548
|
-
* @param errorMessage Custom error message to throw if TTY not available
|
|
549
|
-
* @param logger Optional logger instance
|
|
550
|
-
* @throws Error if not in TTY environment
|
|
551
|
-
*/
|
|
552
|
-
export function requireTTY(errorMessage: string = 'Interactive mode requires a terminal. Use --dry-run instead.', logger?: Logger): void {
|
|
553
|
-
const log = logger || getLogger();
|
|
554
|
-
if (!process.stdin.isTTY) {
|
|
555
|
-
log.error('❌ Interactive mode requires a terminal (TTY)');
|
|
556
|
-
log.error(' Solutions:');
|
|
557
|
-
log.error(' • Run without piping input');
|
|
558
|
-
log.error(' • Use --dry-run to see the generated content');
|
|
559
|
-
throw new Error(errorMessage);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
package/src/logger.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional logger support
|
|
3
|
-
* If winston is available as a peer dependency, use it
|
|
4
|
-
* Otherwise, provide a no-op logger
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Logger } from './types';
|
|
8
|
-
|
|
9
|
-
let logger: Logger | undefined;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Set a custom logger instance
|
|
13
|
-
*/
|
|
14
|
-
export function setLogger(customLogger: Logger): void {
|
|
15
|
-
logger = customLogger;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create a no-op logger that does nothing
|
|
20
|
-
*/
|
|
21
|
-
export function createNoOpLogger(): Logger {
|
|
22
|
-
return {
|
|
23
|
-
info: () => {},
|
|
24
|
-
error: () => {},
|
|
25
|
-
warn: () => {},
|
|
26
|
-
debug: () => {},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Attempt to load winston logger
|
|
32
|
-
* @returns winston logger if available, otherwise null
|
|
33
|
-
*/
|
|
34
|
-
export function tryLoadWinston(): Logger | null {
|
|
35
|
-
try {
|
|
36
|
-
// Dynamic import to avoid hard dependency
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
-
const winston = require('winston');
|
|
39
|
-
if (winston && winston.createLogger) {
|
|
40
|
-
return winston.createLogger({
|
|
41
|
-
level: 'info',
|
|
42
|
-
format: winston.format.simple(),
|
|
43
|
-
transports: [new winston.transports.Console()],
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Winston not available
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the current logger or a no-op logger
|
|
54
|
-
*/
|
|
55
|
-
export function getLogger(): Logger {
|
|
56
|
-
if (logger) {
|
|
57
|
-
return logger;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Try to load winston if available
|
|
61
|
-
const winstonLogger = tryLoadWinston();
|
|
62
|
-
if (winstonLogger) {
|
|
63
|
-
logger = winstonLogger;
|
|
64
|
-
return winstonLogger;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Return no-op logger
|
|
68
|
-
return createNoOpLogger();
|
|
69
|
-
}
|