@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,469 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildStatuslineResult = buildStatuslineResult;
|
|
4
|
+
/**
|
|
5
|
+
* Statusline builder
|
|
6
|
+
*/
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const constants_1 = require("../constants");
|
|
11
|
+
const type_1 = require("../profile/type");
|
|
12
|
+
const usage_1 = require("../usage");
|
|
13
|
+
const COLOR_ENABLED = !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
14
|
+
const ANSI_RESET = "\x1b[0m";
|
|
15
|
+
const ICON_GIT = "⎇";
|
|
16
|
+
const ICON_PROFILE = "👤";
|
|
17
|
+
const ICON_MODEL = "⚙";
|
|
18
|
+
const ICON_USAGE = "⚡";
|
|
19
|
+
const ICON_CONTEXT = "🧠";
|
|
20
|
+
const ICON_REVIEW = "📝";
|
|
21
|
+
const ICON_CWD = "📁";
|
|
22
|
+
function colorize(text, colorCode) {
|
|
23
|
+
if (!COLOR_ENABLED)
|
|
24
|
+
return text;
|
|
25
|
+
return `\x1b[${colorCode}m${text}${ANSI_RESET}`;
|
|
26
|
+
}
|
|
27
|
+
function dim(text) {
|
|
28
|
+
return colorize(text, "2");
|
|
29
|
+
}
|
|
30
|
+
function getCwdSegment(cwd) {
|
|
31
|
+
if (!cwd)
|
|
32
|
+
return null;
|
|
33
|
+
const base = path.basename(cwd) || cwd;
|
|
34
|
+
const segment = `${ICON_CWD} ${base}`;
|
|
35
|
+
return dim(segment);
|
|
36
|
+
}
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function readStdinJson() {
|
|
41
|
+
if (process.stdin.isTTY)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
45
|
+
const trimmed = raw.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
return null;
|
|
48
|
+
const parsed = JSON.parse(trimmed);
|
|
49
|
+
if (!isRecord(parsed))
|
|
50
|
+
return null;
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function firstNonEmpty(...values) {
|
|
58
|
+
for (const value of values) {
|
|
59
|
+
if (value === null || value === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
const text = String(value).trim();
|
|
62
|
+
if (text)
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function coerceNumber(value) {
|
|
68
|
+
if (value === null || value === undefined || value === "")
|
|
69
|
+
return null;
|
|
70
|
+
const num = Number(value);
|
|
71
|
+
if (!Number.isFinite(num))
|
|
72
|
+
return null;
|
|
73
|
+
return num;
|
|
74
|
+
}
|
|
75
|
+
function firstNumber(...values) {
|
|
76
|
+
for (const value of values) {
|
|
77
|
+
const num = coerceNumber(value);
|
|
78
|
+
if (num !== null)
|
|
79
|
+
return num;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function normalizeTypeValue(value) {
|
|
84
|
+
if (!value)
|
|
85
|
+
return null;
|
|
86
|
+
const normalized = (0, type_1.normalizeType)(value);
|
|
87
|
+
if (normalized)
|
|
88
|
+
return normalized;
|
|
89
|
+
const trimmed = String(value).trim();
|
|
90
|
+
return trimmed ? trimmed : null;
|
|
91
|
+
}
|
|
92
|
+
function detectTypeFromEnv() {
|
|
93
|
+
const matches = constants_1.DEFAULT_PROFILE_TYPES.filter((type) => {
|
|
94
|
+
const suffix = type.toUpperCase();
|
|
95
|
+
return (process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] ||
|
|
96
|
+
process.env[`CODE_ENV_PROFILE_NAME_${suffix}`]);
|
|
97
|
+
});
|
|
98
|
+
if (matches.length === 1)
|
|
99
|
+
return matches[0];
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function resolveEnvProfile(type) {
|
|
103
|
+
const genericKey = process.env.CODE_ENV_PROFILE_KEY || null;
|
|
104
|
+
const genericName = process.env.CODE_ENV_PROFILE_NAME || null;
|
|
105
|
+
if (!type) {
|
|
106
|
+
return { key: genericKey, name: genericName };
|
|
107
|
+
}
|
|
108
|
+
const suffix = type.toUpperCase();
|
|
109
|
+
const key = process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] || genericKey;
|
|
110
|
+
const name = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`] || genericName;
|
|
111
|
+
return { key: key || null, name: name || null };
|
|
112
|
+
}
|
|
113
|
+
function getModelFromInput(input) {
|
|
114
|
+
if (!input)
|
|
115
|
+
return null;
|
|
116
|
+
const raw = input.model;
|
|
117
|
+
if (!raw)
|
|
118
|
+
return null;
|
|
119
|
+
if (typeof raw === "string")
|
|
120
|
+
return raw;
|
|
121
|
+
if (isRecord(raw)) {
|
|
122
|
+
const displayName = raw.displayName || raw.display_name;
|
|
123
|
+
if (displayName)
|
|
124
|
+
return String(displayName);
|
|
125
|
+
if (raw.id)
|
|
126
|
+
return String(raw.id);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function getModelProviderFromInput(input) {
|
|
131
|
+
if (!input || !input.model_provider)
|
|
132
|
+
return null;
|
|
133
|
+
const provider = String(input.model_provider).trim();
|
|
134
|
+
return provider ? provider : null;
|
|
135
|
+
}
|
|
136
|
+
function getInputProfile(input) {
|
|
137
|
+
if (!input || !isRecord(input.profile))
|
|
138
|
+
return null;
|
|
139
|
+
return input.profile;
|
|
140
|
+
}
|
|
141
|
+
function getInputUsage(input) {
|
|
142
|
+
var _a, _b, _c, _d;
|
|
143
|
+
if (!input)
|
|
144
|
+
return null;
|
|
145
|
+
if (isRecord(input.usage)) {
|
|
146
|
+
return input.usage;
|
|
147
|
+
}
|
|
148
|
+
const tokenUsage = input.token_usage;
|
|
149
|
+
if (tokenUsage === null || tokenUsage === undefined)
|
|
150
|
+
return null;
|
|
151
|
+
if (typeof tokenUsage === "number") {
|
|
152
|
+
return {
|
|
153
|
+
todayTokens: null,
|
|
154
|
+
totalTokens: coerceNumber(tokenUsage),
|
|
155
|
+
inputTokens: null,
|
|
156
|
+
outputTokens: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (isRecord(tokenUsage)) {
|
|
160
|
+
const record = tokenUsage;
|
|
161
|
+
return {
|
|
162
|
+
todayTokens: (_a = firstNumber(record.todayTokens, record.today, record.today_tokens, record.daily, record.daily_tokens)) !== null && _a !== void 0 ? _a : null,
|
|
163
|
+
totalTokens: (_b = firstNumber(record.totalTokens, record.total, record.total_tokens)) !== null && _b !== void 0 ? _b : null,
|
|
164
|
+
inputTokens: (_c = firstNumber(record.inputTokens, record.input, record.input_tokens)) !== null && _c !== void 0 ? _c : null,
|
|
165
|
+
outputTokens: (_d = firstNumber(record.outputTokens, record.output, record.output_tokens)) !== null && _d !== void 0 ? _d : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function getContextUsedTokens(input) {
|
|
171
|
+
if (!input)
|
|
172
|
+
return null;
|
|
173
|
+
return coerceNumber(input.context_window_used_tokens);
|
|
174
|
+
}
|
|
175
|
+
function getContextLeftPercent(input, type) {
|
|
176
|
+
if (!input)
|
|
177
|
+
return null;
|
|
178
|
+
const raw = coerceNumber(input.context_window_percent);
|
|
179
|
+
if (raw === null || raw < 0)
|
|
180
|
+
return null;
|
|
181
|
+
const percent = raw <= 1 ? raw * 100 : raw;
|
|
182
|
+
if (percent > 100)
|
|
183
|
+
return null;
|
|
184
|
+
const usedTokens = getContextUsedTokens(input);
|
|
185
|
+
const normalizedType = normalizeTypeValue(type);
|
|
186
|
+
// Prefer treating the percent as "remaining" for codex/claude and when usage is absent.
|
|
187
|
+
const preferRemaining = normalizedType === "codex" ||
|
|
188
|
+
normalizedType === "claude" ||
|
|
189
|
+
usedTokens === null ||
|
|
190
|
+
(usedTokens <= 0 && percent >= 99);
|
|
191
|
+
const left = preferRemaining ? percent : 100 - percent;
|
|
192
|
+
return Math.max(0, Math.min(100, left));
|
|
193
|
+
}
|
|
194
|
+
function getWorkspaceDir(input) {
|
|
195
|
+
if (!input || !isRecord(input.workspace))
|
|
196
|
+
return null;
|
|
197
|
+
const currentDir = input.workspace.current_dir;
|
|
198
|
+
if (currentDir) {
|
|
199
|
+
const trimmed = String(currentDir).trim();
|
|
200
|
+
if (trimmed)
|
|
201
|
+
return trimmed;
|
|
202
|
+
}
|
|
203
|
+
const projectDir = input.workspace.project_dir;
|
|
204
|
+
if (!projectDir)
|
|
205
|
+
return null;
|
|
206
|
+
const trimmed = String(projectDir).trim();
|
|
207
|
+
return trimmed ? trimmed : null;
|
|
208
|
+
}
|
|
209
|
+
function getGitStatusFromInput(input) {
|
|
210
|
+
if (!input || !input.git_branch)
|
|
211
|
+
return null;
|
|
212
|
+
const branch = String(input.git_branch).trim();
|
|
213
|
+
if (!branch)
|
|
214
|
+
return null;
|
|
215
|
+
return {
|
|
216
|
+
branch,
|
|
217
|
+
ahead: 0,
|
|
218
|
+
behind: 0,
|
|
219
|
+
staged: 0,
|
|
220
|
+
unstaged: 0,
|
|
221
|
+
untracked: 0,
|
|
222
|
+
conflicted: 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function getGitStatus(cwd) {
|
|
226
|
+
if (!cwd)
|
|
227
|
+
return null;
|
|
228
|
+
const result = (0, child_process_1.spawnSync)("git", ["-C", cwd, "status", "--porcelain=v2", "-b"], {
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
231
|
+
});
|
|
232
|
+
if (result.status !== 0 || !result.stdout)
|
|
233
|
+
return null;
|
|
234
|
+
const status = {
|
|
235
|
+
branch: null,
|
|
236
|
+
ahead: 0,
|
|
237
|
+
behind: 0,
|
|
238
|
+
staged: 0,
|
|
239
|
+
unstaged: 0,
|
|
240
|
+
untracked: 0,
|
|
241
|
+
conflicted: 0,
|
|
242
|
+
};
|
|
243
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (!line)
|
|
246
|
+
continue;
|
|
247
|
+
if (line.startsWith("# branch.head ")) {
|
|
248
|
+
status.branch = line.slice("# branch.head ".length).trim();
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (line.startsWith("# branch.ab ")) {
|
|
252
|
+
const parts = line
|
|
253
|
+
.slice("# branch.ab ".length)
|
|
254
|
+
.trim()
|
|
255
|
+
.split(/\s+/);
|
|
256
|
+
for (const part of parts) {
|
|
257
|
+
if (part.startsWith("+"))
|
|
258
|
+
status.ahead = Number(part.slice(1)) || 0;
|
|
259
|
+
if (part.startsWith("-"))
|
|
260
|
+
status.behind = Number(part.slice(1)) || 0;
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (line.startsWith("? ")) {
|
|
265
|
+
status.untracked += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (line.startsWith("u ")) {
|
|
269
|
+
status.conflicted += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
273
|
+
const parts = line.split(/\s+/);
|
|
274
|
+
const xy = parts[1] || "";
|
|
275
|
+
const staged = xy[0];
|
|
276
|
+
const unstaged = xy[1];
|
|
277
|
+
if (staged && staged !== ".")
|
|
278
|
+
status.staged += 1;
|
|
279
|
+
if (unstaged && unstaged !== ".")
|
|
280
|
+
status.unstaged += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!status.branch) {
|
|
285
|
+
status.branch = "HEAD";
|
|
286
|
+
}
|
|
287
|
+
return status;
|
|
288
|
+
}
|
|
289
|
+
function formatGitSegment(status) {
|
|
290
|
+
if (!status || !status.branch)
|
|
291
|
+
return null;
|
|
292
|
+
const meta = [];
|
|
293
|
+
const dirtyCount = status.staged + status.unstaged + status.untracked;
|
|
294
|
+
if (status.ahead > 0)
|
|
295
|
+
meta.push(`↑${status.ahead}`);
|
|
296
|
+
if (status.behind > 0)
|
|
297
|
+
meta.push(`↓${status.behind}`);
|
|
298
|
+
if (status.conflicted > 0)
|
|
299
|
+
meta.push(`✖${status.conflicted}`);
|
|
300
|
+
if (dirtyCount > 0)
|
|
301
|
+
meta.push(`+${dirtyCount}`);
|
|
302
|
+
const suffix = meta.length > 0 ? ` [${meta.join("")}]` : "";
|
|
303
|
+
const text = `${ICON_GIT} ${status.branch}${suffix}`;
|
|
304
|
+
const hasConflicts = status.conflicted > 0;
|
|
305
|
+
const isDirty = dirtyCount > 0;
|
|
306
|
+
if (hasConflicts)
|
|
307
|
+
return colorize(text, "31");
|
|
308
|
+
if (isDirty)
|
|
309
|
+
return colorize(text, "33");
|
|
310
|
+
if (status.ahead > 0 || status.behind > 0)
|
|
311
|
+
return colorize(text, "36");
|
|
312
|
+
return colorize(text, "32");
|
|
313
|
+
}
|
|
314
|
+
function resolveUsageFromRecords(config, configPath, type, profileKey, profileName, syncUsage) {
|
|
315
|
+
try {
|
|
316
|
+
const normalized = (0, type_1.normalizeType)(type || "");
|
|
317
|
+
if (!normalized || (!profileKey && !profileName))
|
|
318
|
+
return null;
|
|
319
|
+
const totals = (0, usage_1.readUsageTotalsIndex)(config, configPath, syncUsage);
|
|
320
|
+
if (!totals)
|
|
321
|
+
return null;
|
|
322
|
+
const usage = (0, usage_1.resolveUsageTotalsForProfile)(totals, normalized, profileKey, profileName);
|
|
323
|
+
if (!usage)
|
|
324
|
+
return null;
|
|
325
|
+
return {
|
|
326
|
+
todayTokens: usage.today,
|
|
327
|
+
totalTokens: usage.total,
|
|
328
|
+
inputTokens: null,
|
|
329
|
+
outputTokens: null,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function formatUsageSegment(usage) {
|
|
337
|
+
var _a;
|
|
338
|
+
if (!usage)
|
|
339
|
+
return null;
|
|
340
|
+
const today = (_a = usage.todayTokens) !== null && _a !== void 0 ? _a : (usage.inputTokens !== null || usage.outputTokens !== null
|
|
341
|
+
? (usage.inputTokens || 0) + (usage.outputTokens || 0)
|
|
342
|
+
: usage.totalTokens);
|
|
343
|
+
if (today === null)
|
|
344
|
+
return null;
|
|
345
|
+
const text = `Today ${(0, usage_1.formatTokenCount)(today)}`;
|
|
346
|
+
return colorize(`${ICON_USAGE} ${text}`, "33");
|
|
347
|
+
}
|
|
348
|
+
function formatModelSegment(model, provider) {
|
|
349
|
+
if (!model)
|
|
350
|
+
return null;
|
|
351
|
+
const providerLabel = provider ? `${provider}:${model}` : model;
|
|
352
|
+
return colorize(`${ICON_MODEL} ${providerLabel}`, "35");
|
|
353
|
+
}
|
|
354
|
+
function formatProfileSegment(type, profileKey, profileName) {
|
|
355
|
+
const name = profileName || profileKey;
|
|
356
|
+
if (!name)
|
|
357
|
+
return null;
|
|
358
|
+
const label = type ? `${type}:${name}` : name;
|
|
359
|
+
return colorize(`${ICON_PROFILE} ${label}`, "37");
|
|
360
|
+
}
|
|
361
|
+
function formatContextSegment(contextLeft) {
|
|
362
|
+
if (contextLeft === null)
|
|
363
|
+
return null;
|
|
364
|
+
const left = Math.max(0, Math.min(100, Math.round(contextLeft)));
|
|
365
|
+
return colorize(`${ICON_CONTEXT} ${left}% left`, "36");
|
|
366
|
+
}
|
|
367
|
+
function formatContextUsedSegment(usedTokens) {
|
|
368
|
+
if (usedTokens === null)
|
|
369
|
+
return null;
|
|
370
|
+
return colorize(`${ICON_CONTEXT} ${(0, usage_1.formatTokenCount)(usedTokens)} used`, "36");
|
|
371
|
+
}
|
|
372
|
+
function formatModeSegment(reviewMode) {
|
|
373
|
+
if (!reviewMode)
|
|
374
|
+
return null;
|
|
375
|
+
return colorize(`${ICON_REVIEW} review`, "34");
|
|
376
|
+
}
|
|
377
|
+
function buildStatuslineResult(args, config, configPath) {
|
|
378
|
+
const stdinInput = readStdinJson();
|
|
379
|
+
const inputProfile = getInputProfile(stdinInput);
|
|
380
|
+
let typeCandidate = firstNonEmpty(args.type, process.env.CODE_ENV_TYPE, inputProfile ? inputProfile.type : null, stdinInput ? stdinInput.type : null);
|
|
381
|
+
if (!typeCandidate) {
|
|
382
|
+
typeCandidate = detectTypeFromEnv();
|
|
383
|
+
}
|
|
384
|
+
let type = normalizeTypeValue(typeCandidate);
|
|
385
|
+
const envProfile = resolveEnvProfile(type);
|
|
386
|
+
let profileKey = firstNonEmpty(args.profileKey, envProfile.key, inputProfile ? inputProfile.key : null);
|
|
387
|
+
let profileName = firstNonEmpty(args.profileName, envProfile.name, inputProfile ? inputProfile.name : null);
|
|
388
|
+
if (profileKey && !profileName && config.profiles && config.profiles[profileKey]) {
|
|
389
|
+
const profile = config.profiles[profileKey];
|
|
390
|
+
profileName = (0, type_1.getProfileDisplayName)(profileKey, profile, type || undefined);
|
|
391
|
+
if (!type) {
|
|
392
|
+
const inferred = (0, type_1.inferProfileType)(profileKey, profile, null);
|
|
393
|
+
if (inferred)
|
|
394
|
+
type = inferred;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (!type && profileKey && config.profiles && config.profiles[profileKey]) {
|
|
398
|
+
const profile = config.profiles[profileKey];
|
|
399
|
+
const inferred = (0, type_1.inferProfileType)(profileKey, profile, null);
|
|
400
|
+
if (inferred)
|
|
401
|
+
type = inferred;
|
|
402
|
+
}
|
|
403
|
+
const cwd = firstNonEmpty(args.cwd, process.env.CODE_ENV_CWD, getWorkspaceDir(stdinInput), stdinInput ? stdinInput.cwd : null, process.cwd());
|
|
404
|
+
const model = firstNonEmpty(args.model, process.env.CODE_ENV_MODEL, getModelFromInput(stdinInput));
|
|
405
|
+
const modelProvider = firstNonEmpty(process.env.CODE_ENV_MODEL_PROVIDER, getModelProviderFromInput(stdinInput));
|
|
406
|
+
const usage = {
|
|
407
|
+
todayTokens: firstNumber(args.usageToday, process.env.CODE_ENV_USAGE_TODAY),
|
|
408
|
+
totalTokens: firstNumber(args.usageTotal, process.env.CODE_ENV_USAGE_TOTAL),
|
|
409
|
+
inputTokens: firstNumber(args.usageInput, process.env.CODE_ENV_USAGE_INPUT),
|
|
410
|
+
outputTokens: firstNumber(args.usageOutput, process.env.CODE_ENV_USAGE_OUTPUT),
|
|
411
|
+
};
|
|
412
|
+
const hasExplicitUsage = usage.todayTokens !== null ||
|
|
413
|
+
usage.totalTokens !== null ||
|
|
414
|
+
usage.inputTokens !== null ||
|
|
415
|
+
usage.outputTokens !== null;
|
|
416
|
+
let finalUsage = hasExplicitUsage ? usage : null;
|
|
417
|
+
if (!finalUsage) {
|
|
418
|
+
finalUsage = resolveUsageFromRecords(config, configPath, type, profileKey, profileName, args.syncUsage);
|
|
419
|
+
}
|
|
420
|
+
let gitStatus = getGitStatus(cwd);
|
|
421
|
+
if (!gitStatus) {
|
|
422
|
+
gitStatus = getGitStatusFromInput(stdinInput);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const inputGit = getGitStatusFromInput(stdinInput);
|
|
426
|
+
if (inputGit && (!gitStatus.branch || gitStatus.branch === "HEAD")) {
|
|
427
|
+
gitStatus.branch = inputGit.branch;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const gitSegment = formatGitSegment(gitStatus);
|
|
431
|
+
const profileSegment = formatProfileSegment(type, profileKey, profileName);
|
|
432
|
+
const modelSegment = formatModelSegment(model, modelProvider);
|
|
433
|
+
const usageSegment = formatUsageSegment(finalUsage);
|
|
434
|
+
const contextLeft = getContextLeftPercent(stdinInput, type);
|
|
435
|
+
const contextSegment = formatContextSegment(contextLeft);
|
|
436
|
+
const contextUsedTokens = getContextUsedTokens(stdinInput);
|
|
437
|
+
const contextUsedSegment = contextSegment === null ? formatContextUsedSegment(contextUsedTokens) : null;
|
|
438
|
+
const modeSegment = formatModeSegment((stdinInput === null || stdinInput === void 0 ? void 0 : stdinInput.review_mode) === true);
|
|
439
|
+
const cwdSegment = getCwdSegment(cwd);
|
|
440
|
+
const segments = [];
|
|
441
|
+
if (gitSegment)
|
|
442
|
+
segments.push(gitSegment);
|
|
443
|
+
if (profileSegment)
|
|
444
|
+
segments.push(profileSegment);
|
|
445
|
+
if (modeSegment)
|
|
446
|
+
segments.push(modeSegment);
|
|
447
|
+
if (modelSegment)
|
|
448
|
+
segments.push(modelSegment);
|
|
449
|
+
if (usageSegment)
|
|
450
|
+
segments.push(usageSegment);
|
|
451
|
+
if (contextSegment)
|
|
452
|
+
segments.push(contextSegment);
|
|
453
|
+
if (contextUsedSegment)
|
|
454
|
+
segments.push(contextUsedSegment);
|
|
455
|
+
if (cwdSegment)
|
|
456
|
+
segments.push(cwdSegment);
|
|
457
|
+
const text = segments.join(" ");
|
|
458
|
+
return {
|
|
459
|
+
text,
|
|
460
|
+
json: {
|
|
461
|
+
cwd,
|
|
462
|
+
type,
|
|
463
|
+
profile: { key: profileKey, name: profileName },
|
|
464
|
+
model,
|
|
465
|
+
usage: finalUsage,
|
|
466
|
+
git: gitStatus,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
package/bin/types.js
ADDED
package/bin/ui/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInteractiveUse = exports.runInteractiveAdd = exports.askProfileName = exports.askType = exports.askConfirm = exports.askRequired = exports.ask = exports.createReadline = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* UI module exports
|
|
6
|
+
*/
|
|
7
|
+
var readline_1 = require("./readline");
|
|
8
|
+
Object.defineProperty(exports, "createReadline", { enumerable: true, get: function () { return readline_1.createReadline; } });
|
|
9
|
+
Object.defineProperty(exports, "ask", { enumerable: true, get: function () { return readline_1.ask; } });
|
|
10
|
+
Object.defineProperty(exports, "askRequired", { enumerable: true, get: function () { return readline_1.askRequired; } });
|
|
11
|
+
Object.defineProperty(exports, "askConfirm", { enumerable: true, get: function () { return readline_1.askConfirm; } });
|
|
12
|
+
Object.defineProperty(exports, "askType", { enumerable: true, get: function () { return readline_1.askType; } });
|
|
13
|
+
Object.defineProperty(exports, "askProfileName", { enumerable: true, get: function () { return readline_1.askProfileName; } });
|
|
14
|
+
var interactive_1 = require("./interactive");
|
|
15
|
+
Object.defineProperty(exports, "runInteractiveAdd", { enumerable: true, get: function () { return interactive_1.runInteractiveAdd; } });
|
|
16
|
+
Object.defineProperty(exports, "runInteractiveUse", { enumerable: true, get: function () { return interactive_1.runInteractiveUse; } });
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInteractiveAdd = runInteractiveAdd;
|
|
4
|
+
exports.runInteractiveUse = runInteractiveUse;
|
|
5
|
+
/**
|
|
6
|
+
* Interactive UI components
|
|
7
|
+
*/
|
|
8
|
+
const readline = require("readline");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const resolve_1 = require("../profile/resolve");
|
|
11
|
+
const type_1 = require("../profile/type");
|
|
12
|
+
const display_1 = require("../profile/display");
|
|
13
|
+
const readline_1 = require("./readline");
|
|
14
|
+
async function runInteractiveAdd(configPath) {
|
|
15
|
+
const config = (0, config_1.readConfigIfExists)(configPath);
|
|
16
|
+
const rl = (0, readline_1.createReadline)();
|
|
17
|
+
try {
|
|
18
|
+
const type = await (0, readline_1.askType)(rl);
|
|
19
|
+
const defaultName = "default";
|
|
20
|
+
const profileInfo = await (0, readline_1.askProfileName)(rl, config, defaultName, type);
|
|
21
|
+
const profileKey = profileInfo.key || (0, resolve_1.generateProfileKey)(config);
|
|
22
|
+
const baseUrl = await (0, readline_1.askRequired)(rl, "Base URL (required): ");
|
|
23
|
+
const apiKey = await (0, readline_1.askRequired)(rl, "API key (required): ");
|
|
24
|
+
if (!config.profiles || typeof config.profiles !== "object") {
|
|
25
|
+
config.profiles = {};
|
|
26
|
+
}
|
|
27
|
+
if (!config.profiles[profileKey]) {
|
|
28
|
+
config.profiles[profileKey] = {};
|
|
29
|
+
}
|
|
30
|
+
const profile = config.profiles[profileKey];
|
|
31
|
+
profile.name = profileInfo.name;
|
|
32
|
+
profile.type = type;
|
|
33
|
+
if (!profile.env || typeof profile.env !== "object") {
|
|
34
|
+
profile.env = {};
|
|
35
|
+
}
|
|
36
|
+
if (type === "codex") {
|
|
37
|
+
profile.env.OPENAI_BASE_URL = baseUrl;
|
|
38
|
+
profile.env.OPENAI_API_KEY = apiKey;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
profile.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
42
|
+
profile.env.ANTHROPIC_API_KEY = apiKey;
|
|
43
|
+
console.log("Note: ANTHROPIC_AUTH_TOKEN will be set to the same value when applying.");
|
|
44
|
+
}
|
|
45
|
+
(0, config_1.writeConfig)(configPath, config);
|
|
46
|
+
console.log(`Updated config: ${configPath}`);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
rl.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function runInteractiveUse(config, printUse) {
|
|
53
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
54
|
+
throw new Error("Interactive selection requires a TTY. Provide a profile name.");
|
|
55
|
+
}
|
|
56
|
+
const rows = (0, display_1.buildListRows)(config, config_1.getResolvedDefaultProfileKeys);
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
throw new Error("No profiles found.");
|
|
59
|
+
}
|
|
60
|
+
const nameTypeCounts = new Map();
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const key = `${row.name}||${row.type}`;
|
|
63
|
+
nameTypeCounts.set(key, (nameTypeCounts.get(key) || 0) + 1);
|
|
64
|
+
}
|
|
65
|
+
const displayRows = rows.map((row) => {
|
|
66
|
+
const key = `${row.name}||${row.type}`;
|
|
67
|
+
const displayName = (nameTypeCounts.get(key) || 0) > 1 ? `${row.name} [${row.key}]` : row.name;
|
|
68
|
+
const noteText = row.note;
|
|
69
|
+
const profile = config.profiles && config.profiles[row.key];
|
|
70
|
+
const inferredType = (0, type_1.inferProfileType)(row.key, profile, null);
|
|
71
|
+
const resolvedType = inferredType || (0, type_1.normalizeType)(row.type) || null;
|
|
72
|
+
return { ...row, displayName, noteText, resolvedType };
|
|
73
|
+
});
|
|
74
|
+
const headerName = "PROFILE";
|
|
75
|
+
const headerType = "TYPE";
|
|
76
|
+
const headerNote = "NOTE";
|
|
77
|
+
const nameWidth = Math.max(headerName.length, ...displayRows.map((row) => row.displayName.length));
|
|
78
|
+
const typeWidth = Math.max(headerType.length, ...displayRows.map((row) => row.type.length));
|
|
79
|
+
const noteWidth = Math.max(headerNote.length, ...displayRows.map((row) => row.noteText.length));
|
|
80
|
+
const formatRow = (name, type, note) => `${name.padEnd(nameWidth)} ${type.padEnd(typeWidth)} ${note.padEnd(noteWidth)}`;
|
|
81
|
+
const activeKeys = new Set();
|
|
82
|
+
const keyToType = new Map();
|
|
83
|
+
for (const row of displayRows) {
|
|
84
|
+
keyToType.set(row.key, row.resolvedType || null);
|
|
85
|
+
if (row.active)
|
|
86
|
+
activeKeys.add(row.key);
|
|
87
|
+
}
|
|
88
|
+
let index = displayRows.findIndex((row) => row.active);
|
|
89
|
+
if (index < 0)
|
|
90
|
+
index = 0;
|
|
91
|
+
const ANSI_CLEAR = "\x1b[2J\x1b[H";
|
|
92
|
+
const ANSI_HIDE_CURSOR = "\x1b[?25l";
|
|
93
|
+
const ANSI_SHOW_CURSOR = "\x1b[?25h";
|
|
94
|
+
const ANSI_INVERT = "\x1b[7m";
|
|
95
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
96
|
+
const ANSI_RESET = "\x1b[0m";
|
|
97
|
+
const render = () => {
|
|
98
|
+
const lines = [];
|
|
99
|
+
lines.push("Select profile (up/down, Enter to apply, q to exit)");
|
|
100
|
+
lines.push(formatRow(headerName, headerType, headerNote));
|
|
101
|
+
lines.push(formatRow("-".repeat(nameWidth), "-".repeat(typeWidth), "-".repeat(noteWidth)));
|
|
102
|
+
for (let i = 0; i < displayRows.length; i++) {
|
|
103
|
+
const row = displayRows[i];
|
|
104
|
+
const isActive = activeKeys.has(row.key);
|
|
105
|
+
const line = ` ${formatRow(row.displayName, row.type, row.noteText)}`;
|
|
106
|
+
if (i === index) {
|
|
107
|
+
const prefix = isActive ? `${ANSI_INVERT}${ANSI_GREEN}` : ANSI_INVERT;
|
|
108
|
+
lines.push(`${prefix}${line}${ANSI_RESET}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (isActive) {
|
|
112
|
+
lines.push(`${ANSI_GREEN}${line}${ANSI_RESET}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push(line);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
process.stderr.write(`${ANSI_CLEAR}${ANSI_HIDE_CURSOR}${lines.join("\n")}\n`);
|
|
120
|
+
};
|
|
121
|
+
return await new Promise((resolve) => {
|
|
122
|
+
readline.emitKeypressEvents(process.stdin);
|
|
123
|
+
const stdin = process.stdin;
|
|
124
|
+
const wasRaw = !!stdin.isRaw;
|
|
125
|
+
stdin.setRawMode(true);
|
|
126
|
+
stdin.resume();
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
stdin.removeListener("keypress", onKeypress);
|
|
129
|
+
if (!wasRaw)
|
|
130
|
+
stdin.setRawMode(false);
|
|
131
|
+
stdin.pause();
|
|
132
|
+
process.stderr.write(`${ANSI_RESET}${ANSI_SHOW_CURSOR}`);
|
|
133
|
+
};
|
|
134
|
+
const finish = () => {
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve();
|
|
137
|
+
};
|
|
138
|
+
const onKeypress = (str, key) => {
|
|
139
|
+
if (key && key.ctrl && key.name === "c") {
|
|
140
|
+
finish();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key && key.name === "up") {
|
|
144
|
+
index = (index - 1 + displayRows.length) % displayRows.length;
|
|
145
|
+
render();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (key && key.name === "down") {
|
|
149
|
+
index = (index + 1) % displayRows.length;
|
|
150
|
+
render();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (key && key.name === "home") {
|
|
154
|
+
index = 0;
|
|
155
|
+
render();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key && key.name === "end") {
|
|
159
|
+
index = displayRows.length - 1;
|
|
160
|
+
render();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
164
|
+
const selectedKey = displayRows[index].key;
|
|
165
|
+
const selectedType = keyToType.get(selectedKey) || null;
|
|
166
|
+
if (selectedType) {
|
|
167
|
+
for (const activeKey of Array.from(activeKeys)) {
|
|
168
|
+
if (keyToType.get(activeKey) === selectedType) {
|
|
169
|
+
activeKeys.delete(activeKey);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
activeKeys.add(selectedKey);
|
|
174
|
+
printUse(config, selectedKey, null);
|
|
175
|
+
render();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key && key.name === "escape") {
|
|
179
|
+
finish();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (str === "q" || str === "Q") {
|
|
183
|
+
finish();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
stdin.on("keypress", onKeypress);
|
|
187
|
+
render();
|
|
188
|
+
});
|
|
189
|
+
}
|