@plexor-dev/claude-code-plugin-staging 0.1.0-beta.2 → 0.1.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -7
- package/commands/plexor-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-enabled.js +177 -18
- package/commands/plexor-enabled.md +31 -13
- package/commands/plexor-login.js +211 -42
- package/commands/plexor-login.md +4 -21
- package/commands/plexor-logout.js +72 -14
- package/commands/plexor-logout.md +2 -20
- package/commands/plexor-provider.js +62 -81
- package/commands/plexor-provider.md +23 -13
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +161 -123
- package/commands/plexor-settings.md +38 -14
- package/commands/plexor-setup.js +253 -0
- package/commands/plexor-setup.md +16 -160
- package/commands/plexor-status.js +244 -18
- package/commands/plexor-status.md +1 -13
- package/commands/plexor-uninstall.js +319 -0
- package/commands/plexor-uninstall.md +12 -0
- package/hooks/intercept.js +211 -32
- package/hooks/track-response.js +302 -2
- package/lib/config-utils.js +314 -0
- package/lib/config.js +22 -3
- package/lib/constants.js +19 -1
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +233 -24
- package/lib/verify-route.js +77 -0
- package/package.json +6 -4
- package/scripts/postinstall.js +271 -44
- package/scripts/uninstall.js +194 -41
- package/commands/plexor-config.js +0 -170
- package/commands/plexor-config.md +0 -28
- package/commands/plexor-mode.js +0 -107
- package/commands/plexor-mode.md +0 -27
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared config utilities for Plexor slash commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { PLEXOR_DIR, CONFIG_PATH } = require('./constants');
|
|
10
|
+
|
|
11
|
+
const DISABLED_HINT_VALUES = new Set(['', 'auto', 'none', 'off']);
|
|
12
|
+
const VALID_ORCHESTRATION_MODES = new Set(['supervised', 'autonomous', 'danger-full-auto']);
|
|
13
|
+
const VALID_ROUTING_MODES = new Set(['eco', 'balanced', 'quality', 'passthrough', 'cost']);
|
|
14
|
+
const MANAGED_HEADER_KEYS = new Set([
|
|
15
|
+
'x-force-provider',
|
|
16
|
+
'x-force-model',
|
|
17
|
+
'x-allow-providers',
|
|
18
|
+
'x-deny-providers',
|
|
19
|
+
'x-allow-models',
|
|
20
|
+
'x-deny-models',
|
|
21
|
+
'x-plexor-mode',
|
|
22
|
+
'x-plexor-orchestration-mode'
|
|
23
|
+
]);
|
|
24
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
25
|
+
|
|
26
|
+
function normalizeForcedProvider(value) {
|
|
27
|
+
if (typeof value !== 'string') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const normalized = value.trim().toLowerCase();
|
|
31
|
+
if (!normalized || normalized === 'auto') {
|
|
32
|
+
return 'auto';
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeForcedModel(value) {
|
|
38
|
+
if (typeof value !== 'string') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const normalized = value.trim();
|
|
42
|
+
if (!normalized || DISABLED_HINT_VALUES.has(normalized.toLowerCase())) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeCsv(value) {
|
|
49
|
+
if (typeof value !== 'string') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const tokens = value
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((token) => token.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (!tokens.length) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return tokens.join(',');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseCustomHeaders(raw) {
|
|
63
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const trimmedRaw = raw.trim();
|
|
67
|
+
|
|
68
|
+
// Backward compatibility: older plugin versions persisted ANTHROPIC_CUSTOM_HEADERS
|
|
69
|
+
// as a JSON object string. Parse that first so managed key replacement works.
|
|
70
|
+
if (trimmedRaw.startsWith('{')) {
|
|
71
|
+
try {
|
|
72
|
+
const legacy = JSON.parse(trimmedRaw);
|
|
73
|
+
if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
76
|
+
const normalizedKey = String(key || '').trim().toLowerCase();
|
|
77
|
+
const normalizedValue = String(value ?? '').trim();
|
|
78
|
+
if (!normalizedKey || !normalizedValue) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out[normalizedKey] = normalizedValue;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Fall through to line-based parser.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parsed = {};
|
|
91
|
+
for (const line of raw.split('\n')) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const idx = trimmed.indexOf(':');
|
|
97
|
+
if (idx <= 0) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const key = trimmed.slice(0, idx).trim().toLowerCase();
|
|
101
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
102
|
+
if (!key || !value) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
parsed[key] = value;
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function serializeCustomHeaders(headers) {
|
|
111
|
+
return Object.entries(headers)
|
|
112
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
113
|
+
.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildManagedAnthropicHeaders(config) {
|
|
117
|
+
const settings = config?.settings || {};
|
|
118
|
+
const headers = {};
|
|
119
|
+
|
|
120
|
+
const modeRaw = String(settings.mode || '')
|
|
121
|
+
.trim()
|
|
122
|
+
.toLowerCase();
|
|
123
|
+
if (VALID_ROUTING_MODES.has(modeRaw)) {
|
|
124
|
+
headers['x-plexor-mode'] = modeRaw === 'cost' ? 'eco' : modeRaw;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const orchestrationRaw = String(
|
|
128
|
+
settings.orchestrationMode || settings.orchestration_mode || ''
|
|
129
|
+
)
|
|
130
|
+
.trim()
|
|
131
|
+
.toLowerCase();
|
|
132
|
+
if (VALID_ORCHESTRATION_MODES.has(orchestrationRaw)) {
|
|
133
|
+
headers['x-plexor-orchestration-mode'] = orchestrationRaw;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const forceProvider = normalizeForcedProvider(
|
|
137
|
+
settings.preferred_provider ?? settings.preferredProvider ?? 'auto'
|
|
138
|
+
);
|
|
139
|
+
if (forceProvider && forceProvider !== 'auto') {
|
|
140
|
+
headers['x-force-provider'] = forceProvider;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const forceModel = normalizeForcedModel(settings.preferred_model ?? settings.preferredModel);
|
|
144
|
+
if (forceModel) {
|
|
145
|
+
headers['x-force-model'] = forceModel;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const allowProviders = normalizeCsv(
|
|
149
|
+
settings.provider_allowlist ?? settings.providerAllowlist ?? ''
|
|
150
|
+
);
|
|
151
|
+
if (allowProviders) {
|
|
152
|
+
headers['x-allow-providers'] = allowProviders;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const denyProviders = normalizeCsv(settings.provider_denylist ?? settings.providerDenylist ?? '');
|
|
156
|
+
if (denyProviders) {
|
|
157
|
+
headers['x-deny-providers'] = denyProviders;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const allowModels = normalizeCsv(settings.model_allowlist ?? settings.modelAllowlist ?? '');
|
|
161
|
+
if (allowModels) {
|
|
162
|
+
headers['x-allow-models'] = allowModels;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const denyModels = normalizeCsv(settings.model_denylist ?? settings.modelDenylist ?? '');
|
|
166
|
+
if (denyModels) {
|
|
167
|
+
headers['x-deny-models'] = denyModels;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return headers;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function syncClaudeCustomHeaders(config) {
|
|
174
|
+
try {
|
|
175
|
+
let settings = {};
|
|
176
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
177
|
+
const raw = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
178
|
+
settings = raw.trim() ? JSON.parse(raw) : {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
|
|
182
|
+
settings = {};
|
|
183
|
+
}
|
|
184
|
+
settings.env = settings.env && typeof settings.env === 'object' ? settings.env : {};
|
|
185
|
+
|
|
186
|
+
const existing = parseCustomHeaders(settings.env.ANTHROPIC_CUSTOM_HEADERS);
|
|
187
|
+
for (const key of MANAGED_HEADER_KEYS) {
|
|
188
|
+
delete existing[key];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const managed = buildManagedAnthropicHeaders(config);
|
|
192
|
+
const merged = { ...existing, ...managed };
|
|
193
|
+
|
|
194
|
+
if (Object.keys(merged).length) {
|
|
195
|
+
settings.env.ANTHROPIC_CUSTOM_HEADERS = serializeCustomHeaders(merged);
|
|
196
|
+
} else {
|
|
197
|
+
delete settings.env.ANTHROPIC_CUSTOM_HEADERS;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
201
|
+
if (!fs.existsSync(claudeDir)) {
|
|
202
|
+
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
203
|
+
}
|
|
204
|
+
const tempPath = path.join(claudeDir, `.settings.${crypto.randomBytes(8).toString('hex')}.tmp`);
|
|
205
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
206
|
+
fs.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function hasForcedHintConflict(config) {
|
|
214
|
+
const settings = config?.settings || {};
|
|
215
|
+
const provider = normalizeForcedProvider(
|
|
216
|
+
settings.preferred_provider ?? settings.preferredProvider ?? 'auto'
|
|
217
|
+
);
|
|
218
|
+
const model = normalizeForcedModel(settings.preferred_model ?? settings.preferredModel);
|
|
219
|
+
return Boolean(model) && provider !== 'auto';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateForcedHintConfig(config) {
|
|
223
|
+
if (!hasForcedHintConflict(config)) {
|
|
224
|
+
return { ok: true };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
message:
|
|
229
|
+
'Invalid Plexor config: set only one force hint. Use preferred_provider OR preferred_model, not both.'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function loadConfig() {
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
236
|
+
return { version: 1, auth: {}, settings: {} };
|
|
237
|
+
}
|
|
238
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
239
|
+
if (!data || data.trim() === '') {
|
|
240
|
+
return { version: 1, auth: {}, settings: {} };
|
|
241
|
+
}
|
|
242
|
+
const config = JSON.parse(data);
|
|
243
|
+
if (typeof config !== 'object' || config === null) {
|
|
244
|
+
return { version: 1, auth: {}, settings: {} };
|
|
245
|
+
}
|
|
246
|
+
return config;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err instanceof SyntaxError) {
|
|
249
|
+
try { fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.corrupted'); } catch { /* ignore */ }
|
|
250
|
+
}
|
|
251
|
+
return { version: 1, auth: {}, settings: {} };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function saveConfig(config) {
|
|
256
|
+
try {
|
|
257
|
+
const validation = validateForcedHintConfig(config);
|
|
258
|
+
if (!validation.ok) {
|
|
259
|
+
console.error(`Error: ${validation.message}`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
264
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
265
|
+
}
|
|
266
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
267
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
268
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
269
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
270
|
+
// Best-effort sync to Claude client headers so force hints are respected.
|
|
271
|
+
syncClaudeCustomHeaders(config);
|
|
272
|
+
return true;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
275
|
+
console.error('Error: Cannot write to ~/.plexor/config.json');
|
|
276
|
+
} else {
|
|
277
|
+
console.error('Failed to save config:', err.message);
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read a setting with env-var → config → default precedence.
|
|
285
|
+
* @param {object} config - Loaded config object
|
|
286
|
+
* @param {string} configKey - Primary key in config.settings (camelCase)
|
|
287
|
+
* @param {string|null} configKeyAlt - Alternative key (snake_case legacy)
|
|
288
|
+
* @param {string} envVar - Environment variable name
|
|
289
|
+
* @param {string[]} validValues - Accepted values
|
|
290
|
+
* @param {string} defaultValue - Fallback
|
|
291
|
+
* @returns {{ value: string, source: string }}
|
|
292
|
+
*/
|
|
293
|
+
function readSetting(config, configKey, configKeyAlt, envVar, validValues, defaultValue) {
|
|
294
|
+
const env = process.env[envVar];
|
|
295
|
+
if (env && validValues.includes(env.toLowerCase())) {
|
|
296
|
+
return { value: env.toLowerCase(), source: 'environment' };
|
|
297
|
+
}
|
|
298
|
+
const cfg = config.settings?.[configKey] || (configKeyAlt && config.settings?.[configKeyAlt]);
|
|
299
|
+
if (cfg && validValues.includes(cfg.toLowerCase())) {
|
|
300
|
+
return { value: cfg.toLowerCase(), source: 'config' };
|
|
301
|
+
}
|
|
302
|
+
return { value: defaultValue, source: 'default' };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
loadConfig,
|
|
307
|
+
saveConfig,
|
|
308
|
+
readSetting,
|
|
309
|
+
hasForcedHintConflict,
|
|
310
|
+
validateForcedHintConfig,
|
|
311
|
+
parseCustomHeaders,
|
|
312
|
+
serializeCustomHeaders,
|
|
313
|
+
buildManagedAnthropicHeaders
|
|
314
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { CONFIG_PATH, PLEXOR_DIR, DEFAULT_API_URL, DEFAULT_TIMEOUT } = require('./constants');
|
|
8
|
+
const { validateForcedHintConfig } = require('./config-utils');
|
|
8
9
|
|
|
9
10
|
class ConfigManager {
|
|
10
11
|
constructor() {
|
|
@@ -22,7 +23,10 @@ class ConfigManager {
|
|
|
22
23
|
timeout: cfg.settings?.timeout || DEFAULT_TIMEOUT,
|
|
23
24
|
localCacheEnabled: cfg.settings?.localCacheEnabled ?? false,
|
|
24
25
|
mode: cfg.settings?.mode || 'balanced',
|
|
25
|
-
preferredProvider: cfg.settings?.preferred_provider || 'auto'
|
|
26
|
+
preferredProvider: cfg.settings?.preferred_provider || 'auto',
|
|
27
|
+
preferredModel: cfg.settings?.preferredModel || cfg.settings?.preferred_model || null,
|
|
28
|
+
orchestrationMode:
|
|
29
|
+
cfg.settings?.orchestrationMode || cfg.settings?.orchestration_mode || 'autonomous'
|
|
26
30
|
};
|
|
27
31
|
} catch {
|
|
28
32
|
return { enabled: false, apiKey: '', apiUrl: DEFAULT_API_URL };
|
|
@@ -52,11 +56,26 @@ class ConfigManager {
|
|
|
52
56
|
timeout: config.timeout ?? existing.settings?.timeout,
|
|
53
57
|
localCacheEnabled: config.localCacheEnabled ?? existing.settings?.localCacheEnabled,
|
|
54
58
|
mode: config.mode ?? existing.settings?.mode,
|
|
55
|
-
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider
|
|
59
|
+
preferred_provider: config.preferredProvider ?? existing.settings?.preferred_provider,
|
|
60
|
+
preferred_model:
|
|
61
|
+
config.preferredModel ??
|
|
62
|
+
config.preferred_model ??
|
|
63
|
+
existing.settings?.preferredModel ??
|
|
64
|
+
existing.settings?.preferred_model,
|
|
65
|
+
orchestrationMode:
|
|
66
|
+
config.orchestrationMode ??
|
|
67
|
+
config.orchestration_mode ??
|
|
68
|
+
existing.settings?.orchestrationMode ??
|
|
69
|
+
existing.settings?.orchestration_mode
|
|
56
70
|
}
|
|
57
71
|
};
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
const validation = validateForcedHintConfig(updated);
|
|
74
|
+
if (!validation.ok) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(this.configPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
60
79
|
return true;
|
|
61
80
|
} catch {
|
|
62
81
|
return false;
|
package/lib/constants.js
CHANGED
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Get the user's home directory with proper validation
|
|
9
|
+
* @returns {string} Home directory path
|
|
10
|
+
* @throws {Error} If HOME is not set or empty
|
|
11
|
+
*/
|
|
12
|
+
function getHomeDir() {
|
|
13
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
14
|
+
if (!home || home.trim() === '') {
|
|
15
|
+
console.error('Error: HOME environment variable is not set.');
|
|
16
|
+
console.error(' Please set HOME to your user directory before running Plexor commands.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return home;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HOME_DIR = getHomeDir();
|
|
23
|
+
const PLEXOR_DIR = path.join(HOME_DIR, '.plexor');
|
|
8
24
|
const CONFIG_PATH = path.join(PLEXOR_DIR, 'config.json');
|
|
9
25
|
const SESSION_PATH = path.join(PLEXOR_DIR, 'session.json');
|
|
10
26
|
const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
@@ -16,6 +32,8 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
|
16
32
|
const DEFAULT_TIMEOUT = 5000;
|
|
17
33
|
|
|
18
34
|
module.exports = {
|
|
35
|
+
getHomeDir,
|
|
36
|
+
HOME_DIR,
|
|
19
37
|
PLEXOR_DIR,
|
|
20
38
|
CONFIG_PATH,
|
|
21
39
|
SESSION_PATH,
|
package/lib/logger.js
CHANGED
|
@@ -2,35 +2,94 @@
|
|
|
2
2
|
* Plexor Logger
|
|
3
3
|
*
|
|
4
4
|
* Simple logger that outputs to stderr to avoid interfering with stdout JSON.
|
|
5
|
+
* Uses ANSI-colored badges for branded Plexor messages.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
// ANSI color codes for branded badge output
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const BOLD = '\x1b[1m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const WHITE = '\x1b[37m';
|
|
13
|
+
const YELLOW_FG = '\x1b[33m';
|
|
14
|
+
const RED_FG = '\x1b[31m';
|
|
15
|
+
const CYAN_FG = '\x1b[36m';
|
|
16
|
+
const BLUE_BG = '\x1b[44m';
|
|
17
|
+
const YELLOW_BG = '\x1b[43m';
|
|
18
|
+
const RED_BG = '\x1b[41m';
|
|
19
|
+
|
|
20
|
+
// Pre-built badge strings
|
|
21
|
+
const BADGE_INFO = `${BOLD}${WHITE}${BLUE_BG} PLEXOR ${RESET}`;
|
|
22
|
+
const BADGE_WARN = `${BOLD}${WHITE}${YELLOW_BG} PLEXOR ${RESET}`;
|
|
23
|
+
const BADGE_ERROR = `${BOLD}${WHITE}${RED_BG} PLEXOR ${RESET}`;
|
|
24
|
+
|
|
25
|
+
function parseBooleanEnv(name, defaultValue) {
|
|
26
|
+
const raw = process.env[name];
|
|
27
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
31
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatUxMessage(message) {
|
|
41
|
+
const text = String(message || '').trim();
|
|
42
|
+
if (!text) {
|
|
43
|
+
return '[PLEXOR: message]';
|
|
44
|
+
}
|
|
45
|
+
if (text.startsWith('[PLEXOR:')) {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
if (text.startsWith('[PLEXOR') && text.endsWith(']')) {
|
|
49
|
+
return text.replace(/^\[PLEXOR[^\]]*\]\s*/i, '[PLEXOR: ');
|
|
50
|
+
}
|
|
51
|
+
return `[PLEXOR: ${text}]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
7
54
|
class Logger {
|
|
8
55
|
constructor(component = 'plexor') {
|
|
9
56
|
this.component = component;
|
|
10
|
-
this.debug_enabled =
|
|
57
|
+
this.debug_enabled = parseBooleanEnv('PLEXOR_DEBUG', false);
|
|
58
|
+
this.ux_messages_enabled =
|
|
59
|
+
parseBooleanEnv('PLEXOR_UX_MESSAGES', true) &&
|
|
60
|
+
parseBooleanEnv('PLEXOR_UX_DEBUG_MESSAGES', true);
|
|
11
61
|
}
|
|
12
62
|
|
|
13
63
|
debug(msg, data = null) {
|
|
14
64
|
if (this.debug_enabled) {
|
|
15
|
-
const output = data ?
|
|
65
|
+
const output = data ? `${DIM}[${this.component}]${RESET} ${msg} ${JSON.stringify(data)}` : `${DIM}[${this.component}]${RESET} ${msg}`;
|
|
16
66
|
console.error(output);
|
|
17
67
|
}
|
|
18
68
|
}
|
|
19
69
|
|
|
20
70
|
info(msg, data = null) {
|
|
21
|
-
const output = data ? `${msg} ${JSON.stringify(data)}` : msg
|
|
71
|
+
const output = data ? `${BADGE_INFO} ${msg} ${JSON.stringify(data)}` : `${BADGE_INFO} ${msg}`;
|
|
22
72
|
console.error(output);
|
|
23
73
|
}
|
|
24
74
|
|
|
25
75
|
warn(msg, data = null) {
|
|
26
|
-
const output = data ?
|
|
76
|
+
const output = data ? `${BADGE_WARN} ${YELLOW_FG}${msg} ${JSON.stringify(data)}${RESET}` : `${BADGE_WARN} ${YELLOW_FG}${msg}${RESET}`;
|
|
27
77
|
console.error(output);
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
error(msg, data = null) {
|
|
31
|
-
const output = data ?
|
|
81
|
+
const output = data ? `${BADGE_ERROR} ${RED_FG}${msg} ${JSON.stringify(data)}${RESET}` : `${BADGE_ERROR} ${RED_FG}${msg}${RESET}`;
|
|
32
82
|
console.error(output);
|
|
33
83
|
}
|
|
84
|
+
|
|
85
|
+
ux(msg, data = null) {
|
|
86
|
+
if (!this.ux_messages_enabled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const prefixed = formatUxMessage(msg);
|
|
90
|
+
const output = data ? `${prefixed} ${JSON.stringify(data)}` : prefixed;
|
|
91
|
+
console.error(`${CYAN_FG}${output}${RESET}`);
|
|
92
|
+
}
|
|
34
93
|
}
|
|
35
94
|
|
|
36
95
|
module.exports = Logger;
|