@say8425/cc-statusline 3.1.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 +43 -2
- package/dist/index.js +220 -325
- package/package.json +17 -13
- package/dist/viewer/index.html +0 -199
- package/dist/viewer/main.js +0 -2842
package/dist/index.js
CHANGED
|
@@ -1,351 +1,177 @@
|
|
|
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
|
-
|
|
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
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
function getCacheDir(env = process.env) {
|
|
19
|
+
};
|
|
20
|
+
var isDiffViewerDisabled = (env = process.env) => env.CC_STATUSLINE_DIFF_DISABLE === "1";
|
|
21
|
+
var getCacheDir = (env = process.env) => {
|
|
33
22
|
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
34
23
|
return join(base, "cc-statusline");
|
|
35
|
-
}
|
|
36
|
-
var
|
|
37
|
-
|
|
24
|
+
};
|
|
25
|
+
var getDiffdeckCacheDir = (env = process.env) => {
|
|
26
|
+
const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
|
|
27
|
+
return join(base, "diffdeck");
|
|
28
|
+
};
|
|
38
29
|
|
|
39
30
|
// src/diff-server/token.ts
|
|
40
|
-
import {
|
|
31
|
+
import { readFileSync } from "fs";
|
|
41
32
|
import { join as join2 } from "path";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
function generateToken() {
|
|
46
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
47
|
-
}
|
|
48
|
-
function readTokenSync(env = process.env) {
|
|
33
|
+
var getTokenPath = (env = process.env) => join2(getDiffdeckCacheDir(env), "diff-server.token");
|
|
34
|
+
var readTokenSync = (env = process.env) => {
|
|
49
35
|
try {
|
|
50
36
|
const value = readFileSync(getTokenPath(env), "utf8").trim();
|
|
51
37
|
return value || null;
|
|
52
38
|
} catch {
|
|
53
39
|
return null;
|
|
54
40
|
}
|
|
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";
|
|
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";
|
|
41
|
+
};
|
|
273
42
|
|
|
274
43
|
// 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
44
|
var ENSURE_TTL_MS = 5000;
|
|
280
45
|
var LOCK_STALE_MS = 30000;
|
|
46
|
+
var PORT_FREE_TRIES = 20;
|
|
47
|
+
var PORT_FREE_INTERVAL_MS = 100;
|
|
281
48
|
var checkedAt = 0;
|
|
282
|
-
async
|
|
49
|
+
var probeServer = async (port) => {
|
|
283
50
|
try {
|
|
284
51
|
const res = await fetch(`http://127.0.0.1:${port}/api/ping`, {
|
|
285
52
|
signal: AbortSignal.timeout(150)
|
|
286
53
|
});
|
|
287
|
-
|
|
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 };
|
|
288
61
|
} catch {
|
|
289
|
-
return
|
|
62
|
+
return null;
|
|
290
63
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
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;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var acquireSpawnLock = (env) => {
|
|
293
79
|
const lock = join3(getCacheDir(env), "diff-server.lock");
|
|
294
|
-
|
|
80
|
+
mkdirSync(getCacheDir(env), { recursive: true, mode: 448 });
|
|
295
81
|
try {
|
|
296
|
-
|
|
82
|
+
mkdirSync(lock);
|
|
297
83
|
return true;
|
|
298
84
|
} catch {
|
|
299
85
|
try {
|
|
300
86
|
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
301
87
|
rmSync(lock, { recursive: true, force: true });
|
|
302
|
-
|
|
88
|
+
mkdirSync(lock);
|
|
303
89
|
return true;
|
|
304
90
|
}
|
|
305
91
|
} catch {}
|
|
306
92
|
return false;
|
|
307
93
|
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
const
|
|
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
|
+
};
|
|
100
|
+
var spawnDaemon = (port, env) => {
|
|
101
|
+
const { cli } = resolveDiffdeck();
|
|
312
102
|
Bun.spawn([
|
|
313
103
|
"sh",
|
|
314
104
|
"-c",
|
|
315
|
-
'nohup "$0" "$1" --
|
|
316
|
-
execPath,
|
|
317
|
-
|
|
105
|
+
'nohup "$0" "$1" --no-open --port "$2" >/dev/null 2>&1 &',
|
|
106
|
+
process.execPath,
|
|
107
|
+
cli,
|
|
108
|
+
String(port)
|
|
318
109
|
], {
|
|
319
|
-
env: { ...process.env, ...env,
|
|
110
|
+
env: { ...process.env, ...env, DIFFDECK_PORT: String(port) },
|
|
320
111
|
stdin: "ignore",
|
|
321
112
|
stdout: "ignore",
|
|
322
113
|
stderr: "ignore"
|
|
323
114
|
}).unref();
|
|
324
|
-
}
|
|
325
|
-
|
|
115
|
+
};
|
|
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 };
|
|
326
140
|
try {
|
|
327
|
-
|
|
141
|
+
const found = await d.probe(port);
|
|
142
|
+
const current = d.currentVersion();
|
|
143
|
+
if (found && (current == null || found.version === current))
|
|
328
144
|
return;
|
|
329
145
|
if (!acquireSpawnLock(env))
|
|
330
146
|
return;
|
|
331
|
-
|
|
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);
|
|
332
158
|
} catch {}
|
|
333
|
-
}
|
|
334
|
-
|
|
159
|
+
};
|
|
160
|
+
var ensureDiffServer = (_repo, env = process.env, deps = {}) => {
|
|
335
161
|
if (isDiffViewerDisabled(env))
|
|
336
162
|
return null;
|
|
337
163
|
const port = resolveDiffPort(env);
|
|
338
164
|
const now = Date.now();
|
|
339
165
|
if (now - checkedAt >= ENSURE_TTL_MS) {
|
|
340
166
|
checkedAt = now;
|
|
341
|
-
maybeSpawn(port, env,
|
|
167
|
+
maybeSpawn(port, env, deps);
|
|
342
168
|
}
|
|
343
169
|
const token = readTokenSync(env);
|
|
344
170
|
return token ? { port, token } : null;
|
|
345
|
-
}
|
|
171
|
+
};
|
|
346
172
|
|
|
347
173
|
// src/diff-server/link.ts
|
|
348
|
-
|
|
174
|
+
var buildDiffViewerUrl = (params) => {
|
|
349
175
|
const query = new URLSearchParams({
|
|
350
176
|
repo: params.repo,
|
|
351
177
|
token: params.token
|
|
@@ -353,7 +179,7 @@ function buildDiffViewerUrl(params) {
|
|
|
353
179
|
if (params.mode)
|
|
354
180
|
query.set("mode", params.mode);
|
|
355
181
|
return `http://127.0.0.1:${params.port}/?${query.toString()}`;
|
|
356
|
-
}
|
|
182
|
+
};
|
|
357
183
|
|
|
358
184
|
// src/git/baseChanges.ts
|
|
359
185
|
var {$: $2 } = globalThis.Bun;
|
|
@@ -367,31 +193,66 @@ var cache = {
|
|
|
367
193
|
baseRef: {
|
|
368
194
|
value: null,
|
|
369
195
|
timestamp: 0
|
|
370
|
-
}
|
|
196
|
+
},
|
|
197
|
+
ultracode: { value: false, timestamp: 0 }
|
|
371
198
|
};
|
|
372
199
|
var CACHE_TTL = {
|
|
373
200
|
branch: 5000,
|
|
374
201
|
gitChanges: 0,
|
|
375
202
|
prUrl: 30000,
|
|
376
203
|
mainProjectName: 300000,
|
|
377
|
-
baseRef: 30000
|
|
204
|
+
baseRef: 30000,
|
|
205
|
+
ultracode: 5000
|
|
378
206
|
};
|
|
379
207
|
|
|
380
|
-
// src/git/
|
|
381
|
-
|
|
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
|
+
};
|
|
382
243
|
|
|
383
244
|
// src/git/shortstat.ts
|
|
384
|
-
|
|
245
|
+
var parseShortstat = (text) => {
|
|
385
246
|
const [files, insertions, deletions] = [
|
|
386
247
|
/(\d+) files?/g,
|
|
387
248
|
/(\d+) insertions?/g,
|
|
388
249
|
/(\d+) deletions?/g
|
|
389
250
|
].map((regex) => (text.match(regex) || []).reduce((sum, m) => sum + Number.parseInt(m, 10), 0));
|
|
390
251
|
return { files, insertions, deletions };
|
|
391
|
-
}
|
|
252
|
+
};
|
|
392
253
|
|
|
393
254
|
// src/git/baseChanges.ts
|
|
394
|
-
async
|
|
255
|
+
var getBaseChangesCached = async () => {
|
|
395
256
|
try {
|
|
396
257
|
let resolved = cache.baseRef.value;
|
|
397
258
|
if (resolved === null || Date.now() - cache.baseRef.timestamp >= CACHE_TTL.baseRef) {
|
|
@@ -411,10 +272,10 @@ async function getBaseChangesCached() {
|
|
|
411
272
|
} catch {
|
|
412
273
|
return null;
|
|
413
274
|
}
|
|
414
|
-
}
|
|
275
|
+
};
|
|
415
276
|
// src/git/branch.ts
|
|
416
277
|
var {$: $3 } = globalThis.Bun;
|
|
417
|
-
async
|
|
278
|
+
var getBranchCached = async () => {
|
|
418
279
|
if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
|
|
419
280
|
return cache.branch.value;
|
|
420
281
|
}
|
|
@@ -425,10 +286,10 @@ async function getBranchCached() {
|
|
|
425
286
|
} catch {
|
|
426
287
|
return cache.branch.value;
|
|
427
288
|
}
|
|
428
|
-
}
|
|
289
|
+
};
|
|
429
290
|
// src/git/changes.ts
|
|
430
291
|
var {$: $4 } = globalThis.Bun;
|
|
431
|
-
async
|
|
292
|
+
var getGitChangesCached = async () => {
|
|
432
293
|
if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
|
|
433
294
|
return cache.gitChanges;
|
|
434
295
|
}
|
|
@@ -444,10 +305,10 @@ ${staged}`);
|
|
|
444
305
|
} catch {
|
|
445
306
|
return cache.gitChanges;
|
|
446
307
|
}
|
|
447
|
-
}
|
|
308
|
+
};
|
|
448
309
|
// src/git/pr.ts
|
|
449
310
|
var {$: $5 } = globalThis.Bun;
|
|
450
|
-
async
|
|
311
|
+
var getPrUrlCached = async () => {
|
|
451
312
|
if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
|
|
452
313
|
return cache.prUrl.value;
|
|
453
314
|
}
|
|
@@ -458,10 +319,10 @@ async function getPrUrlCached() {
|
|
|
458
319
|
} catch {
|
|
459
320
|
return cache.prUrl.value;
|
|
460
321
|
}
|
|
461
|
-
}
|
|
322
|
+
};
|
|
462
323
|
// src/git/worktree.ts
|
|
463
324
|
var {$: $6 } = globalThis.Bun;
|
|
464
|
-
async
|
|
325
|
+
var getMainProjectNameCached = async () => {
|
|
465
326
|
if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
|
|
466
327
|
return cache.mainProjectName.value;
|
|
467
328
|
}
|
|
@@ -479,7 +340,7 @@ async function getMainProjectNameCached() {
|
|
|
479
340
|
} catch {
|
|
480
341
|
return cache.mainProjectName.value;
|
|
481
342
|
}
|
|
482
|
-
}
|
|
343
|
+
};
|
|
483
344
|
// src/colors.ts
|
|
484
345
|
var C = {
|
|
485
346
|
RESET: "\x1B[0m",
|
|
@@ -492,32 +353,28 @@ var C = {
|
|
|
492
353
|
WHITE: "\x1B[38;2;200;200;200m",
|
|
493
354
|
UNDERLINE: "\x1B[4m"
|
|
494
355
|
};
|
|
495
|
-
|
|
356
|
+
var getUsageColor = (pct) => {
|
|
496
357
|
if (pct < 50)
|
|
497
358
|
return C.WHITE;
|
|
498
359
|
if (pct < 80)
|
|
499
360
|
return C.YELLOW;
|
|
500
361
|
return C.RED;
|
|
501
|
-
}
|
|
362
|
+
};
|
|
502
363
|
|
|
503
364
|
// src/format/formatNumber.ts
|
|
504
|
-
|
|
505
|
-
return n.toLocaleString("en-US");
|
|
506
|
-
}
|
|
365
|
+
var formatNumber = (n) => n.toLocaleString("en-US");
|
|
507
366
|
// src/format/formatResetDate.ts
|
|
508
|
-
|
|
367
|
+
var formatResetDate = (date) => {
|
|
509
368
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
510
369
|
const day = date.getDate().toString().padStart(2, "0");
|
|
511
370
|
const hours = date.getHours().toString().padStart(2, "0");
|
|
512
371
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
513
372
|
return `${month}/${day} ${hours}:${minutes}`;
|
|
514
|
-
}
|
|
373
|
+
};
|
|
515
374
|
// src/format/formatTime.ts
|
|
516
|
-
|
|
517
|
-
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
518
|
-
}
|
|
375
|
+
var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
519
376
|
// src/render.ts
|
|
520
|
-
|
|
377
|
+
var renderStatusLine = (ctx) => {
|
|
521
378
|
const lines = [];
|
|
522
379
|
const folder = ctx.claudeJson.workspace?.project_dir?.split("/").pop() || "";
|
|
523
380
|
const sessionMs = ctx.claudeJson.cost?.total_duration_ms || 0;
|
|
@@ -544,6 +401,14 @@ function renderStatusLine(ctx) {
|
|
|
544
401
|
const ctxColor = getUsageColor(contextPct);
|
|
545
402
|
line2 += ` | ${ctxColor}\uD83E\uDDE0 ${formatNumber(totalTokens)} (${contextPct}%)${C.RESET}`;
|
|
546
403
|
}
|
|
404
|
+
const modelName = ctx.claudeJson.model?.display_name;
|
|
405
|
+
if (modelName) {
|
|
406
|
+
const effortLevel = ctx.claudeJson.effort?.level;
|
|
407
|
+
let modelText = effortLevel ? `${modelName} ${effortLevel}` : modelName;
|
|
408
|
+
if (ctx.ultracode && effortLevel === "xhigh")
|
|
409
|
+
modelText += " \u26A1ultra";
|
|
410
|
+
line2 += ` | ${C.WHITE}\uD83E\uDD16 ${modelText}${C.RESET}`;
|
|
411
|
+
}
|
|
547
412
|
lines.push(line2);
|
|
548
413
|
if (ctx.rateLimits) {
|
|
549
414
|
const parts = [];
|
|
@@ -593,27 +458,64 @@ function renderStatusLine(ctx) {
|
|
|
593
458
|
lines.push(line4);
|
|
594
459
|
}
|
|
595
460
|
return lines;
|
|
596
|
-
}
|
|
461
|
+
};
|
|
597
462
|
|
|
598
463
|
// src/stdin.ts
|
|
599
|
-
async
|
|
464
|
+
var readStdin = async () => {
|
|
600
465
|
const chunks = [];
|
|
601
466
|
for await (const chunk of Bun.stdin.stream()) {
|
|
602
467
|
chunks.push(chunk);
|
|
603
468
|
}
|
|
604
469
|
return Buffer.concat(chunks).toString("utf-8");
|
|
605
|
-
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// src/ultracode.ts
|
|
473
|
+
import { readFile } from "fs/promises";
|
|
474
|
+
import { homedir as homedir2 } from "os";
|
|
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";
|
|
477
|
+
var ultracodeSettingsPaths = (projectDir, homeDir) => {
|
|
478
|
+
const paths = [MANAGED_SETTINGS_PATH];
|
|
479
|
+
if (projectDir) {
|
|
480
|
+
paths.push(join4(projectDir, ".claude", "settings.local.json"), join4(projectDir, ".claude", "settings.json"));
|
|
481
|
+
}
|
|
482
|
+
paths.push(join4(homeDir, ".claude", "settings.json"));
|
|
483
|
+
return paths;
|
|
484
|
+
};
|
|
485
|
+
var resolveUltracodeFromFiles = async (paths) => {
|
|
486
|
+
const contents = await Promise.all(paths.map((path) => readFile(path, "utf8").catch(() => null)));
|
|
487
|
+
for (const content of contents) {
|
|
488
|
+
if (content === null)
|
|
489
|
+
continue;
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(content);
|
|
492
|
+
if (typeof parsed === "object" && parsed !== null && "ultracode" in parsed && typeof parsed.ultracode === "boolean") {
|
|
493
|
+
return parsed.ultracode;
|
|
494
|
+
}
|
|
495
|
+
} catch {}
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
};
|
|
499
|
+
var getUltracodeCached = async (projectDir, homeDir = homedir2()) => {
|
|
500
|
+
if (Date.now() - cache.ultracode.timestamp < CACHE_TTL.ultracode) {
|
|
501
|
+
return cache.ultracode.value;
|
|
502
|
+
}
|
|
503
|
+
const value = await resolveUltracodeFromFiles(ultracodeSettingsPaths(projectDir, homeDir));
|
|
504
|
+
cache.ultracode = { value, timestamp: Date.now() };
|
|
505
|
+
return value;
|
|
506
|
+
};
|
|
606
507
|
|
|
607
508
|
// src/index.ts
|
|
608
|
-
async
|
|
509
|
+
var main = async () => {
|
|
609
510
|
const claudeJson = JSON.parse(await readStdin() || "{}");
|
|
610
|
-
const
|
|
511
|
+
const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
|
|
512
|
+
const [branch, gitChanges, prUrl, mainProjectName, ultracode] = await Promise.all([
|
|
611
513
|
getBranchCached(),
|
|
612
514
|
getGitChangesCached(),
|
|
613
515
|
getPrUrlCached(),
|
|
614
|
-
getMainProjectNameCached()
|
|
516
|
+
getMainProjectNameCached(),
|
|
517
|
+
getUltracodeCached(repo)
|
|
615
518
|
]);
|
|
616
|
-
const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
|
|
617
519
|
const hasChanges = gitChanges.files > 0 || gitChanges.insertions > 0 || gitChanges.deletions > 0;
|
|
618
520
|
let diffViewerUrl = null;
|
|
619
521
|
let baseDiffViewerUrl = null;
|
|
@@ -622,7 +524,7 @@ async function main() {
|
|
|
622
524
|
baseChanges = await getBaseChangesCached();
|
|
623
525
|
}
|
|
624
526
|
if (repo && (hasChanges || baseChanges)) {
|
|
625
|
-
const ensured =
|
|
527
|
+
const ensured = ensureDiffServer(repo);
|
|
626
528
|
if (ensured) {
|
|
627
529
|
if (hasChanges) {
|
|
628
530
|
diffViewerUrl = buildDiffViewerUrl({
|
|
@@ -646,6 +548,7 @@ async function main() {
|
|
|
646
548
|
branch,
|
|
647
549
|
gitChanges,
|
|
648
550
|
prUrl,
|
|
551
|
+
ultracode,
|
|
649
552
|
rateLimits: claudeJson.rate_limits ?? null,
|
|
650
553
|
mainProjectName,
|
|
651
554
|
diffViewerUrl,
|
|
@@ -655,17 +558,9 @@ async function main() {
|
|
|
655
558
|
for (const line of lines) {
|
|
656
559
|
console.log(line);
|
|
657
560
|
}
|
|
658
|
-
}
|
|
561
|
+
};
|
|
659
562
|
if (import.meta.main) {
|
|
660
|
-
|
|
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
|
-
}
|
|
563
|
+
main().catch(console.error);
|
|
669
564
|
}
|
|
670
565
|
export {
|
|
671
566
|
main
|