@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- 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 };
|