@praeviso/code-env-switch 0.1.0 → 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/.eslintrc.cjs +18 -0
- package/.github/workflows/npm-publish.yml +25 -0
- package/.vscode/settings.json +4 -0
- package/AGENTS.md +32 -0
- package/LICENSE +21 -0
- package/PLAN.md +33 -0
- package/README.md +208 -32
- package/README_zh.md +265 -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 +36 -23
- package/package.json +14 -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/tsconfig.json +12 -0
- package/bin/codenv.js +0 -377
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateProfileKey = generateProfileKey;
|
|
4
|
+
exports.resolveProfileName = resolveProfileName;
|
|
5
|
+
exports.resolveProfileByType = resolveProfileByType;
|
|
6
|
+
const type_1 = require("./type");
|
|
7
|
+
const match_1 = require("./match");
|
|
8
|
+
function generateProfileKey(config) {
|
|
9
|
+
const profiles = config && config.profiles ? config.profiles : {};
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
const key = `p_${Date.now().toString(36)}_${Math.random()
|
|
12
|
+
.toString(36)
|
|
13
|
+
.slice(2, 8)}`;
|
|
14
|
+
if (!profiles[key])
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
let idx = 0;
|
|
18
|
+
while (true) {
|
|
19
|
+
const key = `p_${Date.now().toString(36)}_${idx}`;
|
|
20
|
+
if (!profiles[key])
|
|
21
|
+
return key;
|
|
22
|
+
idx++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function resolveProfileName(config, params) {
|
|
26
|
+
if (!params || params.length === 0) {
|
|
27
|
+
throw new Error("Missing profile name.");
|
|
28
|
+
}
|
|
29
|
+
if (params.length >= 2) {
|
|
30
|
+
const maybeType = (0, type_1.normalizeType)(params[0]);
|
|
31
|
+
if (maybeType) {
|
|
32
|
+
const name = params[1];
|
|
33
|
+
return resolveProfileByType(config, maybeType, name, params[0]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const name = params[0];
|
|
37
|
+
const profiles = config && config.profiles ? config.profiles : {};
|
|
38
|
+
if (profiles[name])
|
|
39
|
+
return name;
|
|
40
|
+
const matches = (0, match_1.findProfileKeysByName)(config, name, null);
|
|
41
|
+
if (matches.length === 1)
|
|
42
|
+
return matches[0];
|
|
43
|
+
if (matches.length > 1) {
|
|
44
|
+
throw new Error(`Multiple profiles named "${name}". ` +
|
|
45
|
+
`Use: codenv <type> ${name} (or profile key: ${matches.join(", ")})`);
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
function resolveProfileByType(config, type, name, rawType) {
|
|
50
|
+
if (!name)
|
|
51
|
+
throw new Error("Missing profile name.");
|
|
52
|
+
const profiles = config && config.profiles ? config.profiles : {};
|
|
53
|
+
if (profiles[name] && (0, match_1.profileMatchesType)(profiles[name], type)) {
|
|
54
|
+
return name;
|
|
55
|
+
}
|
|
56
|
+
const matches = (0, match_1.findProfileKeysByName)(config, name, type);
|
|
57
|
+
if (matches.length === 1)
|
|
58
|
+
return matches[0];
|
|
59
|
+
if (matches.length > 1) {
|
|
60
|
+
throw new Error(`Multiple profiles named "${name}" for type "${type}". ` +
|
|
61
|
+
`Use profile key: ${matches.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
if (rawType) {
|
|
64
|
+
const prefixes = [];
|
|
65
|
+
const raw = String(rawType).trim();
|
|
66
|
+
if (raw && raw.toLowerCase() !== type)
|
|
67
|
+
prefixes.push(raw);
|
|
68
|
+
prefixes.push(type);
|
|
69
|
+
for (const prefix of prefixes) {
|
|
70
|
+
for (const sep of ["-", "_", "."]) {
|
|
71
|
+
const candidate = `${prefix}${sep}${name}`;
|
|
72
|
+
if (profiles[candidate] && (0, match_1.profileMatchesType)(profiles[candidate], type)) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unknown profile for type "${type}": ${name}`);
|
|
79
|
+
}
|
|
@@ -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
|
+
}
|