@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.
Files changed (71) hide show
  1. package/README.md +1 -0
  2. package/dist/application.js +25 -3
  3. package/dist/application.js.map +1 -1
  4. package/dist/arguments.js +103 -18
  5. package/dist/arguments.js.map +1 -1
  6. package/dist/commands/audio-commit.js +28 -7
  7. package/dist/commands/audio-commit.js.map +1 -1
  8. package/dist/commands/audio-review.js +28 -7
  9. package/dist/commands/audio-review.js.map +1 -1
  10. package/dist/commands/commit.js +75 -18
  11. package/dist/commands/commit.js.map +1 -1
  12. package/dist/commands/development.js +264 -0
  13. package/dist/commands/development.js.map +1 -0
  14. package/dist/commands/link.js +356 -181
  15. package/dist/commands/link.js.map +1 -1
  16. package/dist/commands/publish.js +166 -32
  17. package/dist/commands/publish.js.map +1 -1
  18. package/dist/commands/release.js +78 -13
  19. package/dist/commands/release.js.map +1 -1
  20. package/dist/commands/review.js +10 -6
  21. package/dist/commands/review.js.map +1 -1
  22. package/dist/commands/tree.js +450 -24
  23. package/dist/commands/tree.js.map +1 -1
  24. package/dist/commands/unlink.js +267 -372
  25. package/dist/commands/unlink.js.map +1 -1
  26. package/dist/commands/versions.js +224 -0
  27. package/dist/commands/versions.js.map +1 -0
  28. package/dist/constants.js +29 -10
  29. package/dist/constants.js.map +1 -1
  30. package/dist/content/diff.js.map +1 -1
  31. package/dist/content/files.js +192 -0
  32. package/dist/content/files.js.map +1 -0
  33. package/dist/content/log.js +16 -0
  34. package/dist/content/log.js.map +1 -1
  35. package/dist/main.js +0 -0
  36. package/dist/prompt/commit.js +9 -2
  37. package/dist/prompt/commit.js.map +1 -1
  38. package/dist/prompt/instructions/commit.md +20 -2
  39. package/dist/prompt/instructions/release.md +27 -10
  40. package/dist/prompt/instructions/review.md +75 -8
  41. package/dist/prompt/release.js +13 -5
  42. package/dist/prompt/release.js.map +1 -1
  43. package/dist/types.js +21 -5
  44. package/dist/types.js.map +1 -1
  45. package/dist/util/child.js +112 -26
  46. package/dist/util/child.js.map +1 -1
  47. package/dist/util/countdown.js +215 -0
  48. package/dist/util/countdown.js.map +1 -0
  49. package/dist/util/general.js +31 -7
  50. package/dist/util/general.js.map +1 -1
  51. package/dist/util/git.js +587 -0
  52. package/dist/util/git.js.map +1 -0
  53. package/dist/util/github.js +519 -3
  54. package/dist/util/github.js.map +1 -1
  55. package/dist/util/interactive.js +245 -79
  56. package/dist/util/interactive.js.map +1 -1
  57. package/dist/util/openai.js +70 -22
  58. package/dist/util/openai.js.map +1 -1
  59. package/dist/util/performance.js +1 -69
  60. package/dist/util/performance.js.map +1 -1
  61. package/dist/util/storage.js +28 -1
  62. package/dist/util/storage.js.map +1 -1
  63. package/dist/util/validation.js +1 -25
  64. package/dist/util/validation.js.map +1 -1
  65. package/package.json +10 -8
  66. package/test-multiline/cli/package.json +8 -0
  67. package/test-multiline/core/package.json +5 -0
  68. package/test-multiline/mobile/package.json +8 -0
  69. package/test-multiline/web/package.json +8 -0
  70. package/dist/util/npmOptimizations.js +0 -174
  71. package/dist/util/npmOptimizations.js.map +0 -1
@@ -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
- // Ensure stdin is referenced so the process doesn't exit while waiting for input
37
- if (typeof process.stdin.ref === 'function') {
38
- process.stdin.ref();
39
- }
40
- process.stdin.setRawMode(true);
41
- process.stdin.resume();
42
- process.stdin.on('data', (key)=>{
43
- const keyStr = key.toString().toLowerCase();
44
- const choice = choices.find((c)=>c.key === keyStr);
45
- if (choice) {
46
- process.stdin.setRawMode(false);
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
- logger.info(`Selected: ${choice.label}\n`);
53
- resolve(choice.key);
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
- * Create a secure temporary file for editing with proper permissions
60
- * @param prefix Prefix for the temporary filename
61
- * @param extension File extension (e.g., '.txt', '.md')
62
- * @returns Promise resolving to the temporary file path
63
- */ async function createSecureTempFile(prefix = 'kodrdriv', extension = '.txt') {
64
- const tmpDir = os.tmpdir();
65
- const tmpFilePath = path.join(tmpDir, `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`);
66
- // Create file with restrictive permissions (owner read/write only)
67
- const fd = await fs.open(tmpFilePath, 'w', 0o600);
68
- await fd.close();
69
- return tmpFilePath;
70
- }
71
- /**
72
- * Clean up a temporary file
73
- * @param filePath Path to the temporary file to clean up
74
- */ async function cleanupTempFile(filePath) {
75
- try {
76
- await fs.unlink(filePath);
77
- } catch (error) {
78
- // Only ignore ENOENT (file not found) errors
79
- if (error.code !== 'ENOENT') {
80
- const logger = getDryRunLogger(false);
81
- logger.warn(`Failed to cleanup temp file ${filePath}: ${error.message}`);
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
- let tmpFilePath = null;
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 fs.writeFile(tmpFilePath, templateContent, 'utf8');
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
- tmpFilePath
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(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
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
- if (tmpFilePath) {
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
- // Ensure stdin is referenced so the process doesn't exit while waiting for input
182
- if (typeof process.stdin.ref === 'function') {
183
- process.stdin.ref();
184
- }
185
- process.stdin.setEncoding('utf8');
186
- process.stdin.resume();
187
- const onData = (chunk)=>{
188
- inputBuffer += chunk;
189
- // Check if user pressed Enter (newline character)
190
- if (inputBuffer.includes('\n')) {
191
- cleanup();
192
- const userInput = inputBuffer.replace(/\n$/, '').trim();
193
- if (userInput === '') {
194
- logger.warn('Empty input received. Please provide feedback text.');
195
- reject(new Error('Empty input received'));
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 onError = (error)=>{
203
- cleanup();
204
- reject(error);
336
+ const safeResolve = (value)=>{
337
+ if (!isResolved) {
338
+ isResolved = true;
339
+ cleanup();
340
+ resolve(value);
341
+ }
205
342
  };
206
- const cleanup = ()=>{
207
- process.stdin.pause();
208
- process.stdin.removeListener('data', onData);
209
- process.stdin.removeListener('error', onError);
210
- // Detach stdin again now that we're done
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
- process.stdin.on('data', onData);
216
- process.stdin.on('error', onError);
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, cleanupTempFile, createSecureTempFile, editContentInEditor, getLLMFeedbackInEditor, getUserChoice, getUserTextInput, improveContentWithLLM, requireTTY };
462
+ export { STANDARD_CHOICES, SecureTempFile, editContentInEditor, getLLMFeedbackInEditor, getUserChoice, getUserTextInput, improveContentWithLLM, requireTTY };
297
463
  //# sourceMappingURL=interactive.js.map