@say8425/cc-statusline 3.2.0 → 4.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/dist/index.js CHANGED
@@ -1,412 +1,170 @@
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
+
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, resolveDiffPort = (env = process.env) => {
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
- }, isDiffViewerDisabled = (env = process.env) => env.CC_STATUSLINE_DIFF_DISABLE === "1", getCacheDir = (env = process.env) => {
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 init_config = () => {};
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 { mkdirSync, readFileSync, writeFileSync } from "fs";
31
+ import { readFileSync } from "fs";
36
32
  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) => {
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
41
  };
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
- };
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 probeOurServer = async (port) => {
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
- return res.headers.get("x-cc-statusline") === "1";
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 false;
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
- mkdirSync2(getCacheDir(env), { recursive: true, mode: 448 });
80
+ mkdirSync(getCacheDir(env), { recursive: true, mode: 448 });
363
81
  try {
364
- mkdirSync2(lock);
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
- mkdirSync2(lock);
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 execPath = process.execPath;
379
- const selfPath = Bun.main;
101
+ const { cli } = resolveDiffdeck();
380
102
  Bun.spawn([
381
103
  "sh",
382
104
  "-c",
383
- 'nohup "$0" "$1" --diff-server >/dev/null 2>&1 &',
384
- execPath,
385
- selfPath
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, CC_STATUSLINE_DIFF_PORT: String(port) },
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 maybeSpawn = async (port, env, spawn = spawnDaemon) => {
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
- if (await probeOurServer(port))
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
- spawn(port, env);
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, spawn = spawnDaemon) => {
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, spawn);
167
+ maybeSpawn(port, env, deps);
410
168
  }
411
169
  const token = readTokenSync(env);
412
170
  return token ? { port, token } : null;
@@ -423,6 +181,96 @@ var buildDiffViewerUrl = (params) => {
423
181
  return `http://127.0.0.1:${params.port}/?${query.toString()}`;
424
182
  };
425
183
 
184
+ // src/format/formatNumber.ts
185
+ var formatNumber = (n) => n.toLocaleString("en-US");
186
+ // src/format/formatResetDate.ts
187
+ var formatResetDate = (date) => {
188
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
189
+ const day = date.getDate().toString().padStart(2, "0");
190
+ const hours = date.getHours().toString().padStart(2, "0");
191
+ const minutes = date.getMinutes().toString().padStart(2, "0");
192
+ return `${month}/${day} ${hours}:${minutes}`;
193
+ };
194
+ // src/format/formatTime.ts
195
+ var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
196
+ // src/colors.ts
197
+ var C = {
198
+ RESET: "\x1B[0m",
199
+ CYAN: "\x1B[38;2;0;255;255m",
200
+ MAGENTA: "\x1B[38;2;255;100;200m",
201
+ GREEN: "\x1B[38;2;100;255;100m",
202
+ YELLOW: "\x1B[38;2;255;220;100m",
203
+ RED: "\x1B[38;2;255;100;100m",
204
+ BLUE: "\x1B[38;2;100;150;255m",
205
+ WHITE: "\x1B[38;2;200;200;200m",
206
+ GREEN_MUTED: "\x1B[38;2;140;195;140m",
207
+ YELLOW_MUTED: "\x1B[38;2;210;185;130m",
208
+ RED_MUTED: "\x1B[38;2;210;130;130m",
209
+ MAGENTA_MUTED: "\x1B[38;2;195;140;175m",
210
+ UNDERLINE: "\x1B[4m"
211
+ };
212
+ var getUsageColor = (pct) => {
213
+ if (pct < 50)
214
+ return C.WHITE;
215
+ if (pct < 80)
216
+ return C.YELLOW;
217
+ return C.RED;
218
+ };
219
+
220
+ // src/format/prStatus.ts
221
+ var prStateText = (state, isDraft) => {
222
+ if (isDraft)
223
+ return "Draft";
224
+ if (state === "OPEN")
225
+ return "Open";
226
+ if (state === "MERGED")
227
+ return "Merged";
228
+ return "Closed";
229
+ };
230
+ var prStateColor = (state, isDraft) => {
231
+ if (isDraft)
232
+ return C.WHITE;
233
+ if (state === "OPEN")
234
+ return C.GREEN_MUTED;
235
+ if (state === "MERGED")
236
+ return C.MAGENTA_MUTED;
237
+ return C.RED_MUTED;
238
+ };
239
+ var ciSummaryText = (ci) => {
240
+ if (ci === null)
241
+ return "";
242
+ if (ci.conclusion === "failure")
243
+ return `${ci.count} failed`;
244
+ if (ci.conclusion === "pending")
245
+ return `${ci.count} running`;
246
+ return `${ci.count} passed`;
247
+ };
248
+ var ciSummaryColor = (ci) => {
249
+ if (ci === null)
250
+ return "";
251
+ if (ci.conclusion === "failure")
252
+ return C.RED_MUTED;
253
+ if (ci.conclusion === "pending")
254
+ return C.YELLOW_MUTED;
255
+ return C.GREEN_MUTED;
256
+ };
257
+ // src/format/toFileUrl.ts
258
+ var toFileUrl = (absolutePath) => {
259
+ const normalized = absolutePath.replace(/\\/g, "/");
260
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
261
+ try {
262
+ return `file://${encodeURI(withLeadingSlash)}`;
263
+ } catch {
264
+ const safe = Array.from(withLeadingSlash).map((ch) => {
265
+ try {
266
+ return encodeURI(ch);
267
+ } catch {
268
+ return "%EF%BF%BD";
269
+ }
270
+ }).join("");
271
+ return `file://${safe}`;
272
+ }
273
+ };
426
274
  // src/git/baseChanges.ts
427
275
  var {$: $2 } = globalThis.Bun;
428
276
 
@@ -430,8 +278,11 @@ var {$: $2 } = globalThis.Bun;
430
278
  var cache = {
431
279
  branch: { value: "", timestamp: 0 },
432
280
  gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
433
- prUrl: { value: null, timestamp: 0 },
434
- mainProjectName: { value: null, timestamp: 0 },
281
+ prInfo: { value: null, timestamp: 0 },
282
+ mainProject: {
283
+ value: null,
284
+ timestamp: 0
285
+ },
435
286
  baseRef: {
436
287
  value: null,
437
288
  timestamp: 0
@@ -441,14 +292,47 @@ var cache = {
441
292
  var CACHE_TTL = {
442
293
  branch: 5000,
443
294
  gitChanges: 0,
444
- prUrl: 30000,
445
- mainProjectName: 300000,
295
+ prInfo: 30000,
296
+ mainProject: 300000,
446
297
  baseRef: 30000,
447
298
  ultracode: 5000
448
299
  };
449
300
 
450
- // src/git/baseChanges.ts
451
- init_diff();
301
+ // src/git/baseRef.ts
302
+ var {$ } = globalThis.Bun;
303
+ var refExists = async (repo, ref) => {
304
+ const r = await $`git -C ${repo} rev-parse --verify --quiet ${ref}`.nothrow().quiet();
305
+ return r.exitCode === 0;
306
+ };
307
+ var prBaseName = async (repo) => {
308
+ try {
309
+ const out = await $`gh pr view --json baseRefName -q .baseRefName`.cwd(repo).nothrow().quiet().text();
310
+ return out.trim() || null;
311
+ } catch {
312
+ return null;
313
+ }
314
+ };
315
+ var defaultBranchName = async (repo) => {
316
+ const r = await $`git -C ${repo} rev-parse --abbrev-ref origin/HEAD 2>/dev/null`.nothrow().quiet();
317
+ if (r.exitCode !== 0)
318
+ return null;
319
+ const t = r.text().trim();
320
+ if (!t.startsWith("origin/"))
321
+ return null;
322
+ const name = t.slice("origin/".length);
323
+ return name && name !== "HEAD" ? name : null;
324
+ };
325
+ var resolveBaseRef = async (repo) => {
326
+ const named = await prBaseName(repo) ?? await defaultBranchName(repo);
327
+ const candidates = named ? [`origin/${named}`, named] : ["origin/main", "origin/master", "main", "master"];
328
+ for (const ref of candidates) {
329
+ if (await refExists(repo, ref)) {
330
+ const base = ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
331
+ return { base, ref };
332
+ }
333
+ }
334
+ return { base: named, ref: null };
335
+ };
452
336
 
453
337
  // src/git/shortstat.ts
454
338
  var parseShortstat = (text) => {
@@ -517,71 +401,91 @@ ${staged}`);
517
401
  };
518
402
  // src/git/pr.ts
519
403
  var {$: $5 } = globalThis.Bun;
520
- var getPrUrlCached = async () => {
521
- if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
522
- return cache.prUrl.value;
404
+
405
+ // src/git/ciStatus.ts
406
+ var FAILURE_CONCLUSIONS = new Set([
407
+ "FAILURE",
408
+ "TIMED_OUT",
409
+ "CANCELLED",
410
+ "ACTION_REQUIRED",
411
+ "STALE"
412
+ ]);
413
+ var PASS_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
414
+ var FAILURE_STATES = new Set(["FAILURE", "ERROR"]);
415
+ var aggregateCiStatus = (checks) => {
416
+ if (checks.length === 0)
417
+ return null;
418
+ let failedCount = 0;
419
+ let pendingCount = 0;
420
+ let passedCount = 0;
421
+ for (const check of checks) {
422
+ if (check.conclusion != null) {
423
+ if (FAILURE_CONCLUSIONS.has(check.conclusion))
424
+ failedCount++;
425
+ else if (PASS_CONCLUSIONS.has(check.conclusion))
426
+ passedCount++;
427
+ else
428
+ pendingCount++;
429
+ } else if (check.status != null && check.status !== "COMPLETED") {
430
+ pendingCount++;
431
+ } else if (check.state != null) {
432
+ if (FAILURE_STATES.has(check.state))
433
+ failedCount++;
434
+ else if (check.state === "SUCCESS")
435
+ passedCount++;
436
+ else
437
+ pendingCount++;
438
+ }
439
+ }
440
+ if (failedCount > 0)
441
+ return { conclusion: "failure", count: failedCount };
442
+ if (pendingCount > 0)
443
+ return { conclusion: "pending", count: pendingCount };
444
+ return { conclusion: "success", count: passedCount };
445
+ };
446
+
447
+ // src/git/pr.ts
448
+ var getPrInfoCached = async () => {
449
+ if (Date.now() - cache.prInfo.timestamp < CACHE_TTL.prInfo) {
450
+ return cache.prInfo.value;
523
451
  }
524
452
  try {
525
- const result = await $5`gh pr view --json url -q .url 2>/dev/null`.text();
526
- cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
527
- return cache.prUrl.value;
453
+ const result = await $5`gh pr view --json url,state,isDraft,statusCheckRollup 2>/dev/null`.text();
454
+ const parsed = JSON.parse(result);
455
+ const value = {
456
+ url: parsed.url,
457
+ state: parsed.state,
458
+ isDraft: parsed.isDraft,
459
+ ciStatus: aggregateCiStatus(parsed.statusCheckRollup ?? [])
460
+ };
461
+ cache.prInfo = { value, timestamp: Date.now() };
462
+ return value;
528
463
  } catch {
529
- return cache.prUrl.value;
464
+ return cache.prInfo.value;
530
465
  }
531
466
  };
532
467
  // src/git/worktree.ts
533
468
  var {$: $6 } = globalThis.Bun;
534
- var getMainProjectNameCached = async () => {
535
- if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
536
- return cache.mainProjectName.value;
469
+ var getMainProjectCached = async () => {
470
+ if (Date.now() - cache.mainProject.timestamp < CACHE_TTL.mainProject) {
471
+ return cache.mainProject.value;
537
472
  }
538
473
  try {
539
474
  const result = await $6`git rev-parse --git-common-dir 2>/dev/null`.text();
540
475
  const gitCommonDir = result.trim();
541
476
  if (gitCommonDir === ".git") {
542
- cache.mainProjectName = { value: null, timestamp: Date.now() };
477
+ cache.mainProject = { value: null, timestamp: Date.now() };
543
478
  return null;
544
479
  }
545
480
  const projectRoot = gitCommonDir.replace(/\/.git$/, "");
546
- const projectName = projectRoot.split("/").pop() || null;
547
- cache.mainProjectName = { value: projectName, timestamp: Date.now() };
548
- return projectName;
481
+ const projectName = projectRoot.split("/").pop();
482
+ const value = projectName ? { name: projectName, path: projectRoot } : null;
483
+ cache.mainProject = { value, timestamp: Date.now() };
484
+ return value;
549
485
  } catch {
550
- return cache.mainProjectName.value;
486
+ return cache.mainProject.value;
551
487
  }
552
488
  };
553
- // src/colors.ts
554
- var C = {
555
- RESET: "\x1B[0m",
556
- CYAN: "\x1B[38;2;0;255;255m",
557
- MAGENTA: "\x1B[38;2;255;100;200m",
558
- GREEN: "\x1B[38;2;100;255;100m",
559
- YELLOW: "\x1B[38;2;255;220;100m",
560
- RED: "\x1B[38;2;255;100;100m",
561
- BLUE: "\x1B[38;2;100;150;255m",
562
- WHITE: "\x1B[38;2;200;200;200m",
563
- UNDERLINE: "\x1B[4m"
564
- };
565
- var getUsageColor = (pct) => {
566
- if (pct < 50)
567
- return C.WHITE;
568
- if (pct < 80)
569
- return C.YELLOW;
570
- return C.RED;
571
- };
572
-
573
- // src/format/formatNumber.ts
574
- var formatNumber = (n) => n.toLocaleString("en-US");
575
- // src/format/formatResetDate.ts
576
- var formatResetDate = (date) => {
577
- const month = (date.getMonth() + 1).toString().padStart(2, "0");
578
- const day = date.getDate().toString().padStart(2, "0");
579
- const hours = date.getHours().toString().padStart(2, "0");
580
- const minutes = date.getMinutes().toString().padStart(2, "0");
581
- return `${month}/${day} ${hours}:${minutes}`;
582
- };
583
- // src/format/formatTime.ts
584
- var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
585
489
  // src/render.ts
586
490
  var renderStatusLine = (ctx) => {
587
491
  const lines = [];
@@ -593,9 +497,17 @@ var renderStatusLine = (ctx) => {
593
497
  const costUsd = ctx.claudeJson.cost?.total_cost_usd || 0;
594
498
  let line1;
595
499
  if (ctx.mainProjectName) {
596
- line1 = `${C.WHITE}\uD83D\uDCC1 ${ctx.mainProjectName}${C.RESET} | ${C.WHITE}\uD83C\uDF32 ${folder}${C.RESET}`;
500
+ const mainU = ctx.mainProjectUrl ? C.UNDERLINE : "";
501
+ const mainText = `${C.WHITE}\uD83D\uDCC1 ${mainU}${ctx.mainProjectName}${C.RESET}`;
502
+ const mainSeg = ctx.mainProjectUrl ? `\x1B]8;;${ctx.mainProjectUrl}\x07${mainText}\x1B]8;;\x07` : mainText;
503
+ const folderU = ctx.projectDirUrl ? C.UNDERLINE : "";
504
+ const folderText = `${C.WHITE}\uD83C\uDF32 ${folderU}${folder}${C.RESET}`;
505
+ const folderSeg = ctx.projectDirUrl ? `\x1B]8;;${ctx.projectDirUrl}\x07${folderText}\x1B]8;;\x07` : folderText;
506
+ line1 = `${mainSeg} | ${folderSeg}`;
597
507
  } else {
598
- line1 = `${C.WHITE}\uD83D\uDCC1 ${folder}${C.RESET}`;
508
+ const folderU = ctx.projectDirUrl ? C.UNDERLINE : "";
509
+ const folderText = `${C.WHITE}\uD83D\uDCC1 ${folderU}${folder}${C.RESET}`;
510
+ line1 = ctx.projectDirUrl ? `\x1B]8;;${ctx.projectDirUrl}\x07${folderText}\x1B]8;;\x07` : folderText;
599
511
  }
600
512
  if (ctx.branch) {
601
513
  line1 += ` | ${C.WHITE}\uD83C\uDF3F ${ctx.branch}${C.RESET}`;
@@ -646,7 +558,7 @@ var renderStatusLine = (ctx) => {
646
558
  }
647
559
  }
648
560
  const hasGitChanges = ctx.gitChanges.files > 0 || ctx.gitChanges.insertions > 0 || ctx.gitChanges.deletions > 0;
649
- if (hasGitChanges || ctx.baseChanges || ctx.prUrl) {
561
+ if (hasGitChanges || ctx.baseChanges || ctx.prInfo) {
650
562
  let line4 = "";
651
563
  if (hasGitChanges) {
652
564
  const u = ctx.diffViewerUrl ? C.UNDERLINE : "";
@@ -658,11 +570,17 @@ var renderStatusLine = (ctx) => {
658
570
  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
571
  line4 += ctx.baseDiffViewerUrl ? `\x1B]8;;${ctx.baseDiffViewerUrl}\x07${baseText}\x1B]8;;\x07` : baseText;
660
572
  }
661
- if (ctx.prUrl) {
662
- const prLabel = ctx.prUrl.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
573
+ if (ctx.prInfo) {
574
+ const { url, state, isDraft, ciStatus } = ctx.prInfo;
575
+ const prLabel = url.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
576
+ const stateText = prStateText(state, isDraft);
577
+ const stateColor = prStateColor(state, isDraft);
578
+ const ciText = ciSummaryText(ciStatus);
579
+ const ciColor = ciSummaryColor(ciStatus);
580
+ const ciSuffix = ciText ? `${ciColor}${C.UNDERLINE}(${ciText})` : "";
663
581
  if (line4)
664
582
  line4 += " | ";
665
- line4 += `\uD83D\uDCCE ${C.WHITE}${C.UNDERLINE}\x1B]8;;${ctx.prUrl}\x07${prLabel}\x1B]8;;\x07${C.RESET}`;
583
+ line4 += `\uD83D\uDCCE ${C.WHITE}${C.UNDERLINE}\x1B]8;;${url}\x07${prLabel} ` + `${stateColor}${C.UNDERLINE}[${stateText}]${ciSuffix}${C.RESET}\x1B]8;;\x07`;
666
584
  }
667
585
  lines.push(line4);
668
586
  }
@@ -681,14 +599,14 @@ var readStdin = async () => {
681
599
  // src/ultracode.ts
682
600
  import { readFile } from "fs/promises";
683
601
  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";
602
+ import { join as join4 } from "path";
603
+ 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
604
  var ultracodeSettingsPaths = (projectDir, homeDir) => {
687
605
  const paths = [MANAGED_SETTINGS_PATH];
688
606
  if (projectDir) {
689
- paths.push(join5(projectDir, ".claude", "settings.local.json"), join5(projectDir, ".claude", "settings.json"));
607
+ paths.push(join4(projectDir, ".claude", "settings.local.json"), join4(projectDir, ".claude", "settings.json"));
690
608
  }
691
- paths.push(join5(homeDir, ".claude", "settings.json"));
609
+ paths.push(join4(homeDir, ".claude", "settings.json"));
692
610
  return paths;
693
611
  };
694
612
  var resolveUltracodeFromFiles = async (paths) => {
@@ -718,13 +636,16 @@ var getUltracodeCached = async (projectDir, homeDir = homedir2()) => {
718
636
  var main = async () => {
719
637
  const claudeJson = JSON.parse(await readStdin() || "{}");
720
638
  const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
721
- const [branch, gitChanges, prUrl, mainProjectName, ultracode] = await Promise.all([
639
+ const [branch, gitChanges, prInfo, mainProject, ultracode] = await Promise.all([
722
640
  getBranchCached(),
723
641
  getGitChangesCached(),
724
- getPrUrlCached(),
725
- getMainProjectNameCached(),
642
+ getPrInfoCached(),
643
+ getMainProjectCached(),
726
644
  getUltracodeCached(repo)
727
645
  ]);
646
+ const mainProjectName = mainProject?.name ?? null;
647
+ const projectDirUrl = repo ? toFileUrl(repo) : null;
648
+ const mainProjectUrl = mainProject ? toFileUrl(mainProject.path) : null;
728
649
  const hasChanges = gitChanges.files > 0 || gitChanges.insertions > 0 || gitChanges.deletions > 0;
729
650
  let diffViewerUrl = null;
730
651
  let baseDiffViewerUrl = null;
@@ -756,28 +677,22 @@ var main = async () => {
756
677
  claudeJson,
757
678
  branch,
758
679
  gitChanges,
759
- prUrl,
680
+ prInfo,
760
681
  ultracode,
761
682
  rateLimits: claudeJson.rate_limits ?? null,
762
683
  mainProjectName,
763
684
  diffViewerUrl,
764
685
  baseChanges,
765
- baseDiffViewerUrl
686
+ baseDiffViewerUrl,
687
+ projectDirUrl,
688
+ mainProjectUrl
766
689
  });
767
690
  for (const line of lines) {
768
691
  console.log(line);
769
692
  }
770
693
  };
771
694
  if (import.meta.main) {
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
- }
695
+ main().catch(console.error);
781
696
  }
782
697
  export {
783
698
  main