@praeviso/code-env-switch 0.1.1 → 0.1.3

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.
Files changed (75) hide show
  1. package/.github/workflows/npm-publish.yml +25 -0
  2. package/AGENTS.md +32 -0
  3. package/PLAN.md +33 -0
  4. package/README.md +24 -0
  5. package/README_zh.md +24 -0
  6. package/bin/cli/args.js +303 -0
  7. package/bin/cli/help.js +77 -0
  8. package/bin/cli/index.js +13 -0
  9. package/bin/commands/add.js +81 -0
  10. package/bin/commands/index.js +21 -0
  11. package/bin/commands/launch.js +330 -0
  12. package/bin/commands/list.js +57 -0
  13. package/bin/commands/show.js +10 -0
  14. package/bin/commands/statusline.js +12 -0
  15. package/bin/commands/unset.js +20 -0
  16. package/bin/commands/use.js +92 -0
  17. package/bin/config/defaults.js +85 -0
  18. package/bin/config/index.js +20 -0
  19. package/bin/config/io.js +72 -0
  20. package/bin/constants.js +27 -0
  21. package/bin/index.js +279 -0
  22. package/bin/profile/display.js +78 -0
  23. package/bin/profile/index.js +26 -0
  24. package/bin/profile/match.js +40 -0
  25. package/bin/profile/resolve.js +79 -0
  26. package/bin/profile/type.js +90 -0
  27. package/bin/shell/detect.js +40 -0
  28. package/bin/shell/index.js +18 -0
  29. package/bin/shell/snippet.js +92 -0
  30. package/bin/shell/utils.js +35 -0
  31. package/bin/statusline/claude.js +153 -0
  32. package/bin/statusline/codex.js +356 -0
  33. package/bin/statusline/index.js +631 -0
  34. package/bin/types.js +5 -0
  35. package/bin/ui/index.js +16 -0
  36. package/bin/ui/interactive.js +189 -0
  37. package/bin/ui/readline.js +76 -0
  38. package/bin/usage/index.js +832 -0
  39. package/code-env.example.json +11 -0
  40. package/package.json +2 -2
  41. package/src/cli/args.ts +318 -0
  42. package/src/cli/help.ts +75 -0
  43. package/src/cli/index.ts +5 -0
  44. package/src/commands/add.ts +91 -0
  45. package/src/commands/index.ts +10 -0
  46. package/src/commands/launch.ts +395 -0
  47. package/src/commands/list.ts +91 -0
  48. package/src/commands/show.ts +12 -0
  49. package/src/commands/statusline.ts +18 -0
  50. package/src/commands/unset.ts +19 -0
  51. package/src/commands/use.ts +121 -0
  52. package/src/config/defaults.ts +88 -0
  53. package/src/config/index.ts +19 -0
  54. package/src/config/io.ts +69 -0
  55. package/src/constants.ts +28 -0
  56. package/src/index.ts +359 -0
  57. package/src/profile/display.ts +77 -0
  58. package/src/profile/index.ts +12 -0
  59. package/src/profile/match.ts +41 -0
  60. package/src/profile/resolve.ts +84 -0
  61. package/src/profile/type.ts +83 -0
  62. package/src/shell/detect.ts +30 -0
  63. package/src/shell/index.ts +6 -0
  64. package/src/shell/snippet.ts +92 -0
  65. package/src/shell/utils.ts +30 -0
  66. package/src/statusline/claude.ts +172 -0
  67. package/src/statusline/codex.ts +393 -0
  68. package/src/statusline/index.ts +920 -0
  69. package/src/types.ts +95 -0
  70. package/src/ui/index.ts +5 -0
  71. package/src/ui/interactive.ts +220 -0
  72. package/src/ui/readline.ts +85 -0
  73. package/src/usage/index.ts +979 -0
  74. package/bin/codenv.js +0 -1316
  75. package/src/codenv.ts +0 -1478
@@ -4,6 +4,17 @@
4
4
  "codex": "primary",
5
5
  "claude": "default"
6
6
  },
