@cxmyu/ccglance 1.0.3 → 1.1.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/README.md +9 -2
- package/README.zh-CN.md +7 -2
- package/dist/cli.js +3 -0
- package/dist/runtime/bg.js +208 -0
- package/dist/runtime/git.js +3 -60
- package/dist/runtime/refresh.js +78 -0
- package/dist/runtime/version.js +3 -15
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Update or remove later with `npm update -g @cxmyu/ccglance` / `npm uninstall -g
|
|
|
56
56
|
```bash
|
|
57
57
|
git clone https://github.com/CxMYu/CcGlanceLine.git
|
|
58
58
|
cd CcGlanceLine
|
|
59
|
-
npm install # installs devDeps; the `prepare` script then compiles src/
|
|
59
|
+
npm install # installs devDeps; the `prepare` script then compiles src/ -> dist/
|
|
60
60
|
npm link # optional: expose `ccglance` on your PATH
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -102,10 +102,17 @@ On Windows use a full path, e.g. `node D:\\path\\to\\ccglance\\dist\\cli.js`.
|
|
|
102
102
|
> command needs no path at all. `"padding": 0` drops Claude Code's default leading
|
|
103
103
|
> indent so the row starts at the left edge.
|
|
104
104
|
|
|
105
|
+
> **Windows latency tip:** the global `ccglance` command is an npm `.cmd` shim,
|
|
106
|
+
> which adds a cmd batch layer plus a PATH lookup on every redraw — tens to
|
|
107
|
+
> hundreds of milliseconds depending on antivirus and system cache state. For
|
|
108
|
+
> the lowest latency, point `command` directly at the compiled file:
|
|
109
|
+
> `node D:\\absolute\\path\\to\\ccglance\\dist\\cli.js` (a global install lives
|
|
110
|
+
> at `<npm global dir>\\node_modules\\@cxmyu\\ccglance\\dist\\cli.js`).
|
|
111
|
+
|
|
105
112
|
## Contributing
|
|
106
113
|
|
|
107
114
|
Issues and pull requests are welcome. For code changes, run `npm run typecheck`,
|
|
108
|
-
`npm run build`, and `
|
|
115
|
+
`npm run build`, and `npm run preview` before opening a PR; keep the runtime at
|
|
109
116
|
**zero dependencies** and the style fixed (no config loader).
|
|
110
117
|
|
|
111
118
|
## Star History
|
package/README.zh-CN.md
CHANGED
|
@@ -54,7 +54,7 @@ npm 会下载**预编译好的 `dist/`**(本机不跑编译器),并把 `ccglance
|
|
|
54
54
|
```bash
|
|
55
55
|
git clone https://github.com/CxMYu/CcGlanceLine.git
|
|
56
56
|
cd CcGlanceLine
|
|
57
|
-
npm install # 安装 devDeps;随后 prepare 脚本自动编译 src/
|
|
57
|
+
npm install # 安装 devDeps;随后 prepare 脚本自动编译 src/ -> dist/
|
|
58
58
|
npm link # 可选:把 ccglance 链到本机 PATH
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -97,9 +97,14 @@ Windows 用完整路径,例如 `node D:\\path\\to\\ccglance\\dist\\cli.js`。
|
|
|
97
97
|
> 全局的 `ccglance` 命令则完全不需要写路径。`"padding": 0` 去掉 Claude Code 默认的左侧缩进,
|
|
98
98
|
> 让状态栏从最左开始。
|
|
99
99
|
|
|
100
|
+
> **Windows 延时提示**:全局命令 `ccglance` 实际是 npm 的 `.cmd` shim,每次重绘要多走一层
|
|
101
|
+
> cmd 批处理 + PATH 查找,视杀软/系统缓存状态可能多几十到上百毫秒。追求最低延时时,
|
|
102
|
+
> 建议 `command` 直接写 `node D:\\绝对路径\\ccglance\\dist\\cli.js`(全局安装的包在
|
|
103
|
+
> `<npm 全局目录>\\node_modules\\@cxmyu\\ccglance\\dist\\cli.js`)。
|
|
104
|
+
|
|
100
105
|
## 贡献
|
|
101
106
|
|
|
102
|
-
欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`
|
|
107
|
+
欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`npm run preview`;
|
|
103
108
|
请保持**运行时零依赖**和固定风格(不加配置加载器)。
|
|
104
109
|
|
|
105
110
|
## Star History
|
package/dist/cli.js
CHANGED
|
@@ -70,6 +70,7 @@ if (argv[0] === 'preview') {
|
|
|
70
70
|
const { scheduleRefresh } = require('./runtime/version');
|
|
71
71
|
scheduleRefresh();
|
|
72
72
|
}
|
|
73
|
+
require('./runtime/refresh').flushRefreshTasks();
|
|
73
74
|
process.exit(0);
|
|
74
75
|
}
|
|
75
76
|
// 在真实终端直接运行(stdin 是 TTY、没有管道喂 JSON)时,显示帮助而非阻塞等待 stdin。
|
|
@@ -89,3 +90,5 @@ if (data.version && hasEnabledSegment(DEFAULT_SPEC, 'version')) {
|
|
|
89
90
|
const { scheduleRefresh } = require('./runtime/version');
|
|
90
91
|
scheduleRefresh();
|
|
91
92
|
}
|
|
93
|
+
// 渲染期间入队的后台刷新(git / version)最后一次性 spawn,不阻塞状态栏输出。
|
|
94
|
+
require('./runtime/refresh').flushRefreshTasks();
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
// 后台刷新 helper —— 由 refresh.ts 以 detached 进程启动,绝不被状态栏进程等待。
|
|
7
|
+
// 任务列表从 argv[2] 指向的 task-*.json 读入,全部尽力而为、静默失败:
|
|
8
|
+
// git —— 单次有界 `git status --porcelain=v2 --branch`,解析后原子写缓存
|
|
9
|
+
// version —— 拉 npm registry 的 Claude Code 最新版本,原子写缓存
|
|
10
|
+
// 每个任务无论成败都要移除自己的 .refresh marker,否则只能等节流窗口过期。
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const paths_1 = require("../utils/paths");
|
|
14
|
+
const GIT_TIMEOUT_MS = 700;
|
|
15
|
+
// 缓存清理:每天最多一次(stamp 节流)。缓存 key 是仓库/transcript 路径的哈希,
|
|
16
|
+
// 临时目录一去不返时对应 json 便成死条目,按 mtime 过期删除(活跃条目会被刷新持续续命)。
|
|
17
|
+
const PRUNE_INTERVAL_MS = 24 * 3600 * 1000;
|
|
18
|
+
const PRUNE_MAX_AGE_MS = 30 * 24 * 3600 * 1000;
|
|
19
|
+
const PRUNE_TRANSIENT_MAX_AGE_MS = 3600 * 1000; // 残留 .refresh 标记 / .tmp 半成品 / task 文件
|
|
20
|
+
// git status --porcelain=v2 --branch 输出解析(与渲染端 GitCache 结构保持一致)。
|
|
21
|
+
function parseStatus(out) {
|
|
22
|
+
let head = '';
|
|
23
|
+
let oid = '';
|
|
24
|
+
let ahead = 0;
|
|
25
|
+
let behind = 0;
|
|
26
|
+
let dirty = false;
|
|
27
|
+
let conflict = false;
|
|
28
|
+
for (const raw of out.split(/\r?\n/)) {
|
|
29
|
+
const line = raw.trimEnd();
|
|
30
|
+
if (!line)
|
|
31
|
+
continue;
|
|
32
|
+
if (line.startsWith('# branch.head ')) {
|
|
33
|
+
head = line.slice('# branch.head '.length).trim();
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (line.startsWith('# branch.oid ')) {
|
|
37
|
+
oid = line.slice('# branch.oid '.length).trim();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (line.startsWith('# branch.ab ')) {
|
|
41
|
+
const m = /# branch\.ab \+(\d+) -(\d+)/.exec(line);
|
|
42
|
+
if (m) {
|
|
43
|
+
ahead = parseInt(m[1], 10) || 0;
|
|
44
|
+
behind = parseInt(m[2], 10) || 0;
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line[0] === '#')
|
|
49
|
+
continue;
|
|
50
|
+
if (line.startsWith('u '))
|
|
51
|
+
conflict = true;
|
|
52
|
+
if (!line.startsWith('! '))
|
|
53
|
+
dirty = true;
|
|
54
|
+
}
|
|
55
|
+
const branch = head && head !== '(detached)' ? head : oid.slice(0, 8);
|
|
56
|
+
if (!branch)
|
|
57
|
+
return null;
|
|
58
|
+
return { branch, glyph: conflict ? '⚠' : dirty ? '●' : '✓', ahead, behind };
|
|
59
|
+
}
|
|
60
|
+
function atomicWrite(file, data) {
|
|
61
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
62
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
63
|
+
fs_1.default.writeFileSync(tmp, data);
|
|
64
|
+
fs_1.default.renameSync(tmp, file);
|
|
65
|
+
}
|
|
66
|
+
function rmMarker(marker) {
|
|
67
|
+
try {
|
|
68
|
+
fs_1.default.rmSync(marker, { force: true });
|
|
69
|
+
}
|
|
70
|
+
catch { /* ignore */ }
|
|
71
|
+
}
|
|
72
|
+
function runGitTask(root, cacheFile) {
|
|
73
|
+
const { execFileSync } = require('child_process');
|
|
74
|
+
const out = execFileSync('git', ['--no-optional-locks', '-C', root, 'status', '--porcelain=v2', '--branch'], {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
77
|
+
timeout: GIT_TIMEOUT_MS,
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
});
|
|
80
|
+
const info = parseStatus(out);
|
|
81
|
+
if (info)
|
|
82
|
+
atomicWrite(cacheFile, JSON.stringify({ ...info, checkedAt: Date.now() }));
|
|
83
|
+
}
|
|
84
|
+
function runVersionTask(cacheFile, done) {
|
|
85
|
+
let prev = '';
|
|
86
|
+
try {
|
|
87
|
+
prev = JSON.parse(fs_1.default.readFileSync(cacheFile, 'utf8')).latest || '';
|
|
88
|
+
}
|
|
89
|
+
catch { /* no previous cache */ }
|
|
90
|
+
let finished = false;
|
|
91
|
+
const finish = (latest) => {
|
|
92
|
+
if (finished)
|
|
93
|
+
return;
|
|
94
|
+
finished = true;
|
|
95
|
+
try {
|
|
96
|
+
atomicWrite(cacheFile, JSON.stringify({ latest: latest || prev, checkedAt: Date.now() }));
|
|
97
|
+
}
|
|
98
|
+
catch { /* ignore */ }
|
|
99
|
+
done();
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const https = require('https');
|
|
103
|
+
const req = https.get('https://registry.npmjs.org/@anthropic-ai/claude-code/latest', (r) => {
|
|
104
|
+
if (r.statusCode !== 200) {
|
|
105
|
+
r.resume();
|
|
106
|
+
return finish('');
|
|
107
|
+
}
|
|
108
|
+
let d = '';
|
|
109
|
+
r.on('data', (c) => { d += c; });
|
|
110
|
+
r.on('end', () => {
|
|
111
|
+
try {
|
|
112
|
+
finish(JSON.parse(d).version || '');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
finish('');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
req.on('error', () => finish(''));
|
|
120
|
+
req.setTimeout(5000, () => { req.destroy(); finish(''); });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
finish('');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 过期清理:正常缓存 json 超过 30 天未被刷新(mtime)视为死条目;
|
|
127
|
+
// .refresh / .tmp / tasks 超过 1 小时视为进程夭折的残留。逐文件尽力而为。
|
|
128
|
+
function pruneCaches(nowMs = Date.now()) {
|
|
129
|
+
const root = (0, paths_1.appCacheDir)();
|
|
130
|
+
const stamp = path_1.default.join(root, 'prune.stamp');
|
|
131
|
+
try {
|
|
132
|
+
if (nowMs - fs_1.default.statSync(stamp).mtimeMs < PRUNE_INTERVAL_MS)
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
catch { /* 无 stamp → 首次清理 */ }
|
|
136
|
+
try {
|
|
137
|
+
fs_1.default.mkdirSync(root, { recursive: true });
|
|
138
|
+
fs_1.default.writeFileSync(stamp, String(nowMs));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const sub of ['git', 'transcript', 'version', 'tasks']) {
|
|
144
|
+
const dir = path_1.default.join(root, sub);
|
|
145
|
+
let names;
|
|
146
|
+
try {
|
|
147
|
+
names = fs_1.default.readdirSync(dir);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
const file = path_1.default.join(dir, name);
|
|
154
|
+
try {
|
|
155
|
+
const st = fs_1.default.statSync(file);
|
|
156
|
+
if (!st.isFile())
|
|
157
|
+
continue;
|
|
158
|
+
const age = nowMs - st.mtimeMs;
|
|
159
|
+
const transient = sub === 'tasks' || name.endsWith('.refresh') || name.endsWith('.tmp');
|
|
160
|
+
if (age > (transient ? PRUNE_TRANSIENT_MAX_AGE_MS : PRUNE_MAX_AGE_MS)) {
|
|
161
|
+
fs_1.default.rmSync(file, { force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch { /* ignore */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function main() {
|
|
169
|
+
const taskFile = process.argv[2] || '';
|
|
170
|
+
try {
|
|
171
|
+
// 任务文件已由 refresh.ts 原子写入;读损坏时 markers 交给节流窗口 + prune 兜底。
|
|
172
|
+
const tasks = taskFile ? JSON.parse(fs_1.default.readFileSync(taskFile, 'utf8')) : [];
|
|
173
|
+
for (const task of tasks) {
|
|
174
|
+
if (task.kind === 'git') {
|
|
175
|
+
try {
|
|
176
|
+
runGitTask(task.root, task.cacheFile);
|
|
177
|
+
}
|
|
178
|
+
catch { /* best effort */ }
|
|
179
|
+
rmMarker(task.marker);
|
|
180
|
+
}
|
|
181
|
+
else if (task.kind === 'version') {
|
|
182
|
+
try {
|
|
183
|
+
runVersionTask(task.cacheFile, () => rmMarker(task.marker));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
rmMarker(task.marker);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// 任务文件缺失 / 损坏:静默退出,仍走 finally 清理。
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
if (taskFile) {
|
|
196
|
+
try {
|
|
197
|
+
fs_1.default.rmSync(taskFile, { force: true });
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
pruneCaches();
|
|
203
|
+
}
|
|
204
|
+
catch { /* best effort */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (require.main === module)
|
|
208
|
+
main();
|
package/dist/runtime/git.js
CHANGED
|
@@ -9,9 +9,9 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const colors_1 = require("../render/colors");
|
|
11
11
|
const paths_1 = require("../utils/paths");
|
|
12
|
+
const refresh_1 = require("./refresh");
|
|
12
13
|
const CACHE_DIR = (0, paths_1.cacheDir)('git');
|
|
13
14
|
const DEFAULT_TTL_MS = 20 * 60 * 1000;
|
|
14
|
-
const GIT_TIMEOUT_MS = 700;
|
|
15
15
|
const REFRESH_THROTTLE_MS = 5000;
|
|
16
16
|
function resolveGitPaths(cwd) {
|
|
17
17
|
const abs = path_1.default.resolve(cwd);
|
|
@@ -83,45 +83,6 @@ function shouldRefresh(paths) {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
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
86
|
function readHeadFallback(root) {
|
|
126
87
|
if (!root)
|
|
127
88
|
return null;
|
|
@@ -149,29 +110,11 @@ function branchHintFallback(branchHint) {
|
|
|
149
110
|
const branch = branchHint && branchHint.trim();
|
|
150
111
|
return branch ? { branch, ahead: 0, behind: 0 } : null;
|
|
151
112
|
}
|
|
113
|
+
// 只入队不 spawn:gitSegment 在渲染期间被调用,spawn 留到 stdout 写出后统一 flush。
|
|
152
114
|
function scheduleGitRefresh(paths) {
|
|
153
115
|
if (!paths.root || !shouldRefresh(paths))
|
|
154
116
|
return;
|
|
155
|
-
|
|
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
|
-
}
|
|
117
|
+
(0, refresh_1.queueRefreshTask)({ kind: 'git', root: paths.root, cacheFile: paths.cacheFile, marker: paths.refreshFile });
|
|
175
118
|
}
|
|
176
119
|
function formatGit(info, icon, worktreeName) {
|
|
177
120
|
const status = [];
|
|
@@ -0,0 +1,78 @@
|
|
|
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.queueRefreshTask = queueRefreshTask;
|
|
7
|
+
exports.flushRefreshTasks = flushRefreshTasks;
|
|
8
|
+
// 后台刷新调度 —— 渲染期间各段只「入队」(调用方先抢占各自的 .refresh marker),
|
|
9
|
+
// stdout 写出后由 cli 调 flushRefreshTasks() 一次性 spawn 单个 detached helper(bg.js)。
|
|
10
|
+
//
|
|
11
|
+
// 为什么不各自直接 spawn:Windows 上发起一个 node.exe 子进程的同步成本很高
|
|
12
|
+
// (CreateProcess + 杀软扫描,实测 ~80-110ms),冷缓存首帧会连发两个,直接拖慢状态栏。
|
|
13
|
+
// 合并为一次,并在 Windows 上经 cmd `start /b` 蹦床发起(实测 ~17ms):node 的真实
|
|
14
|
+
// 启动成本由蹦床在本进程退出后承担,不再占用状态栏进程的生命周期。
|
|
15
|
+
// 任务负载写入 ~/.claude/ccglance/tasks/task-*.json,helper 只接收任务文件路径。
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const paths_1 = require("../utils/paths");
|
|
19
|
+
const queue = [];
|
|
20
|
+
// 入队前调用方必须已独占抢到对应 marker(多进程节流语义保持不变)。
|
|
21
|
+
function queueRefreshTask(task) {
|
|
22
|
+
queue.push(task);
|
|
23
|
+
}
|
|
24
|
+
function removeMarkers(tasks) {
|
|
25
|
+
for (const t of tasks) {
|
|
26
|
+
try {
|
|
27
|
+
fs_1.default.rmSync(t.marker, { force: true });
|
|
28
|
+
}
|
|
29
|
+
catch { /* ignore */ }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function writeTaskFile(tasks) {
|
|
33
|
+
const dir = (0, paths_1.cacheDir)('tasks');
|
|
34
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
35
|
+
const file = path_1.default.join(dir, `task-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`);
|
|
36
|
+
const tmp = `${file}.tmp`;
|
|
37
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(tasks));
|
|
38
|
+
fs_1.default.renameSync(tmp, file);
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
function quoteCmdArg(s) {
|
|
42
|
+
return `"${s}"`;
|
|
43
|
+
}
|
|
44
|
+
// 渲染输出完成后调用:把积累的刷新任务交给一个 detached 后台 helper 进程。
|
|
45
|
+
function flushRefreshTasks() {
|
|
46
|
+
if (!queue.length)
|
|
47
|
+
return;
|
|
48
|
+
const tasks = queue.splice(0);
|
|
49
|
+
let taskFile = '';
|
|
50
|
+
try {
|
|
51
|
+
const helper = path_1.default.join(__dirname, 'bg.js');
|
|
52
|
+
taskFile = writeTaskFile(tasks);
|
|
53
|
+
const { spawn } = require('child_process');
|
|
54
|
+
// Windows:node.exe 的 CreateProcess 同步成本很高(杀软扫描),改用 cmd `start /b`
|
|
55
|
+
// 蹦床(发起 ~17ms),真正的 node 启动开销由蹦床在本进程退出后承担。
|
|
56
|
+
// 任务本身在文件里;cmd 只需要转发 node/helper/taskFile 三个路径参数。
|
|
57
|
+
const child = process.platform === 'win32'
|
|
58
|
+
? spawn('cmd.exe', ['/d', '/s', '/c', `start "" /b ${quoteCmdArg(process.execPath)} ${quoteCmdArg(helper)} ${quoteCmdArg(taskFile)}`], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true })
|
|
59
|
+
: spawn(process.execPath, [helper, taskFile], { detached: true, stdio: 'ignore' });
|
|
60
|
+
child.on('error', () => {
|
|
61
|
+
try {
|
|
62
|
+
fs_1.default.rmSync(taskFile, { force: true });
|
|
63
|
+
}
|
|
64
|
+
catch { /* ignore */ }
|
|
65
|
+
removeMarkers(tasks);
|
|
66
|
+
});
|
|
67
|
+
child.unref();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (taskFile) {
|
|
71
|
+
try {
|
|
72
|
+
fs_1.default.rmSync(taskFile, { force: true });
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
removeMarkers(tasks);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/runtime/version.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.scheduleRefresh = scheduleRefresh;
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const paths_1 = require("../utils/paths");
|
|
11
|
+
const refresh_1 = require("./refresh");
|
|
11
12
|
const CACHE_TTL_MS = 4 * 3600 * 1000;
|
|
12
13
|
const REFRESH_THROTTLE_MS = 60 * 1000;
|
|
13
14
|
const cacheFile = path_1.default.join((0, paths_1.cacheDir)('version'), 'claude-code.json');
|
|
@@ -48,6 +49,7 @@ function shouldRefresh() {
|
|
|
48
49
|
return false;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
// 只入队不 spawn:实际后台进程由 cli 在 stdout 写出后统一 flush(见 runtime/refresh.ts)。
|
|
51
53
|
function scheduleRefresh() {
|
|
52
54
|
try {
|
|
53
55
|
const age = Date.now() - (readCache().checkedAt || 0);
|
|
@@ -55,21 +57,7 @@ function scheduleRefresh() {
|
|
|
55
57
|
return;
|
|
56
58
|
if (!shouldRefresh())
|
|
57
59
|
return;
|
|
58
|
-
|
|
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();
|
|
60
|
+
(0, refresh_1.queueRefreshTask)({ kind: 'version', cacheFile, marker: refreshFile });
|
|
73
61
|
}
|
|
74
62
|
catch {
|
|
75
63
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cxmyu/ccglance",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A fast, zero-dependency, multi-line status line for Claude Code — model, effort, context, cache, git and session, all at a glance.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ccglance": "dist/cli.js"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
23
23
|
"build": "npm run clean && tsc",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
+
"preview": "node dist/cli.js preview",
|
|
25
26
|
"test": "npm run build && node --test test/*.test.js",
|
|
26
27
|
"benchmark": "npm run build && node bench/latency.js",
|
|
27
28
|
"prepare": "npm run build",
|