@cxmyu/ccglance 1.0.3 → 1.1.1
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 +6 -2
- package/README.zh-CN.md +5 -2
- package/dist/cli.js +3 -0
- package/dist/runtime/bg.js +241 -0
- package/dist/runtime/git.js +3 -60
- package/dist/runtime/refresh.js +52 -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,14 @@ 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 tip:** if status-line startup latency matters, point `command`
|
|
106
|
+
> directly at the compiled file (`node D:\\absolute\\path\\to\\ccglance\\dist\\cli.js`)
|
|
107
|
+
> instead of the global `ccglance` command — it usually starts a bit faster.
|
|
108
|
+
|
|
105
109
|
## Contributing
|
|
106
110
|
|
|
107
111
|
Issues and pull requests are welcome. For code changes, run `npm run typecheck`,
|
|
108
|
-
`npm run build`, and `
|
|
112
|
+
`npm run build`, and `npm run preview` before opening a PR; keep the runtime at
|
|
109
113
|
**zero dependencies** and the style fixed (no config loader).
|
|
110
114
|
|
|
111
115
|
## 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,12 @@ Windows 用完整路径,例如 `node D:\\path\\to\\ccglance\\dist\\cli.js`。
|
|
|
97
97
|
> 全局的 `ccglance` 命令则完全不需要写路径。`"padding": 0` 去掉 Claude Code 默认的左侧缩进,
|
|
98
98
|
> 让状态栏从最左开始。
|
|
99
99
|
|
|
100
|
+
> **Windows 提速小贴士**:若在意状态栏启动延时,可把 `command` 直接指向编译产物
|
|
101
|
+
> `node D:\\绝对路径\\ccglance\\dist\\cli.js`,通常比走全局 `ccglance` 命令更快一些。
|
|
102
|
+
|
|
100
103
|
## 贡献
|
|
101
104
|
|
|
102
|
-
欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`
|
|
105
|
+
欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`npm run preview`;
|
|
103
106
|
请保持**运行时零依赖**和固定风格(不加配置加载器)。
|
|
104
107
|
|
|
105
108
|
## 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,241 @@
|
|
|
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](JSON)读入,全部尽力而为、静默失败:
|
|
8
|
+
// git —— 单次有界 `git status --porcelain=v2 --branch`,解析后原子写缓存
|
|
9
|
+
// version —— 拉 npm registry 的 Claude Code 最新版本,原子写缓存
|
|
10
|
+
// 每个任务无论成败都要移除自己的 .refresh marker,否则只能等节流窗口过期。
|
|
11
|
+
// 每次运行还顺手清理孤儿产物:过期的 .refresh / .tmp(及历史遗留的 tasks/ 目录)。
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const paths_1 = require("../utils/paths");
|
|
15
|
+
const GIT_TIMEOUT_MS = 700;
|
|
16
|
+
// 孤儿瞬时产物阈值:.refresh/.tmp 本应几秒内被删(helper 最长约 5s);超过此值即视为
|
|
17
|
+
// 夭折进程的残留。每次运行开头 + 结尾各清一次,故取较短的 2 分钟让残留更快被接管清理。
|
|
18
|
+
const STALE_TRANSIENT_MS = 2 * 60 * 1000;
|
|
19
|
+
// 死缓存清理:缓存 key 是仓库/transcript 路径哈希,临时目录一去不返时对应 json 便成死条目。
|
|
20
|
+
// 每天最多清一次(stamp 节流),删除长期未刷新的缓存 json(活跃条目会被刷新持续续命)。
|
|
21
|
+
const PRUNE_INTERVAL_MS = 24 * 3600 * 1000;
|
|
22
|
+
const PRUNE_MAX_AGE_MS = 30 * 24 * 3600 * 1000;
|
|
23
|
+
// git status --porcelain=v2 --branch 输出解析(与渲染端 GitCache 结构保持一致)。
|
|
24
|
+
function parseStatus(out) {
|
|
25
|
+
let head = '';
|
|
26
|
+
let oid = '';
|
|
27
|
+
let ahead = 0;
|
|
28
|
+
let behind = 0;
|
|
29
|
+
let dirty = false;
|
|
30
|
+
let conflict = false;
|
|
31
|
+
for (const raw of out.split(/\r?\n/)) {
|
|
32
|
+
const line = raw.trimEnd();
|
|
33
|
+
if (!line)
|
|
34
|
+
continue;
|
|
35
|
+
if (line.startsWith('# branch.head ')) {
|
|
36
|
+
head = line.slice('# branch.head '.length).trim();
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (line.startsWith('# branch.oid ')) {
|
|
40
|
+
oid = line.slice('# branch.oid '.length).trim();
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (line.startsWith('# branch.ab ')) {
|
|
44
|
+
const m = /# branch\.ab \+(\d+) -(\d+)/.exec(line);
|
|
45
|
+
if (m) {
|
|
46
|
+
ahead = parseInt(m[1], 10) || 0;
|
|
47
|
+
behind = parseInt(m[2], 10) || 0;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line[0] === '#')
|
|
52
|
+
continue;
|
|
53
|
+
if (line.startsWith('u '))
|
|
54
|
+
conflict = true;
|
|
55
|
+
if (!line.startsWith('! '))
|
|
56
|
+
dirty = true;
|
|
57
|
+
}
|
|
58
|
+
const branch = head && head !== '(detached)' ? head : oid.slice(0, 8);
|
|
59
|
+
if (!branch)
|
|
60
|
+
return null;
|
|
61
|
+
return { branch, glyph: conflict ? '⚠' : dirty ? '●' : '✓', ahead, behind };
|
|
62
|
+
}
|
|
63
|
+
function atomicWrite(file, data) {
|
|
64
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
65
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
66
|
+
fs_1.default.writeFileSync(tmp, data);
|
|
67
|
+
fs_1.default.renameSync(tmp, file);
|
|
68
|
+
}
|
|
69
|
+
function rmMarker(marker) {
|
|
70
|
+
try {
|
|
71
|
+
fs_1.default.rmSync(marker, { force: true });
|
|
72
|
+
}
|
|
73
|
+
catch { /* ignore */ }
|
|
74
|
+
}
|
|
75
|
+
function runGitTask(root, cacheFile) {
|
|
76
|
+
const { execFileSync } = require('child_process');
|
|
77
|
+
const out = execFileSync('git', ['--no-optional-locks', '-C', root, 'status', '--porcelain=v2', '--branch'], {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
80
|
+
timeout: GIT_TIMEOUT_MS,
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
const info = parseStatus(out);
|
|
84
|
+
if (info)
|
|
85
|
+
atomicWrite(cacheFile, JSON.stringify({ ...info, checkedAt: Date.now() }));
|
|
86
|
+
}
|
|
87
|
+
function runVersionTask(cacheFile, done) {
|
|
88
|
+
let prev = '';
|
|
89
|
+
try {
|
|
90
|
+
prev = JSON.parse(fs_1.default.readFileSync(cacheFile, 'utf8')).latest || '';
|
|
91
|
+
}
|
|
92
|
+
catch { /* no previous cache */ }
|
|
93
|
+
let finished = false;
|
|
94
|
+
const finish = (latest) => {
|
|
95
|
+
if (finished)
|
|
96
|
+
return;
|
|
97
|
+
finished = true;
|
|
98
|
+
try {
|
|
99
|
+
atomicWrite(cacheFile, JSON.stringify({ latest: latest || prev, checkedAt: Date.now() }));
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
done();
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const https = require('https');
|
|
106
|
+
const req = https.get('https://registry.npmjs.org/@anthropic-ai/claude-code/latest', (r) => {
|
|
107
|
+
if (r.statusCode !== 200) {
|
|
108
|
+
r.resume();
|
|
109
|
+
return finish('');
|
|
110
|
+
}
|
|
111
|
+
let d = '';
|
|
112
|
+
r.on('data', (c) => { d += c; });
|
|
113
|
+
r.on('end', () => {
|
|
114
|
+
try {
|
|
115
|
+
finish(JSON.parse(d).version || '');
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
finish('');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
req.on('error', () => finish(''));
|
|
123
|
+
req.setTimeout(5000, () => { req.destroy(); finish(''); });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
finish('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 每次运行都做的轻清理:删除过期的孤儿瞬时产物(.refresh / .tmp),以及历史遗留的
|
|
130
|
+
// tasks/ 目录内容。成本很低(这些目录文件很少),能让残留不再累积。
|
|
131
|
+
function pruneStaleTransient(nowMs = Date.now()) {
|
|
132
|
+
const root = (0, paths_1.appCacheDir)();
|
|
133
|
+
for (const sub of ['git', 'version', 'transcript', 'tasks']) {
|
|
134
|
+
const dir = path_1.default.join(root, sub);
|
|
135
|
+
let names;
|
|
136
|
+
try {
|
|
137
|
+
names = fs_1.default.readdirSync(dir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
// tasks/ 已弃用:其中任何文件都算历史遗留;其他目录只清 .refresh / .tmp 瞬时产物。
|
|
144
|
+
if (sub !== 'tasks' && !name.endsWith('.refresh') && !name.endsWith('.tmp'))
|
|
145
|
+
continue;
|
|
146
|
+
const file = path_1.default.join(dir, name);
|
|
147
|
+
try {
|
|
148
|
+
if (nowMs - fs_1.default.statSync(file).mtimeMs > STALE_TRANSIENT_MS)
|
|
149
|
+
fs_1.default.rmSync(file, { force: true });
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
if (sub === 'tasks') {
|
|
154
|
+
try {
|
|
155
|
+
fs_1.default.rmdirSync(dir);
|
|
156
|
+
}
|
|
157
|
+
catch { /* 非空/不存在都忽略 */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 每天最多一次(stamp 节流):删除长期未刷新的死缓存 json。
|
|
162
|
+
function pruneDeadCaches(nowMs = Date.now()) {
|
|
163
|
+
const root = (0, paths_1.appCacheDir)();
|
|
164
|
+
const stamp = path_1.default.join(root, 'prune.stamp');
|
|
165
|
+
try {
|
|
166
|
+
if (nowMs - fs_1.default.statSync(stamp).mtimeMs < PRUNE_INTERVAL_MS)
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
catch { /* 无 stamp → 首次清理 */ }
|
|
170
|
+
try {
|
|
171
|
+
fs_1.default.mkdirSync(root, { recursive: true });
|
|
172
|
+
fs_1.default.writeFileSync(stamp, String(nowMs));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const sub of ['git', 'transcript', 'version']) {
|
|
178
|
+
const dir = path_1.default.join(root, sub);
|
|
179
|
+
let names;
|
|
180
|
+
try {
|
|
181
|
+
names = fs_1.default.readdirSync(dir);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
if (!name.endsWith('.json'))
|
|
188
|
+
continue;
|
|
189
|
+
const file = path_1.default.join(dir, name);
|
|
190
|
+
try {
|
|
191
|
+
if (nowMs - fs_1.default.statSync(file).mtimeMs > PRUNE_MAX_AGE_MS)
|
|
192
|
+
fs_1.default.rmSync(file, { force: true });
|
|
193
|
+
}
|
|
194
|
+
catch { /* ignore */ }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function main() {
|
|
199
|
+
const payload = process.argv[2] || '';
|
|
200
|
+
// 开头先接管清理上一轮遗留的孤儿(含被强杀进程没来得及删的 marker):即便本次任务中途
|
|
201
|
+
// 被杀、finally 没跑到,这些残留也会在下一次后台刷新一启动时被清掉,不会长期堆积。
|
|
202
|
+
try {
|
|
203
|
+
pruneStaleTransient();
|
|
204
|
+
}
|
|
205
|
+
catch { /* best effort */ }
|
|
206
|
+
try {
|
|
207
|
+
const tasks = payload ? JSON.parse(payload) : [];
|
|
208
|
+
for (const task of tasks) {
|
|
209
|
+
if (task.kind === 'git') {
|
|
210
|
+
try {
|
|
211
|
+
runGitTask(task.root, task.cacheFile);
|
|
212
|
+
}
|
|
213
|
+
catch { /* best effort */ }
|
|
214
|
+
rmMarker(task.marker);
|
|
215
|
+
}
|
|
216
|
+
else if (task.kind === 'version') {
|
|
217
|
+
try {
|
|
218
|
+
runVersionTask(task.cacheFile, () => rmMarker(task.marker));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
rmMarker(task.marker);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// payload 缺失 / 损坏:静默退出,仍走 finally 清理。
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
try {
|
|
231
|
+
pruneStaleTransient();
|
|
232
|
+
}
|
|
233
|
+
catch { /* best effort */ }
|
|
234
|
+
try {
|
|
235
|
+
pruneDeadCaches();
|
|
236
|
+
}
|
|
237
|
+
catch { /* best effort */ }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (require.main === module)
|
|
241
|
+
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,52 @@
|
|
|
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 单个后台 helper(bg.js)。
|
|
10
|
+
// 合并为一次 spawn,避免冷缓存首帧连发两个后台进程;缓存都新鲜时 queue 为空、直接返回,
|
|
11
|
+
// 常规重绘不起任何后台进程。任务数据经 argv 直接传给 helper(不经 shell、无需临时文件)。
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const queue = [];
|
|
15
|
+
// 入队前调用方必须已独占抢到对应 marker(多进程节流语义保持不变)。
|
|
16
|
+
function queueRefreshTask(task) {
|
|
17
|
+
queue.push(task);
|
|
18
|
+
}
|
|
19
|
+
function removeMarkers(tasks) {
|
|
20
|
+
for (const t of tasks) {
|
|
21
|
+
try {
|
|
22
|
+
fs_1.default.rmSync(t.marker, { force: true });
|
|
23
|
+
}
|
|
24
|
+
catch { /* ignore */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// 渲染输出完成后调用:把积累的刷新任务交给一个 detached 后台 helper 进程。
|
|
28
|
+
function flushRefreshTasks() {
|
|
29
|
+
if (!queue.length)
|
|
30
|
+
return;
|
|
31
|
+
const tasks = queue.splice(0);
|
|
32
|
+
try {
|
|
33
|
+
const helper = path_1.default.join(__dirname, 'bg.js');
|
|
34
|
+
const payload = JSON.stringify(tasks);
|
|
35
|
+
const { spawn } = require('child_process');
|
|
36
|
+
// 直接 detached spawn node,不经 shell:
|
|
37
|
+
// - 任务数据用一个 argv 参数(JSON)传给 helper;spawn 不走 shell,无引号/转义问题,
|
|
38
|
+
// 因此不需要临时任务文件(历史上用过 task-*.json,是残留主因,已弃用)。
|
|
39
|
+
// - detached 让 helper 脱离父进程独立跑完,并在结束时清理自己的 marker,避免残留。
|
|
40
|
+
// - Windows 关键坑:绝不同时设 detached + windowsHide —— 两个 flag 冲突会意外弹出
|
|
41
|
+
// 控制台黑窗。detached 单独即 DETACHED_PROCESS,本就不分配控制台,因此静默。
|
|
42
|
+
const child = spawn(process.execPath, [helper, payload], {
|
|
43
|
+
detached: true,
|
|
44
|
+
stdio: 'ignore',
|
|
45
|
+
});
|
|
46
|
+
child.on('error', () => removeMarkers(tasks));
|
|
47
|
+
child.unref();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
removeMarkers(tasks);
|
|
51
|
+
}
|
|
52
|
+
}
|
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.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|