@praeviso/code-env-switch 0.1.1 → 0.1.2
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/.github/workflows/npm-publish.yml +25 -0
- package/AGENTS.md +32 -0
- package/PLAN.md +33 -0
- package/README.md +24 -0
- package/README_zh.md +24 -0
- package/bin/cli/args.js +303 -0
- package/bin/cli/help.js +77 -0
- package/bin/cli/index.js +13 -0
- package/bin/commands/add.js +81 -0
- package/bin/commands/index.js +21 -0
- package/bin/commands/launch.js +330 -0
- package/bin/commands/list.js +57 -0
- package/bin/commands/show.js +10 -0
- package/bin/commands/statusline.js +12 -0
- package/bin/commands/unset.js +20 -0
- package/bin/commands/use.js +92 -0
- package/bin/config/defaults.js +85 -0
- package/bin/config/index.js +20 -0
- package/bin/config/io.js +72 -0
- package/bin/constants.js +27 -0
- package/bin/index.js +279 -0
- package/bin/profile/display.js +78 -0
- package/bin/profile/index.js +26 -0
- package/bin/profile/match.js +40 -0
- package/bin/profile/resolve.js +79 -0
- package/bin/profile/type.js +90 -0
- package/bin/shell/detect.js +40 -0
- package/bin/shell/index.js +18 -0
- package/bin/shell/snippet.js +92 -0
- package/bin/shell/utils.js +35 -0
- package/bin/statusline/claude.js +153 -0
- package/bin/statusline/codex.js +356 -0
- package/bin/statusline/index.js +469 -0
- package/bin/types.js +5 -0
- package/bin/ui/index.js +16 -0
- package/bin/ui/interactive.js +189 -0
- package/bin/ui/readline.js +76 -0
- package/bin/usage/index.js +709 -0
- package/code-env.example.json +11 -0
- package/package.json +2 -2
- package/src/cli/args.ts +318 -0
- package/src/cli/help.ts +75 -0
- package/src/cli/index.ts +5 -0
- package/src/commands/add.ts +91 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/launch.ts +395 -0
- package/src/commands/list.ts +91 -0
- package/src/commands/show.ts +12 -0
- package/src/commands/statusline.ts +18 -0
- package/src/commands/unset.ts +19 -0
- package/src/commands/use.ts +121 -0
- package/src/config/defaults.ts +88 -0
- package/src/config/index.ts +19 -0
- package/src/config/io.ts +69 -0
- package/src/constants.ts +28 -0
- package/src/index.ts +359 -0
- package/src/profile/display.ts +77 -0
- package/src/profile/index.ts +12 -0
- package/src/profile/match.ts +41 -0
- package/src/profile/resolve.ts +84 -0
- package/src/profile/type.ts +83 -0
- package/src/shell/detect.ts +30 -0
- package/src/shell/index.ts +6 -0
- package/src/shell/snippet.ts +92 -0
- package/src/shell/utils.ts +30 -0
- package/src/statusline/claude.ts +172 -0
- package/src/statusline/codex.ts +393 -0
- package/src/statusline/index.ts +626 -0
- package/src/types.ts +95 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/interactive.ts +220 -0
- package/src/ui/readline.ts +85 -0
- package/src/usage/index.ts +833 -0
- package/bin/codenv.js +0 -1316
- package/src/codenv.ts +0 -1478
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive UI components
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import type { Config, ProfileType } from "../types";
|
|
6
|
+
import { readConfigIfExists, writeConfig, getResolvedDefaultProfileKeys } from "../config";
|
|
7
|
+
import { generateProfileKey } from "../profile/resolve";
|
|
8
|
+
import { normalizeType, inferProfileType } from "../profile/type";
|
|
9
|
+
import { buildListRows } from "../profile/display";
|
|
10
|
+
import { createReadline, askRequired, askType, askProfileName } from "./readline";
|
|
11
|
+
|
|
12
|
+
export async function runInteractiveAdd(configPath: string): Promise<void> {
|
|
13
|
+
const config = readConfigIfExists(configPath);
|
|
14
|
+
const rl = createReadline();
|
|
15
|
+
try {
|
|
16
|
+
const type = await askType(rl);
|
|
17
|
+
const defaultName = "default";
|
|
18
|
+
const profileInfo = await askProfileName(rl, config, defaultName, type);
|
|
19
|
+
const profileKey = profileInfo.key || generateProfileKey(config);
|
|
20
|
+
const baseUrl = await askRequired(rl, "Base URL (required): ");
|
|
21
|
+
const apiKey = await askRequired(rl, "API key (required): ");
|
|
22
|
+
|
|
23
|
+
if (!config.profiles || typeof config.profiles !== "object") {
|
|
24
|
+
config.profiles = {};
|
|
25
|
+
}
|
|
26
|
+
if (!config.profiles[profileKey]) {
|
|
27
|
+
config.profiles[profileKey] = {};
|
|
28
|
+
}
|
|
29
|
+
const profile = config.profiles[profileKey];
|
|
30
|
+
profile.name = profileInfo.name;
|
|
31
|
+
profile.type = type;
|
|
32
|
+
if (!profile.env || typeof profile.env !== "object") {
|
|
33
|
+
profile.env = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (type === "codex") {
|
|
37
|
+
profile.env.OPENAI_BASE_URL = baseUrl;
|
|
38
|
+
profile.env.OPENAI_API_KEY = apiKey;
|
|
39
|
+
} else {
|
|
40
|
+
profile.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
41
|
+
profile.env.ANTHROPIC_API_KEY = apiKey;
|
|
42
|
+
console.log(
|
|
43
|
+
"Note: ANTHROPIC_AUTH_TOKEN will be set to the same value when applying."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
writeConfig(configPath, config);
|
|
48
|
+
console.log(`Updated config: ${configPath}`);
|
|
49
|
+
} finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runInteractiveUse(
|
|
55
|
+
config: Config,
|
|
56
|
+
printUse: (config: Config, profileName: string, requestedType: ProfileType | null) => void
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
59
|
+
throw new Error("Interactive selection requires a TTY. Provide a profile name.");
|
|
60
|
+
}
|
|
61
|
+
const rows = buildListRows(config, getResolvedDefaultProfileKeys);
|
|
62
|
+
if (rows.length === 0) {
|
|
63
|
+
throw new Error("No profiles found.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nameTypeCounts = new Map<string, number>();
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const key = `${row.name}||${row.type}`;
|
|
69
|
+
nameTypeCounts.set(key, (nameTypeCounts.get(key) || 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const displayRows = rows.map((row) => {
|
|
73
|
+
const key = `${row.name}||${row.type}`;
|
|
74
|
+
const displayName =
|
|
75
|
+
(nameTypeCounts.get(key) || 0) > 1 ? `${row.name} [${row.key}]` : row.name;
|
|
76
|
+
const noteText = row.note;
|
|
77
|
+
const profile = config.profiles && config.profiles[row.key];
|
|
78
|
+
const inferredType = inferProfileType(row.key, profile, null);
|
|
79
|
+
const resolvedType = inferredType || normalizeType(row.type) || null;
|
|
80
|
+
return { ...row, displayName, noteText, resolvedType };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const headerName = "PROFILE";
|
|
84
|
+
const headerType = "TYPE";
|
|
85
|
+
const headerNote = "NOTE";
|
|
86
|
+
const nameWidth = Math.max(
|
|
87
|
+
headerName.length,
|
|
88
|
+
...displayRows.map((row) => row.displayName.length)
|
|
89
|
+
);
|
|
90
|
+
const typeWidth = Math.max(
|
|
91
|
+
headerType.length,
|
|
92
|
+
...displayRows.map((row) => row.type.length)
|
|
93
|
+
);
|
|
94
|
+
const noteWidth = Math.max(
|
|
95
|
+
headerNote.length,
|
|
96
|
+
...displayRows.map((row) => row.noteText.length)
|
|
97
|
+
);
|
|
98
|
+
const formatRow = (name: string, type: string, note: string) =>
|
|
99
|
+
`${name.padEnd(nameWidth)} ${type.padEnd(typeWidth)} ${note.padEnd(
|
|
100
|
+
noteWidth
|
|
101
|
+
)}`;
|
|
102
|
+
|
|
103
|
+
const activeKeys = new Set<string>();
|
|
104
|
+
const keyToType = new Map<string, ProfileType | null>();
|
|
105
|
+
for (const row of displayRows) {
|
|
106
|
+
keyToType.set(row.key, row.resolvedType || null);
|
|
107
|
+
if (row.active) activeKeys.add(row.key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let index = displayRows.findIndex((row) => row.active);
|
|
111
|
+
if (index < 0) index = 0;
|
|
112
|
+
|
|
113
|
+
const ANSI_CLEAR = "\x1b[2J\x1b[H";
|
|
114
|
+
const ANSI_HIDE_CURSOR = "\x1b[?25l";
|
|
115
|
+
const ANSI_SHOW_CURSOR = "\x1b[?25h";
|
|
116
|
+
const ANSI_INVERT = "\x1b[7m";
|
|
117
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
118
|
+
const ANSI_RESET = "\x1b[0m";
|
|
119
|
+
|
|
120
|
+
const render = () => {
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
lines.push("Select profile (up/down, Enter to apply, q to exit)");
|
|
123
|
+
lines.push(formatRow(headerName, headerType, headerNote));
|
|
124
|
+
lines.push(
|
|
125
|
+
formatRow(
|
|
126
|
+
"-".repeat(nameWidth),
|
|
127
|
+
"-".repeat(typeWidth),
|
|
128
|
+
"-".repeat(noteWidth)
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
for (let i = 0; i < displayRows.length; i++) {
|
|
132
|
+
const row = displayRows[i];
|
|
133
|
+
const isActive = activeKeys.has(row.key);
|
|
134
|
+
const line = ` ${formatRow(row.displayName, row.type, row.noteText)}`;
|
|
135
|
+
if (i === index) {
|
|
136
|
+
const prefix = isActive ? `${ANSI_INVERT}${ANSI_GREEN}` : ANSI_INVERT;
|
|
137
|
+
lines.push(`${prefix}${line}${ANSI_RESET}`);
|
|
138
|
+
} else {
|
|
139
|
+
if (isActive) {
|
|
140
|
+
lines.push(`${ANSI_GREEN}${line}${ANSI_RESET}`);
|
|
141
|
+
} else {
|
|
142
|
+
lines.push(line);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
process.stderr.write(`${ANSI_CLEAR}${ANSI_HIDE_CURSOR}${lines.join("\n")}\n`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return await new Promise<void>((resolve) => {
|
|
150
|
+
readline.emitKeypressEvents(process.stdin);
|
|
151
|
+
const stdin = process.stdin;
|
|
152
|
+
const wasRaw = !!stdin.isRaw;
|
|
153
|
+
stdin.setRawMode(true);
|
|
154
|
+
stdin.resume();
|
|
155
|
+
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
stdin.removeListener("keypress", onKeypress);
|
|
158
|
+
if (!wasRaw) stdin.setRawMode(false);
|
|
159
|
+
stdin.pause();
|
|
160
|
+
process.stderr.write(`${ANSI_RESET}${ANSI_SHOW_CURSOR}`);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const finish = () => {
|
|
164
|
+
cleanup();
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const onKeypress = (str: string, key: readline.Key | undefined) => {
|
|
169
|
+
if (key && key.ctrl && key.name === "c") {
|
|
170
|
+
finish();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key && key.name === "up") {
|
|
174
|
+
index = (index - 1 + displayRows.length) % displayRows.length;
|
|
175
|
+
render();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key && key.name === "down") {
|
|
179
|
+
index = (index + 1) % displayRows.length;
|
|
180
|
+
render();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key && key.name === "home") {
|
|
184
|
+
index = 0;
|
|
185
|
+
render();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (key && key.name === "end") {
|
|
189
|
+
index = displayRows.length - 1;
|
|
190
|
+
render();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
194
|
+
const selectedKey = displayRows[index].key;
|
|
195
|
+
const selectedType = keyToType.get(selectedKey) || null;
|
|
196
|
+
if (selectedType) {
|
|
197
|
+
for (const activeKey of Array.from(activeKeys)) {
|
|
198
|
+
if (keyToType.get(activeKey) === selectedType) {
|
|
199
|
+
activeKeys.delete(activeKey);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
activeKeys.add(selectedKey);
|
|
204
|
+
printUse(config, selectedKey, null);
|
|
205
|
+
render();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (key && key.name === "escape") {
|
|
209
|
+
finish();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (str === "q" || str === "Q") {
|
|
213
|
+
finish();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
stdin.on("keypress", onKeypress);
|
|
218
|
+
render();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readline utilities
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import type { Config, ProfileType } from "../types";
|
|
6
|
+
import { findProfileKeysByName } from "../profile/match";
|
|
7
|
+
|
|
8
|
+
export function createReadline(): readline.Interface {
|
|
9
|
+
return readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(question, (answer) => resolve(answer));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function askRequired(rl: readline.Interface, question: string): Promise<string> {
|
|
22
|
+
while (true) {
|
|
23
|
+
const answer = String(await ask(rl, question)).trim();
|
|
24
|
+
if (answer) return answer;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function askConfirm(rl: readline.Interface, question: string): Promise<boolean> {
|
|
29
|
+
const answer = String(await ask(rl, question)).trim().toLowerCase();
|
|
30
|
+
return answer === "y" || answer === "yes";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function askType(rl: readline.Interface): Promise<ProfileType> {
|
|
34
|
+
while (true) {
|
|
35
|
+
const answer = String(
|
|
36
|
+
await ask(rl, "Select type (1=codex, 2=claude): ")
|
|
37
|
+
)
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase();
|
|
40
|
+
if (answer === "1") return "codex";
|
|
41
|
+
if (answer === "2") return "claude";
|
|
42
|
+
const normalized = answer.replace(/[\s-]+/g, "");
|
|
43
|
+
if (normalized === "codex") return "codex";
|
|
44
|
+
if (
|
|
45
|
+
normalized === "claude" ||
|
|
46
|
+
normalized === "claudecode" ||
|
|
47
|
+
normalized === "cc"
|
|
48
|
+
)
|
|
49
|
+
return "claude";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function askProfileName(
|
|
54
|
+
rl: readline.Interface,
|
|
55
|
+
config: Config,
|
|
56
|
+
defaultName: string,
|
|
57
|
+
type: ProfileType
|
|
58
|
+
): Promise<{ name: string; key: string | null }> {
|
|
59
|
+
while (true) {
|
|
60
|
+
const answer = String(
|
|
61
|
+
await ask(rl, `Profile name (default: ${defaultName}): `)
|
|
62
|
+
).trim();
|
|
63
|
+
const baseName = answer || defaultName;
|
|
64
|
+
if (!baseName) continue;
|
|
65
|
+
const matches = findProfileKeysByName(config, baseName, type);
|
|
66
|
+
if (matches.length === 0) {
|
|
67
|
+
return { name: baseName, key: null };
|
|
68
|
+
}
|
|
69
|
+
if (matches.length === 1) {
|
|
70
|
+
const overwrite = String(
|
|
71
|
+
await ask(rl, `Profile "${baseName}" exists. Overwrite? (y/N): `)
|
|
72
|
+
)
|
|
73
|
+
.trim()
|
|
74
|
+
.toLowerCase();
|
|
75
|
+
if (overwrite === "y" || overwrite === "yes") {
|
|
76
|
+
return { name: baseName, key: matches[0] };
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
console.log(
|
|
81
|
+
`Multiple profiles named "${baseName}" for type "${type}". ` +
|
|
82
|
+
`Use a unique name or update by key in config.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|