@say8425/cc-statusline 3.0.0 → 3.2.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 +44 -2
- package/dist/index.js +596 -48
- package/dist/viewer/index.html +490 -0
- package/dist/viewer/main.js +2842 -0
- package/package.json +13 -6
package/dist/index.js
CHANGED
|
@@ -1,82 +1,542 @@
|
|
|
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
|
+
var DEFAULT_DIFF_PORT = 49573, 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
|
+
}, isDiffViewerDisabled = (env = process.env) => env.CC_STATUSLINE_DIFF_DISABLE === "1", getCacheDir = (env = process.env) => {
|
|
29
|
+
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
30
|
+
return join(base, "cc-statusline");
|
|
31
|
+
};
|
|
32
|
+
var init_config = () => {};
|
|
33
|
+
|
|
34
|
+
// src/diff-server/token.ts
|
|
35
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
36
|
+
import { join as join2 } from "path";
|
|
37
|
+
var getTokenPath = (env = process.env) => join2(getCacheDir(env), "diff-server.token"), generateToken = () => crypto.randomUUID().replaceAll("-", ""), readTokenSync = (env = process.env) => {
|
|
38
|
+
try {
|
|
39
|
+
const value = readFileSync(getTokenPath(env), "utf8").trim();
|
|
40
|
+
return value || null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
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";
|
|
5
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
|
+
};
|
|
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
|
+
|
|
342
|
+
// 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
|
+
var ENSURE_TTL_MS = 5000;
|
|
348
|
+
var LOCK_STALE_MS = 30000;
|
|
349
|
+
var checkedAt = 0;
|
|
350
|
+
var probeOurServer = async (port) => {
|
|
351
|
+
try {
|
|
352
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/ping`, {
|
|
353
|
+
signal: AbortSignal.timeout(150)
|
|
354
|
+
});
|
|
355
|
+
return res.headers.get("x-cc-statusline") === "1";
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var acquireSpawnLock = (env) => {
|
|
361
|
+
const lock = join3(getCacheDir(env), "diff-server.lock");
|
|
362
|
+
mkdirSync2(getCacheDir(env), { recursive: true, mode: 448 });
|
|
363
|
+
try {
|
|
364
|
+
mkdirSync2(lock);
|
|
365
|
+
return true;
|
|
366
|
+
} catch {
|
|
367
|
+
try {
|
|
368
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
369
|
+
rmSync(lock, { recursive: true, force: true });
|
|
370
|
+
mkdirSync2(lock);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
} catch {}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var spawnDaemon = (port, env) => {
|
|
378
|
+
const execPath = process.execPath;
|
|
379
|
+
const selfPath = Bun.main;
|
|
380
|
+
Bun.spawn([
|
|
381
|
+
"sh",
|
|
382
|
+
"-c",
|
|
383
|
+
'nohup "$0" "$1" --diff-server >/dev/null 2>&1 &',
|
|
384
|
+
execPath,
|
|
385
|
+
selfPath
|
|
386
|
+
], {
|
|
387
|
+
env: { ...process.env, ...env, CC_STATUSLINE_DIFF_PORT: String(port) },
|
|
388
|
+
stdin: "ignore",
|
|
389
|
+
stdout: "ignore",
|
|
390
|
+
stderr: "ignore"
|
|
391
|
+
}).unref();
|
|
392
|
+
};
|
|
393
|
+
var maybeSpawn = async (port, env, spawn = spawnDaemon) => {
|
|
394
|
+
try {
|
|
395
|
+
if (await probeOurServer(port))
|
|
396
|
+
return;
|
|
397
|
+
if (!acquireSpawnLock(env))
|
|
398
|
+
return;
|
|
399
|
+
spawn(port, env);
|
|
400
|
+
} catch {}
|
|
401
|
+
};
|
|
402
|
+
var ensureDiffServer = (_repo, env = process.env, spawn = spawnDaemon) => {
|
|
403
|
+
if (isDiffViewerDisabled(env))
|
|
404
|
+
return null;
|
|
405
|
+
const port = resolveDiffPort(env);
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
if (now - checkedAt >= ENSURE_TTL_MS) {
|
|
408
|
+
checkedAt = now;
|
|
409
|
+
maybeSpawn(port, env, spawn);
|
|
410
|
+
}
|
|
411
|
+
const token = readTokenSync(env);
|
|
412
|
+
return token ? { port, token } : null;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/diff-server/link.ts
|
|
416
|
+
var buildDiffViewerUrl = (params) => {
|
|
417
|
+
const query = new URLSearchParams({
|
|
418
|
+
repo: params.repo,
|
|
419
|
+
token: params.token
|
|
420
|
+
});
|
|
421
|
+
if (params.mode)
|
|
422
|
+
query.set("mode", params.mode);
|
|
423
|
+
return `http://127.0.0.1:${params.port}/?${query.toString()}`;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/git/baseChanges.ts
|
|
427
|
+
var {$: $2 } = globalThis.Bun;
|
|
6
428
|
|
|
7
429
|
// src/cache.ts
|
|
8
430
|
var cache = {
|
|
9
431
|
branch: { value: "", timestamp: 0 },
|
|
10
432
|
gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
|
|
11
433
|
prUrl: { value: null, timestamp: 0 },
|
|
12
|
-
mainProjectName: { value: null, timestamp: 0 }
|
|
434
|
+
mainProjectName: { value: null, timestamp: 0 },
|
|
435
|
+
baseRef: {
|
|
436
|
+
value: null,
|
|
437
|
+
timestamp: 0
|
|
438
|
+
},
|
|
439
|
+
ultracode: { value: false, timestamp: 0 }
|
|
13
440
|
};
|
|
14
441
|
var CACHE_TTL = {
|
|
15
442
|
branch: 5000,
|
|
16
443
|
gitChanges: 0,
|
|
17
444
|
prUrl: 30000,
|
|
18
|
-
mainProjectName: 300000
|
|
445
|
+
mainProjectName: 300000,
|
|
446
|
+
baseRef: 30000,
|
|
447
|
+
ultracode: 5000
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/git/baseChanges.ts
|
|
451
|
+
init_diff();
|
|
452
|
+
|
|
453
|
+
// src/git/shortstat.ts
|
|
454
|
+
var parseShortstat = (text) => {
|
|
455
|
+
const [files, insertions, deletions] = [
|
|
456
|
+
/(\d+) files?/g,
|
|
457
|
+
/(\d+) insertions?/g,
|
|
458
|
+
/(\d+) deletions?/g
|
|
459
|
+
].map((regex) => (text.match(regex) || []).reduce((sum, m) => sum + Number.parseInt(m, 10), 0));
|
|
460
|
+
return { files, insertions, deletions };
|
|
19
461
|
};
|
|
20
462
|
|
|
463
|
+
// src/git/baseChanges.ts
|
|
464
|
+
var getBaseChangesCached = async () => {
|
|
465
|
+
try {
|
|
466
|
+
let resolved = cache.baseRef.value;
|
|
467
|
+
if (resolved === null || Date.now() - cache.baseRef.timestamp >= CACHE_TTL.baseRef) {
|
|
468
|
+
resolved = await resolveBaseRef(".");
|
|
469
|
+
cache.baseRef = { value: resolved, timestamp: Date.now() };
|
|
470
|
+
}
|
|
471
|
+
if (!resolved.ref || !resolved.base)
|
|
472
|
+
return null;
|
|
473
|
+
const mb = (await $2`git merge-base ${resolved.ref} HEAD 2>/dev/null`.nothrow().text()).trim();
|
|
474
|
+
if (!mb)
|
|
475
|
+
return null;
|
|
476
|
+
const out = await $2`git diff ${mb} --shortstat 2>/dev/null`.nothrow().text();
|
|
477
|
+
const { files, insertions, deletions } = parseShortstat(out);
|
|
478
|
+
if (files === 0 && insertions === 0 && deletions === 0)
|
|
479
|
+
return null;
|
|
480
|
+
return { base: resolved.base, files, insertions, deletions };
|
|
481
|
+
} catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
21
485
|
// src/git/branch.ts
|
|
22
|
-
|
|
486
|
+
var {$: $3 } = globalThis.Bun;
|
|
487
|
+
var getBranchCached = async () => {
|
|
23
488
|
if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
|
|
24
489
|
return cache.branch.value;
|
|
25
490
|
}
|
|
26
491
|
try {
|
|
27
|
-
const result = await
|
|
492
|
+
const result = await $3`git branch --show-current 2>/dev/null`.text();
|
|
28
493
|
cache.branch = { value: result.trim(), timestamp: Date.now() };
|
|
29
494
|
return cache.branch.value;
|
|
30
495
|
} catch {
|
|
31
496
|
return cache.branch.value;
|
|
32
497
|
}
|
|
33
|
-
}
|
|
498
|
+
};
|
|
34
499
|
// src/git/changes.ts
|
|
35
|
-
var {$: $
|
|
36
|
-
async
|
|
500
|
+
var {$: $4 } = globalThis.Bun;
|
|
501
|
+
var getGitChangesCached = async () => {
|
|
37
502
|
if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
|
|
38
503
|
return cache.gitChanges;
|
|
39
504
|
}
|
|
40
505
|
try {
|
|
41
506
|
const [diff, staged] = await Promise.all([
|
|
42
|
-
$
|
|
43
|
-
$
|
|
507
|
+
$4`git diff --shortstat 2>/dev/null`.text(),
|
|
508
|
+
$4`git diff --cached --shortstat 2>/dev/null`.text()
|
|
44
509
|
]);
|
|
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));
|
|
510
|
+
const { files, insertions, deletions } = parseShortstat(`${diff}
|
|
511
|
+
${staged}`);
|
|
52
512
|
cache.gitChanges = { files, insertions, deletions, timestamp: Date.now() };
|
|
53
513
|
return cache.gitChanges;
|
|
54
514
|
} catch {
|
|
55
515
|
return cache.gitChanges;
|
|
56
516
|
}
|
|
57
|
-
}
|
|
517
|
+
};
|
|
58
518
|
// src/git/pr.ts
|
|
59
|
-
var {$: $
|
|
60
|
-
async
|
|
519
|
+
var {$: $5 } = globalThis.Bun;
|
|
520
|
+
var getPrUrlCached = async () => {
|
|
61
521
|
if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
|
|
62
522
|
return cache.prUrl.value;
|
|
63
523
|
}
|
|
64
524
|
try {
|
|
65
|
-
const result = await $
|
|
525
|
+
const result = await $5`gh pr view --json url -q .url 2>/dev/null`.text();
|
|
66
526
|
cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
|
|
67
527
|
return cache.prUrl.value;
|
|
68
528
|
} catch {
|
|
69
529
|
return cache.prUrl.value;
|
|
70
530
|
}
|
|
71
|
-
}
|
|
531
|
+
};
|
|
72
532
|
// src/git/worktree.ts
|
|
73
|
-
var {$: $
|
|
74
|
-
async
|
|
533
|
+
var {$: $6 } = globalThis.Bun;
|
|
534
|
+
var getMainProjectNameCached = async () => {
|
|
75
535
|
if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
|
|
76
536
|
return cache.mainProjectName.value;
|
|
77
537
|
}
|
|
78
538
|
try {
|
|
79
|
-
const result = await $
|
|
539
|
+
const result = await $6`git rev-parse --git-common-dir 2>/dev/null`.text();
|
|
80
540
|
const gitCommonDir = result.trim();
|
|
81
541
|
if (gitCommonDir === ".git") {
|
|
82
542
|
cache.mainProjectName = { value: null, timestamp: Date.now() };
|
|
@@ -89,7 +549,7 @@ async function getMainProjectNameCached() {
|
|
|
89
549
|
} catch {
|
|
90
550
|
return cache.mainProjectName.value;
|
|
91
551
|
}
|
|
92
|
-
}
|
|
552
|
+
};
|
|
93
553
|
// src/colors.ts
|
|
94
554
|
var C = {
|
|
95
555
|
RESET: "\x1B[0m",
|
|
@@ -102,32 +562,28 @@ var C = {
|
|
|
102
562
|
WHITE: "\x1B[38;2;200;200;200m",
|
|
103
563
|
UNDERLINE: "\x1B[4m"
|
|
104
564
|
};
|
|
105
|
-
|
|
565
|
+
var getUsageColor = (pct) => {
|
|
106
566
|
if (pct < 50)
|
|
107
567
|
return C.WHITE;
|
|
108
568
|
if (pct < 80)
|
|
109
569
|
return C.YELLOW;
|
|
110
570
|
return C.RED;
|
|
111
|
-
}
|
|
571
|
+
};
|
|
112
572
|
|
|
113
573
|
// src/format/formatNumber.ts
|
|
114
|
-
|
|
115
|
-
return n.toLocaleString("en-US");
|
|
116
|
-
}
|
|
574
|
+
var formatNumber = (n) => n.toLocaleString("en-US");
|
|
117
575
|
// src/format/formatResetDate.ts
|
|
118
|
-
|
|
576
|
+
var formatResetDate = (date) => {
|
|
119
577
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
120
578
|
const day = date.getDate().toString().padStart(2, "0");
|
|
121
579
|
const hours = date.getHours().toString().padStart(2, "0");
|
|
122
580
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
123
581
|
return `${month}/${day} ${hours}:${minutes}`;
|
|
124
|
-
}
|
|
582
|
+
};
|
|
125
583
|
// src/format/formatTime.ts
|
|
126
|
-
|
|
127
|
-
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
128
|
-
}
|
|
584
|
+
var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
129
585
|
// src/render.ts
|
|
130
|
-
|
|
586
|
+
var renderStatusLine = (ctx) => {
|
|
131
587
|
const lines = [];
|
|
132
588
|
const folder = ctx.claudeJson.workspace?.project_dir?.split("/").pop() || "";
|
|
133
589
|
const sessionMs = ctx.claudeJson.cost?.total_duration_ms || 0;
|
|
@@ -154,6 +610,14 @@ function renderStatusLine(ctx) {
|
|
|
154
610
|
const ctxColor = getUsageColor(contextPct);
|
|
155
611
|
line2 += ` | ${ctxColor}\uD83E\uDDE0 ${formatNumber(totalTokens)} (${contextPct}%)${C.RESET}`;
|
|
156
612
|
}
|
|
613
|
+
const modelName = ctx.claudeJson.model?.display_name;
|
|
614
|
+
if (modelName) {
|
|
615
|
+
const effortLevel = ctx.claudeJson.effort?.level;
|
|
616
|
+
let modelText = effortLevel ? `${modelName} ${effortLevel}` : modelName;
|
|
617
|
+
if (ctx.ultracode && effortLevel === "xhigh")
|
|
618
|
+
modelText += " \u26A1ultra";
|
|
619
|
+
line2 += ` | ${C.WHITE}\uD83E\uDD16 ${modelText}${C.RESET}`;
|
|
620
|
+
}
|
|
157
621
|
lines.push(line2);
|
|
158
622
|
if (ctx.rateLimits) {
|
|
159
623
|
const parts = [];
|
|
@@ -182,10 +646,17 @@ function renderStatusLine(ctx) {
|
|
|
182
646
|
}
|
|
183
647
|
}
|
|
184
648
|
const hasGitChanges = ctx.gitChanges.files > 0 || ctx.gitChanges.insertions > 0 || ctx.gitChanges.deletions > 0;
|
|
185
|
-
if (hasGitChanges || ctx.prUrl) {
|
|
649
|
+
if (hasGitChanges || ctx.baseChanges || ctx.prUrl) {
|
|
186
650
|
let line4 = "";
|
|
187
651
|
if (hasGitChanges) {
|
|
188
|
-
|
|
652
|
+
const u = ctx.diffViewerUrl ? C.UNDERLINE : "";
|
|
653
|
+
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}`;
|
|
654
|
+
line4 += ctx.diffViewerUrl ? `\x1B]8;;${ctx.diffViewerUrl}\x07${changesText}\x1B]8;;\x07` : changesText;
|
|
655
|
+
} else if (ctx.baseChanges) {
|
|
656
|
+
const bc = ctx.baseChanges;
|
|
657
|
+
const u = ctx.baseDiffViewerUrl ? C.UNDERLINE : "";
|
|
658
|
+
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}`;
|
|
659
|
+
line4 += ctx.baseDiffViewerUrl ? `\x1B]8;;${ctx.baseDiffViewerUrl}\x07${baseText}\x1B]8;;\x07` : baseText;
|
|
189
660
|
}
|
|
190
661
|
if (ctx.prUrl) {
|
|
191
662
|
const prLabel = ctx.prUrl.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
|
|
@@ -196,40 +667,117 @@ function renderStatusLine(ctx) {
|
|
|
196
667
|
lines.push(line4);
|
|
197
668
|
}
|
|
198
669
|
return lines;
|
|
199
|
-
}
|
|
670
|
+
};
|
|
200
671
|
|
|
201
672
|
// src/stdin.ts
|
|
202
|
-
async
|
|
673
|
+
var readStdin = async () => {
|
|
203
674
|
const chunks = [];
|
|
204
675
|
for await (const chunk of Bun.stdin.stream()) {
|
|
205
676
|
chunks.push(chunk);
|
|
206
677
|
}
|
|
207
678
|
return Buffer.concat(chunks).toString("utf-8");
|
|
208
|
-
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/ultracode.ts
|
|
682
|
+
import { readFile } from "fs/promises";
|
|
683
|
+
import { homedir as homedir2 } from "os";
|
|
684
|
+
import { join as join5 } from "path";
|
|
685
|
+
var MANAGED_SETTINGS_PATH = process.platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : process.platform === "win32" ? join5(process.env.ProgramData || "C:\\ProgramData", "ClaudeCode", "managed-settings.json") : "/etc/claude-code/managed-settings.json";
|
|
686
|
+
var ultracodeSettingsPaths = (projectDir, homeDir) => {
|
|
687
|
+
const paths = [MANAGED_SETTINGS_PATH];
|
|
688
|
+
if (projectDir) {
|
|
689
|
+
paths.push(join5(projectDir, ".claude", "settings.local.json"), join5(projectDir, ".claude", "settings.json"));
|
|
690
|
+
}
|
|
691
|
+
paths.push(join5(homeDir, ".claude", "settings.json"));
|
|
692
|
+
return paths;
|
|
693
|
+
};
|
|
694
|
+
var resolveUltracodeFromFiles = async (paths) => {
|
|
695
|
+
const contents = await Promise.all(paths.map((path) => readFile(path, "utf8").catch(() => null)));
|
|
696
|
+
for (const content of contents) {
|
|
697
|
+
if (content === null)
|
|
698
|
+
continue;
|
|
699
|
+
try {
|
|
700
|
+
const parsed = JSON.parse(content);
|
|
701
|
+
if (typeof parsed === "object" && parsed !== null && "ultracode" in parsed && typeof parsed.ultracode === "boolean") {
|
|
702
|
+
return parsed.ultracode;
|
|
703
|
+
}
|
|
704
|
+
} catch {}
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
};
|
|
708
|
+
var getUltracodeCached = async (projectDir, homeDir = homedir2()) => {
|
|
709
|
+
if (Date.now() - cache.ultracode.timestamp < CACHE_TTL.ultracode) {
|
|
710
|
+
return cache.ultracode.value;
|
|
711
|
+
}
|
|
712
|
+
const value = await resolveUltracodeFromFiles(ultracodeSettingsPaths(projectDir, homeDir));
|
|
713
|
+
cache.ultracode = { value, timestamp: Date.now() };
|
|
714
|
+
return value;
|
|
715
|
+
};
|
|
209
716
|
|
|
210
717
|
// src/index.ts
|
|
211
|
-
async
|
|
718
|
+
var main = async () => {
|
|
212
719
|
const claudeJson = JSON.parse(await readStdin() || "{}");
|
|
213
|
-
const
|
|
720
|
+
const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
|
|
721
|
+
const [branch, gitChanges, prUrl, mainProjectName, ultracode] = await Promise.all([
|
|
214
722
|
getBranchCached(),
|
|
215
723
|
getGitChangesCached(),
|
|
216
724
|
getPrUrlCached(),
|
|
217
|
-
getMainProjectNameCached()
|
|
725
|
+
getMainProjectNameCached(),
|
|
726
|
+
getUltracodeCached(repo)
|
|
218
727
|
]);
|
|
728
|
+
const hasChanges = gitChanges.files > 0 || gitChanges.insertions > 0 || gitChanges.deletions > 0;
|
|
729
|
+
let diffViewerUrl = null;
|
|
730
|
+
let baseDiffViewerUrl = null;
|
|
731
|
+
let baseChanges = null;
|
|
732
|
+
if (!hasChanges && repo) {
|
|
733
|
+
baseChanges = await getBaseChangesCached();
|
|
734
|
+
}
|
|
735
|
+
if (repo && (hasChanges || baseChanges)) {
|
|
736
|
+
const ensured = ensureDiffServer(repo);
|
|
737
|
+
if (ensured) {
|
|
738
|
+
if (hasChanges) {
|
|
739
|
+
diffViewerUrl = buildDiffViewerUrl({
|
|
740
|
+
port: ensured.port,
|
|
741
|
+
repo,
|
|
742
|
+
token: ensured.token,
|
|
743
|
+
mode: "working"
|
|
744
|
+
});
|
|
745
|
+
} else if (baseChanges) {
|
|
746
|
+
baseDiffViewerUrl = buildDiffViewerUrl({
|
|
747
|
+
port: ensured.port,
|
|
748
|
+
repo,
|
|
749
|
+
token: ensured.token,
|
|
750
|
+
mode: "base"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
219
755
|
const lines = renderStatusLine({
|
|
220
756
|
claudeJson,
|
|
221
757
|
branch,
|
|
222
758
|
gitChanges,
|
|
223
759
|
prUrl,
|
|
760
|
+
ultracode,
|
|
224
761
|
rateLimits: claudeJson.rate_limits ?? null,
|
|
225
|
-
mainProjectName
|
|
762
|
+
mainProjectName,
|
|
763
|
+
diffViewerUrl,
|
|
764
|
+
baseChanges,
|
|
765
|
+
baseDiffViewerUrl
|
|
226
766
|
});
|
|
227
767
|
for (const line of lines) {
|
|
228
768
|
console.log(line);
|
|
229
769
|
}
|
|
230
|
-
}
|
|
770
|
+
};
|
|
231
771
|
if (import.meta.main) {
|
|
232
|
-
|
|
772
|
+
if (process.argv.includes("--diff-server")) {
|
|
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
|
+
}
|
|
233
781
|
}
|
|
234
782
|
export {
|
|
235
783
|
main
|