@eldrforge/kodrdriv 0.1.0 → 1.2.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/README.md +1 -0
- package/dist/application.js +25 -3
- package/dist/application.js.map +1 -1
- package/dist/arguments.js +103 -18
- package/dist/arguments.js.map +1 -1
- package/dist/commands/audio-commit.js +28 -7
- package/dist/commands/audio-commit.js.map +1 -1
- package/dist/commands/audio-review.js +28 -7
- package/dist/commands/audio-review.js.map +1 -1
- package/dist/commands/commit.js +75 -18
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/development.js +264 -0
- package/dist/commands/development.js.map +1 -0
- package/dist/commands/link.js +356 -181
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/publish.js +166 -32
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/release.js +78 -13
- package/dist/commands/release.js.map +1 -1
- package/dist/commands/review.js +10 -6
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/tree.js +450 -24
- package/dist/commands/tree.js.map +1 -1
- package/dist/commands/unlink.js +267 -372
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/versions.js +224 -0
- package/dist/commands/versions.js.map +1 -0
- package/dist/constants.js +29 -10
- package/dist/constants.js.map +1 -1
- package/dist/content/diff.js.map +1 -1
- package/dist/content/files.js +192 -0
- package/dist/content/files.js.map +1 -0
- package/dist/content/log.js +16 -0
- package/dist/content/log.js.map +1 -1
- package/dist/main.js +0 -0
- package/dist/prompt/commit.js +9 -2
- package/dist/prompt/commit.js.map +1 -1
- package/dist/prompt/instructions/commit.md +20 -2
- package/dist/prompt/instructions/release.md +27 -10
- package/dist/prompt/instructions/review.md +75 -8
- package/dist/prompt/release.js +13 -5
- package/dist/prompt/release.js.map +1 -1
- package/dist/types.js +21 -5
- package/dist/types.js.map +1 -1
- package/dist/util/child.js +112 -26
- package/dist/util/child.js.map +1 -1
- package/dist/util/countdown.js +215 -0
- package/dist/util/countdown.js.map +1 -0
- package/dist/util/general.js +31 -7
- package/dist/util/general.js.map +1 -1
- package/dist/util/git.js +587 -0
- package/dist/util/git.js.map +1 -0
- package/dist/util/github.js +519 -3
- package/dist/util/github.js.map +1 -1
- package/dist/util/interactive.js +245 -79
- package/dist/util/interactive.js.map +1 -1
- package/dist/util/openai.js +70 -22
- package/dist/util/openai.js.map +1 -1
- package/dist/util/performance.js +1 -69
- package/dist/util/performance.js.map +1 -1
- package/dist/util/storage.js +28 -1
- package/dist/util/storage.js.map +1 -1
- package/dist/util/validation.js +1 -25
- package/dist/util/validation.js.map +1 -1
- package/package.json +10 -8
- package/test-multiline/cli/package.json +8 -0
- package/test-multiline/core/package.json +5 -0
- package/test-multiline/mobile/package.json +8 -0
- package/test-multiline/web/package.json +8 -0
- package/dist/util/npmOptimizations.js +0 -174
- package/dist/util/npmOptimizations.js.map +0 -1
package/dist/util/interactive.js
CHANGED
|
@@ -5,6 +5,19 @@ import * as path from 'path';
|
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
|
|
8
|
+
function _define_property(obj, key, value) {
|
|
9
|
+
if (key in obj) {
|
|
10
|
+
Object.defineProperty(obj, key, {
|
|
11
|
+
value: value,
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true
|
|
15
|
+
});
|
|
16
|
+
} else {
|
|
17
|
+
obj[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* Get user choice interactively from terminal input
|
|
10
23
|
* @param prompt The prompt message to display
|
|
@@ -32,54 +45,179 @@ import * as fs from 'fs/promises';
|
|
|
32
45
|
}
|
|
33
46
|
return 's'; // Default to skip
|
|
34
47
|
}
|
|
35
|
-
return new Promise((resolve)=>{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
return new Promise((resolve, reject)=>{
|
|
49
|
+
let isResolved = false;
|
|
50
|
+
let dataHandler = null;
|
|
51
|
+
let errorHandler = null;
|
|
52
|
+
const cleanup = ()=>{
|
|
53
|
+
if (dataHandler) {
|
|
54
|
+
process.stdin.removeListener('data', dataHandler);
|
|
55
|
+
}
|
|
56
|
+
if (errorHandler) {
|
|
57
|
+
process.stdin.removeListener('error', errorHandler);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
if (process.stdin.setRawMode) {
|
|
61
|
+
process.stdin.setRawMode(false);
|
|
62
|
+
}
|
|
47
63
|
process.stdin.pause();
|
|
48
64
|
// Detach stdin again now that we're done
|
|
49
65
|
if (typeof process.stdin.unref === 'function') {
|
|
50
66
|
process.stdin.unref();
|
|
51
67
|
}
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore cleanup errors
|
|
54
70
|
}
|
|
55
|
-
}
|
|
71
|
+
};
|
|
72
|
+
const safeResolve = (value)=>{
|
|
73
|
+
if (!isResolved) {
|
|
74
|
+
isResolved = true;
|
|
75
|
+
cleanup();
|
|
76
|
+
resolve(value);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const safeReject = (error)=>{
|
|
80
|
+
if (!isResolved) {
|
|
81
|
+
isResolved = true;
|
|
82
|
+
cleanup();
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
// Ensure stdin is referenced so the process doesn't exit while waiting for input
|
|
88
|
+
if (typeof process.stdin.ref === 'function') {
|
|
89
|
+
process.stdin.ref();
|
|
90
|
+
}
|
|
91
|
+
process.stdin.setRawMode(true);
|
|
92
|
+
process.stdin.resume();
|
|
93
|
+
dataHandler = (key)=>{
|
|
94
|
+
try {
|
|
95
|
+
const keyStr = key.toString().toLowerCase();
|
|
96
|
+
const choice = choices.find((c)=>c.key === keyStr);
|
|
97
|
+
if (choice) {
|
|
98
|
+
logger.info(`Selected: ${choice.label}\n`);
|
|
99
|
+
safeResolve(choice.key);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
errorHandler = (error)=>{
|
|
106
|
+
safeReject(error);
|
|
107
|
+
};
|
|
108
|
+
process.stdin.on('data', dataHandler);
|
|
109
|
+
process.stdin.on('error', errorHandler);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
|
|
112
|
+
}
|
|
56
113
|
});
|
|
57
114
|
}
|
|
58
115
|
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
* Secure temporary file handle that prevents TOCTOU vulnerabilities
|
|
117
|
+
*/ class SecureTempFile {
|
|
118
|
+
/**
|
|
119
|
+
* Create a secure temporary file with proper permissions and atomic operations
|
|
120
|
+
* @param prefix Prefix for the temporary filename
|
|
121
|
+
* @param extension File extension (e.g., '.txt', '.md')
|
|
122
|
+
* @returns Promise resolving to SecureTempFile instance
|
|
123
|
+
*/ static async create(prefix = 'kodrdriv', extension = '.txt') {
|
|
124
|
+
const tmpDir = os.tmpdir();
|
|
125
|
+
// Ensure temp directory exists and is writable (skip check in test environments)
|
|
126
|
+
if (!process.env.VITEST) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.access(tmpDir, fs.constants.W_OK);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Try to create the directory if it doesn't exist
|
|
131
|
+
try {
|
|
132
|
+
await fs.mkdir(tmpDir, {
|
|
133
|
+
recursive: true,
|
|
134
|
+
mode: 0o700
|
|
135
|
+
});
|
|
136
|
+
} catch (mkdirError) {
|
|
137
|
+
throw new Error(`Temp directory not writable: ${tmpDir} - ${error.message}. Failed to create: ${mkdirError.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const tmpFilePath = path.join(tmpDir, `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`);
|
|
142
|
+
// Create file with exclusive access and restrictive permissions (owner read/write only)
|
|
143
|
+
// Using 'wx' flag ensures exclusive creation (fails if file exists)
|
|
144
|
+
let fd;
|
|
145
|
+
try {
|
|
146
|
+
fd = await fs.open(tmpFilePath, 'wx', 0o600);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error.code === 'EEXIST') {
|
|
149
|
+
// Highly unlikely with timestamp + random suffix, but handle it
|
|
150
|
+
throw new Error(`Temporary file already exists: ${tmpFilePath}`);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`Failed to create temporary file: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
return new SecureTempFile(tmpFilePath, fd);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get the file path (use with caution in external commands)
|
|
158
|
+
*/ get path() {
|
|
159
|
+
if (this.isCleanedUp) {
|
|
160
|
+
throw new Error('Temp file has been cleaned up');
|
|
82
161
|
}
|
|
162
|
+
return this.filePath;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Write content to the temporary file
|
|
166
|
+
*/ async writeContent(content) {
|
|
167
|
+
if (!this.fd || this.isCleanedUp) {
|
|
168
|
+
throw new Error('Temp file is not available for writing');
|
|
169
|
+
}
|
|
170
|
+
await this.fd.writeFile(content, 'utf8');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Read content from the temporary file
|
|
174
|
+
*/ async readContent() {
|
|
175
|
+
if (!this.fd || this.isCleanedUp) {
|
|
176
|
+
throw new Error('Temp file is not available for reading');
|
|
177
|
+
}
|
|
178
|
+
const content = await this.fd.readFile('utf8');
|
|
179
|
+
return content;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Close the file handle
|
|
183
|
+
*/ async close() {
|
|
184
|
+
if (this.fd && !this.isCleanedUp) {
|
|
185
|
+
await this.fd.close();
|
|
186
|
+
this.fd = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Securely cleanup the temporary file - prevents TOCTOU by using file descriptor
|
|
191
|
+
*/ async cleanup() {
|
|
192
|
+
if (this.isCleanedUp) {
|
|
193
|
+
return; // Already cleaned up
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
// Close file descriptor first if still open
|
|
197
|
+
if (this.fd) {
|
|
198
|
+
await this.fd.close();
|
|
199
|
+
this.fd = null;
|
|
200
|
+
}
|
|
201
|
+
// Now safely remove the file
|
|
202
|
+
// Use fs.unlink which is safer than checking existence first
|
|
203
|
+
await fs.unlink(this.filePath);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Only ignore ENOENT (file not found) errors
|
|
206
|
+
if (error.code !== 'ENOENT') {
|
|
207
|
+
const logger = getDryRunLogger(false);
|
|
208
|
+
logger.warn(`Failed to cleanup temp file ${this.filePath}: ${error.message}`);
|
|
209
|
+
// Don't throw here to avoid masking main operations
|
|
210
|
+
}
|
|
211
|
+
} finally{
|
|
212
|
+
this.isCleanedUp = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
constructor(filePath, fd){
|
|
216
|
+
_define_property(this, "fd", null);
|
|
217
|
+
_define_property(this, "filePath", void 0);
|
|
218
|
+
_define_property(this, "isCleanedUp", false);
|
|
219
|
+
this.filePath = filePath;
|
|
220
|
+
this.fd = fd;
|
|
83
221
|
}
|
|
84
222
|
}
|
|
85
223
|
/**
|
|
@@ -91,10 +229,8 @@ import * as fs from 'fs/promises';
|
|
|
91
229
|
*/ async function editContentInEditor(content, templateLines = [], fileExtension = '.txt') {
|
|
92
230
|
const logger = getDryRunLogger(false);
|
|
93
231
|
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
94
|
-
|
|
232
|
+
const secureTempFile = await SecureTempFile.create('kodrdriv_edit', fileExtension);
|
|
95
233
|
try {
|
|
96
|
-
// Create secure temporary file
|
|
97
|
-
tmpFilePath = await createSecureTempFile('kodrdriv_edit', fileExtension);
|
|
98
234
|
// Build template content
|
|
99
235
|
const templateContent = [
|
|
100
236
|
...templateLines,
|
|
@@ -104,11 +240,12 @@ import * as fs from 'fs/promises';
|
|
|
104
240
|
content,
|
|
105
241
|
''
|
|
106
242
|
].join('\n');
|
|
107
|
-
await
|
|
243
|
+
await secureTempFile.writeContent(templateContent);
|
|
244
|
+
await secureTempFile.close(); // Close before external editor access
|
|
108
245
|
logger.info(`📝 Opening ${editor} to edit content...`);
|
|
109
246
|
// Open the editor synchronously
|
|
110
247
|
const result = spawnSync(editor, [
|
|
111
|
-
|
|
248
|
+
secureTempFile.path
|
|
112
249
|
], {
|
|
113
250
|
stdio: 'inherit'
|
|
114
251
|
});
|
|
@@ -116,7 +253,7 @@ import * as fs from 'fs/promises';
|
|
|
116
253
|
throw new Error(`Failed to launch editor '${editor}': ${result.error.message}`);
|
|
117
254
|
}
|
|
118
255
|
// Read the file back in, stripping comment lines
|
|
119
|
-
const fileContent = (await fs.readFile(
|
|
256
|
+
const fileContent = (await fs.readFile(secureTempFile.path, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
|
|
120
257
|
if (!fileContent) {
|
|
121
258
|
throw new Error('Content is empty after editing');
|
|
122
259
|
}
|
|
@@ -126,10 +263,8 @@ import * as fs from 'fs/promises';
|
|
|
126
263
|
wasEdited: fileContent !== content.trim()
|
|
127
264
|
};
|
|
128
265
|
} finally{
|
|
129
|
-
// Always clean up the temp file
|
|
130
|
-
|
|
131
|
-
await cleanupTempFile(tmpFilePath);
|
|
132
|
-
}
|
|
266
|
+
// Always clean up the temp file securely
|
|
267
|
+
await secureTempFile.cleanup();
|
|
133
268
|
}
|
|
134
269
|
}
|
|
135
270
|
/**
|
|
@@ -178,42 +313,73 @@ import * as fs from 'fs/promises';
|
|
|
178
313
|
logger.info('');
|
|
179
314
|
return new Promise((resolve, reject)=>{
|
|
180
315
|
let inputBuffer = '';
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
} else {
|
|
197
|
-
logger.info(`✅ Received feedback: "${userInput}"\n`);
|
|
198
|
-
resolve(userInput);
|
|
316
|
+
let isResolved = false;
|
|
317
|
+
let dataHandler = null;
|
|
318
|
+
let errorHandler = null;
|
|
319
|
+
const cleanup = ()=>{
|
|
320
|
+
if (dataHandler) {
|
|
321
|
+
process.stdin.removeListener('data', dataHandler);
|
|
322
|
+
}
|
|
323
|
+
if (errorHandler) {
|
|
324
|
+
process.stdin.removeListener('error', errorHandler);
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
process.stdin.pause();
|
|
328
|
+
// Detach stdin again now that we're done
|
|
329
|
+
if (typeof process.stdin.unref === 'function') {
|
|
330
|
+
process.stdin.unref();
|
|
199
331
|
}
|
|
332
|
+
} catch {
|
|
333
|
+
// Ignore cleanup errors
|
|
200
334
|
}
|
|
201
335
|
};
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
336
|
+
const safeResolve = (value)=>{
|
|
337
|
+
if (!isResolved) {
|
|
338
|
+
isResolved = true;
|
|
339
|
+
cleanup();
|
|
340
|
+
resolve(value);
|
|
341
|
+
}
|
|
205
342
|
};
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (typeof process.stdin.unref === 'function') {
|
|
212
|
-
process.stdin.unref();
|
|
343
|
+
const safeReject = (error)=>{
|
|
344
|
+
if (!isResolved) {
|
|
345
|
+
isResolved = true;
|
|
346
|
+
cleanup();
|
|
347
|
+
reject(error);
|
|
213
348
|
}
|
|
214
349
|
};
|
|
215
|
-
|
|
216
|
-
|
|
350
|
+
try {
|
|
351
|
+
// Ensure stdin is referenced so the process doesn't exit while waiting for input
|
|
352
|
+
if (typeof process.stdin.ref === 'function') {
|
|
353
|
+
process.stdin.ref();
|
|
354
|
+
}
|
|
355
|
+
process.stdin.setEncoding('utf8');
|
|
356
|
+
process.stdin.resume();
|
|
357
|
+
dataHandler = (chunk)=>{
|
|
358
|
+
try {
|
|
359
|
+
inputBuffer += chunk;
|
|
360
|
+
// Check if user pressed Enter (newline character)
|
|
361
|
+
if (inputBuffer.includes('\n')) {
|
|
362
|
+
const userInput = inputBuffer.replace(/\n$/, '').trim();
|
|
363
|
+
if (userInput === '') {
|
|
364
|
+
logger.warn('Empty input received. Please provide feedback text.');
|
|
365
|
+
safeReject(new Error('Empty input received'));
|
|
366
|
+
} else {
|
|
367
|
+
logger.info(`✅ Received feedback: "${userInput}"\n`);
|
|
368
|
+
safeResolve(userInput);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
errorHandler = (error)=>{
|
|
376
|
+
safeReject(error);
|
|
377
|
+
};
|
|
378
|
+
process.stdin.on('data', dataHandler);
|
|
379
|
+
process.stdin.on('error', errorHandler);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
|
|
382
|
+
}
|
|
217
383
|
});
|
|
218
384
|
}
|
|
219
385
|
/**
|
|
@@ -293,5 +459,5 @@ import * as fs from 'fs/promises';
|
|
|
293
459
|
return finalResult;
|
|
294
460
|
}
|
|
295
461
|
|
|
296
|
-
export { STANDARD_CHOICES,
|
|
462
|
+
export { STANDARD_CHOICES, SecureTempFile, editContentInEditor, getLLMFeedbackInEditor, getUserChoice, getUserTextInput, improveContentWithLLM, requireTTY };
|
|
297
463
|
//# sourceMappingURL=interactive.js.map
|