commons-proxy 2.0.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/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI Configuration Utility
|
|
3
|
+
*
|
|
4
|
+
* Handles reading and writing to the global Claude CLI settings file.
|
|
5
|
+
* Location: ~/.claude/settings.json (Windows: %USERPROFILE%\.claude\settings.json)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { logger } from './logger.js';
|
|
12
|
+
import { DEFAULT_PRESETS } from '../constants.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the path to the global Claude CLI settings file
|
|
16
|
+
* @returns {string} Absolute path to settings.json
|
|
17
|
+
*/
|
|
18
|
+
export function getClaudeConfigPath() {
|
|
19
|
+
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the global Claude CLI configuration
|
|
24
|
+
* @returns {Promise<Object>} The configuration object or empty object if file missing
|
|
25
|
+
*/
|
|
26
|
+
export async function readClaudeConfig() {
|
|
27
|
+
const configPath = getClaudeConfigPath();
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
30
|
+
if (!content.trim()) return { env: {} };
|
|
31
|
+
return JSON.parse(content);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') {
|
|
34
|
+
logger.warn(`[ClaudeConfig] Config file not found at ${configPath}, returning empty default`);
|
|
35
|
+
return { env: {} };
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof SyntaxError) {
|
|
38
|
+
logger.error(`[ClaudeConfig] Invalid JSON in config at ${configPath}. Returning safe default.`);
|
|
39
|
+
return { env: {} };
|
|
40
|
+
}
|
|
41
|
+
logger.error(`[ClaudeConfig] Failed to read config at ${configPath}:`, error.message);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Update the global Claude CLI configuration
|
|
48
|
+
* Performs a deep merge with existing configuration to avoid losing other settings.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} updates - The partial configuration to merge in
|
|
51
|
+
* @returns {Promise<Object>} The updated full configuration
|
|
52
|
+
*/
|
|
53
|
+
export async function updateClaudeConfig(updates) {
|
|
54
|
+
const configPath = getClaudeConfigPath();
|
|
55
|
+
let currentConfig = {};
|
|
56
|
+
|
|
57
|
+
// 1. Read existing config
|
|
58
|
+
try {
|
|
59
|
+
currentConfig = await readClaudeConfig();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Ignore ENOENT, otherwise rethrow
|
|
62
|
+
if (error.code !== 'ENOENT') throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Deep merge updates
|
|
66
|
+
const newConfig = deepMerge(currentConfig, updates);
|
|
67
|
+
|
|
68
|
+
// 3. Ensure .claude directory exists
|
|
69
|
+
const configDir = path.dirname(configPath);
|
|
70
|
+
try {
|
|
71
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Ignore if exists
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Write back to file
|
|
77
|
+
try {
|
|
78
|
+
await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
|
79
|
+
logger.info(`[ClaudeConfig] Updated config at ${configPath}`);
|
|
80
|
+
return newConfig;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(`[ClaudeConfig] Failed to write config:`, error.message);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Replace the global Claude CLI configuration entirely
|
|
89
|
+
* Unlike updateClaudeConfig, this replaces the config instead of merging.
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} config - The new configuration to write
|
|
92
|
+
* @returns {Promise<Object>} The written configuration
|
|
93
|
+
*/
|
|
94
|
+
export async function replaceClaudeConfig(config) {
|
|
95
|
+
const configPath = getClaudeConfigPath();
|
|
96
|
+
|
|
97
|
+
// 1. Ensure .claude directory exists
|
|
98
|
+
const configDir = path.dirname(configPath);
|
|
99
|
+
try {
|
|
100
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Ignore if exists
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Write config directly (no merge)
|
|
106
|
+
try {
|
|
107
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
108
|
+
logger.info(`[ClaudeConfig] Replaced config at ${configPath}`);
|
|
109
|
+
return config;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error(`[ClaudeConfig] Failed to write config:`, error.message);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Simple deep merge for objects
|
|
118
|
+
*/
|
|
119
|
+
function deepMerge(target, source) {
|
|
120
|
+
const output = { ...target };
|
|
121
|
+
|
|
122
|
+
if (isObject(target) && isObject(source)) {
|
|
123
|
+
Object.keys(source).forEach(key => {
|
|
124
|
+
if (isObject(source[key])) {
|
|
125
|
+
if (!(key in target)) {
|
|
126
|
+
Object.assign(output, { [key]: source[key] });
|
|
127
|
+
} else {
|
|
128
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
Object.assign(output, { [key]: source[key] });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isObject(item) {
|
|
140
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ==========================================
|
|
144
|
+
// Claude CLI Presets
|
|
145
|
+
// ==========================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the path to the presets file
|
|
149
|
+
* @returns {string} Absolute path to claude-presets.json
|
|
150
|
+
*/
|
|
151
|
+
export function getPresetsPath() {
|
|
152
|
+
return path.join(os.homedir(), '.config', 'commons-proxy', 'claude-presets.json');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read all Claude CLI presets
|
|
157
|
+
* Creates the file with default presets if it doesn't exist.
|
|
158
|
+
* @returns {Promise<Array>} Array of preset objects
|
|
159
|
+
*/
|
|
160
|
+
export async function readPresets() {
|
|
161
|
+
const presetsPath = getPresetsPath();
|
|
162
|
+
try {
|
|
163
|
+
const content = await fs.readFile(presetsPath, 'utf8');
|
|
164
|
+
if (!content.trim()) return DEFAULT_PRESETS;
|
|
165
|
+
return JSON.parse(content);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.code === 'ENOENT') {
|
|
168
|
+
// Create with defaults
|
|
169
|
+
try {
|
|
170
|
+
await fs.mkdir(path.dirname(presetsPath), { recursive: true });
|
|
171
|
+
await fs.writeFile(presetsPath, JSON.stringify(DEFAULT_PRESETS, null, 2), 'utf8');
|
|
172
|
+
logger.info(`[ClaudePresets] Created presets file with defaults at ${presetsPath}`);
|
|
173
|
+
} catch (writeError) {
|
|
174
|
+
logger.warn(`[ClaudePresets] Could not create presets file: ${writeError.message}`);
|
|
175
|
+
}
|
|
176
|
+
return DEFAULT_PRESETS;
|
|
177
|
+
}
|
|
178
|
+
if (error instanceof SyntaxError) {
|
|
179
|
+
logger.error(`[ClaudePresets] Invalid JSON in presets at ${presetsPath}. Returning defaults.`);
|
|
180
|
+
return DEFAULT_PRESETS;
|
|
181
|
+
}
|
|
182
|
+
logger.error(`[ClaudePresets] Failed to read presets at ${presetsPath}:`, error.message);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Save a preset (add or update)
|
|
189
|
+
* @param {string} name - Preset name
|
|
190
|
+
* @param {Object} config - Environment variables to save
|
|
191
|
+
* @returns {Promise<Array>} Updated array of presets
|
|
192
|
+
*/
|
|
193
|
+
export async function savePreset(name, config) {
|
|
194
|
+
const presetsPath = getPresetsPath();
|
|
195
|
+
let presets = await readPresets();
|
|
196
|
+
|
|
197
|
+
const existingIndex = presets.findIndex(p => p.name === name);
|
|
198
|
+
const newPreset = { name, config: { ...config } };
|
|
199
|
+
|
|
200
|
+
if (existingIndex >= 0) {
|
|
201
|
+
presets[existingIndex] = newPreset;
|
|
202
|
+
logger.info(`[ClaudePresets] Updated preset: ${name}`);
|
|
203
|
+
} else {
|
|
204
|
+
presets.push(newPreset);
|
|
205
|
+
logger.info(`[ClaudePresets] Created preset: ${name}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await fs.mkdir(path.dirname(presetsPath), { recursive: true });
|
|
210
|
+
await fs.writeFile(presetsPath, JSON.stringify(presets, null, 2), 'utf8');
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.error(`[ClaudePresets] Failed to save preset:`, error.message);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return presets;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Delete a preset by name
|
|
221
|
+
* @param {string} name - Preset name to delete
|
|
222
|
+
* @returns {Promise<Array>} Updated array of presets
|
|
223
|
+
*/
|
|
224
|
+
export async function deletePreset(name) {
|
|
225
|
+
const presetsPath = getPresetsPath();
|
|
226
|
+
let presets = await readPresets();
|
|
227
|
+
|
|
228
|
+
const originalLength = presets.length;
|
|
229
|
+
presets = presets.filter(p => p.name !== name);
|
|
230
|
+
|
|
231
|
+
if (presets.length === originalLength) {
|
|
232
|
+
logger.warn(`[ClaudePresets] Preset not found: ${name}`);
|
|
233
|
+
return presets;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await fs.writeFile(presetsPath, JSON.stringify(presets, null, 2), 'utf8');
|
|
238
|
+
logger.info(`[ClaudePresets] Deleted preset: ${name}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logger.error(`[ClaudePresets] Failed to delete preset:`, error.message);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return presets;
|
|
245
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* General-purpose helper functions used across multiple modules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format duration in milliseconds to human-readable string
|
|
9
|
+
* @param {number} ms - Duration in milliseconds
|
|
10
|
+
* @returns {string} Human-readable duration (e.g., "1h23m45s")
|
|
11
|
+
*/
|
|
12
|
+
export function formatDuration(ms) {
|
|
13
|
+
const seconds = Math.floor(ms / 1000);
|
|
14
|
+
const hours = Math.floor(seconds / 3600);
|
|
15
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
16
|
+
const secs = seconds % 60;
|
|
17
|
+
|
|
18
|
+
if (hours > 0) {
|
|
19
|
+
return `${hours}h${minutes}m${secs}s`;
|
|
20
|
+
} else if (minutes > 0) {
|
|
21
|
+
return `${minutes}m${secs}s`;
|
|
22
|
+
}
|
|
23
|
+
return `${secs}s`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sleep for specified milliseconds
|
|
29
|
+
* @param {number} ms - Duration to sleep in milliseconds
|
|
30
|
+
* @returns {Promise<void>} Resolves after the specified duration
|
|
31
|
+
*/
|
|
32
|
+
export function sleep(ms) {
|
|
33
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error is a network error (transient)
|
|
38
|
+
* @param {Error} error - The error to check
|
|
39
|
+
* @returns {boolean} True if it is a network error
|
|
40
|
+
*/
|
|
41
|
+
export function isNetworkError(error) {
|
|
42
|
+
const msg = error.message.toLowerCase();
|
|
43
|
+
return (
|
|
44
|
+
msg.includes('fetch failed') ||
|
|
45
|
+
msg.includes('network error') ||
|
|
46
|
+
msg.includes('econnreset') ||
|
|
47
|
+
msg.includes('etimedout') ||
|
|
48
|
+
msg.includes('socket hang up') ||
|
|
49
|
+
msg.includes('timeout')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging with colors and debug support.
|
|
5
|
+
* Simple ANSI codes used to avoid dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import util from 'util';
|
|
10
|
+
|
|
11
|
+
const COLORS = {
|
|
12
|
+
RESET: '\x1b[0m',
|
|
13
|
+
BRIGHT: '\x1b[1m',
|
|
14
|
+
DIM: '\x1b[2m',
|
|
15
|
+
|
|
16
|
+
RED: '\x1b[31m',
|
|
17
|
+
GREEN: '\x1b[32m',
|
|
18
|
+
YELLOW: '\x1b[33m',
|
|
19
|
+
BLUE: '\x1b[34m',
|
|
20
|
+
MAGENTA: '\x1b[35m',
|
|
21
|
+
CYAN: '\x1b[36m',
|
|
22
|
+
WHITE: '\x1b[37m',
|
|
23
|
+
GRAY: '\x1b[90m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class Logger extends EventEmitter {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.isDebugEnabled = false;
|
|
30
|
+
this.history = [];
|
|
31
|
+
this.maxHistory = 1000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set debug mode
|
|
36
|
+
* @param {boolean} enabled
|
|
37
|
+
*/
|
|
38
|
+
setDebug(enabled) {
|
|
39
|
+
this.isDebugEnabled = !!enabled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get current timestamp string
|
|
44
|
+
*/
|
|
45
|
+
getTimestamp() {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get log history
|
|
51
|
+
*/
|
|
52
|
+
getHistory() {
|
|
53
|
+
return this.history;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format and print a log message
|
|
58
|
+
* @param {string} level
|
|
59
|
+
* @param {string} color
|
|
60
|
+
* @param {string} message
|
|
61
|
+
* @param {...any} args
|
|
62
|
+
*/
|
|
63
|
+
print(level, color, message, ...args) {
|
|
64
|
+
// Format: [TIMESTAMP] [LEVEL] Message
|
|
65
|
+
const timestampStr = this.getTimestamp();
|
|
66
|
+
const timestamp = `${COLORS.GRAY}[${timestampStr}]${COLORS.RESET}`;
|
|
67
|
+
const levelTag = `${color}[${level}]${COLORS.RESET}`;
|
|
68
|
+
|
|
69
|
+
// Format the message with args similar to console.log
|
|
70
|
+
const formattedMessage = util.format(message, ...args);
|
|
71
|
+
|
|
72
|
+
console.log(`${timestamp} ${levelTag} ${formattedMessage}`);
|
|
73
|
+
|
|
74
|
+
// Store structured log
|
|
75
|
+
const logEntry = {
|
|
76
|
+
timestamp: timestampStr,
|
|
77
|
+
level,
|
|
78
|
+
message: formattedMessage
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.history.push(logEntry);
|
|
82
|
+
if (this.history.length > this.maxHistory) {
|
|
83
|
+
this.history.shift();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.emit('log', logEntry);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Standard info log
|
|
91
|
+
*/
|
|
92
|
+
info(message, ...args) {
|
|
93
|
+
this.print('INFO', COLORS.BLUE, message, ...args);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Success log
|
|
98
|
+
*/
|
|
99
|
+
success(message, ...args) {
|
|
100
|
+
this.print('SUCCESS', COLORS.GREEN, message, ...args);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Warning log
|
|
105
|
+
*/
|
|
106
|
+
warn(message, ...args) {
|
|
107
|
+
this.print('WARN', COLORS.YELLOW, message, ...args);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Error log
|
|
112
|
+
*/
|
|
113
|
+
error(message, ...args) {
|
|
114
|
+
this.print('ERROR', COLORS.RED, message, ...args);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Debug log - only prints if debug mode is enabled
|
|
119
|
+
*/
|
|
120
|
+
debug(message, ...args) {
|
|
121
|
+
if (this.isDebugEnabled) {
|
|
122
|
+
this.print('DEBUG', COLORS.MAGENTA, message, ...args);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Direct log (for raw output usually) - proxied to console.log but can be enhanced
|
|
128
|
+
*/
|
|
129
|
+
log(message, ...args) {
|
|
130
|
+
console.log(message, ...args);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Print a section header
|
|
135
|
+
*/
|
|
136
|
+
header(title) {
|
|
137
|
+
console.log(`\n${COLORS.BRIGHT}${COLORS.CYAN}=== ${title} ===${COLORS.RESET}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Export a singleton instance
|
|
142
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Module Helper
|
|
3
|
+
* Detects and auto-rebuilds native Node.js modules when they become
|
|
4
|
+
* incompatible after a Node.js version update.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if an error is a NODE_MODULE_VERSION mismatch error
|
|
14
|
+
* @param {Error} error - The error to check
|
|
15
|
+
* @returns {boolean} True if it's a version mismatch error
|
|
16
|
+
*/
|
|
17
|
+
export function isModuleVersionError(error) {
|
|
18
|
+
const message = error?.message || '';
|
|
19
|
+
return message.includes('NODE_MODULE_VERSION') &&
|
|
20
|
+
message.includes('was compiled against a different Node.js version');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract the module path from a NODE_MODULE_VERSION error message
|
|
25
|
+
* @param {Error} error - The error containing the module path
|
|
26
|
+
* @returns {string|null} The path to the .node file, or null if not found
|
|
27
|
+
*/
|
|
28
|
+
export function extractModulePath(error) {
|
|
29
|
+
const message = error?.message || '';
|
|
30
|
+
// Match pattern like: "The module '/path/to/module.node'"
|
|
31
|
+
const match = message.match(/The module '([^']+\.node)'/);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find the package root directory from a .node file path
|
|
37
|
+
* @param {string} nodeFilePath - Path to the .node file
|
|
38
|
+
* @returns {string|null} Path to the package root, or null if not found
|
|
39
|
+
*/
|
|
40
|
+
export function findPackageRoot(nodeFilePath) {
|
|
41
|
+
// Walk up from the .node file to find package.json
|
|
42
|
+
let dir = dirname(nodeFilePath);
|
|
43
|
+
while (dir) {
|
|
44
|
+
const packageJsonPath = join(dir, 'package.json');
|
|
45
|
+
if (existsSync(packageJsonPath)) {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
const parentDir = dirname(dir);
|
|
49
|
+
// Stop when we've reached the filesystem root (dirname returns same path)
|
|
50
|
+
if (parentDir === dir) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
dir = parentDir;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Attempt to rebuild a native module
|
|
60
|
+
* @param {string} packagePath - Path to the package root directory
|
|
61
|
+
* @returns {boolean} True if rebuild succeeded, false otherwise
|
|
62
|
+
*/
|
|
63
|
+
export function rebuildModule(packagePath) {
|
|
64
|
+
try {
|
|
65
|
+
logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`);
|
|
66
|
+
|
|
67
|
+
// Run npm rebuild in the package directory
|
|
68
|
+
const output = execSync('npm rebuild', {
|
|
69
|
+
cwd: packagePath,
|
|
70
|
+
stdio: 'pipe', // Capture output instead of printing
|
|
71
|
+
timeout: 120000 // 2 minute timeout
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Log rebuild output for debugging
|
|
75
|
+
const outputStr = output?.toString().trim();
|
|
76
|
+
if (outputStr) {
|
|
77
|
+
logger.debug(`[NativeModule] Rebuild output:\n${outputStr}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.success('[NativeModule] Rebuild completed successfully');
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Include stdout/stderr from the failed command for troubleshooting
|
|
84
|
+
const stdout = error.stdout?.toString().trim();
|
|
85
|
+
const stderr = error.stderr?.toString().trim();
|
|
86
|
+
let errorDetails = `[NativeModule] Rebuild failed: ${error.message}`;
|
|
87
|
+
if (stdout) {
|
|
88
|
+
errorDetails += `\n[NativeModule] stdout: ${stdout}`;
|
|
89
|
+
}
|
|
90
|
+
if (stderr) {
|
|
91
|
+
errorDetails += `\n[NativeModule] stderr: ${stderr}`;
|
|
92
|
+
}
|
|
93
|
+
logger.error(errorDetails);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Attempt to auto-rebuild a native module from an error
|
|
100
|
+
* @param {Error} error - The NODE_MODULE_VERSION error
|
|
101
|
+
* @returns {boolean} True if rebuild succeeded, false otherwise
|
|
102
|
+
*/
|
|
103
|
+
export function attemptAutoRebuild(error) {
|
|
104
|
+
const nodePath = extractModulePath(error);
|
|
105
|
+
if (!nodePath) {
|
|
106
|
+
logger.error('[NativeModule] Could not extract module path from error');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const packagePath = findPackageRoot(nodePath);
|
|
111
|
+
if (!packagePath) {
|
|
112
|
+
logger.error('[NativeModule] Could not find package root');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
logger.warn('[NativeModule] Native module version mismatch detected');
|
|
117
|
+
logger.info('[NativeModule] Attempting automatic rebuild...');
|
|
118
|
+
|
|
119
|
+
return rebuildModule(packagePath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Recursively clear a module and its dependencies from the require cache
|
|
124
|
+
* This is needed after rebuilding a native module to force re-import
|
|
125
|
+
* @param {string} modulePath - Resolved path to the module
|
|
126
|
+
* @param {object} cache - The require.cache object
|
|
127
|
+
* @param {Set} [visited] - Set of already-visited paths to prevent cycles
|
|
128
|
+
*/
|
|
129
|
+
export function clearRequireCache(modulePath, cache, visited = new Set()) {
|
|
130
|
+
if (visited.has(modulePath)) return;
|
|
131
|
+
visited.add(modulePath);
|
|
132
|
+
|
|
133
|
+
const mod = cache[modulePath];
|
|
134
|
+
if (!mod) return;
|
|
135
|
+
|
|
136
|
+
// Recursively clear children first
|
|
137
|
+
if (mod.children) {
|
|
138
|
+
for (const child of mod.children) {
|
|
139
|
+
clearRequireCache(child.id, cache, visited);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove from parent's children array
|
|
144
|
+
if (mod.parent && mod.parent.children) {
|
|
145
|
+
const idx = mod.parent.children.indexOf(mod);
|
|
146
|
+
if (idx !== -1) {
|
|
147
|
+
mod.parent.children.splice(idx, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Delete from cache
|
|
152
|
+
delete cache[modulePath];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default {
|
|
156
|
+
isModuleVersionError,
|
|
157
|
+
extractModulePath,
|
|
158
|
+
findPackageRoot,
|
|
159
|
+
rebuildModule,
|
|
160
|
+
attemptAutoRebuild,
|
|
161
|
+
clearRequireCache
|
|
162
|
+
};
|