@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,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureCodexStatuslineConfig = ensureCodexStatuslineConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Codex CLI status line 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_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
13
|
+
const DEFAULT_STATUSLINE_COMMAND = [
|
|
14
|
+
"codenv",
|
|
15
|
+
"statusline",
|
|
16
|
+
"--type",
|
|
17
|
+
"codex",
|
|
18
|
+
"--sync-usage",
|
|
19
|
+
];
|
|
20
|
+
const DEFAULT_SHOW_HINTS = false;
|
|
21
|
+
const DEFAULT_UPDATE_INTERVAL_MS = 300;
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 1000;
|
|
23
|
+
function parseBooleanEnv(value) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
const normalized = String(value).trim().toLowerCase();
|
|
27
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
28
|
+
return true;
|
|
29
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
30
|
+
return false;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function resolveCodexConfigPath(config) {
|
|
34
|
+
var _a;
|
|
35
|
+
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
36
|
+
if (envOverride && String(envOverride).trim()) {
|
|
37
|
+
const expanded = (0, utils_1.expandEnv)(String(envOverride).trim());
|
|
38
|
+
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
39
|
+
}
|
|
40
|
+
const configOverride = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.configPath;
|
|
41
|
+
if (configOverride && String(configOverride).trim()) {
|
|
42
|
+
const expanded = (0, utils_1.expandEnv)(String(configOverride).trim());
|
|
43
|
+
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_CODEX_CONFIG_PATH;
|
|
46
|
+
}
|
|
47
|
+
function readConfig(filePath) {
|
|
48
|
+
if (!fs.existsSync(filePath))
|
|
49
|
+
return "";
|
|
50
|
+
try {
|
|
51
|
+
return fs.readFileSync(filePath, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function stripInlineComment(value) {
|
|
58
|
+
let inSingle = false;
|
|
59
|
+
let inDouble = false;
|
|
60
|
+
for (let i = 0; i < value.length; i++) {
|
|
61
|
+
const ch = value[i];
|
|
62
|
+
if (ch === "\"" && !inSingle && value[i - 1] !== "\\") {
|
|
63
|
+
inDouble = !inDouble;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === "'" && !inDouble && value[i - 1] !== "\\") {
|
|
67
|
+
inSingle = !inSingle;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!inSingle && !inDouble && (ch === "#" || ch === ";")) {
|
|
71
|
+
return value.slice(0, i).trim();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value.trim();
|
|
75
|
+
}
|
|
76
|
+
function unquote(value) {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
79
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
80
|
+
return trimmed.slice(1, -1);
|
|
81
|
+
}
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
function parseCommandValue(raw) {
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
if (!trimmed)
|
|
87
|
+
return null;
|
|
88
|
+
if (trimmed.startsWith("[")) {
|
|
89
|
+
const items = [];
|
|
90
|
+
const regex = /"((?:\\.|[^"\\])*)"/g;
|
|
91
|
+
let match = null;
|
|
92
|
+
while ((match = regex.exec(trimmed))) {
|
|
93
|
+
const item = match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
94
|
+
items.push(item);
|
|
95
|
+
}
|
|
96
|
+
return items.length > 0 ? items : null;
|
|
97
|
+
}
|
|
98
|
+
return unquote(trimmed);
|
|
99
|
+
}
|
|
100
|
+
function tokenizeCommand(command) {
|
|
101
|
+
const tokens = [];
|
|
102
|
+
let current = "";
|
|
103
|
+
let inSingle = false;
|
|
104
|
+
let inDouble = false;
|
|
105
|
+
let escape = false;
|
|
106
|
+
for (let i = 0; i < command.length; i++) {
|
|
107
|
+
const ch = command[i];
|
|
108
|
+
if (escape) {
|
|
109
|
+
current += ch;
|
|
110
|
+
escape = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === "\\" && !inSingle) {
|
|
114
|
+
escape = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ch === "'" && !inDouble) {
|
|
118
|
+
inSingle = !inSingle;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch === "\"" && !inSingle) {
|
|
122
|
+
inDouble = !inDouble;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!inSingle && !inDouble && /\s/.test(ch)) {
|
|
126
|
+
if (current) {
|
|
127
|
+
tokens.push(current);
|
|
128
|
+
current = "";
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
current += ch;
|
|
133
|
+
}
|
|
134
|
+
if (current)
|
|
135
|
+
tokens.push(current);
|
|
136
|
+
return tokens;
|
|
137
|
+
}
|
|
138
|
+
function parseBooleanValue(raw) {
|
|
139
|
+
const normalized = raw.trim().toLowerCase();
|
|
140
|
+
if (normalized === "true")
|
|
141
|
+
return true;
|
|
142
|
+
if (normalized === "false")
|
|
143
|
+
return false;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function parseNumberValue(raw) {
|
|
147
|
+
const trimmed = raw.trim();
|
|
148
|
+
if (!trimmed)
|
|
149
|
+
return null;
|
|
150
|
+
const parsed = Number(trimmed);
|
|
151
|
+
if (!Number.isFinite(parsed))
|
|
152
|
+
return null;
|
|
153
|
+
return Math.floor(parsed);
|
|
154
|
+
}
|
|
155
|
+
function parseStatusLineSection(text) {
|
|
156
|
+
var _a;
|
|
157
|
+
const headerRegex = /^\s*\[tui\.status_line\]\s*$/m;
|
|
158
|
+
const match = headerRegex.exec(text);
|
|
159
|
+
if (!match || match.index === undefined)
|
|
160
|
+
return null;
|
|
161
|
+
const start = match.index;
|
|
162
|
+
const afterHeader = start + match[0].length;
|
|
163
|
+
const rest = text.slice(afterHeader);
|
|
164
|
+
const nextHeaderMatch = rest.match(/^\s*\[.*?\]\s*$/m);
|
|
165
|
+
const end = nextHeaderMatch
|
|
166
|
+
? afterHeader + ((_a = nextHeaderMatch.index) !== null && _a !== void 0 ? _a : rest.length)
|
|
167
|
+
: text.length;
|
|
168
|
+
const sectionText = text.slice(start, end).trimEnd();
|
|
169
|
+
const lines = sectionText.split(/\r?\n/).slice(1);
|
|
170
|
+
const config = {
|
|
171
|
+
command: null,
|
|
172
|
+
showHints: null,
|
|
173
|
+
updateIntervalMs: null,
|
|
174
|
+
timeoutMs: null,
|
|
175
|
+
};
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
if (!trimmed)
|
|
179
|
+
continue;
|
|
180
|
+
if (trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
181
|
+
continue;
|
|
182
|
+
const matchLine = /^([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(trimmed);
|
|
183
|
+
if (!matchLine)
|
|
184
|
+
continue;
|
|
185
|
+
const key = matchLine[1];
|
|
186
|
+
const rawValue = stripInlineComment(matchLine[2]);
|
|
187
|
+
if (key === "command") {
|
|
188
|
+
config.command = parseCommandValue(rawValue);
|
|
189
|
+
}
|
|
190
|
+
else if (key === "show_hints") {
|
|
191
|
+
config.showHints = parseBooleanValue(rawValue);
|
|
192
|
+
}
|
|
193
|
+
else if (key === "update_interval_ms") {
|
|
194
|
+
config.updateIntervalMs = parseNumberValue(rawValue);
|
|
195
|
+
}
|
|
196
|
+
else if (key === "timeout_ms") {
|
|
197
|
+
config.timeoutMs = parseNumberValue(rawValue);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
start,
|
|
202
|
+
end,
|
|
203
|
+
sectionText,
|
|
204
|
+
config,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function commandToArray(value) {
|
|
208
|
+
if (!value)
|
|
209
|
+
return null;
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
return value.map((entry) => String(entry));
|
|
212
|
+
}
|
|
213
|
+
const trimmed = value.trim();
|
|
214
|
+
if (!trimmed)
|
|
215
|
+
return null;
|
|
216
|
+
return tokenizeCommand(trimmed);
|
|
217
|
+
}
|
|
218
|
+
function commandMatches(existing, desired) {
|
|
219
|
+
if (!existing)
|
|
220
|
+
return false;
|
|
221
|
+
const existingTokens = commandToArray(existing);
|
|
222
|
+
const desiredTokens = commandToArray(desired);
|
|
223
|
+
if (existingTokens && desiredTokens) {
|
|
224
|
+
if (existingTokens.length !== desiredTokens.length)
|
|
225
|
+
return false;
|
|
226
|
+
for (let i = 0; i < desiredTokens.length; i++) {
|
|
227
|
+
if (existingTokens[i] !== desiredTokens[i])
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (typeof existing === "string" && typeof desired === "string") {
|
|
233
|
+
return existing.trim() === desired.trim();
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
function configMatches(config, desired) {
|
|
238
|
+
if (!commandMatches(config.command, desired.command))
|
|
239
|
+
return false;
|
|
240
|
+
if (config.showHints !== desired.showHints)
|
|
241
|
+
return false;
|
|
242
|
+
if (config.updateIntervalMs !== desired.updateIntervalMs)
|
|
243
|
+
return false;
|
|
244
|
+
if (config.timeoutMs !== desired.timeoutMs)
|
|
245
|
+
return false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
function resolveDesiredCommand(config) {
|
|
249
|
+
var _a;
|
|
250
|
+
const raw = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.command;
|
|
251
|
+
if (typeof raw === "string") {
|
|
252
|
+
const trimmed = raw.trim();
|
|
253
|
+
if (trimmed)
|
|
254
|
+
return trimmed;
|
|
255
|
+
}
|
|
256
|
+
else if (Array.isArray(raw)) {
|
|
257
|
+
const cleaned = raw
|
|
258
|
+
.map((entry) => String(entry).trim())
|
|
259
|
+
.filter((entry) => entry);
|
|
260
|
+
if (cleaned.length > 0)
|
|
261
|
+
return cleaned;
|
|
262
|
+
}
|
|
263
|
+
return DEFAULT_STATUSLINE_COMMAND;
|
|
264
|
+
}
|
|
265
|
+
function resolveDesiredStatusLineConfig(config) {
|
|
266
|
+
var _a, _b, _c, _d, _e, _f;
|
|
267
|
+
const showHints = (_b = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.showHints) !== null && _b !== void 0 ? _b : DEFAULT_SHOW_HINTS;
|
|
268
|
+
const updateIntervalMs = (_d = (_c = config.codexStatusline) === null || _c === void 0 ? void 0 : _c.updateIntervalMs) !== null && _d !== void 0 ? _d : DEFAULT_UPDATE_INTERVAL_MS;
|
|
269
|
+
const timeoutMs = (_f = (_e = config.codexStatusline) === null || _e === void 0 ? void 0 : _e.timeoutMs) !== null && _f !== void 0 ? _f : DEFAULT_TIMEOUT_MS;
|
|
270
|
+
const command = resolveDesiredCommand(config);
|
|
271
|
+
const configPath = resolveCodexConfigPath(config);
|
|
272
|
+
return {
|
|
273
|
+
command,
|
|
274
|
+
showHints,
|
|
275
|
+
updateIntervalMs,
|
|
276
|
+
timeoutMs,
|
|
277
|
+
configPath,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function formatCommandValue(command) {
|
|
281
|
+
if (Array.isArray(command)) {
|
|
282
|
+
const parts = command.map((part) => JSON.stringify(part)).join(", ");
|
|
283
|
+
return `[${parts}]`;
|
|
284
|
+
}
|
|
285
|
+
return JSON.stringify(command);
|
|
286
|
+
}
|
|
287
|
+
function buildStatusLineSection(desired) {
|
|
288
|
+
const commandText = formatCommandValue(desired.command);
|
|
289
|
+
return (`[tui.status_line]\n` +
|
|
290
|
+
`command = ${commandText}\n` +
|
|
291
|
+
`show_hints = ${desired.showHints ? "true" : "false"}\n` +
|
|
292
|
+
`update_interval_ms = ${desired.updateIntervalMs}\n` +
|
|
293
|
+
`timeout_ms = ${desired.timeoutMs}\n`);
|
|
294
|
+
}
|
|
295
|
+
function upsertSection(text, section, newSection) {
|
|
296
|
+
if (!section) {
|
|
297
|
+
let base = text;
|
|
298
|
+
if (base && !base.endsWith("\n"))
|
|
299
|
+
base += "\n";
|
|
300
|
+
if (base && !base.endsWith("\n\n"))
|
|
301
|
+
base += "\n";
|
|
302
|
+
return `${base}${newSection}`;
|
|
303
|
+
}
|
|
304
|
+
let prefix = text.slice(0, section.start);
|
|
305
|
+
let suffix = text.slice(section.end);
|
|
306
|
+
if (prefix && !prefix.endsWith("\n"))
|
|
307
|
+
prefix += "\n";
|
|
308
|
+
if (suffix && !suffix.startsWith("\n"))
|
|
309
|
+
suffix = `\n${suffix}`;
|
|
310
|
+
return `${prefix}${newSection}${suffix}`;
|
|
311
|
+
}
|
|
312
|
+
function writeConfig(filePath, text) {
|
|
313
|
+
try {
|
|
314
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
315
|
+
fs.writeFileSync(filePath, text.endsWith("\n") ? text : `${text}\n`, "utf8");
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function ensureCodexStatuslineConfig(config, enabled) {
|
|
323
|
+
const disabled = parseBooleanEnv(process.env.CODE_ENV_CODEX_STATUSLINE_DISABLE) === true;
|
|
324
|
+
if (!enabled || disabled)
|
|
325
|
+
return false;
|
|
326
|
+
const desired = resolveDesiredStatusLineConfig(config);
|
|
327
|
+
const configPath = desired.configPath;
|
|
328
|
+
const raw = readConfig(configPath);
|
|
329
|
+
const section = parseStatusLineSection(raw);
|
|
330
|
+
if (section && configMatches(section.config, desired))
|
|
331
|
+
return false;
|
|
332
|
+
const force = parseBooleanEnv(process.env.CODE_ENV_CODEX_STATUSLINE_FORCE) === true;
|
|
333
|
+
if (section && !force) {
|
|
334
|
+
console.log(`codenv: existing Codex status_line config in ${configPath}:`);
|
|
335
|
+
console.log(section.sectionText);
|
|
336
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
337
|
+
console.warn("codenv: no TTY available to confirm status_line overwrite.");
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const rl = (0, ui_1.createReadline)();
|
|
341
|
+
try {
|
|
342
|
+
const confirm = await (0, ui_1.askConfirm)(rl, "Overwrite Codex status_line config? (y/N): ");
|
|
343
|
+
if (!confirm)
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
rl.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const updated = upsertSection(raw, section, buildStatusLineSection(desired));
|
|
351
|
+
if (!writeConfig(configPath, updated)) {
|
|
352
|
+
console.error("codenv: failed to write Codex config; status_line not updated.");
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|