@cxmyu/ccglance 1.0.2 → 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 CHANGED
@@ -1,72 +1,23 @@
1
+ <div align="center">
2
+
1
3
  # ccglance
2
4
 
3
- > A fast, zero-dependency, multi-line status line for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — model, effort, context, cache, git and session, all at a glance.
5
+ **A fast, zero-dependency, multi-line status line for [Claude Code](https://docs.anthropic.com/en/docs/claude-code)**
6
+
7
+ model · effort · context · cache · git · session — all at a glance
4
8
 
5
9
  [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-3c873a)](https://nodejs.org/)
6
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
7
11
 
8
- English | [简体中文](./README.zh-CN.md)
9
-
10
- Repository: [github.com/CxMYu/CcGlanceLine](https://github.com/CxMYu/CcGlanceLine)
11
-
12
- `ccglance` uses the JSON that Claude Code pipes to its status-line command
13
- on stdin as its primary data source; the session `status` segment reads only
14
- the transcript tail when enabled. It's written in
15
- **TypeScript** and ships as compiled JavaScript with **zero runtime
16
- dependencies** (Node standard library only).
12
+ [English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
17
13
 
18
- ## Documentation
19
-
20
- - [Segment & icon reference](./docs/segments.md) — every segment's data source, all icon meanings, rate-limit levels and color semantics
21
- - [Development](./docs/development.md) — build, test, source layout, contribution practices and git cache internals
14
+ ![ccglance preview](./docs/assets/preview.png)
22
15
 
23
- ## Preview
16
+ </div>
24
17
 
25
- ![ccglance preview](./docs/assets/preview.png)
18
+ > **stdin-first · zero runtime dependencies · fixed emoji style** — renders the JSON Claude Code pipes to its status-line command; only the `status` segment reads a bounded transcript tail. Written in TypeScript, shipped as compiled JavaScript; runs on Node.js ≥ 22 or Bun.
26
19
 
27
- Generated from `ccglance preview`.
28
-
29
- ## Features
30
-
31
- - **Compact multi-line layout** — runtime, quota and project/session state are
32
- separate logical rows; empty rows disappear.
33
- - **Zero runtime dependencies** — Node standard library only; TypeScript is a
34
- build-time tool, never shipped in the published package.
35
- - **stdin-first** — core segments consume the JSON Claude Code provides; status
36
- reads a bounded transcript tail only when used.
37
- - **Rich context block** — usage %, this-turn input/output tokens, tokens left.
38
- - **Prompt-cache block** — cache hit rate, read/write tokens.
39
- - **Git segment** — branch, clean/dirty/conflict glyph, ahead/behind counts,
40
- using one bounded `git status` call plus a short local cache.
41
- - **Session segment** — elapsed time plus lines added/removed.
42
- - **Claude Code version + update hint** — shows the `version` field from
43
- Claude Code stdin and appends `↑latest` when the 4h local cache reports a
44
- newer Claude Code release. Refresh runs after stdout and never blocks.
45
- - **Additional session context** — 5h/7d rate-limit quota, USD cost and
46
- worktree name next to the git branch when Claude Code provides it.
47
- - **Responsive multi-line layout** — probes the terminal width
48
- (`CCGLANCE_WIDTH` → `COLUMNS` → TTY → `tput`/`stty`) and wraps each row into
49
- as many lines as fit, down to a minimum of **one segment per line** so
50
- nothing gets hidden; display-width aware (CJK/emoji safe).
51
- - **Fixed built-in style** — no user config file and no external style file;
52
- the status line uses ccglance's own emoji-first style.
53
-
54
- ## Requirements
55
-
56
- - Node.js **>= 22**
57
- - `git` on `PATH` (optional — only for the git segment)
58
-
59
- Claude Code compatibility:
60
-
61
- | Claude Code CLI | ccglance behavior |
62
- |---|---|
63
- | >= 1.0.71 | Basic status-line stdin support |
64
- | >= 2.1.80 | Subscription quota row when official `rate_limits.five_hour` / `rate_limits.seven_day` fields are present |
65
- | >= 2.1.153 | Preferred terminal-width sizing through `COLUMNS` / `LINES`; older versions fall back to TTY width or 80 columns |
66
-
67
- The quota row is subscription-only. Claude.ai Pro/Max-style sessions can expose
68
- `rate_limits`; API-key, Bedrock, Vertex, and other usage-based sessions usually
69
- do not, so ccglance hides the quota row instead of inferring one.
20
+ **Docs:** [Overview](./docs/overview.md) · [Segments & icons](./docs/segments.md) · [Development](./docs/development.md)
70
21
 
71
22
  ## Install
72
23
 
@@ -76,6 +27,7 @@ do not, so ccglance hides the quota row instead of inferring one.
76
27
  npm install -g @cxmyu/ccglance # npm
77
28
  yarn global add @cxmyu/ccglance # or yarn
78
29
  pnpm add -g @cxmyu/ccglance # or pnpm
30
+ bun add -g @cxmyu/ccglance # or bun
79
31
  ```
80
32
 
81
33
  Behind a slow registry? Use a mirror:
@@ -104,7 +56,7 @@ Update or remove later with `npm update -g @cxmyu/ccglance` / `npm uninstall -g
104
56
  ```bash
105
57
  git clone https://github.com/CxMYu/CcGlanceLine.git
106
58
  cd CcGlanceLine
107
- npm install # installs devDeps; the `prepare` script then compiles src/ → dist/
59
+ npm install # installs devDeps; the `prepare` script then compiles src/ -> dist/
108
60
  npm link # optional: expose `ccglance` on your PATH
109
61
  ```
110
62
 
@@ -150,73 +102,17 @@ On Windows use a full path, e.g. `node D:\\path\\to\\ccglance\\dist\\cli.js`.
150
102
  > command needs no path at all. `"padding": 0` drops Claude Code's default leading
151
103
  > indent so the row starts at the left edge.
152
104
 
153
- ## Status line at a glance
154
-
155
- Three logical rows; any row with no data disappears:
156
-
157
- 1. **Runtime** — 🤖 model · 🧠 effort · status · 🚀 fast · ⚡️ context · 💾 cache · 🎯 style
158
- 2. **Quota** *(subscription sessions only)* — 📊 Hour / Week rate-limit meters
159
- 3. **Project / session** — 📁 dir · 🌿 git · 🏷️ session name · ⏱️ session · 💰 cost · 💩 version
160
-
161
- **Full reference** — every segment's stdin source, all icon meanings, rate-limit
162
- moon-phase levels, session-status icons and color semantics — lives in
163
- **[docs/segments.md](./docs/segments.md)**.
164
-
165
- ## How it works
166
-
167
- On every redraw Claude Code runs your status-line `command` and pipes a JSON
168
- object to its **stdin** (model, effort, context window, cost, workspace,
169
- version, …). `ccglance` reads that once via `fs.readFileSync(0)`, formats its
170
- fixed rows, and **prints them first**. The `status` segment reads only a
171
- bounded transcript tail. The version segment synchronously
172
- reads only a tiny local cache; after stdout is written, ccglance refreshes the
173
- Claude Code latest-version cache in a detached background process when it is
174
- older than 4 hours. If the JSON can't be parsed it exits silently so it can never
175
- break the CLI. Text from stdin, transcript, and git is sanitized before being
176
- printed so terminal control sequences cannot escape the status line. The main
177
- external calls are local `git` (single bounded status call with a short cache),
178
- bounded transcript-tail reads, and the post-render detached npm registry check.
179
-
180
- ## Development
181
-
182
- Source build, tests, benchmark, source layout, contribution practices and git
183
- cache internals live in **[docs/development.md](./docs/development.md)**. Quick start:
184
-
185
- ```bash
186
- git clone https://github.com/CxMYu/CcGlanceLine.git
187
- cd CcGlanceLine
188
- npm install # devDeps + prepare build
189
- npm test # build + node:test
190
- ```
191
-
192
- ## Cache Files
193
-
194
- ccglance stores only disposable runtime caches: git status, transcript-derived
195
- status, and the Claude Code latest-version check. Files are grouped under
196
- `git/`, `transcript/`, and `version/`.
197
-
198
- Caches live under Claude Code's own config directory on every platform, so they
199
- travel with your Claude setup and are trivial to clear:
200
-
201
- ```text
202
- ~/.claude/ccglance/
203
- ├── git/ # git status snapshots (one per repo)
204
- ├── transcript/ # transcript-derived session status
205
- └── version/ # Claude Code latest-version check
206
- ```
207
-
208
- If you set `CLAUDE_CONFIG_DIR` to relocate Claude Code's config, ccglance places
209
- its caches under that directory instead. Deleting the whole `ccglance/` folder is
210
- safe — every file is regenerated on the next render.
211
-
212
- Git cache behavior (cache key, `HEAD` fallback, 20-minute TTL, detached refresh,
213
- atomic writes) is documented in
214
- [docs/development.md](./docs/development.md#git-cache-internals).
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`).
215
111
 
216
112
  ## Contributing
217
113
 
218
114
  Issues and pull requests are welcome. For code changes, run `npm run typecheck`,
219
- `npm run build`, and `ccglance preview` before opening a PR; keep the runtime at
115
+ `npm run build`, and `npm run preview` before opening a PR; keep the runtime at
220
116
  **zero dependencies** and the style fixed (no config loader).
221
117
 
222
118
  ## Star History
package/README.zh-CN.md CHANGED
@@ -1,61 +1,23 @@
1
+ <div align="center">
2
+
1
3
  # ccglance
2
4
 
3
- > 面向 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 的轻量多行状态栏 —— 模型、effort、上下文、缓存、git、会话,一眼尽览。TypeScript 编写、运行时零依赖。
5
+ **面向 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 的快速、零依赖多行状态栏**
6
+
7
+ 模型 · effort · 上下文 · 缓存 · git · 会话 —— 一眼尽览
4
8
 
5
9
  [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-3c873a)](https://nodejs.org/)
6
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
7
11
 
8
- [English](./README.md) | 简体中文
9
-
10
- 仓库:[github.com/CxMYu/CcGlanceLine](https://github.com/CxMYu/CcGlanceLine)
11
-
12
- `ccglance` 以 Claude Code 通过 stdin 传入的 JSON 为主数据源;`status`
13
- 段只在启用时读取 transcript(会话记录)尾部。它用 **TypeScript** 编写,以编译后的
14
- JavaScript 发布,**运行时零依赖**(仅用 Node 标准库)。
15
-
16
- ## 文档
17
-
18
- - [字段与图标参考](./docs/segments.zh-CN.md) —— 每个段的数据来源、所有图标含义、配额档位与颜色语义
19
- - [开发](./docs/development.zh-CN.md) —— 构建、测试、代码结构、贡献范式与 git 缓存内部
20
-
21
- ## 预览
12
+ [English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
22
13
 
23
14
  ![ccglance 预览](./docs/assets/preview.png)
24
15
 
25
- 截图由 `ccglance preview` 生成。
26
-
27
- ## 特性
16
+ </div>
28
17
 
29
- - **紧凑多行布局** —— 运行时、订阅配额、项目/会话分成逻辑行;无数据的行自动消失。
30
- - **运行时零依赖** —— 仅用 Node 标准库;TypeScript 仅为构建期工具,不进发布包。
31
- - **stdin 优先** —— 核心段消费 Claude Code 提供的 JSON;status 只读 transcript 尾部固定字节。
32
- - **上下文块** —— 占用百分比、本轮输入/输出 token、剩余 token。
33
- - **缓存块** —— 缓存命中率、读取/写入 token。
34
- - **git 段** —— 分支、干净/脏/冲突标记、领先/落后计数;只跑一次有界 `git status`,并带短缓存。
35
- - **会话段** —— 耗时 + 新增/删除行数。
36
- - **Claude Code 版本 + 更新提示** —— 显示 stdin 里的 Claude Code `version`;
37
- 本地 4 小时缓存发现新版时追加 `↑latest`。刷新在 stdout 写出后异步进行,不阻塞状态栏。
38
- - **额外会话信息** —— 5小时/7天配额、美元成本,以及 Claude Code 提供时显示在 git 分支右侧的 worktree 名。
39
- - **响应式多行布局** —— 读终端宽度(`COLUMNS`),把每行按能容纳的宽度自动折成多行;
40
- 正确处理 CJK/emoji 显示宽度。
41
- - **固定内置风格** —— 不读取用户配置文件,也不读取外部样式文件;状态栏使用 ccglance 自己的 emoji 优先风格。
18
+ > **stdin 优先 · 运行时零依赖 · 固定 emoji 风格** —— 渲染 Claude Code 经 stdin 传入的 JSON;仅 `status` 段读取 transcript 尾部固定字节。TypeScript 编写,以编译后的 JavaScript 发布,可在 Node.js ≥ 22 或 Bun 上运行。
42
19
 
43
- ## 环境要求
44
-
45
- - Node.js **>= 22**
46
- - `git`(可选,仅 git 段需要)
47
-
48
- Claude Code 兼容性:
49
-
50
- | Claude Code CLI | ccglance 行为 |
51
- |---|---|
52
- | >= 1.0.71 | 支持基础 statusline stdin |
53
- | >= 2.1.80 | 官方 `rate_limits.five_hour` / `rate_limits.seven_day` 字段存在时显示订阅配额行 |
54
- | >= 2.1.153 | 优先使用 `COLUMNS` / `LINES` 做终端宽度适配;更旧版本回退到 TTY 宽度或 80 列 |
55
-
56
- 配额行只面向订阅场景。Claude.ai Pro/Max 这类订阅会话可能提供 `rate_limits`;
57
- API key、Bedrock、Vertex 等按量/外部网关场景通常没有该字段,ccglance 会隐藏配额行,
58
- 不会自行推断或伪造。
20
+ **文档:** [概览](./docs/overview.zh-CN.md) · [字段与图标](./docs/segments.zh-CN.md) · [开发](./docs/development.zh-CN.md)
59
21
 
60
22
  ## 安装
61
23
 
@@ -65,6 +27,7 @@ API key、Bedrock、Vertex 等按量/外部网关场景通常没有该字段,c
65
27
  npm install -g @cxmyu/ccglance # npm
66
28
  yarn global add @cxmyu/ccglance # 或 yarn
67
29
  pnpm add -g @cxmyu/ccglance # 或 pnpm
30
+ bun add -g @cxmyu/ccglance # 或 bun
68
31
  ```
69
32
 
70
33
  registry 慢?用国内镜像:
@@ -91,7 +54,7 @@ npm 会下载**预编译好的 `dist/`**(本机不跑编译器),并把 `ccglance
91
54
  ```bash
92
55
  git clone https://github.com/CxMYu/CcGlanceLine.git
93
56
  cd CcGlanceLine
94
- npm install # 安装 devDeps;随后 prepare 脚本自动编译 src/ → dist/
57
+ npm install # 安装 devDeps;随后 prepare 脚本自动编译 src/ -> dist/
95
58
  npm link # 可选:把 ccglance 链到本机 PATH
96
59
  ```
97
60
 
@@ -134,63 +97,14 @@ Windows 用完整路径,例如 `node D:\\path\\to\\ccglance\\dist\\cli.js`。
134
97
  > 全局的 `ccglance` 命令则完全不需要写路径。`"padding": 0` 去掉 Claude Code 默认的左侧缩进,
135
98
  > 让状态栏从最左开始。
136
99
 
137
- ## 状态栏速览
138
-
139
- 三个逻辑行,任意一行无数据时自动消失:
140
-
141
- 1. **运行时** —— 🤖 模型 · 🧠 effort · 状态 · 🚀 fast · ⚡️ 上下文 · 💾 缓存 · 🎯 风格
142
- 2. **配额**(仅订阅会话)—— 📊 Hour / Week 配额仪表
143
- 3. **项目 / 会话** —— 📁 目录 · 🌿 git · 🏷️ 会话名 · ⏱️ 会话 · 💰 成本 · 💩 版本
144
-
145
- **完整参考** —— 每个段的 stdin 来源、所有图标含义、配额月相档位、会话状态图标与颜色语义 ——
146
- 见 **[docs/segments.zh-CN.md](./docs/segments.zh-CN.md)**。
147
-
148
- ## 原理
149
-
150
- 每次刷新时,Claude Code 会运行你配置的 `command`,并把一段 JSON(模型、effort、
151
- 上下文窗口、cost、workspace、版本……)从 **stdin** 传入。`ccglance` 用
152
- `fs.readFileSync(0)` 一次性读入、按固定行格式化并**先打印**;`status`
153
- 只会读取 transcript 尾部固定字节。版本段同步阶段只读本地小缓存;stdout 写出之后,如果
154
- Claude Code 最新版缓存超过 4 小时,才 detached 后台刷新 npm registry。若 JSON 解析失败则
155
- 静默退出,绝不影响 CLI。来自 stdin、transcript、git 的文本在输出前会清理终端控制序列,
156
- 避免逃逸到状态栏之外。主要外部调用是本地 `git`(单次有界 status 调用并带短缓存)、按需
157
- transcript 尾部读取,以及渲染后的后台版本检查。
158
-
159
- ## 开发
160
-
161
- 源码构建、测试、基准、代码结构、贡献范式与 git 缓存内部细节见
162
- **[docs/development.zh-CN.md](./docs/development.zh-CN.md)**。快速开始:
163
-
164
- ```bash
165
- git clone https://github.com/CxMYu/CcGlanceLine.git
166
- cd CcGlanceLine
167
- npm install # devDeps + prepare 构建
168
- npm test # 构建 + node:test
169
- ```
170
-
171
- ## 缓存文件
172
-
173
- `ccglance` 只落盘可丢弃的运行缓存:git 状态、transcript 派生状态、Claude Code 最新版本缓存。
174
- 文件按 `git/`、`transcript/`、`version/` 分组。
175
-
176
- 所有平台都统一落在 Claude Code 自己的配置目录下,随 Claude 配置一起走、清理也方便:
177
-
178
- ```text
179
- ~/.claude/ccglance/
180
- ├── git/ # git 状态快照(每个仓库一个)
181
- ├── transcript/ # transcript 派生的会话状态
182
- └── version/ # Claude Code 最新版本检查
183
- ```
184
-
185
- 若设置了 `CLAUDE_CONFIG_DIR` 改变 Claude Code 配置目录,ccglance 的缓存会跟着落到该目录下。
186
- 整个 `ccglance/` 目录随时可安全删除 —— 下次渲染会自动重建。
187
-
188
- git 缓存逻辑(缓存 key、`HEAD` 兜底、20 分钟 TTL、detached 刷新、原子写)见
189
- [docs/development.zh-CN.md](./docs/development.zh-CN.md#git-缓存内部细节)。
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`)。
190
104
 
191
105
  ## 贡献
192
106
 
193
- 欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`ccglance preview`;
107
+ 欢迎提 issue 和 PR。改代码前请先跑 `npm run typecheck`、`npm run build`、`npm run preview`;
194
108
  请保持**运行时零依赖**和固定风格(不加配置加载器)。
195
109
 
196
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();
@@ -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
- 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
- }
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
+ }
@@ -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
- 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();
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.2",
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",