@plexor-dev/claude-code-plugin-staging 0.1.0-beta.14 → 0.1.0-beta.16
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-agent.js +84 -0
- package/commands/plexor-agent.md +36 -0
- package/commands/plexor-provider.js +84 -0
- package/commands/plexor-provider.md +37 -0
- package/commands/plexor-routing.js +77 -0
- package/commands/plexor-routing.md +37 -0
- package/commands/plexor-settings.js +186 -0
- package/commands/plexor-settings.md +52 -0
- package/hooks/intercept.js +139 -10
- package/hooks/track-response.js +300 -0
- package/lib/config-utils.js +74 -0
- package/lib/config.js +9 -2
- package/lib/logger.js +64 -5
- package/lib/settings-manager.js +20 -6
- package/package.json +6 -2
- package/scripts/postinstall.js +161 -35
- package/scripts/uninstall.js +26 -3
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;
|
package/lib/settings-manager.js
CHANGED
|
@@ -23,6 +23,22 @@ const LOCK_TIMEOUT_MS = 5000; // 5 second lock timeout
|
|
|
23
23
|
const PLEXOR_STAGING_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
24
24
|
const PLEXOR_PROD_URL = 'https://api.plexor.dev/gateway/anthropic';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Check if a base URL is a Plexor-managed gateway URL.
|
|
28
|
+
* Detects all variants: production, staging, localhost, tunnels.
|
|
29
|
+
*/
|
|
30
|
+
function isManagedGatewayUrl(baseUrl) {
|
|
31
|
+
if (!baseUrl) return false;
|
|
32
|
+
return (
|
|
33
|
+
baseUrl.includes('plexor') ||
|
|
34
|
+
baseUrl.includes('staging.api') ||
|
|
35
|
+
baseUrl.includes('localhost') ||
|
|
36
|
+
baseUrl.includes('127.0.0.1') ||
|
|
37
|
+
baseUrl.includes('ngrok') ||
|
|
38
|
+
baseUrl.includes('localtunnel')
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
class ClaudeSettingsManager {
|
|
27
43
|
constructor() {
|
|
28
44
|
this.settingsPath = SETTINGS_PATH;
|
|
@@ -248,11 +264,8 @@ class ClaudeSettingsManager {
|
|
|
248
264
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || null;
|
|
249
265
|
const hasToken = !!settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
250
266
|
|
|
251
|
-
// Check if routing to Plexor
|
|
252
|
-
const isPlexorRouting = baseUrl
|
|
253
|
-
baseUrl.includes('plexor') ||
|
|
254
|
-
baseUrl.includes('staging.api')
|
|
255
|
-
);
|
|
267
|
+
// Check if routing to Plexor (any variant: prod, staging, localhost, tunnel)
|
|
268
|
+
const isPlexorRouting = isManagedGatewayUrl(baseUrl);
|
|
256
269
|
|
|
257
270
|
return {
|
|
258
271
|
enabled: isPlexorRouting,
|
|
@@ -276,7 +289,7 @@ class ClaudeSettingsManager {
|
|
|
276
289
|
const settings = this.load();
|
|
277
290
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
|
|
278
291
|
const authToken = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
279
|
-
const isPlexorUrl =
|
|
292
|
+
const isPlexorUrl = isManagedGatewayUrl(baseUrl);
|
|
280
293
|
|
|
281
294
|
if (isPlexorUrl && !authToken) {
|
|
282
295
|
return { partial: true, issue: 'Plexor URL set but no auth token' };
|
|
@@ -345,6 +358,7 @@ const settingsManager = new ClaudeSettingsManager();
|
|
|
345
358
|
module.exports = {
|
|
346
359
|
ClaudeSettingsManager,
|
|
347
360
|
settingsManager,
|
|
361
|
+
isManagedGatewayUrl,
|
|
348
362
|
CLAUDE_DIR,
|
|
349
363
|
SETTINGS_PATH,
|
|
350
364
|
PLEXOR_STAGING_URL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexor-dev/claude-code-plugin-staging",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.16",
|
|
4
4
|
"description": "STAGING - LLM cost optimization plugin for Claude Code (internal testing)",
|
|
5
5
|
"main": "lib/constants.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"plexor-enabled": "./commands/plexor-enabled.js",
|
|
9
9
|
"plexor-login": "./commands/plexor-login.js",
|
|
10
10
|
"plexor-logout": "./commands/plexor-logout.js",
|
|
11
|
-
"plexor-uninstall": "./commands/plexor-uninstall.js"
|
|
11
|
+
"plexor-uninstall": "./commands/plexor-uninstall.js",
|
|
12
|
+
"plexor-settings": "./commands/plexor-settings.js",
|
|
13
|
+
"plexor-routing": "./commands/plexor-routing.js",
|
|
14
|
+
"plexor-agent": "./commands/plexor-agent.js",
|
|
15
|
+
"plexor-provider": "./commands/plexor-provider.js"
|
|
12
16
|
},
|
|
13
17
|
"scripts": {
|
|
14
18
|
"postinstall": "node scripts/postinstall.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -13,42 +13,109 @@ const os = require('os');
|
|
|
13
13
|
const { execSync } = require('child_process');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Resolve the home directory for a given username by querying /etc/passwd.
|
|
17
|
+
* This is the authoritative source and handles non-standard home paths
|
|
18
|
+
* (e.g., /root, /opt/users/foo, NIS/LDAP users, etc.).
|
|
19
|
+
* Returns null if lookup fails (Windows, missing getent, etc.).
|
|
20
|
+
*/
|
|
21
|
+
function getHomeDirFromPasswd(username) {
|
|
22
|
+
try {
|
|
23
|
+
const entry = execSync(`getent passwd ${username}`, { encoding: 'utf8' }).trim();
|
|
24
|
+
// Format: username:x:uid:gid:gecos:homedir:shell
|
|
25
|
+
const fields = entry.split(':');
|
|
26
|
+
if (fields.length >= 6 && fields[5]) {
|
|
27
|
+
return fields[5];
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// getent not available or user not found
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the correct home directory for the process's effective user.
|
|
37
|
+
*
|
|
38
|
+
* Handles three scenarios:
|
|
39
|
+
* 1. Normal execution: HOME is correct, os.homedir() is correct.
|
|
40
|
+
* 2. `sudo npm install`: SUDO_USER is set, os.homedir() returns /root,
|
|
41
|
+
* but we want the SUDO_USER's home.
|
|
42
|
+
* 3. `sudo -u target npm install`:
|
|
43
|
+
* HOME may still be the *caller's* home (e.g.,
|
|
44
|
+
* /home/azureuser), SUDO_USER is the *caller*
|
|
45
|
+
* (not the target), but process.getuid() returns
|
|
46
|
+
* the *target* UID. We must resolve home from
|
|
47
|
+
* /etc/passwd by UID.
|
|
48
|
+
*
|
|
49
|
+
* Resolution order (most authoritative first):
|
|
50
|
+
* a) Look up the effective UID in /etc/passwd via getent (handles sudo -u)
|
|
51
|
+
* b) Fall back to os.homedir() (works for normal execution)
|
|
52
|
+
* c) Fall back to HOME / USERPROFILE env vars (last resort)
|
|
19
53
|
*/
|
|
20
54
|
function getHomeDir() {
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
// On non-Windows, resolve via the effective UID's passwd entry.
|
|
56
|
+
// This is the most reliable method and correctly handles both
|
|
57
|
+
// `sudo` and `sudo -u <target>` scenarios.
|
|
58
|
+
if (os.platform() !== 'win32') {
|
|
59
|
+
try {
|
|
60
|
+
const uid = process.getuid();
|
|
61
|
+
const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
|
|
62
|
+
const fields = entry.split(':');
|
|
63
|
+
if (fields.length >= 6 && fields[5]) {
|
|
64
|
+
return fields[5];
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall through to other methods
|
|
29
68
|
}
|
|
30
69
|
}
|
|
31
|
-
|
|
70
|
+
|
|
71
|
+
// Fallback: os.homedir() (reads HOME env var, then passwd on Unix)
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
if (home) return home;
|
|
74
|
+
|
|
75
|
+
// Last resort: environment variables
|
|
76
|
+
return process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
32
77
|
}
|
|
33
78
|
|
|
34
79
|
/**
|
|
35
|
-
* Get uid/gid for the
|
|
36
|
-
*
|
|
80
|
+
* Get uid/gid for the effective user running this process.
|
|
81
|
+
* Under `sudo`, the effective user is root but we want to chown to the
|
|
82
|
+
* original (SUDO_USER) or target (`sudo -u target`) user.
|
|
83
|
+
* Under `sudo -u target`, process.getuid() IS the target, so we use that.
|
|
84
|
+
* Returns null on Windows or if no privilege elevation detected.
|
|
37
85
|
*/
|
|
38
86
|
function getTargetUserIds() {
|
|
39
|
-
|
|
40
|
-
if (!sudoUser || os.platform() === 'win32') {
|
|
87
|
+
if (os.platform() === 'win32') {
|
|
41
88
|
return null;
|
|
42
89
|
}
|
|
43
90
|
|
|
44
91
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
92
|
+
const effectiveUid = process.getuid();
|
|
93
|
+
|
|
94
|
+
// If we're running as root (uid 0), we were likely invoked via `sudo`.
|
|
95
|
+
// Chown files to SUDO_USER (the human who ran sudo).
|
|
96
|
+
if (effectiveUid === 0 && process.env.SUDO_USER) {
|
|
97
|
+
const uid = parseInt(execSync(`id -u ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
|
|
98
|
+
const gid = parseInt(execSync(`id -g ${process.env.SUDO_USER}`, { encoding: 'utf8' }).trim(), 10);
|
|
99
|
+
return { uid, gid, user: process.env.SUDO_USER };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If we're NOT root but SUDO_USER is set, we were invoked via `sudo -u target`.
|
|
103
|
+
// The effective UID is already the target user. Chown to that user.
|
|
104
|
+
if (effectiveUid !== 0 && process.env.SUDO_USER) {
|
|
105
|
+
const entry = execSync(`getent passwd ${effectiveUid}`, { encoding: 'utf8' }).trim();
|
|
106
|
+
const fields = entry.split(':');
|
|
107
|
+
if (fields.length >= 4) {
|
|
108
|
+
const username = fields[0];
|
|
109
|
+
const uid = parseInt(fields[2], 10);
|
|
110
|
+
const gid = parseInt(fields[3], 10);
|
|
111
|
+
return { uid, gid, user: username };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
49
114
|
} catch {
|
|
50
|
-
|
|
115
|
+
// Fall through
|
|
51
116
|
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
52
119
|
}
|
|
53
120
|
|
|
54
121
|
/**
|
|
@@ -77,33 +144,71 @@ const PLEXOR_LIB_DIR = path.join(HOME_DIR, '.claude', 'plugins', 'plexor', 'lib'
|
|
|
77
144
|
const PLEXOR_CONFIG_DIR = path.join(HOME_DIR, '.plexor');
|
|
78
145
|
const PLEXOR_CONFIG_FILE = path.join(PLEXOR_CONFIG_DIR, 'config.json');
|
|
79
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Check if a base URL is a Plexor-managed gateway URL.
|
|
149
|
+
* Detects all variants: production, staging, localhost, tunnels.
|
|
150
|
+
*/
|
|
151
|
+
function isManagedGatewayUrl(baseUrl) {
|
|
152
|
+
if (!baseUrl) return false;
|
|
153
|
+
return (
|
|
154
|
+
baseUrl.includes('plexor') ||
|
|
155
|
+
baseUrl.includes('staging.api') ||
|
|
156
|
+
baseUrl.includes('localhost') ||
|
|
157
|
+
baseUrl.includes('127.0.0.1') ||
|
|
158
|
+
baseUrl.includes('ngrok') ||
|
|
159
|
+
baseUrl.includes('localtunnel')
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The expected base URL for THIS plugin variant.
|
|
165
|
+
* Used to detect when a different variant was previously installed.
|
|
166
|
+
*/
|
|
167
|
+
const THIS_VARIANT_URL = 'https://staging.api.plexor.dev/gateway/anthropic';
|
|
168
|
+
|
|
80
169
|
/**
|
|
81
170
|
* Check for orphaned Plexor routing in settings.json without valid config.
|
|
82
|
-
*
|
|
171
|
+
* Also detects variant mismatch (e.g., localhost plugin was installed, now
|
|
172
|
+
* installing staging plugin) and migrates ANTHROPIC_BASE_URL + fixes
|
|
173
|
+
* ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (#2174).
|
|
83
174
|
*/
|
|
84
175
|
function checkOrphanedRouting() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
89
|
-
const configPath = path.join(home, '.plexor', 'config.json');
|
|
176
|
+
// Use the resolved HOME_DIR (not process.env.HOME which may be wrong under sudo -u)
|
|
177
|
+
const settingsPath = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
178
|
+
const configPath = path.join(HOME_DIR, '.plexor', 'config.json');
|
|
90
179
|
|
|
91
180
|
try {
|
|
92
181
|
if (!fs.existsSync(settingsPath)) return;
|
|
93
182
|
|
|
94
183
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
95
184
|
const env = settings.env || {};
|
|
185
|
+
let settingsChanged = false;
|
|
96
186
|
|
|
97
|
-
const hasPlexorUrl = env.ANTHROPIC_BASE_URL
|
|
98
|
-
env.ANTHROPIC_BASE_URL.includes('plexor');
|
|
187
|
+
const hasPlexorUrl = isManagedGatewayUrl(env.ANTHROPIC_BASE_URL);
|
|
99
188
|
|
|
100
189
|
if (hasPlexorUrl) {
|
|
190
|
+
// Fix #2174: Only migrate ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN
|
|
191
|
+
// when routing through a Plexor-managed URL. Non-Plexor setups
|
|
192
|
+
// (direct Anthropic, etc.) should not have their auth mutated.
|
|
193
|
+
if (env.ANTHROPIC_API_KEY && !env.ANTHROPIC_AUTH_TOKEN) {
|
|
194
|
+
env.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_API_KEY;
|
|
195
|
+
delete env.ANTHROPIC_API_KEY;
|
|
196
|
+
settings.env = env;
|
|
197
|
+
settingsChanged = true;
|
|
198
|
+
console.log('\n Migrated ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (fix #2174)');
|
|
199
|
+
} else if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_AUTH_TOKEN) {
|
|
200
|
+
// Both exist — remove the lower-precedence one to avoid confusion
|
|
201
|
+
delete env.ANTHROPIC_API_KEY;
|
|
202
|
+
settings.env = env;
|
|
203
|
+
settingsChanged = true;
|
|
204
|
+
console.log('\n Removed redundant ANTHROPIC_API_KEY (ANTHROPIC_AUTH_TOKEN takes precedence)');
|
|
205
|
+
}
|
|
101
206
|
// Check if there's a valid Plexor config
|
|
102
207
|
let hasValidConfig = false;
|
|
103
208
|
try {
|
|
104
209
|
if (fs.existsSync(configPath)) {
|
|
105
210
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
106
|
-
hasValidConfig = config.
|
|
211
|
+
hasValidConfig = (config.auth?.api_key || config.apiKey || '').startsWith('plx_');
|
|
107
212
|
}
|
|
108
213
|
} catch (e) {}
|
|
109
214
|
|
|
@@ -113,10 +218,31 @@ function checkOrphanedRouting() {
|
|
|
113
218
|
console.log(' Run /plexor-login to reconfigure, or');
|
|
114
219
|
console.log(' Run /plexor-uninstall to clean up\n');
|
|
115
220
|
} else {
|
|
116
|
-
|
|
117
|
-
|
|
221
|
+
// Fix #2176: Detect variant mismatch and migrate URL
|
|
222
|
+
const currentUrl = env.ANTHROPIC_BASE_URL;
|
|
223
|
+
if (currentUrl !== THIS_VARIANT_URL) {
|
|
224
|
+
env.ANTHROPIC_BASE_URL = THIS_VARIANT_URL;
|
|
225
|
+
settings.env = env;
|
|
226
|
+
settingsChanged = true;
|
|
227
|
+
console.log(`\n Migrated ANTHROPIC_BASE_URL to this variant's gateway:`);
|
|
228
|
+
console.log(` Old: ${currentUrl}`);
|
|
229
|
+
console.log(` New: ${THIS_VARIANT_URL}\n`);
|
|
230
|
+
} else {
|
|
231
|
+
console.log('\n Existing Plexor configuration detected');
|
|
232
|
+
console.log(' Your previous settings have been preserved.\n');
|
|
233
|
+
}
|
|
118
234
|
}
|
|
119
235
|
}
|
|
236
|
+
|
|
237
|
+
// Write back settings if any migration was applied
|
|
238
|
+
if (settingsChanged) {
|
|
239
|
+
const crypto = require('crypto');
|
|
240
|
+
const claudeDir = path.join(HOME_DIR, '.claude');
|
|
241
|
+
const tempId = crypto.randomBytes(8).toString('hex');
|
|
242
|
+
const tempPath = path.join(claudeDir, `.settings.${tempId}.tmp`);
|
|
243
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
244
|
+
fs.renameSync(tempPath, settingsPath);
|
|
245
|
+
}
|
|
120
246
|
} catch (e) {
|
|
121
247
|
// Ignore errors in detection - don't break install
|
|
122
248
|
}
|
|
@@ -295,13 +421,13 @@ function main() {
|
|
|
295
421
|
console.log('');
|
|
296
422
|
console.log(' For Claude MAX users (OAuth):');
|
|
297
423
|
console.log('');
|
|
298
|
-
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
424
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
299
425
|
console.log(` source ${shellRc}`);
|
|
300
426
|
console.log('');
|
|
301
427
|
console.log(' For API key users (get key at https://plexor.dev/dashboard):');
|
|
302
428
|
console.log('');
|
|
303
|
-
console.log(` echo 'export ANTHROPIC_BASE_URL="https://api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
304
|
-
console.log(` echo 'export
|
|
429
|
+
console.log(` echo 'export ANTHROPIC_BASE_URL="https://staging.api.plexor.dev/gateway/anthropic"' >> ${shellRc}`);
|
|
430
|
+
console.log(` echo 'export ANTHROPIC_AUTH_TOKEN="plx_your_key_here"' >> ${shellRc}`);
|
|
305
431
|
console.log(` source ${shellRc}`);
|
|
306
432
|
console.log('');
|
|
307
433
|
console.log(' ┌─────────────────────────────────────────────────────────────────┐');
|
package/scripts/uninstall.js
CHANGED
|
@@ -15,11 +15,34 @@
|
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Get the correct home directory for the process's effective user.
|
|
23
|
+
* Resolves via /etc/passwd to handle sudo and sudo -u correctly.
|
|
24
|
+
*/
|
|
25
|
+
function getHomeDir() {
|
|
26
|
+
if (os.platform() !== 'win32') {
|
|
27
|
+
try {
|
|
28
|
+
const uid = process.getuid();
|
|
29
|
+
const entry = execSync(`getent passwd ${uid}`, { encoding: 'utf8' }).trim();
|
|
30
|
+
const fields = entry.split(':');
|
|
31
|
+
if (fields.length >= 6 && fields[5]) {
|
|
32
|
+
return fields[5];
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Fall through
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const h = os.homedir();
|
|
39
|
+
if (h) return h;
|
|
40
|
+
return process.env.HOME || process.env.USERPROFILE || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const home = getHomeDir();
|
|
21
44
|
if (!home) {
|
|
22
|
-
console.log('Warning:
|
|
45
|
+
console.log('Warning: Could not determine home directory, skipping cleanup');
|
|
23
46
|
process.exit(0);
|
|
24
47
|
}
|
|
25
48
|
|