@say8425/cc-statusline 3.0.0 → 3.1.0

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