@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
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeType = normalizeType;
4
+ exports.hasTypePrefix = hasTypePrefix;
5
+ exports.hasEnvKeyPrefix = hasEnvKeyPrefix;
6
+ exports.inferProfileType = inferProfileType;
7
+ exports.stripTypePrefixFromName = stripTypePrefixFromName;
8
+ exports.getProfileDisplayName = getProfileDisplayName;
9
+ function normalizeType(value) {
10
+ if (!value)
11
+ return null;
12
+ const raw = String(value).trim().toLowerCase();
13
+ if (!raw)
14
+ return null;
15
+ const compact = raw.replace(/[\s_-]+/g, "");
16
+ if (compact === "codex")
17
+ return "codex";
18
+ if (compact === "claude" || compact === "claudecode" || compact === "cc") {
19
+ return "claude";
20
+ }
21
+ return null;
22
+ }
23
+ function hasTypePrefix(name, type) {
24
+ if (!name)
25
+ return false;
26
+ const lowered = String(name).toLowerCase();
27
+ const prefixes = type === "claude" ? [type, "cc"] : [type];
28
+ for (const prefix of prefixes) {
29
+ for (const sep of ["-", "_", "."]) {
30
+ if (lowered.startsWith(`${prefix}${sep}`))
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+ function hasEnvKeyPrefix(profile, prefix) {
37
+ if (!profile || !profile.env)
38
+ return false;
39
+ const normalized = prefix.toUpperCase();
40
+ for (const key of Object.keys(profile.env)) {
41
+ if (key.toUpperCase().startsWith(normalized))
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ function inferProfileType(profileName, profile, requestedType) {
47
+ if (requestedType)
48
+ return requestedType;
49
+ const fromProfile = profile ? normalizeType(profile.type) : null;
50
+ if (fromProfile)
51
+ return fromProfile;
52
+ if (hasEnvKeyPrefix(profile, "OPENAI_"))
53
+ return "codex";
54
+ if (hasEnvKeyPrefix(profile, "ANTHROPIC_"))
55
+ return "claude";
56
+ if (hasTypePrefix(profileName, "codex"))
57
+ return "codex";
58
+ if (hasTypePrefix(profileName, "claude"))
59
+ return "claude";
60
+ return null;
61
+ }
62
+ function stripTypePrefixFromName(name, type) {
63
+ if (!name)
64
+ return name;
65
+ const normalizedType = normalizeType(type);
66
+ if (!normalizedType)
67
+ return name;
68
+ const lowered = String(name).toLowerCase();
69
+ const prefixes = normalizedType === "claude" ? [normalizedType, "cc"] : [normalizedType];
70
+ for (const prefix of prefixes) {
71
+ for (const sep of ["-", "_", "."]) {
72
+ const candidate = `${prefix}${sep}`;
73
+ if (lowered.startsWith(candidate)) {
74
+ const stripped = String(name).slice(candidate.length);
75
+ return stripped || name;
76
+ }
77
+ }
78
+ }
79
+ return name;
80
+ }
81
+ function getProfileDisplayName(profileKey, profile, requestedType) {
82
+ if (profile.name)
83
+ return String(profile.name);
84
+ const rawType = profile.type ? String(profile.type) : "";
85
+ if (rawType)
86
+ return stripTypePrefixFromName(profileKey, rawType);
87
+ if (requestedType)
88
+ return stripTypePrefixFromName(profileKey, requestedType);
89
+ return profileKey;
90
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeShell = normalizeShell;
4
+ exports.detectShell = detectShell;
5
+ exports.getShellRcPath = getShellRcPath;
6
+ /**
7
+ * Shell detection utilities
8
+ */
9
+ const path = require("path");
10
+ const os = require("os");
11
+ function normalizeShell(value) {
12
+ if (!value)
13
+ return null;
14
+ const raw = String(value).trim().toLowerCase();
15
+ if (!raw)
16
+ return null;
17
+ if (raw === "bash")
18
+ return "bash";
19
+ if (raw === "zsh")
20
+ return "zsh";
21
+ if (raw === "fish")
22
+ return "fish";
23
+ return null;
24
+ }
25
+ function detectShell(explicitShell) {
26
+ if (explicitShell)
27
+ return normalizeShell(explicitShell);
28
+ const envShell = process.env.SHELL ? path.basename(process.env.SHELL) : "";
29
+ return normalizeShell(envShell);
30
+ }
31
+ function getShellRcPath(shellName) {
32
+ if (shellName === "bash")
33
+ return path.join(os.homedir(), ".bashrc");
34
+ if (shellName === "zsh")
35
+ return path.join(os.homedir(), ".zshrc");
36
+ if (shellName === "fish") {
37
+ return path.join(os.homedir(), ".config", "fish", "config.fish");
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvePath = exports.expandEnv = exports.shellEscape = exports.upsertShellSnippet = exports.escapeRegExp = exports.getShellSnippet = exports.getShellRcPath = exports.detectShell = exports.normalizeShell = void 0;
4
+ /**
5
+ * Shell module exports
6
+ */
7
+ var detect_1 = require("./detect");
8
+ Object.defineProperty(exports, "normalizeShell", { enumerable: true, get: function () { return detect_1.normalizeShell; } });
9
+ Object.defineProperty(exports, "detectShell", { enumerable: true, get: function () { return detect_1.detectShell; } });
10
+ Object.defineProperty(exports, "getShellRcPath", { enumerable: true, get: function () { return detect_1.getShellRcPath; } });
11
+ var snippet_1 = require("./snippet");
12
+ Object.defineProperty(exports, "getShellSnippet", { enumerable: true, get: function () { return snippet_1.getShellSnippet; } });
13
+ Object.defineProperty(exports, "escapeRegExp", { enumerable: true, get: function () { return snippet_1.escapeRegExp; } });
14
+ Object.defineProperty(exports, "upsertShellSnippet", { enumerable: true, get: function () { return snippet_1.upsertShellSnippet; } });
15
+ var utils_1 = require("./utils");
16
+ Object.defineProperty(exports, "shellEscape", { enumerable: true, get: function () { return utils_1.shellEscape; } });
17
+ Object.defineProperty(exports, "expandEnv", { enumerable: true, get: function () { return utils_1.expandEnv; } });
18
+ Object.defineProperty(exports, "resolvePath", { enumerable: true, get: function () { return utils_1.resolvePath; } });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getShellSnippet = getShellSnippet;
4
+ exports.escapeRegExp = escapeRegExp;
5
+ exports.upsertShellSnippet = upsertShellSnippet;
6
+ /**
7
+ * Shell snippet generation
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ function getShellSnippet(shellName) {
12
+ if (shellName === "fish") {
13
+ return [
14
+ "if not set -q CODE_ENV_TERMINAL_TAG",
15
+ " if type -q uuidgen",
16
+ " set -gx CODE_ENV_TERMINAL_TAG (uuidgen)",
17
+ " else",
18
+ " set -gx CODE_ENV_TERMINAL_TAG (date +%s)-$fish_pid-(random)",
19
+ " end",
20
+ "end",
21
+ "function codenv",
22
+ " if test (count $argv) -ge 1",
23
+ " switch $argv[1]",
24
+ " case use unset auto",
25
+ " command codenv $argv | source",
26
+ " case '*'",
27
+ " command codenv $argv",
28
+ " end",
29
+ " else",
30
+ " command codenv",
31
+ " end",
32
+ "end",
33
+ "function codex",
34
+ " command codenv launch codex -- $argv",
35
+ "end",
36
+ "function claude",
37
+ " command codenv launch claude -- $argv",
38
+ "end",
39
+ "codenv auto",
40
+ ].join("\n");
41
+ }
42
+ return [
43
+ 'if [ -z "$CODE_ENV_TERMINAL_TAG" ]; then',
44
+ ' if command -v uuidgen >/dev/null 2>&1; then',
45
+ ' CODE_ENV_TERMINAL_TAG="$(uuidgen)"',
46
+ " else",
47
+ ' CODE_ENV_TERMINAL_TAG="$(date +%s)-$$-$RANDOM"',
48
+ " fi",
49
+ " export CODE_ENV_TERMINAL_TAG",
50
+ "fi",
51
+ "codenv() {",
52
+ ' if [ "$1" = "use" ] || [ "$1" = "unset" ] || [ "$1" = "auto" ]; then',
53
+ ' source <(command codenv "$@")',
54
+ " else",
55
+ ' command codenv "$@"',
56
+ " fi",
57
+ "}",
58
+ "codex() {",
59
+ ' command codenv launch codex -- "$@"',
60
+ "}",
61
+ "claude() {",
62
+ ' command codenv launch claude -- "$@"',
63
+ "}",
64
+ "codenv auto",
65
+ ].join("\n");
66
+ }
67
+ function escapeRegExp(value) {
68
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ }
70
+ function upsertShellSnippet(rcPath, snippet) {
71
+ const markerStart = "# >>> codenv >>>";
72
+ const markerEnd = "# <<< codenv <<<";
73
+ const block = `${markerStart}\n${snippet}\n${markerEnd}`;
74
+ const existing = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, "utf8") : "";
75
+ let updated = "";
76
+ if (existing.includes(markerStart) && existing.includes(markerEnd)) {
77
+ const re = new RegExp(`${escapeRegExp(markerStart)}[\\s\\S]*?${escapeRegExp(markerEnd)}`);
78
+ updated = existing.replace(re, block);
79
+ }
80
+ else if (existing.trim().length === 0) {
81
+ updated = `${block}\n`;
82
+ }
83
+ else {
84
+ const sep = existing.endsWith("\n") ? "\n" : "\n\n";
85
+ updated = `${existing}${sep}${block}\n`;
86
+ }
87
+ const dir = path.dirname(rcPath);
88
+ if (!fs.existsSync(dir)) {
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ }
91
+ fs.writeFileSync(rcPath, updated, "utf8");
92
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shellEscape = shellEscape;
4
+ exports.expandEnv = expandEnv;
5
+ exports.resolvePath = resolvePath;
6
+ /**
7
+ * Shell utility functions
8
+ */
9
+ const path = require("path");
10
+ const os = require("os");
11
+ function shellEscape(value) {
12
+ const str = String(value);
13
+ return `'${str.replace(/'/g, `'\\''`)}'`;
14
+ }
15
+ function expandEnv(input) {
16
+ if (!input)
17
+ return input;
18
+ let out = String(input);
19
+ if (out.startsWith("~")) {
20
+ out = path.join(os.homedir(), out.slice(1));
21
+ }
22
+ out = out.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || "");
23
+ out = out.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, key) => process.env[key] || "");
24
+ return out;
25
+ }
26
+ function resolvePath(p) {
27
+ if (!p)
28
+ return null;
29
+ if (p.startsWith("~")) {
30
+ return path.join(os.homedir(), p.slice(1));
31
+ }
32
+ if (path.isAbsolute(p))
33
+ return p;
34
+ return path.resolve(process.cwd(), p);
35
+ }
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureClaudeStatusline = ensureClaudeStatusline;
4
+ /**
5
+ * Claude Code statusline integration
6
+ */
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const path = require("path");
10
+ const utils_1 = require("../shell/utils");
11
+ const ui_1 = require("../ui");
12
+ const DEFAULT_CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
13
+ const DEFAULT_STATUSLINE_COMMAND = "codenv statusline --type claude --sync-usage";
14
+ const DEFAULT_STATUSLINE_TYPE = "command";
15
+ const DEFAULT_STATUSLINE_PADDING = 0;
16
+ function parseBooleanEnv(value) {
17
+ if (value === undefined)
18
+ return null;
19
+ const normalized = String(value).trim().toLowerCase();
20
+ if (["1", "true", "yes", "on"].includes(normalized))
21
+ return true;
22
+ if (["0", "false", "no", "off"].includes(normalized))
23
+ return false;
24
+ return null;
25
+ }
26
+ function resolveClaudeSettingsPath(config) {
27
+ var _a;
28
+ const override = process.env.CODE_ENV_CLAUDE_SETTINGS_PATH;
29
+ if (override && String(override).trim()) {
30
+ const expanded = (0, utils_1.expandEnv)(String(override).trim());
31
+ return (0, utils_1.resolvePath)(expanded) || DEFAULT_CLAUDE_SETTINGS_PATH;
32
+ }
33
+ const configOverride = (_a = config.claudeStatusline) === null || _a === void 0 ? void 0 : _a.settingsPath;
34
+ if (configOverride && String(configOverride).trim()) {
35
+ const expanded = (0, utils_1.expandEnv)(String(configOverride).trim());
36
+ return (0, utils_1.resolvePath)(expanded) || DEFAULT_CLAUDE_SETTINGS_PATH;
37
+ }
38
+ return DEFAULT_CLAUDE_SETTINGS_PATH;
39
+ }
40
+ function readSettings(filePath) {
41
+ if (!fs.existsSync(filePath))
42
+ return {};
43
+ try {
44
+ const raw = fs.readFileSync(filePath, "utf8");
45
+ const trimmed = raw.trim();
46
+ if (!trimmed)
47
+ return {};
48
+ const parsed = JSON.parse(trimmed);
49
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
50
+ return parsed;
51
+ }
52
+ return null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function isPlainObject(value) {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+ function isCommandStatusLine(value) {
62
+ if (!isPlainObject(value))
63
+ return false;
64
+ const type = value.type;
65
+ const command = value.command;
66
+ return typeof type === "string" && typeof command === "string";
67
+ }
68
+ function resolveCommand(command) {
69
+ if (typeof command === "string") {
70
+ const trimmed = command.trim();
71
+ if (trimmed)
72
+ return trimmed;
73
+ }
74
+ if (Array.isArray(command)) {
75
+ const cleaned = command
76
+ .map((entry) => String(entry).trim())
77
+ .filter((entry) => entry);
78
+ if (cleaned.length > 0)
79
+ return cleaned.join(" ");
80
+ }
81
+ return DEFAULT_STATUSLINE_COMMAND;
82
+ }
83
+ function resolveDesiredStatusLineConfig(config) {
84
+ var _a, _b, _c;
85
+ const type = ((_a = config.claudeStatusline) === null || _a === void 0 ? void 0 : _a.type) || DEFAULT_STATUSLINE_TYPE;
86
+ const command = resolveCommand((_b = config.claudeStatusline) === null || _b === void 0 ? void 0 : _b.command);
87
+ const paddingRaw = (_c = config.claudeStatusline) === null || _c === void 0 ? void 0 : _c.padding;
88
+ const padding = typeof paddingRaw === "number" && Number.isFinite(paddingRaw)
89
+ ? Math.floor(paddingRaw)
90
+ : DEFAULT_STATUSLINE_PADDING;
91
+ const settingsPath = resolveClaudeSettingsPath(config);
92
+ return { type, command, padding, settingsPath };
93
+ }
94
+ function statusLineMatches(existing, desired) {
95
+ if (!isCommandStatusLine(existing))
96
+ return false;
97
+ if (existing.type !== desired.type)
98
+ return false;
99
+ if (existing.command !== desired.command)
100
+ return false;
101
+ const existingPadding = typeof existing.padding === "number" ? existing.padding : undefined;
102
+ if (existingPadding !== desired.padding)
103
+ return false;
104
+ return true;
105
+ }
106
+ async function ensureClaudeStatusline(config, enabled) {
107
+ const disabled = parseBooleanEnv(process.env.CODE_ENV_CLAUDE_STATUSLINE_DISABLE) === true;
108
+ if (!enabled || disabled)
109
+ return false;
110
+ const desired = resolveDesiredStatusLineConfig(config);
111
+ const settingsPath = desired.settingsPath;
112
+ const force = parseBooleanEnv(process.env.CODE_ENV_CLAUDE_STATUSLINE_FORCE) === true;
113
+ const settings = readSettings(settingsPath);
114
+ if (!settings) {
115
+ console.error("codenv: unable to read Claude settings; skipping statusLine update.");
116
+ return false;
117
+ }
118
+ const existing = settings.statusLine;
119
+ if (existing && statusLineMatches(existing, desired)) {
120
+ return false;
121
+ }
122
+ if (typeof existing !== "undefined" && !force) {
123
+ console.log(`codenv: existing Claude statusLine config in ${settingsPath}:`);
124
+ console.log(JSON.stringify(existing, null, 2));
125
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
126
+ console.warn("codenv: no TTY available to confirm statusLine overwrite.");
127
+ return false;
128
+ }
129
+ const rl = (0, ui_1.createReadline)();
130
+ try {
131
+ const confirm = await (0, ui_1.askConfirm)(rl, "Overwrite Claude statusLine config? (y/N): ");
132
+ if (!confirm)
133
+ return false;
134
+ }
135
+ finally {
136
+ rl.close();
137
+ }
138
+ }
139
+ settings.statusLine = {
140
+ type: desired.type,
141
+ command: desired.command,
142
+ padding: desired.padding,
143
+ };
144
+ try {
145
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
146
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
147
+ return true;
148
+ }
149
+ catch {
150
+ console.error("codenv: failed to write Claude settings; statusLine not updated.");
151
+ return false;
152
+ }
153
+ }