@plexor-dev/claude-code-plugin-staging 0.1.0-beta.7 → 0.1.0-beta.9
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/commands/plexor-enabled.js +115 -16
- package/commands/plexor-login.js +51 -8
- package/commands/plexor-logout.js +39 -6
- package/commands/plexor-status.js +112 -9
- package/lib/settings-manager.js +71 -9
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -4
|
@@ -14,26 +14,84 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
-
// Import settings manager
|
|
18
|
-
|
|
17
|
+
// Import settings manager with error handling for missing lib
|
|
18
|
+
let settingsManager, PLEXOR_STAGING_URL, PLEXOR_PROD_URL;
|
|
19
|
+
try {
|
|
20
|
+
const lib = require('../lib/settings-manager');
|
|
21
|
+
settingsManager = lib.settingsManager;
|
|
22
|
+
PLEXOR_STAGING_URL = lib.PLEXOR_STAGING_URL;
|
|
23
|
+
PLEXOR_PROD_URL = lib.PLEXOR_PROD_URL;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
26
|
+
console.error('Error: Plexor plugin files are missing or corrupted.');
|
|
27
|
+
console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
|
|
21
34
|
const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
|
|
22
35
|
|
|
23
36
|
function loadConfig() {
|
|
24
37
|
try {
|
|
38
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
39
|
+
return { version: 1, auth: {}, settings: {} };
|
|
40
|
+
}
|
|
25
41
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
if (!data || data.trim() === '') {
|
|
43
|
+
return { version: 1, auth: {}, settings: {} };
|
|
44
|
+
}
|
|
45
|
+
const config = JSON.parse(data);
|
|
46
|
+
if (typeof config !== 'object' || config === null) {
|
|
47
|
+
console.warn('Warning: Config file has invalid format, using defaults');
|
|
48
|
+
return { version: 1, auth: {}, settings: {} };
|
|
49
|
+
}
|
|
50
|
+
return config;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err instanceof SyntaxError) {
|
|
53
|
+
console.warn('Warning: Config file is corrupted, using defaults');
|
|
54
|
+
// Backup corrupted file
|
|
55
|
+
try {
|
|
56
|
+
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.corrupted');
|
|
57
|
+
} catch { /* ignore */ }
|
|
58
|
+
}
|
|
28
59
|
return { version: 1, auth: {}, settings: {} };
|
|
29
60
|
}
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
function saveConfig(config) {
|
|
33
|
-
|
|
34
|
-
fs.
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
66
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Atomic write: write to temp file, then rename
|
|
70
|
+
const crypto = require('crypto');
|
|
71
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
72
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
75
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
79
|
+
console.error(`Error: Cannot write to ~/.plexor/config.json`);
|
|
80
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
81
|
+
} else {
|
|
82
|
+
console.error('Failed to save config:', err.message);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
35
85
|
}
|
|
36
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate API key format
|
|
90
|
+
* @param {string} key - API key to validate
|
|
91
|
+
* @returns {boolean} true if valid format
|
|
92
|
+
*/
|
|
93
|
+
function isValidApiKeyFormat(key) {
|
|
94
|
+
return key && typeof key === 'string' && key.startsWith('plx_') && key.length >= 20;
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
function main() {
|
|
@@ -80,28 +138,69 @@ function main() {
|
|
|
80
138
|
process.exit(1);
|
|
81
139
|
}
|
|
82
140
|
|
|
83
|
-
// Update Plexor plugin config
|
|
84
|
-
config.settings = config.settings || {};
|
|
85
|
-
config.settings.enabled = newEnabled;
|
|
86
|
-
saveConfig(config);
|
|
87
|
-
|
|
88
141
|
// THE KEY FEATURE: Update Claude Code settings.json routing
|
|
89
142
|
let routingUpdated = false;
|
|
143
|
+
let missingApiKey = false;
|
|
144
|
+
let invalidApiKey = false;
|
|
145
|
+
|
|
90
146
|
if (newEnabled) {
|
|
91
|
-
// Enable routing - need API key from config
|
|
92
|
-
if (apiKey) {
|
|
147
|
+
// Enable routing - need valid API key from config
|
|
148
|
+
if (!apiKey) {
|
|
149
|
+
missingApiKey = true;
|
|
150
|
+
} else if (!isValidApiKeyFormat(apiKey)) {
|
|
151
|
+
invalidApiKey = true;
|
|
152
|
+
} else {
|
|
153
|
+
// Update Plexor plugin config first
|
|
154
|
+
config.settings = config.settings || {};
|
|
155
|
+
config.settings.enabled = newEnabled;
|
|
156
|
+
if (!saveConfig(config)) {
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
93
160
|
// STAGING PACKAGE - uses staging API
|
|
94
161
|
const apiUrl = config.settings?.apiUrl || 'https://staging.api.plexor.dev';
|
|
95
162
|
const useStaging = apiUrl.includes('staging');
|
|
96
163
|
routingUpdated = settingsManager.enablePlexorRouting(apiKey, { useStaging });
|
|
97
|
-
} else {
|
|
98
|
-
console.log('⚠ No API key found. Run /plexor-login first.');
|
|
99
164
|
}
|
|
100
165
|
} else {
|
|
166
|
+
// Update Plexor plugin config
|
|
167
|
+
config.settings = config.settings || {};
|
|
168
|
+
config.settings.enabled = newEnabled;
|
|
169
|
+
if (!saveConfig(config)) {
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
101
173
|
// Disable routing - remove env vars from settings.json
|
|
102
174
|
routingUpdated = settingsManager.disablePlexorRouting();
|
|
103
175
|
}
|
|
104
176
|
|
|
177
|
+
// Show error if no API key when enabling
|
|
178
|
+
if (missingApiKey) {
|
|
179
|
+
console.log(`┌─────────────────────────────────────────────┐`);
|
|
180
|
+
console.log(`│ ✗ Cannot Enable Plexor │`);
|
|
181
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
182
|
+
console.log(`│ No API key configured. │`);
|
|
183
|
+
console.log(`│ Run /plexor-login <api-key> first. │`);
|
|
184
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
185
|
+
console.log(`│ Get your API key at: │`);
|
|
186
|
+
console.log(`│ https://plexor.dev/dashboard/api-keys │`);
|
|
187
|
+
console.log(`└─────────────────────────────────────────────┘`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Show error if API key format is invalid
|
|
192
|
+
if (invalidApiKey) {
|
|
193
|
+
console.log(`┌─────────────────────────────────────────────┐`);
|
|
194
|
+
console.log(`│ ✗ Cannot Enable Plexor │`);
|
|
195
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
196
|
+
console.log(`│ Invalid API key format in config. │`);
|
|
197
|
+
console.log(`│ Keys must start with "plx_" (20+ chars). │`);
|
|
198
|
+
console.log(`├─────────────────────────────────────────────┤`);
|
|
199
|
+
console.log(`│ Run /plexor-login <api-key> to fix. │`);
|
|
200
|
+
console.log(`└─────────────────────────────────────────────┘`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
105
204
|
const newStatus = newEnabled ? '● Enabled' : '○ Disabled';
|
|
106
205
|
const prevStatus = currentEnabled ? 'Enabled' : 'Disabled';
|
|
107
206
|
const routingMsg = routingUpdated
|
package/commands/plexor-login.js
CHANGED
|
@@ -13,8 +13,18 @@ const path = require('path');
|
|
|
13
13
|
const https = require('https');
|
|
14
14
|
const http = require('http');
|
|
15
15
|
|
|
16
|
-
// Import settings manager
|
|
17
|
-
|
|
16
|
+
// Import settings manager with error handling for missing lib
|
|
17
|
+
let settingsManager;
|
|
18
|
+
try {
|
|
19
|
+
settingsManager = require('../lib/settings-manager').settingsManager;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
22
|
+
console.error('Error: Plexor plugin files are missing or corrupted.');
|
|
23
|
+
console.error(' Please reinstall: npm install @plexor-dev/claude-code-plugin-staging');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
const CONFIG_PATH = path.join(process.env.HOME, '.plexor', 'config.json');
|
|
20
30
|
const PLEXOR_DIR = path.join(process.env.HOME, '.plexor');
|
|
@@ -23,18 +33,49 @@ const DEFAULT_API_URL = 'https://staging.api.plexor.dev';
|
|
|
23
33
|
|
|
24
34
|
function loadConfig() {
|
|
25
35
|
try {
|
|
36
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
37
|
+
return { version: 1, auth: {}, settings: {} };
|
|
38
|
+
}
|
|
26
39
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
if (!data || data.trim() === '') {
|
|
41
|
+
return { version: 1, auth: {}, settings: {} };
|
|
42
|
+
}
|
|
43
|
+
const config = JSON.parse(data);
|
|
44
|
+
if (typeof config !== 'object' || config === null) {
|
|
45
|
+
return { version: 1, auth: {}, settings: {} };
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof SyntaxError) {
|
|
50
|
+
console.warn('Warning: Config file is corrupted, will be overwritten');
|
|
51
|
+
}
|
|
29
52
|
return { version: 1, auth: {}, settings: {} };
|
|
30
53
|
}
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
function saveConfig(config) {
|
|
34
|
-
|
|
35
|
-
fs.
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
59
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Atomic write: write to temp file, then rename
|
|
63
|
+
const crypto = require('crypto');
|
|
64
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
65
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
68
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
69
|
+
return true;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
72
|
+
console.error('Error: Cannot write to ~/.plexor/config.json');
|
|
73
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
74
|
+
} else {
|
|
75
|
+
console.error('Failed to save config:', err.message);
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
36
78
|
}
|
|
37
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
38
79
|
}
|
|
39
80
|
|
|
40
81
|
function validateApiKey(apiUrl, apiKey) {
|
|
@@ -132,7 +173,9 @@ async function main() {
|
|
|
132
173
|
config.auth.api_key = apiKey;
|
|
133
174
|
config.settings = config.settings || {};
|
|
134
175
|
config.settings.enabled = true;
|
|
135
|
-
saveConfig(config)
|
|
176
|
+
if (!saveConfig(config)) {
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
136
179
|
|
|
137
180
|
// AUTO-CONFIGURE CLAUDE CODE ROUTING
|
|
138
181
|
// This is the key feature: automatically set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
|
|
@@ -15,18 +15,49 @@ const CACHE_PATH = path.join(PLEXOR_DIR, 'cache.json');
|
|
|
15
15
|
|
|
16
16
|
function loadConfig() {
|
|
17
17
|
try {
|
|
18
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
18
21
|
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
if (!data || data.trim() === '') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const config = JSON.parse(data);
|
|
26
|
+
if (typeof config !== 'object' || config === null) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err instanceof SyntaxError) {
|
|
32
|
+
console.warn('Warning: Config file is corrupted');
|
|
33
|
+
}
|
|
21
34
|
return null;
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
function saveConfig(config) {
|
|
26
|
-
|
|
27
|
-
fs.
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.existsSync(PLEXOR_DIR)) {
|
|
41
|
+
fs.mkdirSync(PLEXOR_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Atomic write: write to temp file, then rename
|
|
45
|
+
const crypto = require('crypto');
|
|
46
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
47
|
+
const tempPath = path.join(PLEXOR_DIR, `.config.${tempId}.tmp`);
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
50
|
+
fs.renameSync(tempPath, CONFIG_PATH);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
54
|
+
console.error('Error: Cannot write to ~/.plexor/config.json');
|
|
55
|
+
console.error(' Check file permissions or run with appropriate access.');
|
|
56
|
+
} else {
|
|
57
|
+
console.error('Failed to save config:', err.message);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
28
60
|
}
|
|
29
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
function deleteFile(filePath) {
|
|
@@ -61,7 +92,9 @@ function main() {
|
|
|
61
92
|
delete config.auth.api_key;
|
|
62
93
|
config.settings = config.settings || {};
|
|
63
94
|
config.settings.enabled = false;
|
|
64
|
-
saveConfig(config)
|
|
95
|
+
if (!saveConfig(config)) {
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
65
98
|
|
|
66
99
|
// Clear session
|
|
67
100
|
deleteFile(SESSION_PATH);
|
|
@@ -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
|