ccglance 1.0.0
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/LICENSE +21 -0
- package/README.md +384 -0
- package/README.zh-CN.md +332 -0
- package/dist/cli.js +91 -0
- package/dist/defaults/index.js +42 -0
- package/dist/readers/index.js +13 -0
- package/dist/readers/stdin.js +17 -0
- package/dist/readers/termwidth.js +55 -0
- package/dist/readers/transcript.js +206 -0
- package/dist/readers/version.js +6 -0
- package/dist/render/colors.js +43 -0
- package/dist/render/icons.js +19 -0
- package/dist/render/index.js +43 -0
- package/dist/render/layout.js +43 -0
- package/dist/runtime/git.js +204 -0
- package/dist/runtime/version.js +81 -0
- package/dist/segments/index.js +203 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/format.js +143 -0
- package/dist/utils/paths.js +25 -0
- package/dist/utils/sanitize.js +18 -0
- package/dist/utils/width.js +93 -0
- package/docs/assets/preview.png +0 -0
- package/package.json +57 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readTailLines = readTailLines;
|
|
7
|
+
exports.getLatestTranscriptUsage = getLatestTranscriptUsage;
|
|
8
|
+
exports.getRecentStatus = getRecentStatus;
|
|
9
|
+
// transcript(.jsonl)流式读取 —— 只从文件「尾部」读固定字节,绝不把整个文件读进内存
|
|
10
|
+
// (优于把整份 readFile 后 split 的做法)。派生指标按 path+mtime+size 落盘缓存,未变化则复用,
|
|
11
|
+
// 从而不阻塞状态栏:默认无 transcript 段时根本不调用;启用时也只做一次有上限的尾部读。
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const paths_1 = require("../utils/paths");
|
|
15
|
+
const TAIL_BYTES = 64 * 1024; // 尾部读取上限:足够覆盖最近若干条消息
|
|
16
|
+
function cacheFileFor(p) {
|
|
17
|
+
const h = hashString(path_1.default.resolve(p));
|
|
18
|
+
return path_1.default.join((0, paths_1.cacheDir)('transcript'), `${h}.usage.json`);
|
|
19
|
+
}
|
|
20
|
+
function statusCacheFileFor(p) {
|
|
21
|
+
const h = hashString(path_1.default.resolve(p));
|
|
22
|
+
return path_1.default.join((0, paths_1.cacheDir)('transcript'), `${h}.status.json`);
|
|
23
|
+
}
|
|
24
|
+
function hashString(input) {
|
|
25
|
+
let h = 0x811c9dc5;
|
|
26
|
+
for (let i = 0; i < input.length; i++) {
|
|
27
|
+
h ^= input.charCodeAt(i);
|
|
28
|
+
h = Math.imul(h, 0x01000193);
|
|
29
|
+
}
|
|
30
|
+
return (h >>> 0).toString(16);
|
|
31
|
+
}
|
|
32
|
+
// 流式尾部读:只读最后 maxBytes 字节,丢弃被截断的首行,返回完整 JSONL 行。
|
|
33
|
+
function readTailLines(filePath, maxBytes = TAIL_BYTES) {
|
|
34
|
+
let fd;
|
|
35
|
+
try {
|
|
36
|
+
const st = fs_1.default.statSync(filePath);
|
|
37
|
+
const size = st.size;
|
|
38
|
+
if (size <= 0)
|
|
39
|
+
return [];
|
|
40
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
41
|
+
const len = size - start;
|
|
42
|
+
fd = fs_1.default.openSync(filePath, 'r');
|
|
43
|
+
const buf = Buffer.allocUnsafe(len);
|
|
44
|
+
fs_1.default.readSync(fd, buf, 0, len, start);
|
|
45
|
+
let text = buf.toString('utf8');
|
|
46
|
+
if (start > 0) {
|
|
47
|
+
const nl = text.indexOf('\n'); // 丢弃因截断产生的不完整首行
|
|
48
|
+
if (nl >= 0)
|
|
49
|
+
text = text.slice(nl + 1);
|
|
50
|
+
}
|
|
51
|
+
return text.split('\n').filter((l) => l.trim().length > 0);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (fd !== undefined) {
|
|
58
|
+
try {
|
|
59
|
+
fs_1.default.closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 从尾部行里取最近一条 assistant 消息的 usage(倒序找)。
|
|
66
|
+
function extractLatestUsage(lines) {
|
|
67
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
68
|
+
let e;
|
|
69
|
+
try {
|
|
70
|
+
e = JSON.parse(lines[i]);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (e.type === 'assistant' && e.message && e.message.usage) {
|
|
76
|
+
const u = e.message.usage;
|
|
77
|
+
return {
|
|
78
|
+
inputTokens: u.input_tokens || 0,
|
|
79
|
+
outputTokens: u.output_tokens || 0,
|
|
80
|
+
cacheReadTokens: u.cache_read_input_tokens || 0,
|
|
81
|
+
cacheCreationTokens: u.cache_creation_input_tokens || 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// 取最近 usage:优先命中「文件未变化」的落盘缓存,否则尾部流式读一次并回写缓存。
|
|
88
|
+
function getLatestTranscriptUsage(transcriptPath) {
|
|
89
|
+
if (!transcriptPath)
|
|
90
|
+
return null;
|
|
91
|
+
let st;
|
|
92
|
+
try {
|
|
93
|
+
st = fs_1.default.statSync(transcriptPath);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const cf = cacheFileFor(transcriptPath);
|
|
99
|
+
try {
|
|
100
|
+
const c = JSON.parse(fs_1.default.readFileSync(cf, 'utf8'));
|
|
101
|
+
if (c.mtimeMs === st.mtimeMs && c.size === st.size)
|
|
102
|
+
return c.usage; // 未变化 → 复用
|
|
103
|
+
}
|
|
104
|
+
catch { /* 无缓存 / 坏缓存 */ }
|
|
105
|
+
const usage = extractLatestUsage(readTailLines(transcriptPath));
|
|
106
|
+
try {
|
|
107
|
+
fs_1.default.mkdirSync(path_1.default.dirname(cf), { recursive: true });
|
|
108
|
+
const payload = { mtimeMs: st.mtimeMs, size: st.size, usage };
|
|
109
|
+
fs_1.default.writeFileSync(cf, JSON.stringify(payload));
|
|
110
|
+
}
|
|
111
|
+
catch { /* 缓存尽力而为 */ }
|
|
112
|
+
return usage;
|
|
113
|
+
}
|
|
114
|
+
const blocks = (c) => {
|
|
115
|
+
if (Array.isArray(c))
|
|
116
|
+
return c;
|
|
117
|
+
if (c && typeof c === 'object')
|
|
118
|
+
return [c];
|
|
119
|
+
return [];
|
|
120
|
+
};
|
|
121
|
+
const contentText = (c) => {
|
|
122
|
+
if (typeof c === 'string')
|
|
123
|
+
return c;
|
|
124
|
+
return blocks(c).map((b) => b.text || b.content || '').filter(Boolean).join('\n');
|
|
125
|
+
};
|
|
126
|
+
function isInterruptedEntry(e) {
|
|
127
|
+
if (e.type !== 'user')
|
|
128
|
+
return false;
|
|
129
|
+
const text = contentText(e.message && e.message.content).trim();
|
|
130
|
+
if (!text)
|
|
131
|
+
return false;
|
|
132
|
+
const loose = /\b(interrupted|interrupt|aborted|abort|cancelled|canceled|keyboardinterrupt)\b|中断|取消/i;
|
|
133
|
+
if (e.isMeta)
|
|
134
|
+
return loose.test(text);
|
|
135
|
+
return /^\[?\s*(request\s+)?(interrupted|cancelled|canceled|aborted)(\s+by\s+user)?\.?\s*\]?$/i.test(text)
|
|
136
|
+
|| /^\[?\s*user\s+(interrupted|cancelled|canceled|aborted)(\s+the\s+request)?\.?\s*\]?$/i.test(text)
|
|
137
|
+
|| /^keyboardinterrupt$/i.test(text);
|
|
138
|
+
}
|
|
139
|
+
// 会话状态推断:从尾部倒序找「最后一条对话消息」(跳过 mode/title 等元数据行),细分为:
|
|
140
|
+
// tool —— assistant 请求工具(tool_use),带工具名 (正在调用/运行工具)
|
|
141
|
+
// idle —— assistant end_turn/stop_sequence (回合结束)
|
|
142
|
+
// paused —— 用户中断 / Esc 取消
|
|
143
|
+
// working —— user 消息含 tool_result (工具已返回,模型继续处理)
|
|
144
|
+
// thinking —— user 纯提示 / assistant 未结束且无工具 (等待模型响应/生成中)
|
|
145
|
+
function extractRecentStatus(lines) {
|
|
146
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
147
|
+
let e;
|
|
148
|
+
try {
|
|
149
|
+
e = JSON.parse(lines[i]);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const msg = e.message;
|
|
155
|
+
if (isInterruptedEntry(e))
|
|
156
|
+
return { state: 'paused' };
|
|
157
|
+
if (e.type === 'assistant') {
|
|
158
|
+
const content = blocks(msg && msg.content);
|
|
159
|
+
const toolUse = content.find((b) => b && b.type === 'tool_use');
|
|
160
|
+
if (toolUse)
|
|
161
|
+
return { state: 'tool', tool: toolUse.name };
|
|
162
|
+
// 已产出文本/思考内容 = 回合结束 → idle。
|
|
163
|
+
// 注意:Claude Code transcript 常把完成的文本回复记为 stop_reason:null(而非 end_turn),
|
|
164
|
+
// 所以判 idle 看「有内容」而非 stop_reason,否则正常回复会被误判成 thinking。
|
|
165
|
+
const hasText = content.some((b) => b && (b.type === 'text' || b.type === 'thinking'))
|
|
166
|
+
|| (typeof (msg && msg.content) === 'string' && String(msg && msg.content).length > 0);
|
|
167
|
+
return { state: hasText ? 'idle' : 'thinking' };
|
|
168
|
+
}
|
|
169
|
+
if (e.type === 'user') {
|
|
170
|
+
const hasToolResult = blocks(msg && msg.content).some((b) => b && b.type === 'tool_result');
|
|
171
|
+
return { state: hasToolResult ? 'working' : 'thinking' };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
// 局限:statusLine 仅在重绘时被 spawn,故这是「重绘时刻的近似态」,非实时事件流。
|
|
177
|
+
function getRecentStatus(transcriptPath) {
|
|
178
|
+
if (!transcriptPath)
|
|
179
|
+
return null;
|
|
180
|
+
let st;
|
|
181
|
+
try {
|
|
182
|
+
st = fs_1.default.statSync(transcriptPath);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const cf = statusCacheFileFor(transcriptPath);
|
|
188
|
+
try {
|
|
189
|
+
const c = JSON.parse(fs_1.default.readFileSync(cf, 'utf8'));
|
|
190
|
+
if (c.mtimeMs === st.mtimeMs && c.size === st.size)
|
|
191
|
+
return c.status;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Cache miss.
|
|
195
|
+
}
|
|
196
|
+
const status = extractRecentStatus(readTailLines(transcriptPath));
|
|
197
|
+
try {
|
|
198
|
+
fs_1.default.mkdirSync(path_1.default.dirname(cf), { recursive: true });
|
|
199
|
+
const payload = { mtimeMs: st.mtimeMs, size: st.size, status };
|
|
200
|
+
fs_1.default.writeFileSync(cf, JSON.stringify(payload));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Cache is best effort.
|
|
204
|
+
}
|
|
205
|
+
return status;
|
|
206
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scheduleRefresh = exports.readCachedLatest = void 0;
|
|
4
|
+
var version_1 = require("../runtime/version");
|
|
5
|
+
Object.defineProperty(exports, "readCachedLatest", { enumerable: true, get: function () { return version_1.readCachedLatest; } });
|
|
6
|
+
Object.defineProperty(exports, "scheduleRefresh", { enumerable: true, get: function () { return version_1.scheduleRefresh; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.seg = exports.DOT = exports.SEP = exports.C = exports.paint = void 0;
|
|
4
|
+
// 上色与配色 —— ANSI 16-color,固定内置风格。
|
|
5
|
+
const sanitize_1 = require("../utils/sanitize");
|
|
6
|
+
const R = '\x1b[0m';
|
|
7
|
+
const GREEN = 92;
|
|
8
|
+
const RED = 91;
|
|
9
|
+
const YELLOW = 33;
|
|
10
|
+
const CYAN = 96;
|
|
11
|
+
const BLUE = 94;
|
|
12
|
+
const MAGENTA = 95;
|
|
13
|
+
const WHITE = 37;
|
|
14
|
+
const paint = (code, s, bold = false) => `\x1b[${bold ? '1;' : ''}${code}m${(0, sanitize_1.sanitizeText)(s)}${R}`;
|
|
15
|
+
exports.paint = paint;
|
|
16
|
+
exports.C = {
|
|
17
|
+
// 第一行(运行时)
|
|
18
|
+
model: CYAN, effort: YELLOW, fast: GREEN,
|
|
19
|
+
ctx: MAGENTA, cache: GREEN, style: CYAN,
|
|
20
|
+
rate: CYAN, // rate_limits 主图标/标签
|
|
21
|
+
rateReset: YELLOW, // 配额重置时间
|
|
22
|
+
safe: GREEN, // 正常/成功/新增
|
|
23
|
+
caution: YELLOW, // 阈值染色:警戒
|
|
24
|
+
danger: RED, // 阈值染色:危险/删除
|
|
25
|
+
idle: GREEN, // 状态:空闲
|
|
26
|
+
paused: YELLOW, // 状态:用户暂停/中断
|
|
27
|
+
working: CYAN, // 状态:工作中/工具返回后处理
|
|
28
|
+
stTool: BLUE, // 状态:调用工具
|
|
29
|
+
stThink: MAGENTA, // 状态:等待模型/生成中
|
|
30
|
+
// 第二行(项目 / 会话)
|
|
31
|
+
name: MAGENTA, dirIcon: YELLOW, dirText: GREEN, git: WHITE,
|
|
32
|
+
gitClean: GREEN, gitDirty: BLUE, gitConflict: RED,
|
|
33
|
+
gitAhead: GREEN, gitBehind: BLUE,
|
|
34
|
+
sess: CYAN, add: GREEN, del: RED,
|
|
35
|
+
cost: YELLOW, // 美元成本
|
|
36
|
+
wt: GREEN, // worktree
|
|
37
|
+
ver: CYAN, upd: RED, // 版本:默认同 style
|
|
38
|
+
};
|
|
39
|
+
exports.SEP = (0, exports.paint)(WHITE, ' | '); // 段间白色分隔符(两侧各一空格)
|
|
40
|
+
exports.DOT = ' · '; // 项内值与值分隔(中点,两侧各一空格)
|
|
41
|
+
// “图标 文字”同色段
|
|
42
|
+
const seg = (color, icon, text, iconColor = color, textColor = color, bold = false) => (0, exports.paint)(iconColor, icon, bold) + ' ' + (0, exports.paint)(textColor, text, bold);
|
|
43
|
+
exports.seg = seg;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.icon = icon;
|
|
4
|
+
const ICONS = {
|
|
5
|
+
model: '🤖',
|
|
6
|
+
dir: '📁',
|
|
7
|
+
git: '🌿',
|
|
8
|
+
context: '⚡️',
|
|
9
|
+
cache: '💾',
|
|
10
|
+
rate_limits: '📊',
|
|
11
|
+
fast: '🚀',
|
|
12
|
+
style: '🎯',
|
|
13
|
+
session: '⏱️',
|
|
14
|
+
cost: '💰',
|
|
15
|
+
version: '💩',
|
|
16
|
+
};
|
|
17
|
+
function icon(id) {
|
|
18
|
+
return ICONS[id];
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasEnabledSegment = hasEnabledSegment;
|
|
4
|
+
exports.render = render;
|
|
5
|
+
const colors_1 = require("./colors");
|
|
6
|
+
const segments_1 = require("../segments");
|
|
7
|
+
const layout_1 = require("./layout");
|
|
8
|
+
const termwidth_1 = require("../readers/termwidth");
|
|
9
|
+
function hasEnabledSegment(spec, id) {
|
|
10
|
+
return spec.segments[id]?.enabled !== false && spec.layout.rows.some((row) => row.includes(id));
|
|
11
|
+
}
|
|
12
|
+
function render(data, spec, options = {}) {
|
|
13
|
+
const latest = options.latest !== undefined
|
|
14
|
+
? options.latest
|
|
15
|
+
: data.version && hasEnabledSegment(spec, 'version')
|
|
16
|
+
? require('../runtime/version').readCachedLatest()
|
|
17
|
+
: '';
|
|
18
|
+
const ctx = {
|
|
19
|
+
data,
|
|
20
|
+
spec,
|
|
21
|
+
nowMs: options.nowMs ?? Date.now(),
|
|
22
|
+
latest,
|
|
23
|
+
previewStatus: options.previewStatus,
|
|
24
|
+
};
|
|
25
|
+
// 按逻辑行收集:跳过 enabled:false,调用段函数,过滤 null。
|
|
26
|
+
const rendered = spec.layout.rows.map((row) => row
|
|
27
|
+
.filter((id) => spec.segments[id] == null || spec.segments[id].enabled !== false)
|
|
28
|
+
.map((id) => {
|
|
29
|
+
const fn = segments_1.SEGMENTS[id];
|
|
30
|
+
return fn ? fn(ctx) : null;
|
|
31
|
+
})
|
|
32
|
+
.filter((s) => s != null && s.length > 0));
|
|
33
|
+
// 固定白色段分隔符。
|
|
34
|
+
const coloredSep = (0, colors_1.paint)(37, spec.layout.separator);
|
|
35
|
+
// 多级健壮宽度探测(CCGLANCE_WIDTH → COLUMNS → stdout.columns → unix 兜底 → minWidth)
|
|
36
|
+
const columns = (0, termwidth_1.detectWidth)(spec.layout.minWidth);
|
|
37
|
+
const lines = (0, layout_1.layout)(rendered, coloredSep, columns, spec.layout.mode);
|
|
38
|
+
if (!lines.length)
|
|
39
|
+
return '';
|
|
40
|
+
// 每行前缀 \x1b[0m:覆盖 Claude Code 施加给状态栏的 dim(发灰),让配色正常显示。
|
|
41
|
+
const RESET = '\x1b[0m';
|
|
42
|
+
return lines.map((l) => RESET + l).join('\n') + '\n';
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.layout = layout;
|
|
4
|
+
// 响应式多行布局引擎。
|
|
5
|
+
// 输入:已渲染的逻辑行(每行是若干段的字符串,已过滤 null/空),join 用的分隔符,终端宽度,模式。
|
|
6
|
+
// responsive:每个逻辑行按显示宽度贪心折成多物理行(行末不留分隔符);组间强制换行。
|
|
7
|
+
// fixed:每个逻辑行原样一行,不折。
|
|
8
|
+
const width_1 = require("../utils/width");
|
|
9
|
+
// 终端宽度探测已统一到 reader/termwidth.ts 的 detectWidth()。
|
|
10
|
+
function layout(rows, sep, columns, mode) {
|
|
11
|
+
const sepW = (0, width_1.stringWidth)(sep);
|
|
12
|
+
const lines = [];
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
const segs = row.filter((s) => s && s.length > 0);
|
|
15
|
+
if (segs.length === 0)
|
|
16
|
+
continue;
|
|
17
|
+
if (mode === 'fixed') {
|
|
18
|
+
lines.push(segs.join(sep));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
// responsive:贪心装箱。
|
|
22
|
+
// 最小保证「一行一个功能」:当前行为空时(cur.length===0)无条件放入该段,
|
|
23
|
+
// 即使单段比整行还宽也让它独占一行 —— 宁可它自己占一行,也不与他段挤在一起被挡住。
|
|
24
|
+
let cur = [];
|
|
25
|
+
let curW = 0;
|
|
26
|
+
for (const s of segs) {
|
|
27
|
+
const w = (0, width_1.stringWidth)(s);
|
|
28
|
+
const add = cur.length ? sepW + w : w;
|
|
29
|
+
if (cur.length > 0 && curW + add > columns) {
|
|
30
|
+
lines.push(cur.join(sep));
|
|
31
|
+
cur = [s];
|
|
32
|
+
curW = w;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
cur.push(s);
|
|
36
|
+
curW += add;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (cur.length > 0)
|
|
40
|
+
lines.push(cur.join(sep));
|
|
41
|
+
}
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.gitSegment = gitSegment;
|
|
7
|
+
// Git segment: one bounded git status call, cached on disk.
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const colors_1 = require("../render/colors");
|
|
11
|
+
const paths_1 = require("../utils/paths");
|
|
12
|
+
const CACHE_DIR = (0, paths_1.cacheDir)('git');
|
|
13
|
+
const DEFAULT_TTL_MS = 20 * 60 * 1000;
|
|
14
|
+
const GIT_TIMEOUT_MS = 700;
|
|
15
|
+
const REFRESH_THROTTLE_MS = 5000;
|
|
16
|
+
function resolveGitPaths(cwd) {
|
|
17
|
+
const abs = path_1.default.resolve(cwd);
|
|
18
|
+
const root = repoRootFor(abs);
|
|
19
|
+
const key = hashString(root || abs);
|
|
20
|
+
return {
|
|
21
|
+
cwd: abs,
|
|
22
|
+
root,
|
|
23
|
+
cacheFile: path_1.default.join(CACHE_DIR, `git-${key}.json`),
|
|
24
|
+
refreshFile: path_1.default.join(CACHE_DIR, `git-${key}.refresh`),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function repoRootFor(cwd) {
|
|
28
|
+
try {
|
|
29
|
+
let cur = path_1.default.resolve(cwd);
|
|
30
|
+
while (true) {
|
|
31
|
+
if (fs_1.default.existsSync(path_1.default.join(cur, '.git')))
|
|
32
|
+
return cur;
|
|
33
|
+
const parent = path_1.default.dirname(cur);
|
|
34
|
+
if (parent === cur)
|
|
35
|
+
return null;
|
|
36
|
+
cur = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function hashString(input) {
|
|
44
|
+
let h1 = 0x811c9dc5;
|
|
45
|
+
let h2 = 0x9e3779b9;
|
|
46
|
+
for (let i = 0; i < input.length; i++) {
|
|
47
|
+
const c = input.charCodeAt(i);
|
|
48
|
+
h1 ^= c;
|
|
49
|
+
h1 = Math.imul(h1, 0x01000193);
|
|
50
|
+
h2 ^= c;
|
|
51
|
+
h2 = Math.imul(h2, 0x85ebca6b);
|
|
52
|
+
}
|
|
53
|
+
return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0');
|
|
54
|
+
}
|
|
55
|
+
function readCache(paths) {
|
|
56
|
+
try {
|
|
57
|
+
const c = JSON.parse(fs_1.default.readFileSync(paths.cacheFile, 'utf8'));
|
|
58
|
+
return c && c.branch && typeof c.checkedAt === 'number' ? c : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function shouldRefresh(paths) {
|
|
65
|
+
const marker = paths.refreshFile;
|
|
66
|
+
try {
|
|
67
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
68
|
+
fs_1.default.writeFileSync(marker, String(Date.now()), { flag: 'wx' });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Existing marker means another statusline process is already refreshing.
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const st = fs_1.default.statSync(marker);
|
|
76
|
+
if (Date.now() - st.mtimeMs < REFRESH_THROTTLE_MS)
|
|
77
|
+
return false;
|
|
78
|
+
fs_1.default.rmSync(marker, { force: true });
|
|
79
|
+
fs_1.default.writeFileSync(marker, String(Date.now()), { flag: 'wx' });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function parseStatus(out) {
|
|
87
|
+
let head = '';
|
|
88
|
+
let oid = '';
|
|
89
|
+
let ahead = 0;
|
|
90
|
+
let behind = 0;
|
|
91
|
+
let dirty = false;
|
|
92
|
+
let conflict = false;
|
|
93
|
+
for (const raw of out.split(/\r?\n/)) {
|
|
94
|
+
const line = raw.trimEnd();
|
|
95
|
+
if (!line)
|
|
96
|
+
continue;
|
|
97
|
+
if (line.startsWith('# branch.head ')) {
|
|
98
|
+
head = line.slice('# branch.head '.length).trim();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (line.startsWith('# branch.oid ')) {
|
|
102
|
+
oid = line.slice('# branch.oid '.length).trim();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith('# branch.ab ')) {
|
|
106
|
+
const m = /# branch\.ab \+(\d+) -(\d+)/.exec(line);
|
|
107
|
+
if (m) {
|
|
108
|
+
ahead = parseInt(m[1], 10) || 0;
|
|
109
|
+
behind = parseInt(m[2], 10) || 0;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (line[0] === '#')
|
|
114
|
+
continue;
|
|
115
|
+
if (line.startsWith('u '))
|
|
116
|
+
conflict = true;
|
|
117
|
+
if (!line.startsWith('! '))
|
|
118
|
+
dirty = true;
|
|
119
|
+
}
|
|
120
|
+
const branch = head && head !== '(detached)' ? head : oid.slice(0, 8);
|
|
121
|
+
if (!branch)
|
|
122
|
+
return null;
|
|
123
|
+
return { branch, glyph: conflict ? '⚠' : dirty ? '●' : '✓', ahead, behind };
|
|
124
|
+
}
|
|
125
|
+
function readHeadFallback(root) {
|
|
126
|
+
if (!root)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const dotgit = path_1.default.join(root, '.git');
|
|
130
|
+
let gitDir = dotgit;
|
|
131
|
+
const st = fs_1.default.statSync(dotgit);
|
|
132
|
+
if (st.isFile()) {
|
|
133
|
+
const raw = fs_1.default.readFileSync(dotgit, 'utf8').trim();
|
|
134
|
+
const m = /^gitdir:\s*(.+)$/i.exec(raw);
|
|
135
|
+
if (!m)
|
|
136
|
+
return null;
|
|
137
|
+
gitDir = path_1.default.resolve(root, m[1]);
|
|
138
|
+
}
|
|
139
|
+
const headRaw = fs_1.default.readFileSync(path_1.default.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
140
|
+
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(headRaw);
|
|
141
|
+
const branch = ref ? ref[1] : headRaw.slice(0, 8);
|
|
142
|
+
return branch ? { branch, ahead: 0, behind: 0 } : null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function branchHintFallback(branchHint) {
|
|
149
|
+
const branch = branchHint && branchHint.trim();
|
|
150
|
+
return branch ? { branch, ahead: 0, behind: 0 } : null;
|
|
151
|
+
}
|
|
152
|
+
function scheduleGitRefresh(paths) {
|
|
153
|
+
if (!paths.root || !shouldRefresh(paths))
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
const bg = "const fs=require('fs'),path=require('path'),{execFileSync}=require('child_process');" +
|
|
157
|
+
"const cwd=" + JSON.stringify(paths.root) + ";" +
|
|
158
|
+
"const cf=" + JSON.stringify(paths.cacheFile) + ";" +
|
|
159
|
+
"const marker=" + JSON.stringify(paths.refreshFile) + ";" +
|
|
160
|
+
"const parseStatus=" + parseStatus.toString() + ";" +
|
|
161
|
+
"const save=obj=>{const tmp=cf+'.'+process.pid+'.tmp';fs.mkdirSync(path.dirname(cf),{recursive:true});fs.writeFileSync(tmp,JSON.stringify(obj));fs.renameSync(tmp,cf)};" +
|
|
162
|
+
"try{" +
|
|
163
|
+
"const out=execFileSync('git',['--no-optional-locks','-C',cwd,'status','--porcelain=v2','--branch'],{encoding:'utf8',stdio:['ignore','pipe','ignore'],timeout:" + GIT_TIMEOUT_MS + ",windowsHide:true});" +
|
|
164
|
+
"const info=parseStatus(out);if(info)save({...info,checkedAt:Date.now()});" +
|
|
165
|
+
"}catch{}finally{try{fs.rmSync(marker,{force:true})}catch{}}";
|
|
166
|
+
const { spawn } = require('child_process');
|
|
167
|
+
spawn(process.execPath, ['-e', bg], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
try {
|
|
171
|
+
fs_1.default.rmSync(paths.refreshFile, { force: true });
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function formatGit(info, icon, worktreeName) {
|
|
177
|
+
const status = [];
|
|
178
|
+
if (info.glyph) {
|
|
179
|
+
const color = info.glyph === '⚠' ? colors_1.C.gitConflict : info.glyph === '●' ? colors_1.C.gitDirty : colors_1.C.gitClean;
|
|
180
|
+
status.push((0, colors_1.paint)(color, info.glyph));
|
|
181
|
+
}
|
|
182
|
+
if (info.ahead > 0)
|
|
183
|
+
status.push((0, colors_1.paint)(colors_1.C.gitAhead, '↑' + info.ahead));
|
|
184
|
+
if (info.behind > 0)
|
|
185
|
+
status.push((0, colors_1.paint)(colors_1.C.gitBehind, '↓' + info.behind));
|
|
186
|
+
const branch = (0, colors_1.seg)(colors_1.C.git, icon, info.branch);
|
|
187
|
+
const details = status.length ? status.join(' ') : '';
|
|
188
|
+
const worktree = worktreeName && worktreeName.trim() ? (0, colors_1.seg)(colors_1.C.wt, '🌲', worktreeName.trim()) : '';
|
|
189
|
+
return [branch, details, worktree].filter(Boolean).join(' ');
|
|
190
|
+
}
|
|
191
|
+
// 分支 + 状态标记(✓/●/⚠)+ ↑ahead ↓behind;无分支或非仓库返回 null。
|
|
192
|
+
function gitSegment(cwd, icon = '🌿', options = {}, branchHint, worktreeName) {
|
|
193
|
+
const ttlMs = typeof options.ttlMs === 'number' && options.ttlMs >= 0 ? options.ttlMs : DEFAULT_TTL_MS;
|
|
194
|
+
const paths = resolveGitPaths(cwd);
|
|
195
|
+
const fallback = branchHintFallback(branchHint) || readHeadFallback(paths.root);
|
|
196
|
+
if (!fallback)
|
|
197
|
+
return null;
|
|
198
|
+
const cached = readCache(paths);
|
|
199
|
+
const cacheFresh = cached && Date.now() - cached.checkedAt <= ttlMs;
|
|
200
|
+
if (cacheFresh && cached.branch === fallback.branch)
|
|
201
|
+
return formatGit(cached, icon, worktreeName);
|
|
202
|
+
scheduleGitRefresh(paths);
|
|
203
|
+
return formatGit(cacheFresh && cached.branch === fallback.branch ? cached : fallback, icon, worktreeName);
|
|
204
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readCachedLatest = readCachedLatest;
|
|
7
|
+
exports.scheduleRefresh = scheduleRefresh;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const paths_1 = require("../utils/paths");
|
|
11
|
+
const CACHE_TTL_MS = 4 * 3600 * 1000;
|
|
12
|
+
const REFRESH_THROTTLE_MS = 60 * 1000;
|
|
13
|
+
const cacheFile = path_1.default.join((0, paths_1.cacheDir)('version'), 'claude-code.json');
|
|
14
|
+
const refreshFile = path_1.default.join((0, paths_1.cacheDir)('version'), 'claude-code.refresh');
|
|
15
|
+
let memo = null;
|
|
16
|
+
function readCache() {
|
|
17
|
+
if (memo)
|
|
18
|
+
return memo;
|
|
19
|
+
try {
|
|
20
|
+
memo = JSON.parse(fs_1.default.readFileSync(cacheFile, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
memo = {};
|
|
24
|
+
}
|
|
25
|
+
return memo;
|
|
26
|
+
}
|
|
27
|
+
function readCachedLatest() {
|
|
28
|
+
return readCache().latest || '';
|
|
29
|
+
}
|
|
30
|
+
function shouldRefresh() {
|
|
31
|
+
try {
|
|
32
|
+
fs_1.default.mkdirSync(path_1.default.dirname(refreshFile), { recursive: true });
|
|
33
|
+
fs_1.default.writeFileSync(refreshFile, String(Date.now()), { flag: 'wx' });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Existing marker means another statusline process is already refreshing.
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const st = fs_1.default.statSync(refreshFile);
|
|
41
|
+
if (Date.now() - st.mtimeMs < REFRESH_THROTTLE_MS)
|
|
42
|
+
return false;
|
|
43
|
+
fs_1.default.rmSync(refreshFile, { force: true });
|
|
44
|
+
fs_1.default.writeFileSync(refreshFile, String(Date.now()), { flag: 'wx' });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function scheduleRefresh() {
|
|
52
|
+
try {
|
|
53
|
+
const age = Date.now() - (readCache().checkedAt || 0);
|
|
54
|
+
if (age <= CACHE_TTL_MS)
|
|
55
|
+
return;
|
|
56
|
+
if (!shouldRefresh())
|
|
57
|
+
return;
|
|
58
|
+
const bg = "const https=require('https'),fs=require('fs'),path=require('path');" +
|
|
59
|
+
"const f=" + JSON.stringify(cacheFile) + ";" +
|
|
60
|
+
"const marker=" + JSON.stringify(refreshFile) + ";" +
|
|
61
|
+
"let prev='';try{prev=(JSON.parse(fs.readFileSync(f,'utf8')).latest)||''}catch{}" +
|
|
62
|
+
"let done=false;const finish=v=>{if(done)return;done=true;" +
|
|
63
|
+
"try{const tmp=f+'.'+process.pid+'.tmp';fs.mkdirSync(path.dirname(f),{recursive:true});fs.writeFileSync(tmp,JSON.stringify({latest:v||prev,checkedAt:Date.now()}));fs.renameSync(tmp,f)}catch{}" +
|
|
64
|
+
"try{fs.rmSync(marker,{force:true})}catch{}};" +
|
|
65
|
+
"try{" +
|
|
66
|
+
"const req=https.get('https://registry.npmjs.org/@anthropic-ai/claude-code/latest',r=>{" +
|
|
67
|
+
"if(r.statusCode!==200){r.resume();return finish('')}" +
|
|
68
|
+
"let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{finish(JSON.parse(d).version)}catch{finish('')}})" +
|
|
69
|
+
"});req.on('error',()=>finish(''));req.setTimeout(5000,()=>{req.destroy();finish('')});" +
|
|
70
|
+
"}catch{finish('')}";
|
|
71
|
+
const { spawn } = require('child_process');
|
|
72
|
+
spawn(process.execPath, ['-e', bg], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
try {
|
|
76
|
+
fs_1.default.rmSync(refreshFile, { force: true });
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
79
|
+
// Version refresh never affects statusline rendering.
|
|
80
|
+
}
|
|
81
|
+
}
|