@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default profile handling
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, ProfileType, DefaultProfiles } from "../types";
|
|
5
|
+
import { DEFAULT_PROFILE_TYPES, DEFAULT_UNSET_KEYS } from "../constants";
|
|
6
|
+
import { normalizeType } from "../profile/type";
|
|
7
|
+
import { resolveProfileName } from "../profile/resolve";
|
|
8
|
+
|
|
9
|
+
export function getDefaultProfiles(config: Config): DefaultProfiles {
|
|
10
|
+
const defaults: DefaultProfiles = {};
|
|
11
|
+
if (!config || typeof config !== "object") return defaults;
|
|
12
|
+
if (!config.defaultProfiles || typeof config.defaultProfiles !== "object") {
|
|
13
|
+
return defaults;
|
|
14
|
+
}
|
|
15
|
+
for (const [rawType, rawValue] of Object.entries(config.defaultProfiles)) {
|
|
16
|
+
const type = normalizeType(rawType);
|
|
17
|
+
if (!type) continue;
|
|
18
|
+
const trimmed = String(rawValue ?? "").trim();
|
|
19
|
+
if (trimmed) defaults[type] = trimmed;
|
|
20
|
+
}
|
|
21
|
+
return defaults;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deleteDefaultProfileEntry(config: Config, type: ProfileType): boolean {
|
|
25
|
+
if (!config.defaultProfiles || typeof config.defaultProfiles !== "object") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
let changed = false;
|
|
29
|
+
for (const key of Object.keys(config.defaultProfiles)) {
|
|
30
|
+
if (normalizeType(key) === type) {
|
|
31
|
+
delete config.defaultProfiles[key];
|
|
32
|
+
changed = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return changed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveDefaultProfileForType(
|
|
39
|
+
config: Config,
|
|
40
|
+
type: ProfileType,
|
|
41
|
+
value: string
|
|
42
|
+
): string | null {
|
|
43
|
+
const trimmed = String(value ?? "").trim();
|
|
44
|
+
if (!trimmed) return null;
|
|
45
|
+
const params = trimmed.split(/\s+/).filter(Boolean);
|
|
46
|
+
if (params.length === 0) return null;
|
|
47
|
+
const explicitType = normalizeType(params[0]);
|
|
48
|
+
if (explicitType) {
|
|
49
|
+
if (explicitType !== type) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Default profile for "${type}" must match type "${type}".`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return resolveProfileName(config, params);
|
|
55
|
+
}
|
|
56
|
+
return resolveProfileName(config, [type, ...params]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getResolvedDefaultProfileKeys(config: Config): DefaultProfiles {
|
|
60
|
+
const defaults = getDefaultProfiles(config);
|
|
61
|
+
const resolved: DefaultProfiles = {};
|
|
62
|
+
for (const type of DEFAULT_PROFILE_TYPES) {
|
|
63
|
+
const value = defaults[type];
|
|
64
|
+
if (!value) continue;
|
|
65
|
+
try {
|
|
66
|
+
const profileName = resolveDefaultProfileForType(config, type, value);
|
|
67
|
+
if (profileName) resolved[type] = profileName;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// ignore invalid defaults for list output
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getTypeDefaultUnsetKeys(type: ProfileType): string[] {
|
|
76
|
+
return DEFAULT_UNSET_KEYS[type] || [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getFilteredUnsetKeys(config: Config, activeType: ProfileType | null): string[] {
|
|
80
|
+
const keys = Array.isArray(config.unset) ? config.unset : [];
|
|
81
|
+
if (!activeType) return [...keys];
|
|
82
|
+
const otherDefaults = new Set(
|
|
83
|
+
DEFAULT_PROFILE_TYPES.filter((type) => type !== activeType).flatMap(
|
|
84
|
+
(type) => DEFAULT_UNSET_KEYS[type]
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
return keys.filter((key) => !otherDefaults.has(key));
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config module exports
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
getDefaultConfigPath,
|
|
6
|
+
findConfigPath,
|
|
7
|
+
findConfigPathForWrite,
|
|
8
|
+
readConfig,
|
|
9
|
+
readConfigIfExists,
|
|
10
|
+
writeConfig,
|
|
11
|
+
} from "./io";
|
|
12
|
+
export {
|
|
13
|
+
getDefaultProfiles,
|
|
14
|
+
deleteDefaultProfileEntry,
|
|
15
|
+
resolveDefaultProfileForType,
|
|
16
|
+
getResolvedDefaultProfileKeys,
|
|
17
|
+
getTypeDefaultUnsetKeys,
|
|
18
|
+
getFilteredUnsetKeys,
|
|
19
|
+
} from "./defaults";
|
package/src/config/io.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config I/O utilities
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import type { Config } from "../types";
|
|
8
|
+
import { resolvePath } from "../shell/utils";
|
|
9
|
+
|
|
10
|
+
export function getDefaultConfigPath(): string {
|
|
11
|
+
return path.join(os.homedir(), ".config", "code-env", "config.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function findConfigPath(explicitPath: string | null): string | null {
|
|
15
|
+
if (explicitPath) {
|
|
16
|
+
const resolved = resolvePath(explicitPath);
|
|
17
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
18
|
+
return resolved; // let readConfig raise a helpful error
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (process.env.CODE_ENV_CONFIG) {
|
|
22
|
+
const fromEnv = resolvePath(process.env.CODE_ENV_CONFIG);
|
|
23
|
+
if (fs.existsSync(fromEnv)) return fromEnv;
|
|
24
|
+
return fromEnv;
|
|
25
|
+
}
|
|
26
|
+
return getDefaultConfigPath();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findConfigPathForWrite(explicitPath: string | null): string {
|
|
30
|
+
if (explicitPath) return resolvePath(explicitPath)!;
|
|
31
|
+
if (process.env.CODE_ENV_CONFIG) return resolvePath(process.env.CODE_ENV_CONFIG)!;
|
|
32
|
+
return getDefaultConfigPath();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readConfig(configPath: string): Config {
|
|
36
|
+
if (!configPath) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"No config file found. Use --config or set CODE_ENV_CONFIG."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!fs.existsSync(configPath)) {
|
|
42
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
43
|
+
}
|
|
44
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(`Invalid JSON in config: ${configPath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readConfigIfExists(configPath: string | null): Config {
|
|
53
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
54
|
+
return { unset: [], profiles: {} };
|
|
55
|
+
}
|
|
56
|
+
return readConfig(configPath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeConfig(configPath: string, config: Config): void {
|
|
60
|
+
if (!configPath) {
|
|
61
|
+
throw new Error("Missing config path for write.");
|
|
62
|
+
}
|
|
63
|
+
const dir = path.dirname(configPath);
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
const data = JSON.stringify(config, null, 2);
|
|
68
|
+
fs.writeFileSync(configPath, `${data}\n`, "utf8");
|
|
69
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for codenv
|
|
3
|
+
*/
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import type { ProfileType } from "./types";
|
|
7
|
+
|
|
8
|
+
export const CODEX_AUTH_PATH = path.join(os.homedir(), ".codex", "auth.json");
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_PROFILE_TYPES: ProfileType[] = ["codex", "claude"];
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_UNSET_KEYS: Record<ProfileType, string[]> = {
|
|
13
|
+
codex: [
|
|
14
|
+
"OPENAI_BASE_URL",
|
|
15
|
+
"OPENAI_API_KEY",
|
|
16
|
+
"CODE_ENV_PROFILE_KEY_CODEX",
|
|
17
|
+
"CODE_ENV_PROFILE_NAME_CODEX",
|
|
18
|
+
"CODE_ENV_CONFIG_PATH",
|
|
19
|
+
],
|
|
20
|
+
claude: [
|
|
21
|
+
"ANTHROPIC_BASE_URL",
|
|
22
|
+
"ANTHROPIC_API_KEY",
|
|
23
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
24
|
+
"CODE_ENV_PROFILE_KEY_CLAUDE",
|
|
25
|
+
"CODE_ENV_PROFILE_NAME_CLAUDE",
|
|
26
|
+
"CODE_ENV_CONFIG_PATH",
|
|
27
|
+
],
|
|
28
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* codenv - switch Claude/Codex env vars
|
|
4
|
+
* Main entry point
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import type { Config, ProfileType } from "./types";
|
|
8
|
+
import { DEFAULT_PROFILE_TYPES } from "./constants";
|
|
9
|
+
import {
|
|
10
|
+
parseArgs,
|
|
11
|
+
parseInitArgs,
|
|
12
|
+
parseAddArgs,
|
|
13
|
+
parseStatuslineArgs,
|
|
14
|
+
printHelp,
|
|
15
|
+
} from "./cli";
|
|
16
|
+
import { detectShell, getShellRcPath, getShellSnippet, upsertShellSnippet } from "./shell";
|
|
17
|
+
import {
|
|
18
|
+
findConfigPath,
|
|
19
|
+
findConfigPathForWrite,
|
|
20
|
+
readConfig,
|
|
21
|
+
readConfigIfExists,
|
|
22
|
+
writeConfig,
|
|
23
|
+
getDefaultProfiles,
|
|
24
|
+
deleteDefaultProfileEntry,
|
|
25
|
+
resolveDefaultProfileForType,
|
|
26
|
+
} from "./config";
|
|
27
|
+
import { normalizeType, inferProfileType, resolveProfileName } from "./profile";
|
|
28
|
+
import {
|
|
29
|
+
addConfig,
|
|
30
|
+
printList,
|
|
31
|
+
printShow,
|
|
32
|
+
printUse,
|
|
33
|
+
printUnset,
|
|
34
|
+
runLaunch,
|
|
35
|
+
printStatusline,
|
|
36
|
+
} from "./commands";
|
|
37
|
+
import { logProfileUse } from "./usage";
|
|
38
|
+
import { createReadline, askConfirm, runInteractiveAdd, runInteractiveUse } from "./ui";
|
|
39
|
+
|
|
40
|
+
function getErrorMessage(err: unknown): string {
|
|
41
|
+
return err instanceof Error ? err.message : String(err);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
46
|
+
if (parsed.help) {
|
|
47
|
+
printHelp();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const args = parsed.args || [];
|
|
52
|
+
|
|
53
|
+
if (args.length === 0) {
|
|
54
|
+
printHelp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cmd = args[0];
|
|
59
|
+
try {
|
|
60
|
+
if (cmd === "init") {
|
|
61
|
+
const initArgs = parseInitArgs(args.slice(1));
|
|
62
|
+
const shellName = detectShell(initArgs.shell);
|
|
63
|
+
if (!shellName) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Unknown shell. Use --shell <bash|zsh|fish> to specify."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const snippet = getShellSnippet(shellName);
|
|
69
|
+
if (initArgs.apply) {
|
|
70
|
+
const rcPath = getShellRcPath(shellName);
|
|
71
|
+
upsertShellSnippet(rcPath!, snippet);
|
|
72
|
+
console.log(`Updated shell config: ${rcPath}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(snippet);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === "add") {
|
|
80
|
+
const writePath = findConfigPathForWrite(parsed.configPath);
|
|
81
|
+
const addArgsRaw = args.slice(1);
|
|
82
|
+
const hasInteractive = addArgsRaw.length === 0;
|
|
83
|
+
if (hasInteractive) {
|
|
84
|
+
await runInteractiveAdd(writePath);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const addArgsResult = parseAddArgs(addArgsRaw);
|
|
88
|
+
const config = readConfigIfExists(writePath);
|
|
89
|
+
const updated = addConfig(config, addArgsResult);
|
|
90
|
+
writeConfig(writePath, updated);
|
|
91
|
+
console.log(`Updated config: ${writePath}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cmd === "auto") {
|
|
96
|
+
const configPath = findConfigPath(parsed.configPath);
|
|
97
|
+
if (!configPath || !fs.existsSync(configPath)) return;
|
|
98
|
+
const config = readConfig(configPath);
|
|
99
|
+
const defaults = getDefaultProfiles(config);
|
|
100
|
+
const hasDefaults = DEFAULT_PROFILE_TYPES.some((type) => defaults[type]);
|
|
101
|
+
if (!hasDefaults) return;
|
|
102
|
+
|
|
103
|
+
let includeGlobalUnset = true;
|
|
104
|
+
for (const type of DEFAULT_PROFILE_TYPES) {
|
|
105
|
+
const value = defaults[type];
|
|
106
|
+
if (!value) continue;
|
|
107
|
+
try {
|
|
108
|
+
const profileName = resolveDefaultProfileForType(
|
|
109
|
+
config,
|
|
110
|
+
type,
|
|
111
|
+
value
|
|
112
|
+
);
|
|
113
|
+
if (!profileName) continue;
|
|
114
|
+
logProfileUse(
|
|
115
|
+
config,
|
|
116
|
+
configPath,
|
|
117
|
+
profileName,
|
|
118
|
+
type,
|
|
119
|
+
process.env.CODE_ENV_TERMINAL_TAG || null,
|
|
120
|
+
process.cwd()
|
|
121
|
+
);
|
|
122
|
+
printUse(config, profileName, type, includeGlobalUnset, configPath);
|
|
123
|
+
includeGlobalUnset = false;
|
|
124
|
+
} catch (err: unknown) {
|
|
125
|
+
console.error(`codenv: ${getErrorMessage(err)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (cmd === "launch") {
|
|
132
|
+
const params = args.slice(1);
|
|
133
|
+
if (params.length === 0) {
|
|
134
|
+
throw new Error("Missing launch target.");
|
|
135
|
+
}
|
|
136
|
+
const target = params[0];
|
|
137
|
+
const passArgs = params.slice(1);
|
|
138
|
+
if (passArgs[0] === "--") passArgs.shift();
|
|
139
|
+
const configPath =
|
|
140
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
141
|
+
const config = readConfigIfExists(configPath);
|
|
142
|
+
const exitCode = await runLaunch(config, configPath, target, passArgs);
|
|
143
|
+
process.exit(exitCode);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (cmd === "statusline") {
|
|
147
|
+
const statuslineArgs = parseStatuslineArgs(args.slice(1));
|
|
148
|
+
const configPath =
|
|
149
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
150
|
+
const config = readConfigIfExists(configPath);
|
|
151
|
+
printStatusline(config, configPath, statuslineArgs);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const configPath = findConfigPath(parsed.configPath);
|
|
156
|
+
const config = readConfig(configPath!);
|
|
157
|
+
|
|
158
|
+
if (cmd === "default") {
|
|
159
|
+
const params = args.slice(1);
|
|
160
|
+
if (params.length === 0) {
|
|
161
|
+
throw new Error("Missing profile name.");
|
|
162
|
+
}
|
|
163
|
+
const clear =
|
|
164
|
+
params.length === 1 &&
|
|
165
|
+
(params[0] === "--clear" || params[0] === "--unset");
|
|
166
|
+
if (clear) {
|
|
167
|
+
const rl = createReadline();
|
|
168
|
+
try {
|
|
169
|
+
const confirmed = await askConfirm(
|
|
170
|
+
rl,
|
|
171
|
+
"Clear all default profiles? (y/N): "
|
|
172
|
+
);
|
|
173
|
+
if (!confirmed) return;
|
|
174
|
+
} finally {
|
|
175
|
+
rl.close();
|
|
176
|
+
}
|
|
177
|
+
let changed = false;
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(config, "defaultProfiles")) {
|
|
179
|
+
delete config.defaultProfiles;
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
if (changed) {
|
|
183
|
+
writeConfig(configPath!, config);
|
|
184
|
+
console.log(`Updated config: ${configPath}`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const requestedType =
|
|
189
|
+
params.length >= 2 ? normalizeType(params[0]) : null;
|
|
190
|
+
const profileName = resolveProfileName(config, params);
|
|
191
|
+
let targetType = requestedType;
|
|
192
|
+
if (!targetType) {
|
|
193
|
+
const profile = config.profiles && config.profiles[profileName];
|
|
194
|
+
targetType = inferProfileType(profileName, profile, null);
|
|
195
|
+
}
|
|
196
|
+
if (!targetType) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"Unable to infer profile type. Use: codenv default <type> <name>."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!config.defaultProfiles || typeof config.defaultProfiles !== "object") {
|
|
202
|
+
config.defaultProfiles = {};
|
|
203
|
+
}
|
|
204
|
+
config.defaultProfiles[targetType] = profileName;
|
|
205
|
+
writeConfig(configPath!, config);
|
|
206
|
+
console.log(`Updated config: ${configPath}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (cmd === "remove") {
|
|
211
|
+
const params = args.slice(1);
|
|
212
|
+
if (params.length === 0) {
|
|
213
|
+
throw new Error("Missing profile name.");
|
|
214
|
+
}
|
|
215
|
+
const isAll = params.length === 1 && params[0] === "--all";
|
|
216
|
+
if (isAll) {
|
|
217
|
+
if (!config.profiles || typeof config.profiles !== "object") {
|
|
218
|
+
config.profiles = {};
|
|
219
|
+
} else {
|
|
220
|
+
config.profiles = {};
|
|
221
|
+
}
|
|
222
|
+
if (Object.prototype.hasOwnProperty.call(config, "defaultProfiles")) {
|
|
223
|
+
delete config.defaultProfiles;
|
|
224
|
+
}
|
|
225
|
+
writeConfig(configPath!, config);
|
|
226
|
+
console.log(`Updated config: ${configPath}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const targets: string[] = [];
|
|
231
|
+
const allPairs =
|
|
232
|
+
params.length >= 2 &&
|
|
233
|
+
params.length % 2 === 0 &&
|
|
234
|
+
params.every((value, idx) =>
|
|
235
|
+
idx % 2 === 0 ? normalizeType(value) : true
|
|
236
|
+
);
|
|
237
|
+
if (allPairs) {
|
|
238
|
+
for (let i = 0; i < params.length; i += 2) {
|
|
239
|
+
targets.push(resolveProfileName(config, params.slice(i, i + 2)));
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
for (const param of params) {
|
|
243
|
+
targets.push(resolveProfileName(config, [param]));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const uniqueTargets = Array.from(new Set(targets));
|
|
248
|
+
const missing = uniqueTargets.filter(
|
|
249
|
+
(name) => !config.profiles || !config.profiles[name]
|
|
250
|
+
);
|
|
251
|
+
if (missing.length > 0) {
|
|
252
|
+
throw new Error(`Unknown profile(s): ${missing.join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const name of uniqueTargets) {
|
|
256
|
+
delete config.profiles![name];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const defaults = getDefaultProfiles(config);
|
|
260
|
+
let changedDefaults = false;
|
|
261
|
+
for (const type of DEFAULT_PROFILE_TYPES) {
|
|
262
|
+
const value = defaults[type];
|
|
263
|
+
if (!value) continue;
|
|
264
|
+
try {
|
|
265
|
+
const resolved = resolveDefaultProfileForType(config, type, value);
|
|
266
|
+
if (resolved && uniqueTargets.includes(resolved)) {
|
|
267
|
+
if (deleteDefaultProfileEntry(config, type)) {
|
|
268
|
+
changedDefaults = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
// keep defaults that cannot be resolved
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
changedDefaults &&
|
|
277
|
+
config.defaultProfiles &&
|
|
278
|
+
Object.keys(config.defaultProfiles).length === 0
|
|
279
|
+
) {
|
|
280
|
+
delete config.defaultProfiles;
|
|
281
|
+
}
|
|
282
|
+
writeConfig(configPath!, config);
|
|
283
|
+
console.log(`Updated config: ${configPath}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (cmd === "config") {
|
|
288
|
+
const cfgPath = findConfigPath(parsed.configPath);
|
|
289
|
+
if (!cfgPath) {
|
|
290
|
+
console.log("(no config found)");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
console.log(cfgPath);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (cmd === "list" || cmd === "ls") {
|
|
298
|
+
printList(config, configPath);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (cmd === "use") {
|
|
303
|
+
const params = args.slice(1);
|
|
304
|
+
if (params.length === 0) {
|
|
305
|
+
const printUseWithLog = (
|
|
306
|
+
cfg: Config,
|
|
307
|
+
profileName: string,
|
|
308
|
+
requestedType: ProfileType | null
|
|
309
|
+
) => {
|
|
310
|
+
logProfileUse(
|
|
311
|
+
cfg,
|
|
312
|
+
configPath,
|
|
313
|
+
profileName,
|
|
314
|
+
requestedType,
|
|
315
|
+
process.env.CODE_ENV_TERMINAL_TAG || null,
|
|
316
|
+
process.cwd()
|
|
317
|
+
);
|
|
318
|
+
printUse(cfg, profileName, requestedType, true, configPath);
|
|
319
|
+
};
|
|
320
|
+
await runInteractiveUse(config, printUseWithLog);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const requestedType =
|
|
324
|
+
params.length >= 2 ? normalizeType(params[0]) : null;
|
|
325
|
+
const profileName = resolveProfileName(config, params);
|
|
326
|
+
logProfileUse(
|
|
327
|
+
config,
|
|
328
|
+
configPath,
|
|
329
|
+
profileName,
|
|
330
|
+
requestedType,
|
|
331
|
+
process.env.CODE_ENV_TERMINAL_TAG || null,
|
|
332
|
+
process.cwd()
|
|
333
|
+
);
|
|
334
|
+
printUse(config, profileName, requestedType, true, configPath);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (cmd === "show") {
|
|
339
|
+
const profileName = resolveProfileName(config, args.slice(1));
|
|
340
|
+
printShow(config, profileName);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (cmd === "unset") {
|
|
345
|
+
printUnset(config);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
350
|
+
} catch (err: unknown) {
|
|
351
|
+
console.error(`codenv: ${getErrorMessage(err)}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main().catch((err: unknown) => {
|
|
357
|
+
console.error(`codenv: ${getErrorMessage(err)}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile display utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, Profile, ProfileType, ListRow, EnvValue } from "../types";
|
|
5
|
+
import { DEFAULT_PROFILE_TYPES } from "../constants";
|
|
6
|
+
import { normalizeType, inferProfileType, getProfileDisplayName } from "./type";
|
|
7
|
+
|
|
8
|
+
export function isEnvValueUnset(value: EnvValue): boolean {
|
|
9
|
+
return value === null || value === undefined || value === "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildEffectiveEnv(
|
|
13
|
+
profile: Profile | undefined,
|
|
14
|
+
activeType: ProfileType | null
|
|
15
|
+
): Record<string, EnvValue> {
|
|
16
|
+
const env = profile && profile.env ? profile.env : {};
|
|
17
|
+
if (!activeType) return env;
|
|
18
|
+
if (activeType !== "claude") return env;
|
|
19
|
+
const apiKey = env.ANTHROPIC_API_KEY;
|
|
20
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN;
|
|
21
|
+
if (isEnvValueUnset(apiKey) || !isEnvValueUnset(authToken)) return env;
|
|
22
|
+
return { ...env, ANTHROPIC_AUTH_TOKEN: apiKey };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function envMatchesProfile(profile: Profile | undefined): boolean {
|
|
26
|
+
if (!profile || !profile.env) return false;
|
|
27
|
+
for (const key of Object.keys(profile.env)) {
|
|
28
|
+
const expected = profile.env[key];
|
|
29
|
+
const actual = process.env[key];
|
|
30
|
+
if (isEnvValueUnset(expected)) {
|
|
31
|
+
if (actual !== undefined && actual !== "") return false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (actual !== String(expected)) return false;
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(profile.env).length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Forward declaration to avoid circular dependency
|
|
40
|
+
// getResolvedDefaultProfileKeys will be imported from config/defaults
|
|
41
|
+
export function buildListRows(
|
|
42
|
+
config: Config,
|
|
43
|
+
getResolvedDefaultProfileKeys: (config: Config) => Record<string, string | undefined>
|
|
44
|
+
): ListRow[] {
|
|
45
|
+
const profiles = config && config.profiles ? config.profiles : {};
|
|
46
|
+
const entries = Object.entries(profiles);
|
|
47
|
+
if (entries.length === 0) return [];
|
|
48
|
+
const defaults = getResolvedDefaultProfileKeys(config);
|
|
49
|
+
const rows = entries.map(([key, profile]) => {
|
|
50
|
+
const safeProfile = profile || {};
|
|
51
|
+
const rawType = safeProfile.type ? String(safeProfile.type) : "";
|
|
52
|
+
const normalizedType = normalizeType(rawType);
|
|
53
|
+
const type = normalizedType || rawType || "-";
|
|
54
|
+
const inferredType = inferProfileType(key, safeProfile, null);
|
|
55
|
+
const usageType = inferredType || normalizedType || null;
|
|
56
|
+
const displayName = getProfileDisplayName(key, safeProfile);
|
|
57
|
+
const note = safeProfile.note ? String(safeProfile.note) : "";
|
|
58
|
+
const defaultTypes = DEFAULT_PROFILE_TYPES.filter(
|
|
59
|
+
(profileType) => defaults[profileType] === key
|
|
60
|
+
);
|
|
61
|
+
const defaultLabel = defaultTypes.length > 0 ? "default" : "";
|
|
62
|
+
const noteParts: string[] = [];
|
|
63
|
+
if (defaultLabel) noteParts.push(defaultLabel);
|
|
64
|
+
if (note) noteParts.push(note);
|
|
65
|
+
const noteText = noteParts.join(" | ");
|
|
66
|
+
const active = envMatchesProfile(safeProfile);
|
|
67
|
+
return { key, name: displayName, type, note: noteText, active, usageType };
|
|
68
|
+
});
|
|
69
|
+
rows.sort((a, b) => {
|
|
70
|
+
const nameCmp = a.name.localeCompare(b.name);
|
|
71
|
+
if (nameCmp !== 0) return nameCmp;
|
|
72
|
+
const typeCmp = a.type.localeCompare(b.type);
|
|
73
|
+
if (typeCmp !== 0) return typeCmp;
|
|
74
|
+
return a.key.localeCompare(b.key);
|
|
75
|
+
});
|
|
76
|
+
return rows;
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile module exports
|
|
3
|
+
*/
|
|
4
|
+
export { normalizeType, hasTypePrefix, hasEnvKeyPrefix, inferProfileType, stripTypePrefixFromName, getProfileDisplayName } from "./type";
|
|
5
|
+
export { profileMatchesType, findProfileKeysByName, shouldRemoveCodexAuth } from "./match";
|
|
6
|
+
export { generateProfileKey, resolveProfileName, resolveProfileByType } from "./resolve";
|
|
7
|
+
export {
|
|
8
|
+
isEnvValueUnset,
|
|
9
|
+
buildEffectiveEnv,
|
|
10
|
+
envMatchesProfile,
|
|
11
|
+
buildListRows,
|
|
12
|
+
} from "./display";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile matching utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, Profile, ProfileType } from "../types";
|
|
5
|
+
import { normalizeType, hasEnvKeyPrefix, hasTypePrefix, getProfileDisplayName } from "./type";
|
|
6
|
+
|
|
7
|
+
export function profileMatchesType(profile: Profile | undefined, type: ProfileType): boolean {
|
|
8
|
+
if (!profile) return false;
|
|
9
|
+
if (!profile.type) return true;
|
|
10
|
+
const t = normalizeType(profile.type);
|
|
11
|
+
if (!t) return false;
|
|
12
|
+
return t === type;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findProfileKeysByName(
|
|
16
|
+
config: Config,
|
|
17
|
+
name: string,
|
|
18
|
+
type?: ProfileType | null
|
|
19
|
+
): string[] {
|
|
20
|
+
const profiles = config && config.profiles ? config.profiles : {};
|
|
21
|
+
const matches: string[] = [];
|
|
22
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
23
|
+
const safeProfile = profile || {};
|
|
24
|
+
if (type && !profileMatchesType(safeProfile, type)) continue;
|
|
25
|
+
const displayName = getProfileDisplayName(key, safeProfile, type || null);
|
|
26
|
+
if (displayName === name) matches.push(key);
|
|
27
|
+
}
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function shouldRemoveCodexAuth(
|
|
32
|
+
profileName: string,
|
|
33
|
+
profile: Profile | undefined,
|
|
34
|
+
requestedType: ProfileType | null
|
|
35
|
+
): boolean {
|
|
36
|
+
if (requestedType === "codex") return true;
|
|
37
|
+
if (!profile) return false;
|
|
38
|
+
if (normalizeType(profile.type) === "codex") return true;
|
|
39
|
+
if (hasEnvKeyPrefix(profile, "OPENAI_")) return true;
|
|
40
|
+
return hasTypePrefix(profileName, "codex");
|
|
41
|
+
}
|