@plexor-dev/claude-code-plugin-staging 0.1.0-beta.6 → 0.1.0-beta.8
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-enabled.js +67 -11
- package/commands/plexor-login.js +35 -5
- package/commands/plexor-status.js +112 -9
- package/lib/settings-manager.js +71 -9
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -7
package/README.md
CHANGED
|
@@ -32,14 +32,11 @@ This installs slash commands to `~/.claude/commands/`.
|
|
|
32
32
|
|
|
33
33
|
| Command | Description |
|
|
34
34
|
|---------|-------------|
|
|
35
|
-
| `/plexor-
|
|
36
|
-
| `/plexor-
|
|
37
|
-
| `/plexor-mode` | Set optimization mode (eco/balanced/quality/passthrough) |
|
|
38
|
-
| `/plexor-provider` | Force specific provider (auto/claude/deepseek/mistral/gemini) |
|
|
39
|
-
| `/plexor-config` | Quick config (enable/disable/cache/reset) |
|
|
40
|
-
| `/plexor-settings` | Advanced settings (API URL, mode, provider) |
|
|
41
|
-
| `/plexor-enabled` | Enable/disable the proxy |
|
|
35
|
+
| `/plexor-setup` | First-time setup wizard |
|
|
36
|
+
| `/plexor-login` | Authenticate with Plexor API key |
|
|
42
37
|
| `/plexor-logout` | Sign out and clear credentials |
|
|
38
|
+
| `/plexor-status` | View usage stats and savings |
|
|
39
|
+
| `/plexor-enabled` | Enable/disable Plexor routing |
|
|
43
40
|
|
|
44
41
|
## How It Works
|
|
45
42
|
|
|
@@ -22,18 +22,53 @@ const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
|
|
|
22
22
|
|
|
23
23
|
function loadConfig() {
|
|
24
24
|
try {
|
|
25
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
26
|
+
return { version: 1, auth: {}, settings: {} };
|
|
27
|
+
}
|
|
25
28
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
if (!data || data.trim() === '') {
|
|
30
|
+
return { version: 1, auth: {}, settings: {} };
|
|
31
|
+
}
|
|
32
|
+
const config = JSON.parse(data);
|
|
33
|
+
if (typeof config !== 'object' || config === null) {
|
|
34
|
+
console.warn('Warning: Config file has invalid format, using defaults');
|
|
35
|
+
return { version: 1, auth: {}, settings: {} };
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof SyntaxError) {
|
|
40
|
+
console.warn('Warning: Config file is corrupted, using defaults');
|
|
41
|
+
// Backup corrupted file
|
|
42
|
+
try {
|
|
43
|
+
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.corrupted');
|
|
44
|
+
} catch { /* ignore */ }
|
|
45
|
+
}
|
|
28
46
|
return { version: 1, auth: {}, settings: {} };
|
|
29
47
|
}
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
function saveConfig(config) {
|
|
33
|
-
|
|
34
|
-
fs.
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
53
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Atomic write: write to temp file, then rename
|
|
57
|
+
const crypto = require('crypto');
|
|
58
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
59
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
62
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
65
|
+
console.error(`Error: Cannot write to ${CONFIG_PATH}`);
|
|
66
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
67
|
+
} else {
|
|
68
|
+
console.error('Failed to save config:', err.message);
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
35
71
|
}
|
|
36
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
37
72
|
}
|
|
38
73
|
|
|
39
74
|
function main() {
|
|
@@ -80,28 +115,49 @@ function main() {
|
|
|
80
115
|
process.exit(1);
|
|
81
116
|
}
|
|
82
117
|
|
|
83
|
-
// Update Plexor plugin config
|
|
84
|
-
config.settings = config.settings || {};
|
|
85
|
-
config.settings.enabled = newEnabled;
|
|
86
|
-
saveConfig(config);
|
|
87
|
-
|
|
88
118
|
// THE KEY FEATURE: Update Claude Code settings.json routing
|
|
89
119
|
let routingUpdated = false;
|
|
120
|
+
let missingApiKey = false;
|
|
121
|
+
|
|
90
122
|
if (newEnabled) {
|
|
91
123
|
// Enable routing - need API key from config
|
|
92
124
|
if (apiKey) {
|
|
125
|
+
// Update Plexor plugin config first
|
|
126
|
+
config.settings = config.settings || {};
|
|
127
|
+
config.settings.enabled = newEnabled;
|
|
128
|
+
saveConfig(config);
|
|
129
|
+
|
|
93
130
|
// STAGING PACKAGE - uses staging API
|
|
94
131
|
const apiUrl = config.settings?.apiUrl || 'https://staging.api.plexor.dev';
|
|
95
132
|
const useStaging = apiUrl.includes('staging');
|
|
96
133
|
routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
|
|
97
134
|
} else {
|
|
98
|
-
|
|
135
|
+
missingApiKey = true;
|
|
99
136
|
}
|
|
100
137
|
} else {
|
|
138
|
+
// Update Plexor plugin config
|
|
139
|
+
config.settings = config.settings || {};
|
|
140
|
+
config.settings.enabled = newEnabled;
|
|
141
|
+
saveConfig(config);
|
|
142
|
+
|
|
101
143
|
// Disable routing - remove env vars from settings.json
|
|
102
144
|
routingUpdated = settingsManager.disablePlexorRouting();
|
|
103
145
|
}
|
|
104
146
|
|
|
147
|
+
// Show error if no API key when enabling
|
|
148
|
+
if (missingApiKey) {
|
|
149
|
+
console.log(`┌─────────────────────────────────────────────┐`);
|
|
150
|
+
console.log(`│ ✗ Cannot Enable Plexor │`);
|
|
151
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
152
|
+
console.log(`│ No API key configured. │`);
|
|
153
|
+
console.log(`│ Run /plexor-login <api-key> first. │`);
|
|
154
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
155
|
+
console.log(`│ Get your API key at: │`);
|
|
156
|
+
console.log(`│ https://plexor.dev/dashboard/api-keys │`);
|
|
157
|
+
console.log(`└─────────────────────────────────────────────┘`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
105
161
|
const newStatus = newEnabled ? '● Enabled' : '○ Disabled';
|
|
106
162
|
const prevStatus = currentEnabled ? 'Enabled' : 'Disabled';
|
|
107
163
|
const routingMsg = routingUpdated
|
package/commands/plexor-login.js
CHANGED
|
@@ -23,18 +23,48 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
|
23
23
|
|
|
24
24
|
function loadConfig() {
|
|
25
25
|
try {
|
|
26
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
27
|
+
return { version: 1, auth: {}, settings: {} };
|
|
28
|
+
}
|
|
26
29
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
if (!data || data.trim() === '') {
|
|
31
|
+
return { version: 1, auth: {}, settings: {} };
|
|
32
|
+
}
|
|
33
|
+
const config = JSON.parse(data);
|
|
34
|
+
if (typeof config !== 'object' || config === null) {
|
|
35
|
+
return { version: 1, auth: {}, settings: {} };
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof SyntaxError) {
|
|
40
|
+
console.warn('Warning: Config file is corrupted, will be overwritten');
|
|
41
|
+
}
|
|
29
42
|
return { version: 1, auth: {}, settings: {} };
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
function saveConfig(config) {
|
|
34
|
-
|
|
35
|
-
fs.
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
49
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Atomic write: write to temp file, then rename
|
|
53
|
+
const crypto = require('crypto');
|
|
54
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
55
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
58
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
61
|
+
console.error(`Error: Cannot write to ${CONFIG_PATH}`);
|
|
62
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
63
|
+
} else {
|
|
64
|
+
console.error('Failed to save config:', err.message);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
36
67
|
}
|
|
37
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
function validateApiKey(apiUrl, apiKey) {
|
|
@@ -49,13 +49,63 @@ function loadSessionStats() {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Validate API key format
|
|
54
|
+
* @param {string} key - API key to validate
|
|
55
|
+
* @returns {boolean} true if valid format
|
|
56
|
+
*/
|
|
57
|
+
function isValidApiKeyFormat(key) {
|
|
58
|
+
return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load config file with integrity checking
|
|
63
|
+
* @returns {Object|null} config object or null if invalid
|
|
64
|
+
*/
|
|
65
|
+
function loadConfig() {
|
|
55
66
|
try {
|
|
67
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
56
70
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
57
|
-
|
|
71
|
+
if (!data || data.trim() === '') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const config = JSON.parse(data);
|
|
75
|
+
if (typeof config !== 'object' || config === null) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
58
79
|
} catch (err) {
|
|
80
|
+
if (err instanceof SyntaxError) {
|
|
81
|
+
console.log('Config file is corrupted. Run /plexor-login to reconfigure.');
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check for environment mismatch between config and routing
|
|
89
|
+
*/
|
|
90
|
+
function checkEnvironmentMismatch(configApiUrl, routingBaseUrl) {
|
|
91
|
+
if (!configApiUrl || !routingBaseUrl) return null;
|
|
92
|
+
|
|
93
|
+
const configIsStaging = configApiUrl.includes('staging');
|
|
94
|
+
const routingIsStaging = routingBaseUrl.includes('staging');
|
|
95
|
+
|
|
96
|
+
if (configIsStaging !== routingIsStaging) {
|
|
97
|
+
return {
|
|
98
|
+
config: configIsStaging ? 'staging' : 'production',
|
|
99
|
+
routing: routingIsStaging ? 'staging' : 'production'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
// Read config with integrity checking
|
|
107
|
+
const config = loadConfig();
|
|
108
|
+
if (!config) {
|
|
59
109
|
console.log('Not configured. Run /plexor-login first.');
|
|
60
110
|
process.exit(1);
|
|
61
111
|
}
|
|
@@ -72,6 +122,13 @@ async function main() {
|
|
|
72
122
|
process.exit(1);
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
// Validate API key format
|
|
126
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
127
|
+
console.log('Invalid API key format. Keys must start with "plx_" and be at least 20 characters.');
|
|
128
|
+
console.log('Run /plexor-login with a valid API key.');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
75
132
|
// Fetch user info and stats
|
|
76
133
|
let user = { email: 'Unknown', tier: { name: 'Free', limits: {} } };
|
|
77
134
|
let stats = { period: {}, summary: {} };
|
|
@@ -165,6 +222,16 @@ ${line(`└── Cost saved: $${sessionCostSaved}`)}
|
|
|
165
222
|
const routingIndicator = routing.active ? '🟢 PLEXOR MODE: ON' : '🔴 PLEXOR MODE: OFF';
|
|
166
223
|
const envLabel = routing.isStaging ? '(staging)' : '(production)';
|
|
167
224
|
|
|
225
|
+
// Check for environment mismatch
|
|
226
|
+
const envMismatch = checkEnvironmentMismatch(apiUrl, routing.baseUrl);
|
|
227
|
+
const mismatchWarning = envMismatch
|
|
228
|
+
? ` ⚠ Warning: Config uses ${envMismatch.config} but routing is ${envMismatch.routing}\n`
|
|
229
|
+
: '';
|
|
230
|
+
|
|
231
|
+
if (mismatchWarning) {
|
|
232
|
+
console.log(mismatchWarning);
|
|
233
|
+
}
|
|
234
|
+
|
|
168
235
|
console.log(` ┌─────────────────────────────────────────────┐
|
|
169
236
|
${line(routingIndicator + (routing.active ? ' ' + envLabel : ''))}
|
|
170
237
|
├─────────────────────────────────────────────┤
|
|
@@ -202,7 +269,13 @@ ${line(`└── Endpoint: ${routing.baseUrl ? routing.baseUrl.replace('https:/
|
|
|
202
269
|
|
|
203
270
|
function fetchJson(apiUrl, endpoint, apiKey) {
|
|
204
271
|
return new Promise((resolve, reject) => {
|
|
205
|
-
|
|
272
|
+
let url;
|
|
273
|
+
try {
|
|
274
|
+
url = new URL(`${apiUrl}${endpoint}`);
|
|
275
|
+
} catch {
|
|
276
|
+
reject(new Error('Invalid API URL'));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
206
279
|
|
|
207
280
|
const options = {
|
|
208
281
|
hostname: url.hostname,
|
|
@@ -218,18 +291,48 @@ function fetchJson(apiUrl, endpoint, apiKey) {
|
|
|
218
291
|
let data = '';
|
|
219
292
|
res.on('data', chunk => data += chunk);
|
|
220
293
|
res.on('end', () => {
|
|
294
|
+
// Check HTTP status code first
|
|
295
|
+
if (res.statusCode === 401) {
|
|
296
|
+
reject(new Error('Invalid API key'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (res.statusCode === 403) {
|
|
300
|
+
reject(new Error('Access denied'));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (res.statusCode >= 500) {
|
|
304
|
+
reject(new Error('Server error'));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (res.statusCode !== 200) {
|
|
308
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check for empty response
|
|
313
|
+
if (!data || data.trim() === '') {
|
|
314
|
+
reject(new Error('Empty response'));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Parse JSON
|
|
221
319
|
try {
|
|
222
|
-
|
|
320
|
+
const parsed = JSON.parse(data);
|
|
321
|
+
if (parsed === null) {
|
|
322
|
+
reject(new Error('Null response'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
resolve(parsed);
|
|
223
326
|
} catch {
|
|
224
|
-
reject(new Error('Invalid response'));
|
|
327
|
+
reject(new Error('Invalid JSON response'));
|
|
225
328
|
}
|
|
226
329
|
});
|
|
227
330
|
});
|
|
228
331
|
|
|
229
|
-
req.on('error', reject);
|
|
332
|
+
req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
230
333
|
req.setTimeout(5000, () => {
|
|
231
334
|
req.destroy();
|
|
232
|
-
reject(new Error('
|
|
335
|
+
reject(new Error('Request timeout'));
|
|
233
336
|
});
|
|
234
337
|
req.end();
|
|
235
338
|
});
|
package/lib/settings-manager.js
CHANGED
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
15
17
|
|
|
16
18
|
const CLAUDE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude');
|
|
17
19
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
18
21
|
|
|
19
22
|
// Plexor gateway endpoints
|
|
20
23
|
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
@@ -27,8 +30,8 @@ class ClaudeSettingsManager {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
* Load current Claude settings
|
|
31
|
-
* @returns {Object} settings object or empty object if not found
|
|
33
|
+
* Load current Claude settings with integrity checking
|
|
34
|
+
* @returns {Object} settings object or empty object if not found/corrupted
|
|
32
35
|
*/
|
|
33
36
|
load() {
|
|
34
37
|
try {
|
|
@@ -36,18 +39,60 @@ class ClaudeSettingsManager {
|
|
|
36
39
|
return {};
|
|
37
40
|
}
|
|
38
41
|
const data = fs.readFileSync(this.settingsPath, 'utf8');
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
// Check for empty file
|
|
44
|
+
if (!data || data.trim() === '') {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = JSON.parse(data);
|
|
49
|
+
|
|
50
|
+
// Basic schema validation - must be an object
|
|
51
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
52
|
+
console.warn('Warning: Claude settings file has invalid format, using defaults');
|
|
53
|
+
this._backupCorruptedFile();
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsed;
|
|
40
58
|
} catch (err) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
if (err.code === 'ENOENT') {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
// JSON parse error or corrupted file
|
|
63
|
+
if (err instanceof SyntaxError) {
|
|
64
|
+
console.warn('Warning: Claude settings file is corrupted, using defaults');
|
|
65
|
+
console.warn(' A backup has been saved to settings.json.corrupted');
|
|
66
|
+
this._backupCorruptedFile();
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
// Permission error
|
|
70
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
71
|
+
console.warn(`Warning: Cannot read ${this.settingsPath} (permission denied)`);
|
|
72
|
+
return {};
|
|
44
73
|
}
|
|
74
|
+
console.warn('Warning: Failed to load Claude settings:', err.message);
|
|
45
75
|
return {};
|
|
46
76
|
}
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
/**
|
|
50
|
-
*
|
|
80
|
+
* Backup a corrupted settings file for debugging
|
|
81
|
+
*/
|
|
82
|
+
_backupCorruptedFile() {
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(this.settingsPath)) {
|
|
85
|
+
const backupPath = this.settingsPath + '.corrupted';
|
|
86
|
+
fs.copyFileSync(this.settingsPath, backupPath);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore backup errors
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Save Claude settings using atomic write pattern
|
|
95
|
+
* Prevents race conditions by writing to temp file then renaming
|
|
51
96
|
* @param {Object} settings - settings object to save
|
|
52
97
|
* @returns {boolean} success status
|
|
53
98
|
*/
|
|
@@ -58,10 +103,27 @@ class ClaudeSettingsManager {
|
|
|
58
103
|
fs.mkdirSync(this.claudeDir, { recursive: true });
|
|
59
104
|
}
|
|
60
105
|
|
|
61
|
-
|
|
106
|
+
// Atomic write: write to temp file, then rename
|
|
107
|
+
// This prevents race conditions where concurrent writes corrupt the file
|
|
108
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
109
|
+
const tempPath = path.join(this.claudeDir, `.settings.${tempId}.tmp`);
|
|
110
|
+
|
|
111
|
+
// Write to temp file
|
|
112
|
+
const content = JSON.stringify(settings, null, 2);
|
|
113
|
+
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
|
114
|
+
|
|
115
|
+
// Atomic rename (on POSIX systems, rename is atomic)
|
|
116
|
+
fs.renameSync(tempPath, this.settingsPath);
|
|
117
|
+
|
|
62
118
|
return true;
|
|
63
119
|
} catch (err) {
|
|
64
|
-
|
|
120
|
+
// Clean error message for permission errors
|
|
121
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
122
|
+
console.error(`Error: Cannot write to ${this.settingsPath}`);
|
|
123
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
124
|
+
} else {
|
|
125
|
+
console.error('Failed to save Claude settings:', err.message);
|
|
126
|
+
}
|
|
65
127
|
return false;
|
|
66
128
|
}
|
|
67
129
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -168,15 +168,37 @@ function main() {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// Copy lib files to ~/.claude/plugins/plexor/lib/
|
|
171
|
+
// CRITICAL: These are required for commands to work
|
|
171
172
|
const libInstalled = [];
|
|
172
173
|
if (fs.existsSync(LIB_SOURCE)) {
|
|
173
174
|
const libFiles = fs.readdirSync(LIB_SOURCE).filter(f => f.endsWith('.js'));
|
|
175
|
+
if (libFiles.length === 0) {
|
|
176
|
+
console.warn(' ⚠ Warning: No lib files found in package. Commands may not work.');
|
|
177
|
+
}
|
|
174
178
|
for (const file of libFiles) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
try {
|
|
180
|
+
const src = path.join(LIB_SOURCE, file);
|
|
181
|
+
const dest = path.join(PLEXOR_LIB_DIR, file);
|
|
182
|
+
fs.copyFileSync(src, dest);
|
|
183
|
+
libInstalled.push(file);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(` ✗ Failed to copy lib/${file}: ${err.message}`);
|
|
186
|
+
}
|
|
179
187
|
}
|
|
188
|
+
} else {
|
|
189
|
+
console.error(' ✗ CRITICAL: lib/ directory not found in package.');
|
|
190
|
+
console.error(' Commands will fail. Please reinstall the package.');
|
|
191
|
+
console.error(` Expected location: ${LIB_SOURCE}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify critical lib file exists
|
|
195
|
+
const criticalLibFile = path.join(PLEXOR_LIB_DIR, 'settings-manager.js');
|
|
196
|
+
if (!fs.existsSync(criticalLibFile)) {
|
|
197
|
+
console.error('');
|
|
198
|
+
console.error(' ✗ CRITICAL: settings-manager.js was not installed.');
|
|
199
|
+
console.error(' This file is required for commands to work.');
|
|
200
|
+
console.error(' Try reinstalling: npm install @plexor-dev/claude-code-plugin-staging');
|
|
201
|
+
console.error('');
|
|
180
202
|
}
|
|
181
203
|
|
|
182
204
|
// Fix file ownership when running with sudo
|
|
@@ -239,10 +261,10 @@ function main() {
|
|
|
239
261
|
console.log(' └─────────────────────────────────────────────────────────────────┘');
|
|
240
262
|
console.log('');
|
|
241
263
|
console.log(' Available commands:');
|
|
242
|
-
console.log(' /plexor-
|
|
243
|
-
console.log(' /plexor-mode - Switch modes (eco/balanced/quality)');
|
|
264
|
+
console.log(' /plexor-setup - First-time setup wizard');
|
|
244
265
|
console.log(' /plexor-login - Authenticate with API key');
|
|
245
|
-
console.log(' /plexor-
|
|
266
|
+
console.log(' /plexor-status - Check connection and see savings');
|
|
267
|
+
console.log(' /plexor-enabled - Enable/disable Plexor routing');
|
|
246
268
|
console.log('');
|
|
247
269
|
console.log(' Documentation: https://plexor.dev/docs');
|
|
248
270
|
console.log('');
|