@praeviso/code-env-switch 0.1.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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # code-env-switch
2
+
3
+ A tiny CLI to switch between Claude Code and Codex environment variables.
4
+
5
+ ## Setup
6
+
7
+ 1) Copy the example config and fill in your keys:
8
+
9
+ ```bash
10
+ cp code-env.example.json code-env.json
11
+ ```
12
+
13
+ 2) Install from npm (after publish) or locally:
14
+
15
+ ```bash
16
+ npm install -g code-env-switch
17
+ # or local dev
18
+ npm install -g .
19
+ # or
20
+ npm link
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ List profiles:
26
+
27
+ ```bash
28
+ codenv list
29
+ ```
30
+
31
+ Add or update a profile from the CLI:
32
+
33
+ ```bash
34
+ codenv add codex-88 OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_API_KEY=sk-REPLACE_ME --note "OpenAI official"
35
+ ```
36
+
37
+ Switch in the current shell (bash/zsh):
38
+
39
+ ```bash
40
+ eval "$(codenv use codex-88)"
41
+ ```
42
+
43
+ Unset all known keys:
44
+
45
+ ```bash
46
+ eval "$(codenv unset)"
47
+ ```
48
+
49
+ ### Config lookup order
50
+
51
+ `codenv` searches in this order:
52
+
53
+ 1) `--config <path>`
54
+ 2) `CODE_ENV_CONFIG`
55
+ 3) `./code-env.json`
56
+ 4) `./profiles.json`
57
+ 5) `./code-env.config.json`
58
+ 6) `~/.config/code-env/config.json`
59
+
60
+ ## Config format
61
+
62
+ ```json
63
+ {
64
+ "unset": ["OPENAI_BASE_URL", "OPENAI_API_KEY"],
65
+ "profiles": {
66
+ "codex-88": {
67
+ "note": "OpenAI official",
68
+ "env": {
69
+ "OPENAI_BASE_URL": "https://api.openai.com/v1",
70
+ "OPENAI_API_KEY": "sk-REPLACE_ME",
71
+ "CODEX_PROVIDER": "OpenAI"
72
+ },
73
+ "removeFiles": ["$HOME/.config/openai/auth.json"],
74
+ "commands": ["echo \"Switched to codex-88\""]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Notes:
81
+ - `removeFiles` is optional; when present, `codenv use <profile>` emits `rm -f` lines for those paths.
82
+ - `commands` is optional; any strings are emitted as-is.
83
+ - `note` is shown in `codenv list` output.
84
+ - `codenv add` creates the config file if it does not exist (default: `./code-env.json`).
85
+
86
+ ## Fish shell
87
+
88
+ ```fish
89
+ codenv use codex-88 | source
90
+ ```
package/bin/codenv.js ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ function printHelp() {
9
+ const msg = `codenv - switch Claude/Codex env vars\n\nUsage:\n codenv list\n codenv use <profile>\n codenv show <profile>\n codenv unset\n codenv add <profile> KEY=VALUE [KEY=VALUE ...]\n\nOptions:\n -c, --config <path> Path to config JSON\n -h, --help Show help\n\nAdd options:\n -n, --note <text> Set profile note\n -r, --remove-file <path> Add a removeFiles entry (repeat)\n -x, --command <cmd> Add a commands entry (repeat)\n -u, --unset <KEY> Add a global unset key (repeat)\n\nExamples:\n eval "$(codenv use codex)"\n codenv list\n CODE_ENV_CONFIG=./code-env.json codenv use claude\n codenv add codex-88 OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_API_KEY=sk-REPLACE_ME\n`;
10
+ console.log(msg);
11
+ }
12
+
13
+ function parseArgs(argv) {
14
+ let configPath = null;
15
+ const args = [];
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const arg = argv[i];
18
+ if (arg === "-h" || arg === "--help") return { help: true };
19
+ if (arg === "-c" || arg === "--config") {
20
+ configPath = argv[i + 1];
21
+ i++;
22
+ continue;
23
+ }
24
+ if (arg.startsWith("--config=")) {
25
+ configPath = arg.slice("--config=".length);
26
+ continue;
27
+ }
28
+ args.push(arg);
29
+ }
30
+ return { args, configPath, help: false };
31
+ }
32
+
33
+ function resolvePath(p) {
34
+ if (!p) return null;
35
+ if (p.startsWith("~")) {
36
+ return path.join(os.homedir(), p.slice(1));
37
+ }
38
+ if (path.isAbsolute(p)) return p;
39
+ return path.resolve(process.cwd(), p);
40
+ }
41
+
42
+ function findConfigPath(explicitPath) {
43
+ if (explicitPath) {
44
+ const resolved = resolvePath(explicitPath);
45
+ if (fs.existsSync(resolved)) return resolved;
46
+ return resolved; // let readConfig raise a helpful error
47
+ }
48
+
49
+ if (process.env.CODE_ENV_CONFIG) {
50
+ const fromEnv = resolvePath(process.env.CODE_ENV_CONFIG);
51
+ if (fs.existsSync(fromEnv)) return fromEnv;
52
+ return fromEnv;
53
+ }
54
+
55
+ const candidates = [
56
+ path.resolve(process.cwd(), "code-env.json"),
57
+ path.resolve(process.cwd(), "profiles.json"),
58
+ path.resolve(process.cwd(), "code-env.config.json"),
59
+ path.join(os.homedir(), ".config", "code-env", "config.json"),
60
+ ];
61
+
62
+ for (const p of candidates) {
63
+ if (fs.existsSync(p)) return p;
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ function findConfigPathForWrite(explicitPath) {
70
+ if (explicitPath) return resolvePath(explicitPath);
71
+ if (process.env.CODE_ENV_CONFIG) return resolvePath(process.env.CODE_ENV_CONFIG);
72
+ const existing = findConfigPath(null);
73
+ if (existing) return existing;
74
+ return path.resolve(process.cwd(), "code-env.json");
75
+ }
76
+
77
+ function readConfig(configPath) {
78
+ if (!configPath) {
79
+ throw new Error(
80
+ "No config file found. Use --config or set CODE_ENV_CONFIG."
81
+ );
82
+ }
83
+ if (!fs.existsSync(configPath)) {
84
+ throw new Error(`Config file not found: ${configPath}`);
85
+ }
86
+ const raw = fs.readFileSync(configPath, "utf8");
87
+ try {
88
+ return JSON.parse(raw);
89
+ } catch (err) {
90
+ throw new Error(`Invalid JSON in config: ${configPath}`);
91
+ }
92
+ }
93
+
94
+ function readConfigIfExists(configPath) {
95
+ if (!configPath || !fs.existsSync(configPath)) {
96
+ return { unset: [], profiles: {} };
97
+ }
98
+ return readConfig(configPath);
99
+ }
100
+
101
+ function writeConfig(configPath, config) {
102
+ if (!configPath) {
103
+ throw new Error("Missing config path for write.");
104
+ }
105
+ const dir = path.dirname(configPath);
106
+ if (!fs.existsSync(dir)) {
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ }
109
+ const data = JSON.stringify(config, null, 2);
110
+ fs.writeFileSync(configPath, `${data}\n`, "utf8");
111
+ }
112
+
113
+ function shellEscape(value) {
114
+ const str = String(value);
115
+ return `'${str.replace(/'/g, `'\\''`)}'`;
116
+ }
117
+
118
+ function expandEnv(input) {
119
+ if (!input) return input;
120
+ let out = String(input);
121
+ if (out.startsWith("~")) {
122
+ out = path.join(os.homedir(), out.slice(1));
123
+ }
124
+ out = out.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || "");
125
+ out = out.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, key) => process.env[key] || "");
126
+ return out;
127
+ }
128
+
129
+ function parseAddArgs(args) {
130
+ const result = {
131
+ profile: null,
132
+ pairs: [],
133
+ note: null,
134
+ removeFiles: [],
135
+ commands: [],
136
+ unset: [],
137
+ };
138
+
139
+ for (let i = 0; i < args.length; i++) {
140
+ const arg = args[i];
141
+ if (!result.profile && !arg.startsWith("-")) {
142
+ result.profile = arg;
143
+ continue;
144
+ }
145
+ if (arg === "-n" || arg === "--note") {
146
+ const val = args[i + 1];
147
+ if (!val) throw new Error("Missing value for --note.");
148
+ result.note = val;
149
+ i++;
150
+ continue;
151
+ }
152
+ if (arg.startsWith("--note=")) {
153
+ result.note = arg.slice("--note=".length);
154
+ continue;
155
+ }
156
+ if (arg === "-r" || arg === "--remove-file") {
157
+ const val = args[i + 1];
158
+ if (!val) throw new Error("Missing value for --remove-file.");
159
+ result.removeFiles.push(val);
160
+ i++;
161
+ continue;
162
+ }
163
+ if (arg.startsWith("--remove-file=")) {
164
+ result.removeFiles.push(arg.slice("--remove-file=".length));
165
+ continue;
166
+ }
167
+ if (arg === "-x" || arg === "--command") {
168
+ const val = args[i + 1];
169
+ if (!val) throw new Error("Missing value for --command.");
170
+ result.commands.push(val);
171
+ i++;
172
+ continue;
173
+ }
174
+ if (arg.startsWith("--command=")) {
175
+ result.commands.push(arg.slice("--command=".length));
176
+ continue;
177
+ }
178
+ if (arg === "-u" || arg === "--unset") {
179
+ const val = args[i + 1];
180
+ if (!val) throw new Error("Missing value for --unset.");
181
+ result.unset.push(val);
182
+ i++;
183
+ continue;
184
+ }
185
+ if (arg.startsWith("--unset=")) {
186
+ result.unset.push(arg.slice("--unset=".length));
187
+ continue;
188
+ }
189
+ if (arg.includes("=")) {
190
+ result.pairs.push(arg);
191
+ continue;
192
+ }
193
+ throw new Error(`Unknown add argument: ${arg}`);
194
+ }
195
+
196
+ if (!result.profile) {
197
+ throw new Error("Missing profile name.");
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ function addConfig(config, addArgs) {
204
+ if (!config.profiles || typeof config.profiles !== "object") {
205
+ config.profiles = {};
206
+ }
207
+ if (!config.profiles[addArgs.profile]) {
208
+ config.profiles[addArgs.profile] = {};
209
+ }
210
+ const profile = config.profiles[addArgs.profile];
211
+ if (!profile.env || typeof profile.env !== "object") {
212
+ profile.env = {};
213
+ }
214
+
215
+ for (const pair of addArgs.pairs) {
216
+ const idx = pair.indexOf("=");
217
+ if (idx <= 0) throw new Error(`Invalid KEY=VALUE: ${pair}`);
218
+ const key = pair.slice(0, idx);
219
+ const value = pair.slice(idx + 1);
220
+ profile.env[key] = value;
221
+ }
222
+
223
+ if (addArgs.note !== null && addArgs.note !== undefined) {
224
+ profile.note = addArgs.note;
225
+ }
226
+
227
+ if (addArgs.removeFiles.length > 0) {
228
+ if (!Array.isArray(profile.removeFiles)) profile.removeFiles = [];
229
+ for (const p of addArgs.removeFiles) {
230
+ if (!profile.removeFiles.includes(p)) profile.removeFiles.push(p);
231
+ }
232
+ }
233
+
234
+ if (addArgs.commands.length > 0) {
235
+ if (!Array.isArray(profile.commands)) profile.commands = [];
236
+ for (const cmd of addArgs.commands) {
237
+ if (!profile.commands.includes(cmd)) profile.commands.push(cmd);
238
+ }
239
+ }
240
+
241
+ if (addArgs.unset.length > 0) {
242
+ if (!Array.isArray(config.unset)) config.unset = [];
243
+ for (const key of addArgs.unset) {
244
+ if (!config.unset.includes(key)) config.unset.push(key);
245
+ }
246
+ }
247
+
248
+ return config;
249
+ }
250
+
251
+ function printList(config) {
252
+ const profiles = config && config.profiles ? config.profiles : {};
253
+ const names = Object.keys(profiles).sort();
254
+ if (names.length === 0) {
255
+ console.log("(no profiles found)");
256
+ return;
257
+ }
258
+ for (const name of names) {
259
+ const note = profiles[name] && profiles[name].note ? `\t${profiles[name].note}` : "";
260
+ console.log(`${name}${note}`);
261
+ }
262
+ }
263
+
264
+ function printShow(config, profileName) {
265
+ const profile = config.profiles && config.profiles[profileName];
266
+ if (!profile) {
267
+ throw new Error(`Unknown profile: ${profileName}`);
268
+ }
269
+ console.log(JSON.stringify(profile, null, 2));
270
+ }
271
+
272
+ function printUnset(config) {
273
+ const keys = Array.isArray(config.unset) ? config.unset : [];
274
+ if (keys.length === 0) return;
275
+ const lines = keys.map((k) => `unset ${k}`);
276
+ console.log(lines.join("\n"));
277
+ }
278
+
279
+ function printUse(config, profileName) {
280
+ const profile = config.profiles && config.profiles[profileName];
281
+ if (!profile) {
282
+ throw new Error(`Unknown profile: ${profileName}`);
283
+ }
284
+
285
+ const env = profile.env || {};
286
+ const lines = [];
287
+
288
+ if (Array.isArray(config.unset)) {
289
+ for (const key of config.unset) {
290
+ if (!Object.prototype.hasOwnProperty.call(env, key)) {
291
+ lines.push(`unset ${key}`);
292
+ }
293
+ }
294
+ }
295
+
296
+ for (const key of Object.keys(env)) {
297
+ const value = env[key];
298
+ if (value === null || value === undefined || value === "") {
299
+ lines.push(`unset ${key}`);
300
+ } else {
301
+ lines.push(`export ${key}=${shellEscape(value)}`);
302
+ }
303
+ }
304
+
305
+ if (Array.isArray(profile.removeFiles)) {
306
+ for (const p of profile.removeFiles) {
307
+ const expanded = expandEnv(p);
308
+ if (expanded) lines.push(`rm -f ${shellEscape(expanded)}`);
309
+ }
310
+ }
311
+
312
+ if (Array.isArray(profile.commands)) {
313
+ for (const cmd of profile.commands) {
314
+ if (cmd && String(cmd).trim()) lines.push(String(cmd));
315
+ }
316
+ }
317
+
318
+ console.log(lines.join("\n"));
319
+ }
320
+
321
+ function main() {
322
+ const parsed = parseArgs(process.argv.slice(2));
323
+ if (parsed.help) {
324
+ printHelp();
325
+ return;
326
+ }
327
+
328
+ const args = parsed.args || [];
329
+
330
+ if (args.length === 0) {
331
+ printHelp();
332
+ return;
333
+ }
334
+
335
+ const cmd = args[0];
336
+ try {
337
+ if (cmd === "add") {
338
+ const addArgs = parseAddArgs(args.slice(1));
339
+ const writePath = findConfigPathForWrite(parsed.configPath);
340
+ const config = readConfigIfExists(writePath);
341
+ const updated = addConfig(config, addArgs);
342
+ writeConfig(writePath, updated);
343
+ console.log(`Updated config: ${writePath}`);
344
+ return;
345
+ }
346
+
347
+ const configPath = findConfigPath(parsed.configPath);
348
+ const config = readConfig(configPath);
349
+ if (cmd === "list") {
350
+ printList(config);
351
+ return;
352
+ }
353
+ if (cmd === "use") {
354
+ const profileName = args[1];
355
+ if (!profileName) throw new Error("Missing profile name.");
356
+ printUse(config, profileName);
357
+ return;
358
+ }
359
+ if (cmd === "show") {
360
+ const profileName = args[1];
361
+ if (!profileName) throw new Error("Missing profile name.");
362
+ printShow(config, profileName);
363
+ return;
364
+ }
365
+ if (cmd === "unset") {
366
+ printUnset(config);
367
+ return;
368
+ }
369
+
370
+ throw new Error(`Unknown command: ${cmd}`);
371
+ } catch (err) {
372
+ console.error(`codenv: ${err.message}`);
373
+ process.exit(1);
374
+ }
375
+ }
376
+
377
+ main();
@@ -0,0 +1,40 @@
1
+ {
2
+ "unset": [
3
+ "OPENAI_BASE_URL",
4
+ "OPENAI_API_KEY",
5
+ "CODEX_PROVIDER",
6
+ "ANTHROPIC_API_KEY",
7
+ "CLAUDE_CODE_BASE_URL"
8
+ ],
9
+ "profiles": {
10
+ "codex-88": {
11
+ "note": "OpenAI official",
12
+ "env": {
13
+ "OPENAI_BASE_URL": "https://api.openai.com/v1",
14
+ "OPENAI_API_KEY": "sk-REPLACE_ME",
15
+ "CODEX_PROVIDER": "OpenAI"
16
+ },
17
+ "removeFiles": [
18
+ "$HOME/.config/openai/auth.json"
19
+ ]
20
+ },
21
+ "codex-mirror": {
22
+ "note": "AICodeMirror",
23
+ "env": {
24
+ "OPENAI_BASE_URL": "https://api.aicodemirror.com/api/codex/backend-api/codex",
25
+ "OPENAI_API_KEY": "sk-REPLACE_ME",
26
+ "CODEX_PROVIDER": "AICodeMirror"
27
+ },
28
+ "removeFiles": [
29
+ "$HOME/.config/openai/auth.json"
30
+ ]
31
+ },
32
+ "claude": {
33
+ "note": "Claude Code",
34
+ "env": {
35
+ "ANTHROPIC_API_KEY": "sk-REPLACE_ME",
36
+ "CLAUDE_CODE_BASE_URL": "https://api.anthropic.com"
37
+ }
38
+ }
39
+ }
40
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@praeviso/code-env-switch",
3
+ "version": "0.1.0",
4
+ "description": "Switch between Claude Code and Codex environment variables from a single CLI",
5
+ "bin": {
6
+ "codenv": "bin/codenv.js"
7
+ },
8
+ "type": "commonjs",
9
+ "license": "MIT",
10
+ "engines": {
11
+ "node": ">=16"
12
+ }
13
+ }