@say8425/cc-statusline 3.0.0 → 3.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 +2 -1
- package/dist/index.js +458 -22
- package/dist/viewer/index.html +199 -0
- package/dist/viewer/main.js +2842 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ Add the following to `~/.claude/settings.json`:
|
|
|
55
55
|
- **Cost**: Session cost in USD
|
|
56
56
|
- **Context**: Token usage with percentage (color-coded)
|
|
57
57
|
- **Git Diff**: File count, insertions, deletions
|
|
58
|
+
- **Clickable Diff Viewer**: Click `✏️` to open a local diff viewer (Pierre `@pierre/diffs` + `@pierre/trees`) in your browser — file tree, working-tree / vs-base modes, watch mode (auto-refresh), file folding, and in-app search (`Cmd/Ctrl+F`) across the full diff, including deleted lines
|
|
58
59
|
- **PR URL**: Clickable OSC 8 hyperlink
|
|
59
60
|
- **Worktree Support**: Shows real project name when running in a `cc --worktree` session
|
|
60
61
|
- **TrueColor**: Dynamic colors based on thresholds
|
|
@@ -77,7 +78,7 @@ Add the following to `~/.claude/settings.json`:
|
|
|
77
78
|
| 📊 | 5-hour utilization % |
|
|
78
79
|
| ⏰ | Weekly limit reset time |
|
|
79
80
|
| 📅 | 7-day utilization % |
|
|
80
|
-
| ✏️ | Uncommitted changes
|
|
81
|
+
| ✏️ | Uncommitted changes (click to open diff viewer) |
|
|
81
82
|
| 📎 | Pull request link |
|
|
82
83
|
|
|
83
84
|
## Usage Metrics
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,425 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
3
18
|
|
|
4
|
-
// src/
|
|
19
|
+
// src/diff-server/config.ts
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
function resolveDiffPort(env = process.env) {
|
|
23
|
+
const raw = env.CC_STATUSLINE_DIFF_PORT;
|
|
24
|
+
if (!raw)
|
|
25
|
+
return DEFAULT_DIFF_PORT;
|
|
26
|
+
const n = Number.parseInt(raw, 10);
|
|
27
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_DIFF_PORT;
|
|
28
|
+
}
|
|
29
|
+
function isDiffViewerDisabled(env = process.env) {
|
|
30
|
+
return env.CC_STATUSLINE_DIFF_DISABLE === "1";
|
|
31
|
+
}
|
|
32
|
+
function getCacheDir(env = process.env) {
|
|
33
|
+
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
34
|
+
return join(base, "cc-statusline");
|
|
35
|
+
}
|
|
36
|
+
var DEFAULT_DIFF_PORT = 49573;
|
|
37
|
+
var init_config = () => {};
|
|
38
|
+
|
|
39
|
+
// src/diff-server/token.ts
|
|
40
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
41
|
+
import { join as join2 } from "path";
|
|
42
|
+
function getTokenPath(env = process.env) {
|
|
43
|
+
return join2(getCacheDir(env), "diff-server.token");
|
|
44
|
+
}
|
|
45
|
+
function generateToken() {
|
|
46
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
47
|
+
}
|
|
48
|
+
function readTokenSync(env = process.env) {
|
|
49
|
+
try {
|
|
50
|
+
const value = readFileSync(getTokenPath(env), "utf8").trim();
|
|
51
|
+
return value || null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ensureToken(env = process.env) {
|
|
57
|
+
const existing = readTokenSync(env);
|
|
58
|
+
if (existing)
|
|
59
|
+
return existing;
|
|
60
|
+
const token = generateToken();
|
|
61
|
+
mkdirSync(getCacheDir(env), { recursive: true, mode: 448 });
|
|
62
|
+
writeFileSync(getTokenPath(env), token, { mode: 384 });
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
var init_token = __esm(() => {
|
|
66
|
+
init_config();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// src/diff-server/diff.ts
|
|
70
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
71
|
+
import { join as join4 } from "path";
|
|
5
72
|
var {$ } = globalThis.Bun;
|
|
73
|
+
async function isGitRepo(repo) {
|
|
74
|
+
try {
|
|
75
|
+
const out = await $`git -C ${repo} rev-parse --is-inside-work-tree 2>/dev/null`.text();
|
|
76
|
+
return out.trim() === "true";
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function refExists(repo, ref) {
|
|
82
|
+
const r = await $`git -C ${repo} rev-parse --verify --quiet ${ref}`.nothrow().quiet();
|
|
83
|
+
return r.exitCode === 0;
|
|
84
|
+
}
|
|
85
|
+
async function prBaseName(repo) {
|
|
86
|
+
try {
|
|
87
|
+
const out = await $`gh pr view --json baseRefName -q .baseRefName`.cwd(repo).nothrow().quiet().text();
|
|
88
|
+
return out.trim() || null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function defaultBranchName(repo) {
|
|
94
|
+
const r = await $`git -C ${repo} rev-parse --abbrev-ref origin/HEAD 2>/dev/null`.nothrow().quiet();
|
|
95
|
+
if (r.exitCode !== 0)
|
|
96
|
+
return null;
|
|
97
|
+
const t = r.text().trim();
|
|
98
|
+
if (!t.startsWith("origin/"))
|
|
99
|
+
return null;
|
|
100
|
+
const name = t.slice("origin/".length);
|
|
101
|
+
return name && name !== "HEAD" ? name : null;
|
|
102
|
+
}
|
|
103
|
+
async function resolveBaseRef(repo) {
|
|
104
|
+
const named = await prBaseName(repo) ?? await defaultBranchName(repo);
|
|
105
|
+
const candidates = named ? [`origin/${named}`, named] : ["origin/main", "origin/master", "main", "master"];
|
|
106
|
+
for (const ref of candidates) {
|
|
107
|
+
if (await refExists(repo, ref)) {
|
|
108
|
+
const base = ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
|
|
109
|
+
return { base, ref };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { base: named, ref: null };
|
|
113
|
+
}
|
|
114
|
+
async function showBytes(repo, rev, path) {
|
|
115
|
+
const buf = await $`git -C ${repo} show ${`${rev}:${path}`} 2>/dev/null`.nothrow().arrayBuffer();
|
|
116
|
+
return new Uint8Array(buf);
|
|
117
|
+
}
|
|
118
|
+
function readWorkingBytes(repo, path) {
|
|
119
|
+
try {
|
|
120
|
+
return new Uint8Array(readFileSync2(join4(repo, path)));
|
|
121
|
+
} catch {
|
|
122
|
+
return new Uint8Array;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function buildFile(repo, base, status, name, oldName) {
|
|
126
|
+
const oldBytes = status === "added" || status === "untracked" ? new Uint8Array : await showBytes(repo, base, oldName ?? name);
|
|
127
|
+
const newBytes = status === "deleted" ? new Uint8Array : readWorkingBytes(repo, name);
|
|
128
|
+
const binary = oldBytes.includes(0) || newBytes.includes(0);
|
|
129
|
+
const decoder = new TextDecoder;
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
...oldName ? { oldName } : {},
|
|
133
|
+
status,
|
|
134
|
+
binary,
|
|
135
|
+
oldContents: binary ? "" : decoder.decode(oldBytes),
|
|
136
|
+
newContents: binary ? "" : decoder.decode(newBytes)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function getDiffFiles(repo, opts = {}) {
|
|
140
|
+
const base = opts.mode === "base" && opts.ref ? (await $`git -C ${repo} merge-base ${opts.ref} HEAD 2>/dev/null`.nothrow().text()).trim() : "HEAD";
|
|
141
|
+
const files = [];
|
|
142
|
+
if (base) {
|
|
143
|
+
const nameStatus = await $`git -C ${repo} diff --name-status ${base} 2>/dev/null`.nothrow().text();
|
|
144
|
+
for (const line of nameStatus.split(`
|
|
145
|
+
`)) {
|
|
146
|
+
if (!line.trim())
|
|
147
|
+
continue;
|
|
148
|
+
const parts = line.split("\t");
|
|
149
|
+
const code = parts[0] ?? "";
|
|
150
|
+
if (code.startsWith("R")) {
|
|
151
|
+
files.push(await buildFile(repo, base, "renamed", parts[2] ?? "", parts[1]));
|
|
152
|
+
} else if (code.startsWith("A")) {
|
|
153
|
+
files.push(await buildFile(repo, base, "added", parts[1] ?? ""));
|
|
154
|
+
} else if (code.startsWith("D")) {
|
|
155
|
+
files.push(await buildFile(repo, base, "deleted", parts[1] ?? ""));
|
|
156
|
+
} else {
|
|
157
|
+
files.push(await buildFile(repo, base, "modified", parts[1] ?? ""));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (opts.untracked) {
|
|
162
|
+
const listed = await $`git -C ${repo} ls-files --others --exclude-standard 2>/dev/null`.nothrow().text();
|
|
163
|
+
for (const path of listed.split(`
|
|
164
|
+
`).map((s) => s.trim()).filter(Boolean)) {
|
|
165
|
+
files.push(await buildFile(repo, base, "untracked", path));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return files;
|
|
169
|
+
}
|
|
170
|
+
var init_diff = () => {};
|
|
171
|
+
|
|
172
|
+
// src/diff-server/server.ts
|
|
173
|
+
var exports_server = {};
|
|
174
|
+
__export(exports_server, {
|
|
175
|
+
startDiffServer: () => startDiffServer
|
|
176
|
+
});
|
|
177
|
+
import { resolve } from "path";
|
|
178
|
+
async function resolveBaseCached(repo) {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const hit = baseCache.get(repo);
|
|
181
|
+
if (hit && now - hit.at < BASE_TTL_MS)
|
|
182
|
+
return hit.value;
|
|
183
|
+
const value = await resolveBaseRef(repo);
|
|
184
|
+
baseCache.set(repo, { value, at: now });
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function createHandler(cfg) {
|
|
188
|
+
const viewerRoot = resolve(cfg.viewerDir);
|
|
189
|
+
return async (req) => {
|
|
190
|
+
const url = new URL(req.url);
|
|
191
|
+
if (url.pathname === "/api/ping") {
|
|
192
|
+
return new Response(null, {
|
|
193
|
+
status: 204,
|
|
194
|
+
headers: { "x-cc-statusline": "1" }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (url.pathname === "/api/diff") {
|
|
198
|
+
if (url.searchParams.get("token") !== cfg.token) {
|
|
199
|
+
return new Response("forbidden", { status: 403 });
|
|
200
|
+
}
|
|
201
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
202
|
+
if (!repo || !await isGitRepo(repo)) {
|
|
203
|
+
return new Response("not a git repository", { status: 400 });
|
|
204
|
+
}
|
|
205
|
+
const untracked = url.searchParams.get("untracked") === "1";
|
|
206
|
+
const mode = url.searchParams.get("mode") === "base" ? "base" : "working";
|
|
207
|
+
const { base, ref } = await resolveBaseCached(repo);
|
|
208
|
+
const files = mode === "base" ? await getDiffFiles(repo, {
|
|
209
|
+
untracked,
|
|
210
|
+
mode: "base",
|
|
211
|
+
ref: ref ?? undefined
|
|
212
|
+
}) : await getDiffFiles(repo, { untracked });
|
|
213
|
+
return new Response(JSON.stringify(files), {
|
|
214
|
+
headers: {
|
|
215
|
+
"content-type": "application/json; charset=utf-8",
|
|
216
|
+
"x-diff-base": base ?? ""
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const rel = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
221
|
+
const filePath = resolve(viewerRoot, rel);
|
|
222
|
+
if (filePath !== viewerRoot && !filePath.startsWith(`${viewerRoot}/`)) {
|
|
223
|
+
return new Response("forbidden", { status: 403 });
|
|
224
|
+
}
|
|
225
|
+
const file = Bun.file(filePath);
|
|
226
|
+
if (await file.exists()) {
|
|
227
|
+
return new Response(file, { headers: { "cache-control": "no-store" } });
|
|
228
|
+
}
|
|
229
|
+
return new Response("not found", { status: 404 });
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function startDiffServer(opts) {
|
|
233
|
+
const env = opts.env ?? process.env;
|
|
234
|
+
const token = ensureToken(env);
|
|
235
|
+
let lastActivity = Date.now();
|
|
236
|
+
const handler = createHandler({ viewerDir: opts.viewerDir, token });
|
|
237
|
+
const server = Bun.serve({
|
|
238
|
+
hostname: "127.0.0.1",
|
|
239
|
+
port: opts.port,
|
|
240
|
+
fetch: (req) => {
|
|
241
|
+
lastActivity = Date.now();
|
|
242
|
+
return handler(req);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
let idleTimer;
|
|
246
|
+
const idleTimeoutMs = opts.idleTimeoutMs;
|
|
247
|
+
if (idleTimeoutMs && idleTimeoutMs > 0) {
|
|
248
|
+
idleTimer = setInterval(() => {
|
|
249
|
+
if (Date.now() - lastActivity > idleTimeoutMs) {
|
|
250
|
+
stop();
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
}, 60000);
|
|
254
|
+
idleTimer.unref?.();
|
|
255
|
+
}
|
|
256
|
+
function stop() {
|
|
257
|
+
if (idleTimer)
|
|
258
|
+
clearInterval(idleTimer);
|
|
259
|
+
server.stop(true);
|
|
260
|
+
}
|
|
261
|
+
return { server, token, stop };
|
|
262
|
+
}
|
|
263
|
+
var BASE_TTL_MS = 1e4, baseCache;
|
|
264
|
+
var init_server = __esm(() => {
|
|
265
|
+
init_diff();
|
|
266
|
+
init_token();
|
|
267
|
+
baseCache = new Map;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/index.ts
|
|
271
|
+
init_config();
|
|
272
|
+
import { join as join5 } from "path";
|
|
273
|
+
|
|
274
|
+
// src/diff-server/ensure.ts
|
|
275
|
+
init_config();
|
|
276
|
+
init_token();
|
|
277
|
+
import { mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
278
|
+
import { join as join3 } from "path";
|
|
279
|
+
var ENSURE_TTL_MS = 5000;
|
|
280
|
+
var LOCK_STALE_MS = 30000;
|
|
281
|
+
var checkedAt = 0;
|
|
282
|
+
async function probeOurServer(port) {
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/ping`, {
|
|
285
|
+
signal: AbortSignal.timeout(150)
|
|
286
|
+
});
|
|
287
|
+
return res.headers.get("x-cc-statusline") === "1";
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function acquireSpawnLock(env) {
|
|
293
|
+
const lock = join3(getCacheDir(env), "diff-server.lock");
|
|
294
|
+
mkdirSync2(getCacheDir(env), { recursive: true, mode: 448 });
|
|
295
|
+
try {
|
|
296
|
+
mkdirSync2(lock);
|
|
297
|
+
return true;
|
|
298
|
+
} catch {
|
|
299
|
+
try {
|
|
300
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
301
|
+
rmSync(lock, { recursive: true, force: true });
|
|
302
|
+
mkdirSync2(lock);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function spawnDaemon(port, env) {
|
|
310
|
+
const execPath = process.execPath;
|
|
311
|
+
const selfPath = Bun.main;
|
|
312
|
+
Bun.spawn([
|
|
313
|
+
"sh",
|
|
314
|
+
"-c",
|
|
315
|
+
'nohup "$0" "$1" --diff-server >/dev/null 2>&1 &',
|
|
316
|
+
execPath,
|
|
317
|
+
selfPath
|
|
318
|
+
], {
|
|
319
|
+
env: { ...process.env, ...env, CC_STATUSLINE_DIFF_PORT: String(port) },
|
|
320
|
+
stdin: "ignore",
|
|
321
|
+
stdout: "ignore",
|
|
322
|
+
stderr: "ignore"
|
|
323
|
+
}).unref();
|
|
324
|
+
}
|
|
325
|
+
async function maybeSpawn(port, env, spawn = spawnDaemon) {
|
|
326
|
+
try {
|
|
327
|
+
if (await probeOurServer(port))
|
|
328
|
+
return;
|
|
329
|
+
if (!acquireSpawnLock(env))
|
|
330
|
+
return;
|
|
331
|
+
spawn(port, env);
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
async function ensureDiffServer(_repo, env = process.env, spawn = spawnDaemon) {
|
|
335
|
+
if (isDiffViewerDisabled(env))
|
|
336
|
+
return null;
|
|
337
|
+
const port = resolveDiffPort(env);
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
if (now - checkedAt >= ENSURE_TTL_MS) {
|
|
340
|
+
checkedAt = now;
|
|
341
|
+
maybeSpawn(port, env, spawn);
|
|
342
|
+
}
|
|
343
|
+
const token = readTokenSync(env);
|
|
344
|
+
return token ? { port, token } : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/diff-server/link.ts
|
|
348
|
+
function buildDiffViewerUrl(params) {
|
|
349
|
+
const query = new URLSearchParams({
|
|
350
|
+
repo: params.repo,
|
|
351
|
+
token: params.token
|
|
352
|
+
});
|
|
353
|
+
if (params.mode)
|
|
354
|
+
query.set("mode", params.mode);
|
|
355
|
+
return `http://127.0.0.1:${params.port}/?${query.toString()}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/git/baseChanges.ts
|
|
359
|
+
var {$: $2 } = globalThis.Bun;
|
|
6
360
|
|
|
7
361
|
// src/cache.ts
|
|
8
362
|
var cache = {
|
|
9
363
|
branch: { value: "", timestamp: 0 },
|
|
10
364
|
gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
|
|
11
365
|
prUrl: { value: null, timestamp: 0 },
|
|
12
|
-
mainProjectName: { value: null, timestamp: 0 }
|
|
366
|
+
mainProjectName: { value: null, timestamp: 0 },
|
|
367
|
+
baseRef: {
|
|
368
|
+
value: null,
|
|
369
|
+
timestamp: 0
|
|
370
|
+
}
|
|
13
371
|
};
|
|
14
372
|
var CACHE_TTL = {
|
|
15
373
|
branch: 5000,
|
|
16
374
|
gitChanges: 0,
|
|
17
375
|
prUrl: 30000,
|
|
18
|
-
mainProjectName: 300000
|
|
376
|
+
mainProjectName: 300000,
|
|
377
|
+
baseRef: 30000
|
|
19
378
|
};
|
|
20
379
|
|
|
380
|
+
// src/git/baseChanges.ts
|
|
381
|
+
init_diff();
|
|
382
|
+
|
|
383
|
+
// src/git/shortstat.ts
|
|
384
|
+
function parseShortstat(text) {
|
|
385
|
+
const [files, insertions, deletions] = [
|
|
386
|
+
/(\d+) files?/g,
|
|
387
|
+
/(\d+) insertions?/g,
|
|
388
|
+
/(\d+) deletions?/g
|
|
389
|
+
].map((regex) => (text.match(regex) || []).reduce((sum, m) => sum + Number.parseInt(m, 10), 0));
|
|
390
|
+
return { files, insertions, deletions };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/git/baseChanges.ts
|
|
394
|
+
async function getBaseChangesCached() {
|
|
395
|
+
try {
|
|
396
|
+
let resolved = cache.baseRef.value;
|
|
397
|
+
if (resolved === null || Date.now() - cache.baseRef.timestamp >= CACHE_TTL.baseRef) {
|
|
398
|
+
resolved = await resolveBaseRef(".");
|
|
399
|
+
cache.baseRef = { value: resolved, timestamp: Date.now() };
|
|
400
|
+
}
|
|
401
|
+
if (!resolved.ref || !resolved.base)
|
|
402
|
+
return null;
|
|
403
|
+
const mb = (await $2`git merge-base ${resolved.ref} HEAD 2>/dev/null`.nothrow().text()).trim();
|
|
404
|
+
if (!mb)
|
|
405
|
+
return null;
|
|
406
|
+
const out = await $2`git diff ${mb} --shortstat 2>/dev/null`.nothrow().text();
|
|
407
|
+
const { files, insertions, deletions } = parseShortstat(out);
|
|
408
|
+
if (files === 0 && insertions === 0 && deletions === 0)
|
|
409
|
+
return null;
|
|
410
|
+
return { base: resolved.base, files, insertions, deletions };
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
21
415
|
// src/git/branch.ts
|
|
416
|
+
var {$: $3 } = globalThis.Bun;
|
|
22
417
|
async function getBranchCached() {
|
|
23
418
|
if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
|
|
24
419
|
return cache.branch.value;
|
|
25
420
|
}
|
|
26
421
|
try {
|
|
27
|
-
const result = await
|
|
422
|
+
const result = await $3`git branch --show-current 2>/dev/null`.text();
|
|
28
423
|
cache.branch = { value: result.trim(), timestamp: Date.now() };
|
|
29
424
|
return cache.branch.value;
|
|
30
425
|
} catch {
|
|
@@ -32,23 +427,18 @@ async function getBranchCached() {
|
|
|
32
427
|
}
|
|
33
428
|
}
|
|
34
429
|
// src/git/changes.ts
|
|
35
|
-
var {$: $
|
|
430
|
+
var {$: $4 } = globalThis.Bun;
|
|
36
431
|
async function getGitChangesCached() {
|
|
37
432
|
if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
|
|
38
433
|
return cache.gitChanges;
|
|
39
434
|
}
|
|
40
435
|
try {
|
|
41
436
|
const [diff, staged] = await Promise.all([
|
|
42
|
-
$
|
|
43
|
-
$
|
|
437
|
+
$4`git diff --shortstat 2>/dev/null`.text(),
|
|
438
|
+
$4`git diff --cached --shortstat 2>/dev/null`.text()
|
|
44
439
|
]);
|
|
45
|
-
const
|
|
46
|
-
${staged}
|
|
47
|
-
const [files, insertions, deletions] = [
|
|
48
|
-
/(\d+) files?/g,
|
|
49
|
-
/(\d+) insertions?/g,
|
|
50
|
-
/(\d+) deletions?/g
|
|
51
|
-
].map((regex) => (combined.match(regex) || []).reduce((sum, m) => sum + Number.parseInt(m, 10), 0));
|
|
440
|
+
const { files, insertions, deletions } = parseShortstat(`${diff}
|
|
441
|
+
${staged}`);
|
|
52
442
|
cache.gitChanges = { files, insertions, deletions, timestamp: Date.now() };
|
|
53
443
|
return cache.gitChanges;
|
|
54
444
|
} catch {
|
|
@@ -56,13 +446,13 @@ ${staged}`;
|
|
|
56
446
|
}
|
|
57
447
|
}
|
|
58
448
|
// src/git/pr.ts
|
|
59
|
-
var {$: $
|
|
449
|
+
var {$: $5 } = globalThis.Bun;
|
|
60
450
|
async function getPrUrlCached() {
|
|
61
451
|
if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
|
|
62
452
|
return cache.prUrl.value;
|
|
63
453
|
}
|
|
64
454
|
try {
|
|
65
|
-
const result = await $
|
|
455
|
+
const result = await $5`gh pr view --json url -q .url 2>/dev/null`.text();
|
|
66
456
|
cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
|
|
67
457
|
return cache.prUrl.value;
|
|
68
458
|
} catch {
|
|
@@ -70,13 +460,13 @@ async function getPrUrlCached() {
|
|
|
70
460
|
}
|
|
71
461
|
}
|
|
72
462
|
// src/git/worktree.ts
|
|
73
|
-
var {$: $
|
|
463
|
+
var {$: $6 } = globalThis.Bun;
|
|
74
464
|
async function getMainProjectNameCached() {
|
|
75
465
|
if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
|
|
76
466
|
return cache.mainProjectName.value;
|
|
77
467
|
}
|
|
78
468
|
try {
|
|
79
|
-
const result = await $
|
|
469
|
+
const result = await $6`git rev-parse --git-common-dir 2>/dev/null`.text();
|
|
80
470
|
const gitCommonDir = result.trim();
|
|
81
471
|
if (gitCommonDir === ".git") {
|
|
82
472
|
cache.mainProjectName = { value: null, timestamp: Date.now() };
|
|
@@ -182,10 +572,17 @@ function renderStatusLine(ctx) {
|
|
|
182
572
|
}
|
|
183
573
|
}
|
|
184
574
|
const hasGitChanges = ctx.gitChanges.files > 0 || ctx.gitChanges.insertions > 0 || ctx.gitChanges.deletions > 0;
|
|
185
|
-
if (hasGitChanges || ctx.prUrl) {
|
|
575
|
+
if (hasGitChanges || ctx.baseChanges || ctx.prUrl) {
|
|
186
576
|
let line4 = "";
|
|
187
577
|
if (hasGitChanges) {
|
|
188
|
-
|
|
578
|
+
const u = ctx.diffViewerUrl ? C.UNDERLINE : "";
|
|
579
|
+
const changesText = `\u270F\uFE0F ${u}${C.WHITE}${ctx.gitChanges.files} files${C.RESET}` + `${u} ${C.GREEN}+${ctx.gitChanges.insertions}${C.RESET}` + `${u} ${C.RED}-${ctx.gitChanges.deletions}${C.RESET}`;
|
|
580
|
+
line4 += ctx.diffViewerUrl ? `\x1B]8;;${ctx.diffViewerUrl}\x07${changesText}\x1B]8;;\x07` : changesText;
|
|
581
|
+
} else if (ctx.baseChanges) {
|
|
582
|
+
const bc = ctx.baseChanges;
|
|
583
|
+
const u = ctx.baseDiffViewerUrl ? C.UNDERLINE : "";
|
|
584
|
+
const baseText = `\u270F\uFE0F ${u}vs ${bc.base} ${C.WHITE}${bc.files} files${C.RESET}` + `${u} ${C.GREEN}+${bc.insertions}${C.RESET}` + `${u} ${C.RED}-${bc.deletions}${C.RESET}`;
|
|
585
|
+
line4 += ctx.baseDiffViewerUrl ? `\x1B]8;;${ctx.baseDiffViewerUrl}\x07${baseText}\x1B]8;;\x07` : baseText;
|
|
189
586
|
}
|
|
190
587
|
if (ctx.prUrl) {
|
|
191
588
|
const prLabel = ctx.prUrl.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
|
|
@@ -216,20 +613,59 @@ async function main() {
|
|
|
216
613
|
getPrUrlCached(),
|
|
217
614
|
getMainProjectNameCached()
|
|
218
615
|
]);
|
|
616
|
+
const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
|
|
617
|
+
const hasChanges = gitChanges.files > 0 || gitChanges.insertions > 0 || gitChanges.deletions > 0;
|
|
618
|
+
let diffViewerUrl = null;
|
|
619
|
+
let baseDiffViewerUrl = null;
|
|
620
|
+
let baseChanges = null;
|
|
621
|
+
if (!hasChanges && repo) {
|
|
622
|
+
baseChanges = await getBaseChangesCached();
|
|
623
|
+
}
|
|
624
|
+
if (repo && (hasChanges || baseChanges)) {
|
|
625
|
+
const ensured = await ensureDiffServer(repo);
|
|
626
|
+
if (ensured) {
|
|
627
|
+
if (hasChanges) {
|
|
628
|
+
diffViewerUrl = buildDiffViewerUrl({
|
|
629
|
+
port: ensured.port,
|
|
630
|
+
repo,
|
|
631
|
+
token: ensured.token,
|
|
632
|
+
mode: "working"
|
|
633
|
+
});
|
|
634
|
+
} else if (baseChanges) {
|
|
635
|
+
baseDiffViewerUrl = buildDiffViewerUrl({
|
|
636
|
+
port: ensured.port,
|
|
637
|
+
repo,
|
|
638
|
+
token: ensured.token,
|
|
639
|
+
mode: "base"
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
219
644
|
const lines = renderStatusLine({
|
|
220
645
|
claudeJson,
|
|
221
646
|
branch,
|
|
222
647
|
gitChanges,
|
|
223
648
|
prUrl,
|
|
224
649
|
rateLimits: claudeJson.rate_limits ?? null,
|
|
225
|
-
mainProjectName
|
|
650
|
+
mainProjectName,
|
|
651
|
+
diffViewerUrl,
|
|
652
|
+
baseChanges,
|
|
653
|
+
baseDiffViewerUrl
|
|
226
654
|
});
|
|
227
655
|
for (const line of lines) {
|
|
228
656
|
console.log(line);
|
|
229
657
|
}
|
|
230
658
|
}
|
|
231
659
|
if (import.meta.main) {
|
|
232
|
-
|
|
660
|
+
if (process.argv.includes("--diff-server")) {
|
|
661
|
+
const { startDiffServer: startDiffServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
662
|
+
startDiffServer2({
|
|
663
|
+
port: resolveDiffPort(),
|
|
664
|
+
viewerDir: join5(import.meta.dir, "viewer")
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
main().catch(console.error);
|
|
668
|
+
}
|
|
233
669
|
}
|
|
234
670
|
export {
|
|
235
671
|
main
|