7
+ "codexStatusline": {
8
+ "command": ["codenv", "statusline", "--type", "codex", "--sync-usage"],
9
+ "showHints": false,
10
+ "updateIntervalMs": 300,
11
+ "timeoutMs": 1000
12
+ },
13
+ "claudeStatusline": {
14
+ "command": "codenv statusline --type claude --sync-usage",
15
+ "type": "command",
16
+ "padding": 0
17
+ },
7
18
  "profiles": {
8
19
  "p_a1b2c3": {
9
20
  "name": "primary",
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@praeviso/code-env-switch",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Switch between Claude Code and Codex environment variables from a single CLI",
5
5
  "bin": {
6
- "codenv": "bin/codenv.js"
6
+ "codenv": "bin/index.js"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,318 @@
1
+ /**
2
+ * CLI argument parsing
3
+ */
4
+ import type {
5
+ ParsedArgs,
6
+ InitArgs,
7
+ AddArgs,
8
+ ProfileType,
9
+ StatuslineArgs,
10
+ } from "../types";
11
+ import { normalizeType } from "../profile/type";
12
+
13
+ export function parseArgs(argv: string[]): ParsedArgs {
14
+ let configPath: string | null = null;
15
+ const args: string[] = [];
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const arg = argv[i];
18
+ if (arg === "-h" || arg === "--help") {
19
+ return { args: [], configPath: null, help: true };
20
+ }
21
+ if (arg === "-c" || arg === "--config") {
22
+ configPath = argv[i + 1];
23
+ i++;
24
+ continue;
25
+ }
26
+ if (arg.startsWith("--config=")) {
27
+ configPath = arg.slice("--config=".length);
28
+ continue;
29
+ }
30
+ args.push(arg);
31
+ }
32
+ return { args, configPath, help: false };
33
+ }
34
+
35
+ export function parseInitArgs(args: string[]): InitArgs {
36
+ const result: InitArgs = { apply: true, print: false, shell: null };
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i];
39
+ if (arg === "--apply") {
40
+ result.apply = true;
41
+ result.print = false;
42
+ continue;
43
+ }
44
+ if (arg === "--print") {
45
+ result.print = true;
46
+ result.apply = false;
47
+ continue;
48
+ }
49
+ if (arg === "--shell") {
50
+ const val = args[i + 1];
51
+ if (!val) throw new Error("Missing value for --shell.");
52
+ result.shell = val;
53
+ i++;
54
+ continue;
55
+ }
56
+ if (arg.startsWith("--shell=")) {
57
+ result.shell = arg.slice("--shell=".length);
58
+ continue;
59
+ }
60
+ throw new Error(`Unknown init argument: ${arg}`);
61
+ }
62
+ return result;
63
+ }
64
+
65
+ export function parseAddArgs(args: string[]): AddArgs {
66
+ const result: AddArgs = {
67
+ profile: null,
68
+ pairs: [],
69
+ note: null,
70
+ removeFiles: [],
71
+ commands: [],
72
+ unset: [],
73
+ type: null,
74
+ };
75
+
76
+ for (let i = 0; i < args.length; i++) {
77
+ const arg = args[i];
78
+ if (!result.profile && !arg.startsWith("-")) {
79
+ result.profile = arg;
80
+ continue;
81
+ }
82
+ if (arg === "-n" || arg === "--note") {
83
+ const val = args[i + 1];
84
+ if (!val) throw new Error("Missing value for --note.");
85
+ result.note = val;
86
+ i++;
87
+ continue;
88
+ }
89
+ if (arg.startsWith("--note=")) {
90
+ result.note = arg.slice("--note=".length);
91
+ continue;
92
+ }
93
+ if (arg === "-t" || arg === "--type") {
94
+ const val = args[i + 1];
95
+ if (!val) throw new Error("Missing value for --type.");
96
+ result.type = val as ProfileType;
97
+ i++;
98
+ continue;
99
+ }
100
+ if (arg.startsWith("--type=")) {
101
+ result.type = arg.slice("--type=".length) as ProfileType;
102
+ continue;
103
+ }
104
+ if (arg === "-r" || arg === "--remove-file") {
105
+ const val = args[i + 1];
106
+ if (!val) throw new Error("Missing value for --remove-file.");
107
+ result.removeFiles.push(val);
108
+ i++;
109
+ continue;
110
+ }
111
+ if (arg.startsWith("--remove-file=")) {
112
+ result.removeFiles.push(arg.slice("--remove-file=".length));
113
+ continue;
114
+ }
115
+ if (arg === "-x" || arg === "--command") {
116
+ const val = args[i + 1];
117
+ if (!val) throw new Error("Missing value for --command.");
118
+ result.commands.push(val);
119
+ i++;
120
+ continue;
121
+ }
122
+ if (arg.startsWith("--command=")) {
123
+ result.commands.push(arg.slice("--command=".length));
124
+ continue;
125
+ }
126
+ if (arg === "-u" || arg === "--unset") {
127
+ const val = args[i + 1];
128
+ if (!val) throw new Error("Missing value for --unset.");
129
+ result.unset.push(val);
130
+ i++;
131
+ continue;
132
+ }
133
+ if (arg.startsWith("--unset=")) {
134
+ result.unset.push(arg.slice("--unset=".length));
135
+ continue;
136
+ }
137
+ if (arg.includes("=")) {
138
+ result.pairs.push(arg);
139
+ continue;
140
+ }
141
+ throw new Error(`Unknown add argument: ${arg}`);
142
+ }
143
+
144
+ if (!result.profile) {
145
+ throw new Error("Missing profile name.");
146
+ }
147
+
148
+ if (result.type) {
149
+ const normalized = normalizeType(result.type);
150
+ if (!normalized) {
151
+ throw new Error(`Unknown type: ${result.type}`);
152
+ }
153
+ result.type = normalized;
154
+ }
155
+
156
+ return result;
157
+ }
158
+
159
+ function parseNumberFlag(value: string | null | undefined, flag: string): number {
160
+ if (value === null || value === undefined || value === "") {
161
+ throw new Error(`Missing value for ${flag}.`);
162
+ }
163
+ const num = Number(value);
164
+ if (!Number.isFinite(num)) {
165
+ throw new Error(`Invalid number for ${flag}: ${value}`);
166
+ }
167
+ return num;
168
+ }
169
+
170
+ export function parseStatuslineArgs(args: string[]): StatuslineArgs {
171
+ const result: StatuslineArgs = {
172
+ format: "text",
173
+ cwd: null,
174
+ type: null,
175
+ profileKey: null,
176
+ profileName: null,
177
+ model: null,
178
+ usageToday: null,
179
+ usageTotal: null,
180
+ usageInput: null,
181
+ usageOutput: null,
182
+ syncUsage: false,
183
+ };
184
+
185
+ for (let i = 0; i < args.length; i++) {
186
+ const arg = args[i];
187
+ if (arg === "--format") {
188
+ const val = args[i + 1];
189
+ if (!val) throw new Error("Missing value for --format.");
190
+ const normalized = val.toLowerCase();
191
+ if (normalized !== "text" && normalized !== "json") {
192
+ throw new Error(`Unknown format: ${val}`);
193
+ }
194
+ result.format = normalized as "text" | "json";
195
+ i++;
196
+ continue;
197
+ }
198
+ if (arg.startsWith("--format=")) {
199
+ const val = arg.slice("--format=".length);
200
+ const normalized = val.toLowerCase();
201
+ if (normalized !== "text" && normalized !== "json") {
202
+ throw new Error(`Unknown format: ${val}`);
203
+ }
204
+ result.format = normalized as "text" | "json";
205
+ continue;
206
+ }
207
+ if (arg === "--cwd") {
208
+ const val = args[i + 1];
209
+ if (!val) throw new Error("Missing value for --cwd.");
210
+ result.cwd = val;
211
+ i++;
212
+ continue;
213
+ }
214
+ if (arg.startsWith("--cwd=")) {
215
+ result.cwd = arg.slice("--cwd=".length);
216
+ continue;
217
+ }
218
+ if (arg === "--type") {
219
+ const val = args[i + 1];
220
+ if (!val) throw new Error("Missing value for --type.");
221
+ result.type = val;
222
+ i++;
223
+ continue;
224
+ }
225
+ if (arg.startsWith("--type=")) {
226
+ result.type = arg.slice("--type=".length);
227
+ continue;
228
+ }
229
+ if (arg === "--profile-key") {
230
+ const val = args[i + 1];
231
+ if (!val) throw new Error("Missing value for --profile-key.");
232
+ result.profileKey = val;
233
+ i++;
234
+ continue;
235
+ }
236
+ if (arg.startsWith("--profile-key=")) {
237
+ result.profileKey = arg.slice("--profile-key=".length);
238
+ continue;
239
+ }
240
+ if (arg === "--profile-name") {
241
+ const val = args[i + 1];
242
+ if (!val) throw new Error("Missing value for --profile-name.");
243
+ result.profileName = val;
244
+ i++;
245
+ continue;
246
+ }
247
+ if (arg.startsWith("--profile-name=")) {
248
+ result.profileName = arg.slice("--profile-name=".length);
249
+ continue;
250
+ }
251
+ if (arg === "--model") {
252
+ const val = args[i + 1];
253
+ if (!val) throw new Error("Missing value for --model.");
254
+ result.model = val;
255
+ i++;
256
+ continue;
257
+ }
258
+ if (arg.startsWith("--model=")) {
259
+ result.model = arg.slice("--model=".length);
260
+ continue;
261
+ }
262
+ if (arg === "--usage-today") {
263
+ result.usageToday = parseNumberFlag(args[i + 1], "--usage-today");
264
+ i++;
265
+ continue;
266
+ }
267
+ if (arg.startsWith("--usage-today=")) {
268
+ result.usageToday = parseNumberFlag(
269
+ arg.slice("--usage-today=".length),
270
+ "--usage-today"
271
+ );
272
+ continue;
273
+ }
274
+ if (arg === "--usage-total") {
275
+ result.usageTotal = parseNumberFlag(args[i + 1], "--usage-total");
276
+ i++;
277
+ continue;
278
+ }
279
+ if (arg.startsWith("--usage-total=")) {
280
+ result.usageTotal = parseNumberFlag(
281
+ arg.slice("--usage-total=".length),
282
+ "--usage-total"
283
+ );
284
+ continue;
285
+ }
286
+ if (arg === "--usage-input") {
287
+ result.usageInput = parseNumberFlag(args[i + 1], "--usage-input");
288
+ i++;
289
+ continue;
290
+ }
291
+ if (arg.startsWith("--usage-input=")) {
292
+ result.usageInput = parseNumberFlag(
293
+ arg.slice("--usage-input=".length),
294
+ "--usage-input"
295
+ );
296
+ continue;
297
+ }
298
+ if (arg === "--usage-output") {
299
+ result.usageOutput = parseNumberFlag(args[i + 1], "--usage-output");
300
+ i++;
301
+ continue;
302
+ }
303
+ if (arg.startsWith("--usage-output=")) {
304
+ result.usageOutput = parseNumberFlag(
305
+ arg.slice("--usage-output=".length),
306
+ "--usage-output"
307
+ );
308
+ continue;
309
+ }
310
+ if (arg === "--sync-usage") {
311
+ result.syncUsage = true;
312
+ continue;
313
+ }
314
+ throw new Error(`Unknown statusline argument: ${arg}`);
315
+ }
316
+
317
+ return result;
318
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Help message for codenv CLI
3
+ */
4
+
5
+ export function printHelp(): void {
6
+ const msg = `codenv - switch Claude/Codex env vars
7
+
8
+ Usage:
9
+ codenv list
10
+ codenv ls
11
+ codenv config
12
+ codenv auto
13
+ codenv use
14
+ codenv use <profile>
15
+ codenv use <type> <name>
16
+ codenv show <profile>
17
+ codenv show <type> <name>
18
+ codenv default <profile>
19
+ codenv default <type> <name>
20
+ codenv default --clear
21
+ codenv remove <profile> [<profile> ...]
22
+ codenv remove <type> <name> [<type> <name> ...]
23
+ codenv remove --all
24
+ codenv unset
25
+ codenv add <profile> KEY=VALUE [KEY=VALUE ...]
26
+ codenv add
27
+ codenv launch <codex|claude> [--] [args...]
28
+ codenv init
29
+ codenv statusline [options]
30
+
31
+ Options:
32
+ -c, --config <path> Path to config JSON
33
+ -h, --help Show help
34
+
35
+ Init options:
36
+ --apply Append shell helper to your shell rc (default)
37
+ --print Print helper snippet to stdout
38
+ --shell <bash|zsh|fish> Explicitly set the target shell
39
+
40
+ Add options:
41
+ -t, --type <codex|claude> Set profile type (alias: cc)
42
+ -n, --note <text> Set profile note
43
+ -r, --remove-file <path> Add a removeFiles entry (repeat)
44
+ -x, --command <cmd> Add a commands entry (repeat)
45
+ -u, --unset <KEY> Add a global unset key (repeat)
46
+
47
+ Statusline options:
48
+ --format <text|json> Output format (default: text)
49
+ --cwd <path> Override working directory
50
+ --type <type> Set profile type
51
+ --profile-key <key> Set profile key
52
+ --profile-name <name> Set profile name
53
+ --model <model> Set model label
54
+ --usage-today <n> Set today's token usage
55
+ --usage-total <n> Set total token usage
56
+ --usage-input <n> Set input token usage
57
+ --usage-output <n> Set output token usage
58
+ --sync-usage Sync usage from sessions before reading
59
+
60
+ Examples:
61
+ codenv init
62
+ codenv use codex primary
63
+ codenv list
64
+ codenv default codex primary
65
+ codenv remove codex primary
66
+ codenv remove codex primary claude default
67
+ codenv remove --all
68
+ codenv launch codex -- --help
69
+ codenv statusline --format json
70
+ CODE_ENV_CONFIG=~/.config/code-env/config.json codenv use claude default
71
+ codenv add --type codex primary OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_API_KEY=YOUR_API_KEY
72
+ codenv add
73
+ `;
74
+ console.log(msg);
75
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI module exports
3
+ */
4
+ export { parseArgs, parseInitArgs, parseAddArgs, parseStatuslineArgs } from "./args";
5
+ export { printHelp } from "./help";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Add command - add/update profile configuration
3
+ */
4
+ import type { Config, AddArgs } from "../types";
5
+ import { findProfileKeysByName } from "../profile/match";
6
+ import { generateProfileKey } from "../profile/resolve";
7
+
8
+ export function addConfig(config: Config, addArgs: AddArgs): Config {
9
+ if (!config.profiles || typeof config.profiles !== "object") {
10
+ config.profiles = {};
11
+ }
12
+ let targetKey: string | null = null;
13
+ let matchedByName = false;
14
+
15
+ if (Object.prototype.hasOwnProperty.call(config.profiles, addArgs.profile!)) {
16
+ targetKey = addArgs.profile;
17
+ } else {
18
+ const matches = findProfileKeysByName(
19
+ config,
20
+ addArgs.profile!,
21
+ addArgs.type
22
+ );
23
+ if (matches.length === 1) {
24
+ targetKey = matches[0];
25
+ matchedByName = true;
26
+ } else if (matches.length > 1) {
27
+ const hint = addArgs.type
28
+ ? `Use profile key: ${matches.join(", ")}`
29
+ : `Use: codenv add --type <type> ${addArgs.profile} ... (or profile key: ${matches.join(
30
+ ", "
31
+ )})`;
32
+ throw new Error(`Multiple profiles named "${addArgs.profile}". ${hint}`);
33
+ }
34
+ }
35
+
36
+ if (!targetKey) {
37
+ targetKey = generateProfileKey(config);
38
+ matchedByName = true;
39
+ }
40
+
41
+ if (!config.profiles[targetKey]) {
42
+ config.profiles[targetKey] = {};
43
+ }
44
+ const profile = config.profiles[targetKey];
45
+ if (!profile.env || typeof profile.env !== "object") {
46
+ profile.env = {};
47
+ }
48
+
49
+ if (matchedByName) {
50
+ profile.name = addArgs.profile!;
51
+ }
52
+
53
+ if (addArgs.type) {
54
+ profile.type = addArgs.type;
55
+ }
56
+
57
+ for (const pair of addArgs.pairs) {
58
+ const idx = pair.indexOf("=");
59
+ if (idx <= 0) throw new Error(`Invalid KEY=VALUE: ${pair}`);
60
+ const key = pair.slice(0, idx);
61
+ const value = pair.slice(idx + 1);
62
+ profile.env[key] = value;
63
+ }
64
+
65
+ if (addArgs.note !== null && addArgs.note !== undefined) {
66
+ profile.note = addArgs.note;
67
+ }
68
+
69
+ if (addArgs.removeFiles.length > 0) {
70
+ if (!Array.isArray(profile.removeFiles)) profile.removeFiles = [];
71
+ for (const p of addArgs.removeFiles) {
72
+ if (!profile.removeFiles.includes(p)) profile.removeFiles.push(p);
73
+ }
74
+ }
75
+
76
+ if (addArgs.commands.length > 0) {
77
+ if (!Array.isArray(profile.commands)) profile.commands = [];
78
+ for (const cmd of addArgs.commands) {
79
+ if (!profile.commands.includes(cmd)) profile.commands.push(cmd);
80
+ }
81
+ }
82
+
83
+ if (addArgs.unset.length > 0) {
84
+ if (!Array.isArray(config.unset)) config.unset = [];
85
+ for (const key of addArgs.unset) {
86
+ if (!config.unset.includes(key)) config.unset.push(key);
87
+ }
88
+ }
89
+
90
+ return config;
91
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Commands module exports
3
+ */
4
+ export { buildUseLines, printUse } from "./use";
5
+ export { printList } from "./list";
6
+ export { addConfig } from "./add";
7
+ export { printShow } from "./show";
8
+ export { printUnset } from "./unset";
9
+ export { runLaunch } from "./launch";
10
+ export { printStatusline } from "./statusline";