@say8425/cc-statusline 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +135 -352
- package/package.json +14 -14
- package/dist/viewer/index.html +0 -490
- package/dist/viewer/main.js +0 -2842
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Add the following to `~/.claude/settings.json`:
|
|
|
56
56
|
- **Context**: Token usage with percentage (color-coded)
|
|
57
57
|
- **Model**: Current model name and reasoning effort (e.g., `Fable 5 high`; effort shown only for models that support it), with an `⚡ultra` badge when ultracode is enabled in your Claude Code settings and the session reports `xhigh` effort
|
|
58
58
|
- **Git Diff**: File count, insertions, deletions
|
|
59
|
-
- **Clickable Diff Viewer**: Click `✏️` to open a local diff viewer
|
|
59
|
+
- **Clickable Diff Viewer**: Click `✏️` to open a local diff viewer in your browser, powered by [diffdeck](https://github.com/say8425/diffdeck) (installed automatically as a dependency) — 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
|
|
60
60
|
- **PR URL**: Clickable OSC 8 hyperlink
|
|
61
61
|
- **Worktree Support**: Shows real project name when running in a `cc --worktree` session
|
|
62
62
|
- **TrueColor**: Dynamic colors based on thresholds
|
|
@@ -85,7 +85,7 @@ Add the following to `~/.claude/settings.json`:
|
|
|
85
85
|
|
|
86
86
|
## Diff Viewer
|
|
87
87
|
|
|
88
|
-
Click `✏️` in the statusline to open a local diff viewer in your browser
|
|
88
|
+
Click `✏️` in the statusline to open a local diff viewer in your browser. The viewer itself is provided by [diffdeck](https://github.com/say8425/diffdeck) ([`@say8425/diffdeck`](https://www.npmjs.com/package/@say8425/diffdeck) on npm), installed automatically as a runtime dependency of cc-statusline — the statusline spawns it as a background daemon and links to it from the `✏️` entry point.
|
|
89
89
|
|
|
90
90
|

|
|
91
91
|
|
|
@@ -109,7 +109,7 @@ Click `✏️` in the statusline to open a local diff viewer in your browser, re
|
|
|
109
109
|
|
|
110
110
|
### How It Works (Diff Viewer)
|
|
111
111
|
|
|
112
|
-
The statusline spawns
|
|
112
|
+
The statusline spawns diffdeck as a background daemon on demand at `127.0.0.1:49573` whenever the repo has something to show. Requests are token-protected and bound to localhost.
|
|
113
113
|
|
|
114
114
|
| Environment variable | Effect |
|
|
115
115
|
| -------------------- | ------ |
|
package/dist/index.js
CHANGED
|
@@ -1,412 +1,170 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
|
|
4
|
+
// src/diff-server/ensure.ts
|
|
5
|
+
import { mkdirSync, readFileSync as readFileSync2, rmSync, statSync } from "fs";
|
|
6
|
+
import { dirname, join as join3 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
18
8
|
|
|
19
9
|
// src/diff-server/config.ts
|
|
20
10
|
import { homedir } from "os";
|
|
21
11
|
import { join } from "path";
|
|
22
|
-
var DEFAULT_DIFF_PORT = 49573
|
|
12
|
+
var DEFAULT_DIFF_PORT = 49573;
|
|
13
|
+
var resolveDiffPort = (env = process.env) => {
|
|
23
14
|
const raw = env.CC_STATUSLINE_DIFF_PORT;
|
|
24
15
|
if (!raw)
|
|
25
16
|
return DEFAULT_DIFF_PORT;
|
|
26
17
|
const n = Number.parseInt(raw, 10);
|
|
27
18
|
return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_DIFF_PORT;
|
|
28
|
-
}
|
|
19
|
+
};
|
|
20
|
+
var isDiffViewerDisabled = (env = process.env) => env.CC_STATUSLINE_DIFF_DISABLE === "1";
|
|
21
|
+
var getCacheDir = (env = process.env) => {
|
|
29
22
|
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
30
23
|
return join(base, "cc-statusline");
|
|
31
24
|
};
|
|
32
|
-
var
|
|
25
|
+
var getDiffdeckCacheDir = (env = process.env) => {
|
|
26
|
+
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
27
|
+
return join(base, "diffdeck");
|
|
28
|
+
};
|
|
33
29
|
|
|
34
30
|
// src/diff-server/token.ts
|
|
35
|
-
import {
|
|
31
|
+
import { readFileSync } from "fs";
|
|
36
32
|
import { join as join2 } from "path";
|
|
37
|
-
var getTokenPath = (env = process.env) => join2(
|
|
33
|
+
var getTokenPath = (env = process.env) => join2(getDiffdeckCacheDir(env), "diff-server.token");
|
|
34
|
+
var readTokenSync = (env = process.env) => {
|
|
38
35
|
try {
|
|
39
36
|
const value = readFileSync(getTokenPath(env), "utf8").trim();
|
|
40
37
|
return value || null;
|
|
41
38
|
} catch {
|
|
42
39
|
return null;
|
|
43
40
|
}
|
|
44
|
-
}, ensureToken = (env = process.env) => {
|
|
45
|
-
const existing = readTokenSync(env);
|
|
46
|
-
if (existing)
|
|
47
|
-
return existing;
|
|
48
|
-
const token = generateToken();
|
|
49
|
-
mkdirSync(getCacheDir(env), { recursive: true, mode: 448 });
|
|
50
|
-
writeFileSync(getTokenPath(env), token, { mode: 384 });
|
|
51
|
-
return token;
|
|
52
|
-
};
|
|
53
|
-
var init_token = __esm(() => {
|
|
54
|
-
init_config();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// src/diff-server/mapLimit.ts
|
|
58
|
-
var mapWithLimit = async (items, limit, fn) => {
|
|
59
|
-
const results = [];
|
|
60
|
-
let next = 0;
|
|
61
|
-
const worker = async () => {
|
|
62
|
-
while (next < items.length) {
|
|
63
|
-
const index = next;
|
|
64
|
-
next += 1;
|
|
65
|
-
results[index] = await fn(items[index], index);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, () => worker());
|
|
69
|
-
await Promise.all(workers);
|
|
70
|
-
return results;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// src/diff-server/diff.ts
|
|
74
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
75
|
-
import { isAbsolute, join as join4, resolve } from "path";
|
|
76
|
-
var {$ } = globalThis.Bun;
|
|
77
|
-
var BUILD_CONCURRENCY = 8, isGitRepo = async (repo) => {
|
|
78
|
-
try {
|
|
79
|
-
const out = await $`git -C ${repo} rev-parse --is-inside-work-tree 2>/dev/null`.text();
|
|
80
|
-
return out.trim() === "true";
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}, refExists = async (repo, ref) => {
|
|
85
|
-
const r = await $`git -C ${repo} rev-parse --verify --quiet ${ref}`.nothrow().quiet();
|
|
86
|
-
return r.exitCode === 0;
|
|
87
|
-
}, prBaseName = async (repo) => {
|
|
88
|
-
try {
|
|
89
|
-
const out = await $`gh pr view --json baseRefName -q .baseRefName`.cwd(repo).nothrow().quiet().text();
|
|
90
|
-
return out.trim() || null;
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}, defaultBranchName = async (repo) => {
|
|
95
|
-
const r = await $`git -C ${repo} rev-parse --abbrev-ref origin/HEAD 2>/dev/null`.nothrow().quiet();
|
|
96
|
-
if (r.exitCode !== 0)
|
|
97
|
-
return null;
|
|
98
|
-
const t = r.text().trim();
|
|
99
|
-
if (!t.startsWith("origin/"))
|
|
100
|
-
return null;
|
|
101
|
-
const name = t.slice("origin/".length);
|
|
102
|
-
return name && name !== "HEAD" ? name : null;
|
|
103
|
-
}, resolveBaseRef = async (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
|
-
}, showBytes = async (repo, rev, path) => {
|
|
114
|
-
const buf = await $`git -C ${repo} show ${`${rev}:${path}`} 2>/dev/null`.nothrow().arrayBuffer();
|
|
115
|
-
return new Uint8Array(buf);
|
|
116
|
-
}, readWorkingBytes = (repo, path) => {
|
|
117
|
-
try {
|
|
118
|
-
return new Uint8Array(readFileSync2(join4(repo, path)));
|
|
119
|
-
} catch {
|
|
120
|
-
return new Uint8Array;
|
|
121
|
-
}
|
|
122
|
-
}, buildFile = async (repo, base, status, name, oldName) => {
|
|
123
|
-
const oldBytes = status === "added" || status === "untracked" ? new Uint8Array : await showBytes(repo, base, oldName ?? name);
|
|
124
|
-
const newBytes = status === "deleted" ? new Uint8Array : readWorkingBytes(repo, name);
|
|
125
|
-
const binary = oldBytes.includes(0) || newBytes.includes(0);
|
|
126
|
-
const decoder = new TextDecoder;
|
|
127
|
-
return {
|
|
128
|
-
name,
|
|
129
|
-
...oldName ? { oldName } : {},
|
|
130
|
-
status,
|
|
131
|
-
binary,
|
|
132
|
-
oldContents: binary ? "" : decoder.decode(oldBytes),
|
|
133
|
-
newContents: binary ? "" : decoder.decode(newBytes),
|
|
134
|
-
...binary ? {
|
|
135
|
-
blobVersion: `${Bun.hash(oldBytes).toString(36)}.${Bun.hash(newBytes).toString(36)}`
|
|
136
|
-
} : {}
|
|
137
|
-
};
|
|
138
|
-
}, resolveDiffBaseRev = async (repo, opts) => opts.mode === "base" && opts.ref ? (await $`git -C ${repo} merge-base ${opts.ref} HEAD 2>/dev/null`.nothrow().text()).trim() : "HEAD", getFileBytes = async (repo, path, side, opts = {}) => {
|
|
139
|
-
const root = resolve(repo);
|
|
140
|
-
const target = resolve(root, path);
|
|
141
|
-
if (!path || isAbsolute(path) || target !== root && !target.startsWith(`${root}/`)) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
if (side === "new") {
|
|
145
|
-
const bytes2 = readWorkingBytes(repo, path);
|
|
146
|
-
return bytes2.length > 0 ? bytes2 : null;
|
|
147
|
-
}
|
|
148
|
-
const base = await resolveDiffBaseRev(repo, opts);
|
|
149
|
-
if (!base)
|
|
150
|
-
return null;
|
|
151
|
-
const bytes = await showBytes(repo, base, path);
|
|
152
|
-
return bytes.length > 0 ? bytes : null;
|
|
153
|
-
}, getDiffFiles = async (repo, opts = {}) => {
|
|
154
|
-
const base = await resolveDiffBaseRev(repo, opts);
|
|
155
|
-
const files = [];
|
|
156
|
-
if (base) {
|
|
157
|
-
const nameStatus = await $`git -C ${repo} diff --name-status ${base} 2>/dev/null`.nothrow().text();
|
|
158
|
-
const specs = [];
|
|
159
|
-
for (const line of nameStatus.split(`
|
|
160
|
-
`)) {
|
|
161
|
-
if (!line.trim())
|
|
162
|
-
continue;
|
|
163
|
-
const parts = line.split("\t");
|
|
164
|
-
const code = parts[0] ?? "";
|
|
165
|
-
if (code.startsWith("R")) {
|
|
166
|
-
specs.push({
|
|
167
|
-
status: "renamed",
|
|
168
|
-
name: parts[2] ?? "",
|
|
169
|
-
oldName: parts[1]
|
|
170
|
-
});
|
|
171
|
-
} else if (code.startsWith("A")) {
|
|
172
|
-
specs.push({ status: "added", name: parts[1] ?? "" });
|
|
173
|
-
} else if (code.startsWith("D")) {
|
|
174
|
-
specs.push({ status: "deleted", name: parts[1] ?? "" });
|
|
175
|
-
} else {
|
|
176
|
-
specs.push({ status: "modified", name: parts[1] ?? "" });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
files.push(...await mapWithLimit(specs, BUILD_CONCURRENCY, (spec) => buildFile(repo, base, spec.status, spec.name, spec.oldName)));
|
|
180
|
-
}
|
|
181
|
-
if (opts.untracked) {
|
|
182
|
-
const listed = await $`git -C ${repo} ls-files --others --exclude-standard 2>/dev/null`.nothrow().text();
|
|
183
|
-
const paths = listed.split(`
|
|
184
|
-
`).map((s) => s.trim()).filter(Boolean);
|
|
185
|
-
files.push(...await mapWithLimit(paths, BUILD_CONCURRENCY, (path) => buildFile(repo, base, "untracked", path)));
|
|
186
|
-
}
|
|
187
|
-
return files;
|
|
188
41
|
};
|
|
189
|
-
var init_diff = () => {};
|
|
190
|
-
|
|
191
|
-
// src/diff-server/imageTypes.ts
|
|
192
|
-
var IMAGE_MIME, extensionOf = (name) => {
|
|
193
|
-
const dot = name.lastIndexOf(".");
|
|
194
|
-
if (dot <= 0 || dot === name.length - 1)
|
|
195
|
-
return null;
|
|
196
|
-
return name.slice(dot + 1).toLowerCase();
|
|
197
|
-
}, isImagePath = (name) => {
|
|
198
|
-
const ext = extensionOf(name);
|
|
199
|
-
return ext !== null && ext in IMAGE_MIME;
|
|
200
|
-
}, imageContentType = (name) => {
|
|
201
|
-
const ext = extensionOf(name);
|
|
202
|
-
return ext && IMAGE_MIME[ext] || "application/octet-stream";
|
|
203
|
-
};
|
|
204
|
-
var init_imageTypes = __esm(() => {
|
|
205
|
-
IMAGE_MIME = {
|
|
206
|
-
png: "image/png",
|
|
207
|
-
jpg: "image/jpeg",
|
|
208
|
-
jpeg: "image/jpeg",
|
|
209
|
-
gif: "image/gif",
|
|
210
|
-
webp: "image/webp",
|
|
211
|
-
avif: "image/avif",
|
|
212
|
-
bmp: "image/bmp",
|
|
213
|
-
ico: "image/x-icon"
|
|
214
|
-
};
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// src/diff-server/server.ts
|
|
218
|
-
var exports_server = {};
|
|
219
|
-
__export(exports_server, {
|
|
220
|
-
startDiffServer: () => startDiffServer
|
|
221
|
-
});
|
|
222
|
-
import { resolve as resolve2 } from "path";
|
|
223
|
-
var BASE_TTL_MS = 1e4, baseCache, resolveBaseCached = async (repo) => {
|
|
224
|
-
const now = Date.now();
|
|
225
|
-
const hit = baseCache.get(repo);
|
|
226
|
-
if (hit && now - hit.at < BASE_TTL_MS)
|
|
227
|
-
return hit.value;
|
|
228
|
-
const value = await resolveBaseRef(repo);
|
|
229
|
-
baseCache.set(repo, { value, at: now });
|
|
230
|
-
return value;
|
|
231
|
-
}, createHandler = (cfg) => {
|
|
232
|
-
const viewerRoot = resolve2(cfg.viewerDir);
|
|
233
|
-
return async (req) => {
|
|
234
|
-
const url = new URL(req.url);
|
|
235
|
-
if (url.pathname === "/api/ping") {
|
|
236
|
-
return new Response(null, {
|
|
237
|
-
status: 204,
|
|
238
|
-
headers: { "x-cc-statusline": "1" }
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
if (url.pathname === "/api/diff") {
|
|
242
|
-
if (url.searchParams.get("token") !== cfg.token) {
|
|
243
|
-
return new Response("forbidden", { status: 403 });
|
|
244
|
-
}
|
|
245
|
-
const repo = url.searchParams.get("repo") ?? "";
|
|
246
|
-
if (!repo || !await isGitRepo(repo)) {
|
|
247
|
-
return new Response("not a git repository", { status: 400 });
|
|
248
|
-
}
|
|
249
|
-
const untracked = url.searchParams.get("untracked") === "1";
|
|
250
|
-
const mode = url.searchParams.get("mode") === "base" ? "base" : "working";
|
|
251
|
-
const { base, ref } = await resolveBaseCached(repo);
|
|
252
|
-
const files = mode === "base" ? await getDiffFiles(repo, {
|
|
253
|
-
untracked,
|
|
254
|
-
mode: "base",
|
|
255
|
-
ref: ref ?? undefined
|
|
256
|
-
}) : await getDiffFiles(repo, { untracked });
|
|
257
|
-
return new Response(JSON.stringify(files), {
|
|
258
|
-
headers: {
|
|
259
|
-
"content-type": "application/json; charset=utf-8",
|
|
260
|
-
"x-diff-base": base ?? ""
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
if (url.pathname === "/api/blob") {
|
|
265
|
-
if (url.searchParams.get("token") !== cfg.token) {
|
|
266
|
-
return new Response("forbidden", { status: 403 });
|
|
267
|
-
}
|
|
268
|
-
const repo = url.searchParams.get("repo") ?? "";
|
|
269
|
-
if (!repo || !await isGitRepo(repo)) {
|
|
270
|
-
return new Response("not a git repository", { status: 400 });
|
|
271
|
-
}
|
|
272
|
-
const path = url.searchParams.get("path") ?? "";
|
|
273
|
-
if (!isImagePath(path)) {
|
|
274
|
-
return new Response("not found", { status: 404 });
|
|
275
|
-
}
|
|
276
|
-
const side = url.searchParams.get("side") === "old" ? "old" : "new";
|
|
277
|
-
const mode = url.searchParams.get("mode") === "base" ? "base" : "working";
|
|
278
|
-
const ref = mode === "base" ? (await resolveBaseCached(repo)).ref : null;
|
|
279
|
-
const bytes = await getFileBytes(repo, path, side, mode === "base" && ref ? { mode, ref } : {});
|
|
280
|
-
if (!bytes)
|
|
281
|
-
return new Response("not found", { status: 404 });
|
|
282
|
-
return new Response(bytes, {
|
|
283
|
-
headers: {
|
|
284
|
-
"content-type": imageContentType(path),
|
|
285
|
-
"cache-control": "no-store"
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
const rel = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
290
|
-
const filePath = resolve2(viewerRoot, rel);
|
|
291
|
-
if (filePath !== viewerRoot && !filePath.startsWith(`${viewerRoot}/`)) {
|
|
292
|
-
return new Response("forbidden", { status: 403 });
|
|
293
|
-
}
|
|
294
|
-
const file = Bun.file(filePath);
|
|
295
|
-
if (await file.exists()) {
|
|
296
|
-
return new Response(file, { headers: { "cache-control": "no-store" } });
|
|
297
|
-
}
|
|
298
|
-
return new Response("not found", { status: 404 });
|
|
299
|
-
};
|
|
300
|
-
}, startDiffServer = (opts) => {
|
|
301
|
-
const env = opts.env ?? process.env;
|
|
302
|
-
const token = ensureToken(env);
|
|
303
|
-
let lastActivity = Date.now();
|
|
304
|
-
const handler = createHandler({ viewerDir: opts.viewerDir, token });
|
|
305
|
-
const server = Bun.serve({
|
|
306
|
-
hostname: "127.0.0.1",
|
|
307
|
-
port: opts.port,
|
|
308
|
-
fetch: (req) => {
|
|
309
|
-
lastActivity = Date.now();
|
|
310
|
-
return handler(req);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
let idleTimer;
|
|
314
|
-
const idleTimeoutMs = opts.idleTimeoutMs;
|
|
315
|
-
if (idleTimeoutMs && idleTimeoutMs > 0) {
|
|
316
|
-
idleTimer = setInterval(() => {
|
|
317
|
-
if (Date.now() - lastActivity > idleTimeoutMs) {
|
|
318
|
-
stop();
|
|
319
|
-
process.exit(0);
|
|
320
|
-
}
|
|
321
|
-
}, 60000);
|
|
322
|
-
idleTimer.unref?.();
|
|
323
|
-
}
|
|
324
|
-
const stop = () => {
|
|
325
|
-
if (idleTimer)
|
|
326
|
-
clearInterval(idleTimer);
|
|
327
|
-
server.stop(true);
|
|
328
|
-
};
|
|
329
|
-
return { server, token, stop };
|
|
330
|
-
};
|
|
331
|
-
var init_server = __esm(() => {
|
|
332
|
-
init_diff();
|
|
333
|
-
init_imageTypes();
|
|
334
|
-
init_token();
|
|
335
|
-
baseCache = new Map;
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// src/index.ts
|
|
339
|
-
init_config();
|
|
340
|
-
import { join as join6 } from "path";
|
|
341
42
|
|
|
342
43
|
// src/diff-server/ensure.ts
|
|
343
|
-
init_config();
|
|
344
|
-
init_token();
|
|
345
|
-
import { mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
346
|
-
import { join as join3 } from "path";
|
|
347
44
|
var ENSURE_TTL_MS = 5000;
|
|
348
45
|
var LOCK_STALE_MS = 30000;
|
|
46
|
+
var PORT_FREE_TRIES = 20;
|
|
47
|
+
var PORT_FREE_INTERVAL_MS = 100;
|
|
349
48
|
var checkedAt = 0;
|
|
350
|
-
var
|
|
49
|
+
var probeServer = async (port) => {
|
|
351
50
|
try {
|
|
352
51
|
const res = await fetch(`http://127.0.0.1:${port}/api/ping`, {
|
|
353
52
|
signal: AbortSignal.timeout(150)
|
|
354
53
|
});
|
|
355
|
-
|
|
54
|
+
const ours = res.headers.get("x-diffdeck") === "1" || res.headers.get("x-cc-statusline") === "1";
|
|
55
|
+
if (!ours)
|
|
56
|
+
return null;
|
|
57
|
+
const version = res.headers.get("x-diffdeck-version");
|
|
58
|
+
const pidRaw = res.headers.get("x-diffdeck-pid");
|
|
59
|
+
const pid = pidRaw == null ? null : Number.parseInt(pidRaw, 10);
|
|
60
|
+
return { version, pid: Number.isInteger(pid) ? pid : null };
|
|
356
61
|
} catch {
|
|
357
|
-
return
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var lsofPortOwner = (port) => {
|
|
66
|
+
try {
|
|
67
|
+
const out = Bun.spawnSync(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
68
|
+
if (out.exitCode !== 0)
|
|
69
|
+
return null;
|
|
70
|
+
const first = out.stdout.toString().trim().split(`
|
|
71
|
+
`)[0] ?? "";
|
|
72
|
+
const pid = Number.parseInt(first, 10);
|
|
73
|
+
return Number.isInteger(pid) ? pid : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
358
76
|
}
|
|
359
77
|
};
|
|
360
78
|
var acquireSpawnLock = (env) => {
|
|
361
79
|
const lock = join3(getCacheDir(env), "diff-server.lock");
|
|
362
|
-
|
|
80
|
+
mkdirSync(getCacheDir(env), { recursive: true, mode: 448 });
|
|
363
81
|
try {
|
|
364
|
-
|
|
82
|
+
mkdirSync(lock);
|
|
365
83
|
return true;
|
|
366
84
|
} catch {
|
|
367
85
|
try {
|
|
368
86
|
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
369
87
|
rmSync(lock, { recursive: true, force: true });
|
|
370
|
-
|
|
88
|
+
mkdirSync(lock);
|
|
371
89
|
return true;
|
|
372
90
|
}
|
|
373
91
|
} catch {}
|
|
374
92
|
return false;
|
|
375
93
|
}
|
|
376
94
|
};
|
|
95
|
+
var resolveDiffdeck = () => {
|
|
96
|
+
const pkgDir = dirname(fileURLToPath(import.meta.resolve("@say8425/diffdeck/package.json")));
|
|
97
|
+
const pkg = JSON.parse(readFileSync2(join3(pkgDir, "package.json"), "utf8"));
|
|
98
|
+
return { cli: join3(pkgDir, pkg.bin.diffdeck), version: pkg.version };
|
|
99
|
+
};
|
|
377
100
|
var spawnDaemon = (port, env) => {
|
|
378
|
-
const
|
|
379
|
-
const selfPath = Bun.main;
|
|
101
|
+
const { cli } = resolveDiffdeck();
|
|
380
102
|
Bun.spawn([
|
|
381
103
|
"sh",
|
|
382
104
|
"-c",
|
|
383
|
-
'nohup "$0" "$1" --
|
|
384
|
-
execPath,
|
|
385
|
-
|
|
105
|
+
'nohup "$0" "$1" --no-open --port "$2" >/dev/null 2>&1 &',
|
|
106
|
+
process.execPath,
|
|
107
|
+
cli,
|
|
108
|
+
String(port)
|
|
386
109
|
], {
|
|
387
|
-
env: { ...process.env, ...env,
|
|
110
|
+
env: { ...process.env, ...env, DIFFDECK_PORT: String(port) },
|
|
388
111
|
stdin: "ignore",
|
|
389
112
|
stdout: "ignore",
|
|
390
113
|
stderr: "ignore"
|
|
391
114
|
}).unref();
|
|
392
115
|
};
|
|
393
|
-
var
|
|
116
|
+
var realDeps = {
|
|
117
|
+
spawn: spawnDaemon,
|
|
118
|
+
probe: probeServer,
|
|
119
|
+
portOwner: lsofPortOwner,
|
|
120
|
+
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
121
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
122
|
+
currentVersion: () => {
|
|
123
|
+
try {
|
|
124
|
+
return resolveDiffdeck().version;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var waitPortFree = async (port, d) => {
|
|
131
|
+
for (let i = 0;i < PORT_FREE_TRIES; i++) {
|
|
132
|
+
await d.sleep(PORT_FREE_INTERVAL_MS);
|
|
133
|
+
if (await d.probe(port) == null)
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
var maybeSpawn = async (port, env, deps = {}) => {
|
|
139
|
+
const d = { ...realDeps, ...deps };
|
|
394
140
|
try {
|
|
395
|
-
|
|
141
|
+
const found = await d.probe(port);
|
|
142
|
+
const current = d.currentVersion();
|
|
143
|
+
if (found && (current == null || found.version === current))
|
|
396
144
|
return;
|
|
397
145
|
if (!acquireSpawnLock(env))
|
|
398
146
|
return;
|
|
399
|
-
|
|
147
|
+
if (found) {
|
|
148
|
+
const owner = d.portOwner(port);
|
|
149
|
+
if (owner == null)
|
|
150
|
+
return;
|
|
151
|
+
if (found.pid != null && found.pid !== owner)
|
|
152
|
+
return;
|
|
153
|
+
d.kill(owner);
|
|
154
|
+
if (!await waitPortFree(port, d))
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
d.spawn(port, env);
|
|
400
158
|
} catch {}
|
|
401
159
|
};
|
|
402
|
-
var ensureDiffServer = (_repo, env = process.env,
|
|
160
|
+
var ensureDiffServer = (_repo, env = process.env, deps = {}) => {
|
|
403
161
|
if (isDiffViewerDisabled(env))
|
|
404
162
|
return null;
|
|
405
163
|
const port = resolveDiffPort(env);
|
|
406
164
|
const now = Date.now();
|
|
407
165
|
if (now - checkedAt >= ENSURE_TTL_MS) {
|
|
408
166
|
checkedAt = now;
|
|
409
|
-
maybeSpawn(port, env,
|
|
167
|
+
maybeSpawn(port, env, deps);
|
|
410
168
|
}
|
|
411
169
|
const token = readTokenSync(env);
|
|
412
170
|
return token ? { port, token } : null;
|
|
@@ -447,8 +205,41 @@ var CACHE_TTL = {
|
|
|
447
205
|
ultracode: 5000
|
|
448
206
|
};
|
|
449
207
|
|
|
450
|
-
// src/git/
|
|
451
|
-
|
|
208
|
+
// src/git/baseRef.ts
|
|
209
|
+
var {$ } = globalThis.Bun;
|
|
210
|
+
var refExists = async (repo, ref) => {
|
|
211
|
+
const r = await $`git -C ${repo} rev-parse --verify --quiet ${ref}`.nothrow().quiet();
|
|
212
|
+
return r.exitCode === 0;
|
|
213
|
+
};
|
|
214
|
+
var prBaseName = async (repo) => {
|
|
215
|
+
try {
|
|
216
|
+
const out = await $`gh pr view --json baseRefName -q .baseRefName`.cwd(repo).nothrow().quiet().text();
|
|
217
|
+
return out.trim() || null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var defaultBranchName = async (repo) => {
|
|
223
|
+
const r = await $`git -C ${repo} rev-parse --abbrev-ref origin/HEAD 2>/dev/null`.nothrow().quiet();
|
|
224
|
+
if (r.exitCode !== 0)
|
|
225
|
+
return null;
|
|
226
|
+
const t = r.text().trim();
|
|
227
|
+
if (!t.startsWith("origin/"))
|
|
228
|
+
return null;
|
|
229
|
+
const name = t.slice("origin/".length);
|
|
230
|
+
return name && name !== "HEAD" ? name : null;
|
|
231
|
+
};
|
|
232
|
+
var resolveBaseRef = async (repo) => {
|
|
233
|
+
const named = await prBaseName(repo) ?? await defaultBranchName(repo);
|
|
234
|
+
const candidates = named ? [`origin/${named}`, named] : ["origin/main", "origin/master", "main", "master"];
|
|
235
|
+
for (const ref of candidates) {
|
|
236
|
+
if (await refExists(repo, ref)) {
|
|
237
|
+
const base = ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
|
|
238
|
+
return { base, ref };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { base: named, ref: null };
|
|
242
|
+
};
|
|
452
243
|
|
|
453
244
|
// src/git/shortstat.ts
|
|
454
245
|
var parseShortstat = (text) => {
|
|
@@ -681,14 +472,14 @@ var readStdin = async () => {
|
|
|
681
472
|
// src/ultracode.ts
|
|
682
473
|
import { readFile } from "fs/promises";
|
|
683
474
|
import { homedir as homedir2 } from "os";
|
|
684
|
-
import { join as
|
|
685
|
-
var MANAGED_SETTINGS_PATH = process.platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : process.platform === "win32" ?
|
|
475
|
+
import { join as join4 } from "path";
|
|
476
|
+
var MANAGED_SETTINGS_PATH = process.platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : process.platform === "win32" ? join4(process.env.ProgramData || "C:\\ProgramData", "ClaudeCode", "managed-settings.json") : "/etc/claude-code/managed-settings.json";
|
|
686
477
|
var ultracodeSettingsPaths = (projectDir, homeDir) => {
|
|
687
478
|
const paths = [MANAGED_SETTINGS_PATH];
|
|
688
479
|
if (projectDir) {
|
|
689
|
-
paths.push(
|
|
480
|
+
paths.push(join4(projectDir, ".claude", "settings.local.json"), join4(projectDir, ".claude", "settings.json"));
|
|
690
481
|
}
|
|
691
|
-
paths.push(
|
|
482
|
+
paths.push(join4(homeDir, ".claude", "settings.json"));
|
|
692
483
|
return paths;
|
|
693
484
|
};
|
|
694
485
|
var resolveUltracodeFromFiles = async (paths) => {
|
|
@@ -769,15 +560,7 @@ var main = async () => {
|
|
|
769
560
|
}
|
|
770
561
|
};
|
|
771
562
|
if (import.meta.main) {
|
|
772
|
-
|
|
773
|
-
const { startDiffServer: startDiffServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
774
|
-
startDiffServer2({
|
|
775
|
-
port: resolveDiffPort(),
|
|
776
|
-
viewerDir: join6(import.meta.dir, "viewer")
|
|
777
|
-
});
|
|
778
|
-
} else {
|
|
779
|
-
main().catch(console.error);
|
|
780
|
-
}
|
|
563
|
+
main().catch(console.error);
|
|
781
564
|
}
|
|
782
565
|
export {
|
|
783
566
|
main
|
package/package.json
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@say8425/cc-statusline",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Custom statusline for Claude Code",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
7
|
"url": "https://github.com/say8425/cc-statusline.git"
|
|
7
8
|
},
|
|
8
|
-
"devDependencies": {
|
|
9
|
-
"@pierre/diffs": "^1.2.12",
|
|
10
|
-
"@pierre/trees": "^1.0.0-beta.5",
|
|
11
|
-
"@types/bun": "^1.3.14",
|
|
12
|
-
"oxfmt": "^0.57.0",
|
|
13
|
-
"oxlint": "^1.72.0",
|
|
14
|
-
"oxlint-tsgolint": "^0.24.0",
|
|
15
|
-
"typescript": "^6.0.3"
|
|
16
|
-
},
|
|
17
9
|
"bin": {
|
|
18
10
|
"cc-statusline": "dist/index.js"
|
|
19
11
|
},
|
|
20
|
-
"description": "Custom statusline for Claude Code",
|
|
21
12
|
"files": [
|
|
22
|
-
"dist/index.js"
|
|
23
|
-
"dist/viewer"
|
|
13
|
+
"dist/index.js"
|
|
24
14
|
],
|
|
15
|
+
"type": "module",
|
|
25
16
|
"publishConfig": {
|
|
26
17
|
"registry": "https://registry.npmjs.org"
|
|
27
18
|
},
|
|
@@ -35,5 +26,14 @@
|
|
|
35
26
|
"format:check": "oxfmt --check src/ build.ts",
|
|
36
27
|
"typecheck": "tsc --noEmit"
|
|
37
28
|
},
|
|
38
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@say8425/diffdeck": "^0.2.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "^1.3.14",
|
|
34
|
+
"oxfmt": "^0.57.0",
|
|
35
|
+
"oxlint": "^1.72.0",
|
|
36
|
+
"oxlint-tsgolint": "^0.24.0",
|
|
37
|
+
"typescript": "^6.0.3"
|
|
38
|
+
}
|
|
39
39
|
}
|