@praeviso/code-env-switch 0.1.2 → 0.1.4
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/bin/statusline/debug.js +42 -0
- package/bin/statusline/format.js +60 -0
- package/bin/statusline/git.js +96 -0
- package/bin/statusline/index.js +73 -400
- package/bin/statusline/input.js +249 -0
- package/bin/statusline/style.js +22 -0
- package/bin/statusline/types.js +2 -0
- package/bin/statusline/usage.js +123 -0
- package/bin/statusline/utils.js +35 -0
- package/bin/usage/index.js +133 -10
- package/package.json +1 -1
- package/src/statusline/debug.ts +40 -0
- package/src/statusline/format.ts +68 -0
- package/src/statusline/git.ts +82 -0
- package/src/statusline/index.ts +93 -470
- package/src/statusline/input.ts +300 -0
- package/src/statusline/style.ts +19 -0
- package/src/statusline/types.ts +105 -0
- package/src/statusline/usage.ts +175 -0
- package/src/statusline/utils.ts +27 -0
- package/src/usage/index.ts +156 -10
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { resolvePath } from "../shell/utils";
|
|
5
|
+
|
|
6
|
+
function isStatuslineDebugEnabled(): boolean {
|
|
7
|
+
const raw = process.env.CODE_ENV_STATUSLINE_DEBUG;
|
|
8
|
+
if (!raw) return false;
|
|
9
|
+
const value = String(raw).trim().toLowerCase();
|
|
10
|
+
if (!value) return false;
|
|
11
|
+
return !["0", "false", "no", "off"].includes(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveDefaultConfigDir(configPath: string | null): string {
|
|
15
|
+
if (configPath) return path.dirname(configPath);
|
|
16
|
+
return path.join(os.homedir(), ".config", "code-env");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getStatuslineDebugPath(configPath: string | null): string {
|
|
20
|
+
const envPath = resolvePath(process.env.CODE_ENV_STATUSLINE_DEBUG_PATH);
|
|
21
|
+
if (envPath) return envPath;
|
|
22
|
+
return path.join(resolveDefaultConfigDir(configPath), "statusline-debug.jsonl");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appendStatuslineDebug(
|
|
26
|
+
configPath: string | null,
|
|
27
|
+
payload: Record<string, unknown>
|
|
28
|
+
) {
|
|
29
|
+
if (!isStatuslineDebugEnabled()) return;
|
|
30
|
+
try {
|
|
31
|
+
const debugPath = getStatuslineDebugPath(configPath);
|
|
32
|
+
const dir = path.dirname(debugPath);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.appendFileSync(debugPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore debug logging failures
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { formatTokenCount } from "../usage";
|
|
2
|
+
import {
|
|
3
|
+
ICON_CONTEXT,
|
|
4
|
+
ICON_CWD,
|
|
5
|
+
ICON_MODEL,
|
|
6
|
+
ICON_PROFILE,
|
|
7
|
+
ICON_REVIEW,
|
|
8
|
+
ICON_USAGE,
|
|
9
|
+
colorize,
|
|
10
|
+
dim,
|
|
11
|
+
} from "./style";
|
|
12
|
+
import type { StatuslineUsage } from "./types";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
export function getCwdSegment(cwd: string): string | null {
|
|
16
|
+
if (!cwd) return null;
|
|
17
|
+
const base = path.basename(cwd) || cwd;
|
|
18
|
+
const segment = `${ICON_CWD} ${base}`;
|
|
19
|
+
return dim(segment);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatUsageSegment(usage: StatuslineUsage | null): string | null {
|
|
23
|
+
if (!usage) return null;
|
|
24
|
+
const today =
|
|
25
|
+
usage.todayTokens ??
|
|
26
|
+
(usage.inputTokens !== null || usage.outputTokens !== null
|
|
27
|
+
? (usage.inputTokens || 0) + (usage.outputTokens || 0)
|
|
28
|
+
: usage.totalTokens);
|
|
29
|
+
if (today === null) return null;
|
|
30
|
+
const text = `Today ${formatTokenCount(today)}`;
|
|
31
|
+
return colorize(`${ICON_USAGE} ${text}`, "33");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatModelSegment(
|
|
35
|
+
model: string | null,
|
|
36
|
+
provider: string | null
|
|
37
|
+
): string | null {
|
|
38
|
+
if (!model) return null;
|
|
39
|
+
const providerLabel = provider ? `${provider}:${model}` : model;
|
|
40
|
+
return colorize(`${ICON_MODEL} ${providerLabel}`, "35");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatProfileSegment(
|
|
44
|
+
type: string | null,
|
|
45
|
+
profileKey: string | null,
|
|
46
|
+
profileName: string | null
|
|
47
|
+
): string | null {
|
|
48
|
+
const name = profileName || profileKey;
|
|
49
|
+
if (!name) return null;
|
|
50
|
+
const label = type ? `${type}:${name}` : name;
|
|
51
|
+
return colorize(`${ICON_PROFILE} ${label}`, "37");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatContextSegment(contextLeft: number | null): string | null {
|
|
55
|
+
if (contextLeft === null) return null;
|
|
56
|
+
const left = Math.max(0, Math.min(100, Math.round(contextLeft)));
|
|
57
|
+
return colorize(`${ICON_CONTEXT} ${left}% left`, "36");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatContextUsedSegment(usedTokens: number | null): string | null {
|
|
61
|
+
if (usedTokens === null) return null;
|
|
62
|
+
return colorize(`${ICON_CONTEXT} ${formatTokenCount(usedTokens)} used`, "36");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatModeSegment(reviewMode: boolean): string | null {
|
|
66
|
+
if (!reviewMode) return null;
|
|
67
|
+
return colorize(`${ICON_REVIEW} review`, "34");
|
|
68
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import type { GitStatus } from "./types";
|
|
3
|
+
import { colorize } from "./style";
|
|
4
|
+
import { ICON_GIT } from "./style";
|
|
5
|
+
|
|
6
|
+
export function getGitStatus(cwd: string): GitStatus | null {
|
|
7
|
+
if (!cwd) return null;
|
|
8
|
+
const result = spawnSync("git", ["-C", cwd, "status", "--porcelain=v2", "-b"], {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11
|
+
});
|
|
12
|
+
if (result.status !== 0 || !result.stdout) return null;
|
|
13
|
+
const status: GitStatus = {
|
|
14
|
+
branch: null,
|
|
15
|
+
ahead: 0,
|
|
16
|
+
behind: 0,
|
|
17
|
+
staged: 0,
|
|
18
|
+
unstaged: 0,
|
|
19
|
+
untracked: 0,
|
|
20
|
+
conflicted: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (!line) continue;
|
|
26
|
+
if (line.startsWith("# branch.head ")) {
|
|
27
|
+
status.branch = line.slice("# branch.head ".length).trim();
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith("# branch.ab ")) {
|
|
31
|
+
const parts = line
|
|
32
|
+
.slice("# branch.ab ".length)
|
|
33
|
+
.trim()
|
|
34
|
+
.split(/\s+/);
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
if (part.startsWith("+")) status.ahead = Number(part.slice(1)) || 0;
|
|
37
|
+
if (part.startsWith("-")) status.behind = Number(part.slice(1)) || 0;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith("? ")) {
|
|
42
|
+
status.untracked += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith("u ")) {
|
|
46
|
+
status.conflicted += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
50
|
+
const parts = line.split(/\s+/);
|
|
51
|
+
const xy = parts[1] || "";
|
|
52
|
+
const staged = xy[0];
|
|
53
|
+
const unstaged = xy[1];
|
|
54
|
+
if (staged && staged !== ".") status.staged += 1;
|
|
55
|
+
if (unstaged && unstaged !== ".") status.unstaged += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!status.branch) {
|
|
61
|
+
status.branch = "HEAD";
|
|
62
|
+
}
|
|
63
|
+
return status;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatGitSegment(status: GitStatus | null): string | null {
|
|
67
|
+
if (!status || !status.branch) return null;
|
|
68
|
+
const meta: string[] = [];
|
|
69
|
+
const dirtyCount = status.staged + status.unstaged + status.untracked;
|
|
70
|
+
if (status.ahead > 0) meta.push(`↑${status.ahead}`);
|
|
71
|
+
if (status.behind > 0) meta.push(`↓${status.behind}`);
|
|
72
|
+
if (status.conflicted > 0) meta.push(`✖${status.conflicted}`);
|
|
73
|
+
if (dirtyCount > 0) meta.push(`+${dirtyCount}`);
|
|
74
|
+
const suffix = meta.length > 0 ? ` [${meta.join("")}]` : "";
|
|
75
|
+
const text = `${ICON_GIT} ${status.branch}${suffix}`;
|
|
76
|
+
const hasConflicts = status.conflicted > 0;
|
|
77
|
+
const isDirty = dirtyCount > 0;
|
|
78
|
+
if (hasConflicts) return colorize(text, "31");
|
|
79
|
+
if (isDirty) return colorize(text, "33");
|
|
80
|
+
if (status.ahead > 0 || status.behind > 0) return colorize(text, "36");
|
|
81
|
+
return colorize(text, "32");
|
|
82
|
+
}
|