@link-assistant/hive-mind 0.39.0

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 (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * lenv-reader.lib.mjs - LINO-based environment configuration reader
5
+ *
6
+ * Reads .lenv files (Links Notation environment files) and provides them as environment variables.
7
+ * This is a simple replacement for traditional .env files, using LINO (Links Notation) format.
8
+ *
9
+ * Format comparison:
10
+ *
11
+ * Traditional .env:
12
+ * VAR1=1
13
+ * VAR2=2
14
+ * LINO_LIST="(
15
+ * 1
16
+ * 2
17
+ * 3
18
+ * )"
19
+ *
20
+ * New .lenv (LINO):
21
+ * VAR1: 1
22
+ * VAR2: 2
23
+ * LINO_LIST: (
24
+ * 1
25
+ * 2
26
+ * 3
27
+ * )
28
+ *
29
+ * Priority: .lenv takes precedence over .env if both exist
30
+ */
31
+
32
+ if (typeof use === 'undefined') {
33
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
34
+ }
35
+
36
+ const linoModule = await use('links-notation');
37
+ const LinoParser = linoModule.Parser || linoModule.default?.Parser;
38
+
39
+ const fs = await import('fs');
40
+
41
+ /**
42
+ * LenvReader - Reads and parses .lenv files using LINO notation
43
+ */
44
+ export class LenvReader {
45
+ constructor() {
46
+ this.parser = new LinoParser();
47
+ }
48
+
49
+ /**
50
+ * Parse LINO configuration string into environment variables object
51
+ * @param {string} content - LINO configuration content
52
+ * @returns {Object} - Object with environment variable key-value pairs
53
+ */
54
+ parse(content) {
55
+ if (!content || typeof content !== 'string') {
56
+ return {};
57
+ }
58
+
59
+ const result = {};
60
+
61
+ try {
62
+ // Parse the entire content as LINO
63
+ const parsed = this.parser.parse(content);
64
+
65
+ if (!parsed || parsed.length === 0) {
66
+ return {};
67
+ }
68
+
69
+ // Process each top-level link as an environment variable
70
+ for (const link of parsed) {
71
+ // The ID of the link is the variable name
72
+ const varName = link.id;
73
+
74
+ if (!varName) {
75
+ continue;
76
+ }
77
+
78
+ // The values are the variable value
79
+ if (link.values && link.values.length > 0) {
80
+ // If there are multiple values, format them as LINO notation
81
+ const values = link.values.map(v => v.id || v);
82
+
83
+ // If it's a single value, just use it as-is
84
+ if (values.length === 1) {
85
+ result[varName] = String(values[0]);
86
+ } else {
87
+ // Multiple values - format as LINO notation
88
+ const formattedValues = values.map(v => ` ${v}`).join('\n');
89
+ result[varName] = `(\n${formattedValues}\n)`;
90
+ }
91
+ } else if (link.id) {
92
+ // No values means it might be a simple variable with no value
93
+ // Try to extract value from the original source
94
+ // For now, we'll just set it to empty string
95
+ result[varName] = '';
96
+ }
97
+ }
98
+
99
+ return result;
100
+ } catch (error) {
101
+ console.error(`Error parsing LINO configuration: ${error.message}`);
102
+ return {};
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Read and parse .lenv file
108
+ * @param {string} filePath - Path to .lenv file
109
+ * @returns {Object} - Object with environment variable key-value pairs
110
+ */
111
+ async readFile(filePath) {
112
+ try {
113
+ // Check if file exists using access
114
+ await fs.promises.access(filePath);
115
+
116
+ const content = await fs.promises.readFile(filePath, 'utf8');
117
+ return this.parse(content);
118
+ } catch (error) {
119
+ if (error.code === 'ENOENT') {
120
+ return null;
121
+ }
122
+ console.error(`Error reading .lenv file ${filePath}: ${error.message}`);
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Load configuration from file or string and inject into process.env
129
+ * @param {Object} options - Configuration options
130
+ * @param {string} options.path - Path to .lenv file (optional, defaults to '.lenv')
131
+ * @param {string} options.configuration - LINO configuration string (optional)
132
+ * @param {boolean} options.override - Whether to override existing env vars (default: false)
133
+ * @param {boolean} options.quiet - Whether to suppress log messages (default: false)
134
+ * @returns {Object} - Object with loaded variables
135
+ */
136
+ async config(options = {}) {
137
+ const {
138
+ path: configPath = '.lenv',
139
+ configuration = null,
140
+ override = false,
141
+ quiet = false
142
+ } = options;
143
+
144
+ let envVars = {};
145
+
146
+ // Priority 1: Configuration string from --configuration option
147
+ if (configuration) {
148
+ envVars = this.parse(configuration);
149
+ if (!quiet && Object.keys(envVars).length > 0) {
150
+ console.log(`Loaded ${Object.keys(envVars).length} variables from --configuration option`);
151
+ }
152
+ }
153
+ // Priority 2: .lenv file
154
+ else if (configPath) {
155
+ const fileVars = await this.readFile(configPath);
156
+ if (fileVars) {
157
+ envVars = fileVars;
158
+ if (!quiet && Object.keys(envVars).length > 0) {
159
+ console.log(`Loaded ${Object.keys(envVars).length} variables from ${configPath}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ // Inject into process.env
165
+ for (const [key, value] of Object.entries(envVars)) {
166
+ if (override || !process.env[key]) {
167
+ process.env[key] = value;
168
+ }
169
+ }
170
+
171
+ return envVars;
172
+ }
173
+
174
+ /**
175
+ * Check if .lenv file exists and has priority over .env
176
+ * @param {string} lenvPath - Path to .lenv file
177
+ * @returns {boolean} - True if .lenv should be used
178
+ */
179
+ async shouldUseLenv(lenvPath = '.lenv') {
180
+ // If .lenv exists, use it (has priority)
181
+ try {
182
+ await fs.promises.access(lenvPath);
183
+ return true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ }
189
+
190
+ export const lenvReader = new LenvReader();
191
+
192
+ /**
193
+ * Load .lenv configuration if it exists
194
+ * This function can be called early in the application to load .lenv configuration
195
+ *
196
+ * Priority:
197
+ * 1. --configuration option (if provided)
198
+ * 2. .lenv file (if exists)
199
+ * 3. .env file (fallback, handled by dotenvx)
200
+ *
201
+ * @param {Object} options - Configuration options
202
+ * @returns {Object} - Loaded environment variables
203
+ */
204
+ export async function loadLenvConfig(options = {}) {
205
+ return await lenvReader.config(options);
206
+ }
package/src/lib.mjs ADDED
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Shared library functions for hive-mind project
4
+
5
+ // Try to import reportError and reportWarning from sentry.lib.mjs, but make it optional
6
+ // This allows the module to work even when @sentry/node is not installed
7
+ let reportError = null;
8
+ let reportWarning = null;
9
+ try {
10
+ const sentryModule = await import('./sentry.lib.mjs');
11
+ reportError = sentryModule.reportError;
12
+ reportWarning = sentryModule.reportWarning;
13
+ } catch (_error) {
14
+ // Sentry module not available, create no-op functions
15
+ if (global.verboseMode) {
16
+ console.debug('Sentry module not available:', _error?.message || 'Import failed');
17
+ }
18
+ reportError = (err) => {
19
+ // Silent no-op when Sentry is not available
20
+ if (global.verboseMode) {
21
+ console.debug('Sentry not available for error reporting:', err?.message);
22
+ }
23
+ };
24
+ reportWarning = () => {
25
+ // Silent no-op when Sentry is not available
26
+ if (global.verboseMode) {
27
+ console.debug('Sentry not available for warning reporting');
28
+ }
29
+ };
30
+ }
31
+
32
+ // Check if use is already defined (when imported from solve.mjs)
33
+ // If not, fetch it (when running standalone)
34
+ if (typeof globalThis.use === 'undefined') {
35
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
36
+ }
37
+
38
+ const fs = (await use('fs')).promises;
39
+
40
+ // Global reference for log file (can be set by importing module)
41
+ export let logFile = null;
42
+
43
+ /**
44
+ * Set the log file path
45
+ * @param {string} path - Path to the log file
46
+ */
47
+ export const setLogFile = (path) => {
48
+ logFile = path;
49
+ };
50
+
51
+ /**
52
+ * Get the current log file path
53
+ * @returns {string|null} Current log file path or null
54
+ */
55
+ export const getLogFile = () => {
56
+ return logFile;
57
+ };
58
+
59
+ /**
60
+ * Get the absolute log file path
61
+ * @returns {Promise<string|null>} Absolute path to log file or null
62
+ */
63
+ export const getAbsoluteLogPath = async () => {
64
+ if (!logFile) return null;
65
+ const path = (await use('path'));
66
+ return path.resolve(logFile);
67
+ };
68
+
69
+ /**
70
+ * Log messages to both console and file
71
+ * @param {string} message - The message to log
72
+ * @param {Object} options - Logging options
73
+ * @param {string} [options.level='info'] - Log level (info, warn, error)
74
+ * @param {boolean} [options.verbose=false] - Whether this is a verbose log
75
+ * @returns {Promise<void>}
76
+ */
77
+ export const log = async (message, options = {}) => {
78
+ const { level = 'info', verbose = false } = options;
79
+
80
+ // Skip verbose logs unless --verbose is enabled
81
+ if (verbose && !global.verboseMode) {
82
+ return;
83
+ }
84
+
85
+ // Write to file if log file is set
86
+ if (logFile) {
87
+ const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
88
+ await fs.appendFile(logFile, logMessage + '\n').catch((error) => {
89
+ // Silent fail for file append errors to avoid infinite loop
90
+ // but report to Sentry in verbose mode
91
+ if (global.verboseMode) {
92
+ reportError(error, {
93
+ context: 'log_file_append',
94
+ level: 'debug',
95
+ logFile
96
+ });
97
+ }
98
+ });
99
+ }
100
+
101
+ // Write to console based on level
102
+ switch (level) {
103
+ case 'error':
104
+ console.error(message);
105
+ break;
106
+ case 'warning':
107
+ case 'warn':
108
+ console.warn(message);
109
+ break;
110
+ case 'info':
111
+ default:
112
+ console.log(message);
113
+ break;
114
+ }
115
+ };
116
+
117
+ /**
118
+ * Mask sensitive tokens in text
119
+ * @param {string} token - Token to mask
120
+ * @param {Object} options - Masking options
121
+ * @param {number} [options.minLength=12] - Minimum length to mask
122
+ * @param {number} [options.startChars=5] - Number of characters to show at start
123
+ * @param {number} [options.endChars=5] - Number of characters to show at end
124
+ * @returns {string} Masked token
125
+ */
126
+ export const maskToken = (token, options = {}) => {
127
+ const { minLength = 12, startChars = 5, endChars = 5 } = options;
128
+
129
+ if (!token || token.length < minLength) {
130
+ return token; // Don't mask very short strings
131
+ }
132
+
133
+ const start = token.substring(0, startChars);
134
+ const end = token.substring(token.length - endChars);
135
+ const middle = '*'.repeat(Math.max(token.length - (startChars + endChars), 3));
136
+
137
+ return start + middle + end;
138
+ };
139
+
140
+
141
+ /**
142
+ * Format timestamps for use in filenames
143
+ * @param {Date} [date=new Date()] - Date to format
144
+ * @returns {string} Formatted timestamp
145
+ */
146
+ export const formatTimestamp = (date = new Date()) => {
147
+ return date.toISOString().replace(/[:.]/g, '-');
148
+ };
149
+
150
+ /**
151
+ * Create safe file names from arbitrary strings
152
+ * @param {string} name - Name to sanitize
153
+ * @returns {string} Sanitized filename
154
+ */
155
+ export const sanitizeFileName = (name) => {
156
+ return name.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
157
+ };
158
+
159
+ /**
160
+ * Check if running in specific runtime
161
+ * @returns {string} Runtime name (node, bun, or deno)
162
+ */
163
+ export const getRuntime = () => {
164
+ if (typeof Bun !== 'undefined') return 'bun';
165
+ if (typeof Deno !== 'undefined') return 'deno';
166
+ return 'node';
167
+ };
168
+
169
+ /**
170
+ * Get platform information
171
+ * @returns {Object} Platform information object
172
+ */
173
+ export const getPlatformInfo = () => {
174
+ return {
175
+ platform: process.platform,
176
+ arch: process.arch,
177
+ runtime: getRuntime(),
178
+ nodeVersion: process.versions?.node,
179
+ bunVersion: process.versions?.bun
180
+ };
181
+ };
182
+
183
+ /**
184
+ * Safely parse JSON with fallback
185
+ * @param {string} text - JSON string to parse
186
+ * @param {*} [defaultValue=null] - Default value if parsing fails
187
+ * @returns {*} Parsed JSON or default value
188
+ */
189
+ export const safeJsonParse = (text, defaultValue = null) => {
190
+ try {
191
+ return JSON.parse(text);
192
+ } catch (error) {
193
+ // This is intentionally silent as it's a safe parse with fallback
194
+ // Only report in verbose mode for debugging
195
+ if (global.verboseMode) {
196
+ reportError(error, {
197
+ context: 'safe_json_parse',
198
+ level: 'debug',
199
+ textPreview: text?.substring(0, 100)
200
+ });
201
+ }
202
+ return defaultValue;
203
+ }
204
+ };
205
+
206
+ /**
207
+ * Sleep/delay execution
208
+ * @param {number} ms - Milliseconds to sleep
209
+ * @returns {Promise<void>}
210
+ */
211
+ export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
212
+
213
+ /**
214
+ * Retry operations with exponential backoff
215
+ * @param {Function} fn - Function to retry
216
+ * @param {Object} options - Retry options
217
+ * @param {number} [options.maxAttempts=3] - Maximum number of attempts
218
+ * @param {number} [options.delay=1000] - Initial delay between retries in ms
219
+ * @param {number} [options.backoff=2] - Backoff multiplier
220
+ * @returns {Promise<*>} Result of successful function execution
221
+ * @throws {Error} Last error if all attempts fail
222
+ */
223
+ export const retry = async (fn, options = {}) => {
224
+ const { maxAttempts = 3, delay = 1000, backoff = 2 } = options;
225
+
226
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
227
+ try {
228
+ return await fn();
229
+ } catch (error) {
230
+ // Report error to Sentry with retry context
231
+ reportError(error, {
232
+ context: 'retry_operation',
233
+ attempt,
234
+ maxAttempts,
235
+ willRetry: attempt < maxAttempts
236
+ });
237
+
238
+ if (attempt === maxAttempts) throw error;
239
+
240
+ const waitTime = delay * Math.pow(backoff, attempt - 1);
241
+ await log(`Attempt ${attempt} failed, retrying in ${waitTime}ms...`, { level: 'warn' });
242
+ await sleep(waitTime);
243
+ }
244
+ }
245
+ };
246
+
247
+ /**
248
+ * Format bytes to human readable string
249
+ * @param {number} bytes - Number of bytes
250
+ * @param {number} [decimals=2] - Number of decimal places
251
+ * @returns {string} Formatted size string
252
+ */
253
+ export const formatBytes = (bytes, decimals = 2) => {
254
+ if (bytes === 0) return '0 Bytes';
255
+
256
+ const k = 1024;
257
+ const dm = decimals < 0 ? 0 : decimals;
258
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
259
+
260
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
261
+
262
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
263
+ };
264
+
265
+ /**
266
+ * Measure execution time of async functions
267
+ * @param {Function} fn - Function to measure
268
+ * @param {string} [label='Operation'] - Label for the operation
269
+ * @returns {Promise<*>} Result of the function
270
+ * @throws {Error} Error from the function if it fails
271
+ */
272
+ export const measureTime = async (fn, label = 'Operation') => {
273
+ const start = Date.now();
274
+ try {
275
+ const result = await fn();
276
+ const duration = Date.now() - start;
277
+ await log(`${label} completed in ${duration}ms`, { verbose: true });
278
+ return result;
279
+ } catch (error) {
280
+ const duration = Date.now() - start;
281
+ await log(`${label} failed after ${duration}ms`, { level: 'error' });
282
+ reportError(error, {
283
+ context: 'measure_time',
284
+ operation: label,
285
+ duration
286
+ });
287
+ throw error;
288
+ }
289
+ };
290
+
291
+ /**
292
+ * Clean up error messages for better user experience
293
+ * @param {Error|string} error - Error object or message
294
+ * @returns {string} Cleaned error message
295
+ */
296
+ export const cleanErrorMessage = (error) => {
297
+ let message = error.message || error.toString();
298
+
299
+ // Remove common noise from error messages
300
+ message = message.split('\n')[0]; // Take only first line
301
+ message = message.replace(/^Command failed: /, ''); // Remove "Command failed: " prefix
302
+ message = message.replace(/^Error: /, ''); // Remove redundant "Error: " prefix
303
+ message = message.replace(/^\/bin\/sh: \d+: /, ''); // Remove shell path info
304
+
305
+ return message;
306
+ };
307
+
308
+ /**
309
+ * Format aligned console output
310
+ * @param {string} icon - Icon to display
311
+ * @param {string} label - Label text
312
+ * @param {string} value - Value text
313
+ * @param {number} [indent=0] - Indentation level
314
+ * @returns {string} Formatted string
315
+ */
316
+ export const formatAligned = (icon, label, value, indent = 0) => {
317
+ const spaces = ' '.repeat(indent);
318
+ const labelWidth = 25 - indent;
319
+ const paddedLabel = label.padEnd(labelWidth, ' ');
320
+ return `${spaces}${icon} ${paddedLabel} ${value || ''}`;
321
+ };
322
+
323
+ /**
324
+ * Display formatted error messages with sections
325
+ * @param {Object} options - Display options
326
+ * @param {string} options.title - Error title
327
+ * @param {string} [options.what] - What happened
328
+ * @param {string|Array} [options.details] - Error details
329
+ * @param {Array<string>} [options.causes] - Possible causes
330
+ * @param {Array<string>} [options.fixes] - Possible fixes
331
+ * @param {string} [options.workDir] - Working directory
332
+ * @param {Function} [options.log] - Log function to use
333
+ * @param {string} [options.level='error'] - Log level
334
+ * @returns {Promise<void>}
335
+ */
336
+ export const displayFormattedError = async (options) => {
337
+ const {
338
+ title,
339
+ what,
340
+ details,
341
+ causes,
342
+ fixes,
343
+ workDir,
344
+ log: logFn = log,
345
+ level = 'error'
346
+ } = options;
347
+
348
+ await logFn('');
349
+ await logFn(`โŒ ${title}`, { level });
350
+ await logFn('');
351
+
352
+ if (what) {
353
+ await logFn(' ๐Ÿ” What happened:');
354
+ await logFn(` ${what}`);
355
+ await logFn('');
356
+ }
357
+
358
+ if (details) {
359
+ await logFn(' ๐Ÿ“ฆ Error details:');
360
+ const detailLines = Array.isArray(details) ? details : details.split('\n');
361
+ for (const line of detailLines) {
362
+ if (line.trim()) await logFn(` ${line.trim()}`);
363
+ }
364
+ await logFn('');
365
+ }
366
+
367
+ if (causes && causes.length > 0) {
368
+ await logFn(' ๐Ÿ’ก Possible causes:');
369
+ for (const cause of causes) {
370
+ await logFn(` โ€ข ${cause}`);
371
+ }
372
+ await logFn('');
373
+ }
374
+
375
+ if (fixes && fixes.length > 0) {
376
+ await logFn(' ๐Ÿ”ง How to fix:');
377
+ for (let i = 0; i < fixes.length; i++) {
378
+ await logFn(` ${i + 1}. ${fixes[i]}`);
379
+ }
380
+ await logFn('');
381
+ }
382
+
383
+ if (workDir) {
384
+ await logFn(` ๐Ÿ“‚ Working directory: ${workDir}`);
385
+ await logFn('');
386
+ }
387
+
388
+ // Always show the log file path if it exists - using absolute path
389
+ if (logFile) {
390
+ const path = (await use('path'));
391
+ const absoluteLogPath = path.resolve(logFile);
392
+ await logFn(` ๐Ÿ“ Full log file: ${absoluteLogPath}`);
393
+ await logFn('');
394
+ }
395
+ };
396
+
397
+ /**
398
+ * Clean up temporary directories
399
+ * @param {Object} argv - Command line arguments
400
+ * @param {boolean} [argv.autoCleanup] - Whether auto-cleanup is enabled
401
+ * @returns {Promise<void>}
402
+ */
403
+ export const cleanupTempDirectories = async (argv) => {
404
+ if (!argv || !argv.autoCleanup) {
405
+ return;
406
+ }
407
+
408
+ // Dynamic import for command-stream
409
+ const { $ } = await use('command-stream');
410
+
411
+ try {
412
+ await log('\n๐Ÿงน Auto-cleanup enabled, removing temporary directories...');
413
+ await log(' โš ๏ธ Executing: sudo rm -rf /tmp/* /var/tmp/*', { verbose: true });
414
+
415
+ // Execute cleanup command using command-stream
416
+ const cleanupCommand = $`sudo rm -rf /tmp/* /var/tmp/*`;
417
+
418
+ let exitCode = 0;
419
+ for await (const chunk of cleanupCommand.stream()) {
420
+ if (chunk.type === 'stderr') {
421
+ const error = chunk.data.toString().trim();
422
+ if (error && !error.includes('cannot remove')) { // Ignore "cannot remove" warnings for files in use
423
+ await log(` [cleanup WARNING] ${error}`, { level: 'warn', verbose: true });
424
+ }
425
+ } else if (chunk.type === 'exit') {
426
+ exitCode = chunk.code;
427
+ }
428
+ }
429
+
430
+ if (exitCode === 0) {
431
+ await log(' โœ… Temporary directories cleaned successfully');
432
+ } else {
433
+ await log(` โš ๏ธ Cleanup completed with warnings (exit code: ${exitCode})`, { level: 'warn' });
434
+ }
435
+ } catch (error) {
436
+ reportError(error, {
437
+ context: 'cleanup_temp_directories',
438
+ autoCleanup: argv?.autoCleanup
439
+ });
440
+ await log(` โŒ Error during cleanup: ${cleanErrorMessage(error)}`, { level: 'error' });
441
+ // Don't fail the entire process if cleanup fails
442
+ }
443
+ };
444
+
445
+ // Export all functions as default object too
446
+ export default {
447
+ log,
448
+ setLogFile,
449
+ getLogFile,
450
+ getAbsoluteLogPath,
451
+ maskToken,
452
+ formatTimestamp,
453
+ sanitizeFileName,
454
+ getRuntime,
455
+ getPlatformInfo,
456
+ safeJsonParse,
457
+ sleep,
458
+ retry,
459
+ formatBytes,
460
+ measureTime,
461
+ cleanErrorMessage,
462
+ formatAligned,
463
+ displayFormattedError,
464
+ cleanupTempDirectories
465
+ };
466
+
467
+ /**
468
+ * Get version information for logging
469
+ * @returns {Promise<string>} Version string
470
+ */
471
+ export const getVersionInfo = async () => {
472
+ const path = (await use('path'));
473
+ const $ = (await use('zx')).$;
474
+ const { getGitVersionAsync } = await import('./git.lib.mjs');
475
+
476
+ try {
477
+ const packagePath = path.join(path.dirname(path.dirname(new globalThis.URL(import.meta.url).pathname)), 'package.json');
478
+ const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
479
+ const currentVersion = packageJson.version;
480
+
481
+ // Use git.lib.mjs to get version with proper git error handling
482
+ return await getGitVersionAsync($, currentVersion);
483
+ } catch {
484
+ // Fallback to hardcoded version if all else fails
485
+ return '0.10.4';
486
+ }
487
+ };
488
+
489
+ // Export reportError for other modules that may import it
490
+ export { reportError, reportWarning };