@xbzbing/dsh-git-panel 1.5.3 → 1.5.4

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/lib/host/index.js CHANGED
@@ -46,1678 +46,25 @@ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use
46
46
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
47
47
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
48
48
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
49
-
50
- // src/host/index.ts
49
+ var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
51
50
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
52
51
  import Schema from "@deepseek-ai/schemastery";
53
- import { readFile as readFile3, readdir, realpath, rm, stat } from "node:fs/promises";
54
-
55
- // src/host/git.ts
56
- import { readFile } from "node:fs/promises";
57
- function createGitRunner(subprocess, timeoutMs, maxBytes) {
58
- const spillMaxBytes = maxBytes * 16;
59
- return {
60
- async run(argv, opts) {
61
- const controller = new AbortController();
62
- const timer = setTimeout(() => controller.abort(), timeoutMs);
63
- try {
64
- const signal = opts.signal === void 0 ? controller.signal : AbortSignal.any([controller.signal, opts.signal]);
65
- const handle = subprocess.spawn({
66
- argv,
67
- cwd: opts.cwd,
68
- // Never let a network command (fetch/pull) block on an interactive
69
- // credential or host-key prompt: with stdin ignored git would hang
70
- // until the timeout. GIT_TERMINAL_PROMPT=0 makes it fail fast instead,
71
- // and is harmless for the non-network commands that share this runner.
72
- env: { GIT_TERMINAL_PROMPT: "0" },
73
- stdio: {
74
- stdin: opts.stdinData === void 0 ? "ignore" : { data: opts.stdinData },
75
- stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },
76
- stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } }
77
- },
78
- graceMs: 200,
79
- signal
80
- });
81
- let outcome;
82
- try {
83
- outcome = await handle.done;
84
- } catch (error) {
85
- const cancelled2 = opts.signal?.aborted === true && !controller.signal.aborted;
86
- if (controller.signal.aborted || opts.signal?.aborted === true) {
87
- return { exitCode: null, stdout: "", stderr: "", timedOut: !cancelled2, cancelled: cancelled2, stdoutLossy: false };
88
- }
89
- throw error;
90
- }
91
- const stdout = handle.collected.stdout?.readFrom(0);
92
- const stderr = handle.collected.stderr?.readFrom(0);
93
- const resolved = await resolveStdout(stdout);
94
- const cancelled = opts.signal?.aborted === true && !controller.signal.aborted;
95
- return {
96
- exitCode: outcome.exitCode,
97
- stdout: resolved.text,
98
- stderr: stderr?.text ?? "",
99
- timedOut: controller.signal.aborted,
100
- cancelled,
101
- stdoutLossy: resolved.lossy
102
- };
103
- } finally {
104
- clearTimeout(timer);
105
- }
106
- }
107
- };
108
- }
109
- async function resolveStdout(read) {
110
- if (read === void 0) return { text: "", lossy: false };
111
- if (!read.lossy || read.spillPath === void 0) return { text: read.text, lossy: read.lossy };
112
- try {
113
- return { text: await readFile(read.spillPath, "utf8"), lossy: false };
114
- } catch {
115
- return { text: read.text, lossy: true };
116
- }
117
- }
118
-
119
- // src/host/core.ts
120
- import { dirname, join } from "node:path";
121
-
122
- // src/host/parser.ts
123
- function commitFromFields(parts) {
124
- const hash = parts[0];
125
- if (parts.length < 5 || hash === void 0 || hash === "") return null;
126
- return {
127
- hash,
128
- shortHash: parts[1] ?? "",
129
- subject: parts[2] ?? "",
130
- author: parts[3] ?? "",
131
- dateIso: parts[4] ?? ""
132
- };
133
- }
134
- function statusOf(code) {
135
- switch (code) {
136
- case "A":
137
- return "added";
138
- case "M":
139
- return "modified";
140
- case "D":
141
- return "deleted";
142
- case "R":
143
- return "renamed";
144
- case "C":
145
- return "added";
146
- case "T":
147
- return "typechange";
148
- case "U":
149
- return "conflicted";
150
- case "?":
151
- return "untracked";
152
- default:
153
- return "modified";
154
- }
155
- }
156
- function parseStatus(stdout) {
157
- const out = [];
158
- const records = stdout.split("\0");
159
- for (let i = 0; i < records.length; i++) {
160
- const rec = records[i];
161
- if (rec === void 0 || rec.length < 3) continue;
162
- const x = rec[0];
163
- const y = rec[1];
164
- let path = rec.slice(3);
165
- if (x === "R" || x === "C" || y === "R" || y === "C") {
166
- i++;
167
- }
168
- const isDirectory = path.endsWith("/");
169
- if (isDirectory) path = path.replace(/\/+$/, "");
170
- if (x === "?" && y === "?") {
171
- out.push({ path, status: "untracked", staged: false, isDirectory });
172
- continue;
173
- }
174
- if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") {
175
- out.push({ path, status: "conflicted", staged: false, isDirectory });
176
- continue;
177
- }
178
- if (x !== " " && x !== "?") {
179
- out.push({ path, status: statusOf(x), staged: true, isDirectory });
180
- }
181
- if (y !== " " && y !== "?") {
182
- out.push({ path, status: statusOf(y), staged: false, isDirectory });
183
- }
184
- }
185
- return out;
186
- }
187
- function parseGraphLog(stdout) {
188
- const out = [];
189
- for (const record of stdout.split("")) {
190
- const rec = record.replace(/^\n+/, "");
191
- if (rec.trim() === "") continue;
192
- const parts = rec.split("");
193
- if (parts.length < 7) continue;
194
- const [hash, shortHash, parentsRaw, author, dateIso, decoration] = parts;
195
- const subject = parts.slice(6).join("");
196
- const parents = (parentsRaw ?? "").trim() === "" ? [] : parentsRaw.trim().split(/\s+/);
197
- out.push({
198
- hash: hash ?? "",
199
- shortHash: shortHash ?? "",
200
- subject,
201
- author: author ?? "",
202
- dateIso: dateIso ?? "",
203
- parents,
204
- refs: parseRefs(decoration ?? "")
205
- });
206
- }
207
- return out;
208
- }
209
- function parseRefs(decoration) {
210
- const refs = [];
211
- for (const raw of decoration.split(",")) {
212
- let token = raw.trim();
213
- if (token === "") continue;
214
- let head = false;
215
- if (token.startsWith("HEAD -> ")) {
216
- head = true;
217
- token = token.slice("HEAD -> ".length).trim();
218
- } else if (token === "HEAD") {
219
- continue;
220
- }
221
- if (token.startsWith("tag: ")) {
222
- refs.push({ kind: "tag", name: token.slice("tag: ".length).trim().replace(/^refs\/tags\//, ""), head: false });
223
- } else if (token.startsWith("refs/tags/")) {
224
- refs.push({ kind: "tag", name: token.slice("refs/tags/".length), head: false });
225
- } else if (token.startsWith("refs/remotes/")) {
226
- refs.push({ kind: "remote", name: token.slice("refs/remotes/".length), head });
227
- } else if (token.startsWith("refs/heads/")) {
228
- refs.push({ kind: "branch", name: token.slice("refs/heads/".length), head });
229
- } else if (head) {
230
- refs.push({ kind: "branch", name: token, head });
231
- } else if (token.startsWith("origin/") || token.includes("/")) {
232
- refs.push({ kind: "remote", name: token, head });
233
- } else {
234
- refs.push({ kind: "branch", name: token, head });
235
- }
236
- }
237
- return refs;
238
- }
239
- function parseBranches(stdout) {
240
- const out = [];
241
- for (const line of stdout.split("\n")) {
242
- if (line.trim() === "") continue;
243
- const [name, shortHash = "", track = ""] = line.split("\0");
244
- if (name === void 0 || name === "") continue;
245
- const branch = {
246
- name,
247
- shortHash: shortHash === "" ? null : shortHash
248
- };
249
- const ahead = /ahead (\d+)/.exec(track);
250
- const behind = /behind (\d+)/.exec(track);
251
- if (ahead) branch.ahead = Number(ahead[1]);
252
- if (behind) branch.behind = Number(behind[1]);
253
- out.push(branch);
254
- }
255
- return out;
256
- }
257
- function parseTags(stdout) {
258
- const out = [];
259
- for (const line of stdout.split("\n")) {
260
- if (line.trim() === "") continue;
261
- const [name, shortHash = ""] = line.split("\0");
262
- if (name) out.push({ name, shortHash: shortHash === "" ? null : shortHash });
263
- }
264
- return out;
265
- }
266
- function parseStashList(stdout) {
267
- const out = [];
268
- for (const record of stdout.split("\0")) {
269
- if (record.trim() === "") continue;
270
- const [selector = "", sha = "", subject = "", relTime = ""] = record.split("");
271
- const idxMatch = /stash@\{(\d+)\}/.exec(selector);
272
- if (idxMatch === null) continue;
273
- const index = Number(idxMatch[1]);
274
- let branch = null;
275
- let message = subject;
276
- const m = /^(?:WIP on|On) (.+?): (.*)$/s.exec(subject);
277
- if (m !== null) {
278
- branch = m[1] === "(no branch)" ? null : m[1] ?? null;
279
- message = m[2] ?? "";
280
- }
281
- out.push({ index, sha: sha.trim(), message: message.trim(), branch, relTime: relTime.trim() });
282
- }
283
- return out;
284
- }
285
- function parseNameStatus(stdout) {
286
- const out = [];
287
- const tokens = stdout.split("\0");
288
- let i = 0;
289
- while (i < tokens.length) {
290
- const tok = tokens[i];
291
- if (tok === void 0 || tok === "") {
292
- i += 1;
293
- continue;
294
- }
295
- const code = tok[0];
296
- if (code === void 0 || !/[AMDRCTU]/.test(code)) break;
297
- if (code === "R" || code === "C") {
298
- const newPath = tokens[i + 2];
299
- if (newPath === void 0) break;
300
- out.push({ path: newPath, status: statusOf(code) });
301
- i += 3;
302
- } else {
303
- const path = tokens[i + 1];
304
- if (path === void 0) break;
305
- if (path !== "") out.push({ path, status: statusOf(code) });
306
- i += 2;
307
- }
308
- }
309
- return out;
310
- }
311
- function sumNumstat(stdout) {
312
- let insertions = 0;
313
- let deletions = 0;
314
- for (const line of stdout.split("\n")) {
315
- if (line.trim() === "") continue;
316
- const parts = line.split(" ");
317
- if (parts.length < 2) continue;
318
- const add = Number(parts[0]);
319
- const del = Number(parts[1]);
320
- if (Number.isFinite(add)) insertions += add;
321
- if (Number.isFinite(del)) deletions += del;
322
- }
323
- return { insertions, deletions };
324
- }
325
-
326
- // src/host/remote.ts
327
- function classifyHost(host) {
328
- const h = host.toLowerCase();
329
- if (h === "github.com" || h.endsWith(".github.com")) return "github";
330
- if (h === "gitlab.com" || h.endsWith(".gitlab.com")) return "gitlab";
331
- if (h === "gitee.com" || h.endsWith(".gitee.com")) return "gitee";
332
- if (h === "bitbucket.org" || h.endsWith(".bitbucket.org")) return "bitbucket";
333
- return "other";
334
- }
335
- function normalizePath(path) {
336
- return path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
337
- }
338
- function splitHostPath(raw) {
339
- const url = raw.trim();
340
- if (url === "") return null;
341
- const scp = url.match(/^(?:[^@/]+@)?([^/:]+):(.+)$/);
342
- if (scp !== null && !/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
343
- const host = scp[1];
344
- const path = normalizePath(scp[2]);
345
- return host !== "" && path !== "" ? { host, path } : null;
346
- }
347
- const scheme = url.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)\/(.+)$/i);
348
- if (scheme !== null) {
349
- let authority = scheme[1];
350
- const at = authority.lastIndexOf("@");
351
- if (at >= 0) authority = authority.slice(at + 1);
352
- const host = authority.replace(/:\d+$/, "");
353
- const path = normalizePath(scheme[2]);
354
- return host !== "" && path !== "" ? { host, path } : null;
355
- }
356
- return null;
357
- }
358
- function parseRemote(name, url) {
359
- const parsed = splitHostPath(url);
360
- if (parsed === null) {
361
- return { name, url, webUrl: null, host: null, hostKind: "other" };
362
- }
363
- return {
364
- name,
365
- url,
366
- webUrl: `https://${parsed.host}/${parsed.path}`,
367
- host: parsed.host,
368
- hostKind: classifyHost(parsed.host)
369
- };
370
- }
371
-
372
- // src/host/validate.ts
373
- import { isAbsolute, normalize } from "node:path";
374
- function isSafePath(path) {
375
- if (path === "" || isAbsolute(path)) return false;
376
- const norm = normalize(path);
377
- if (norm === ".." || norm.startsWith("../") || norm.startsWith("..\\")) return false;
378
- return true;
379
- }
380
- var REV_META = /[\x00-\x20\x7f~^:?*[\\]/;
381
- function isSafeRev(input) {
382
- if (input === "" || input.startsWith("-")) return false;
383
- if (REV_META.test(input)) return false;
384
- if (input.includes("..") || input.includes("@{")) return false;
385
- return true;
386
- }
387
- function isSafeBranchName(name) {
388
- return isSafeRev(name) && !name.startsWith("/");
389
- }
390
- function isSafeRemoteName(name) {
391
- return /^[A-Za-z0-9._][A-Za-z0-9._/-]*$/.test(name) && name.length <= 128;
392
- }
393
-
394
- // src/host/core.ts
395
- var DEFAULT_CONFIG = {
396
- timeoutMs: 8e3,
397
- maxBytes: 4 * 1024 * 1024,
398
- maxChanges: 1e3,
399
- refreshIntervalMs: 3e4,
400
- showInputPill: true,
401
- defaultDiffView: "unified",
402
- graphStyle: "parallel",
403
- suggestEnabled: true,
404
- suggestMaxBytes: 64 * 1024,
405
- suggestMaxOutputTokens: 200,
406
- suggestTimeoutMs: 15e3
407
- };
408
- function normalizeConfig(raw) {
409
- const c = raw ?? {};
410
- const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
411
- const hasProvider = typeof c.suggestProvider === "string" && c.suggestProvider !== "";
412
- const hasModel = typeof c.suggestModel === "string" && c.suggestModel !== "";
413
- const route = hasProvider && hasModel ? { suggestProvider: c.suggestProvider, suggestModel: c.suggestModel } : {};
414
- return {
415
- timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
416
- maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
417
- maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
418
- refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
419
- showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
420
- defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
421
- graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
422
- suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
423
- suggestMaxBytes: num(c.suggestMaxBytes, DEFAULT_CONFIG.suggestMaxBytes),
424
- suggestMaxOutputTokens: num(c.suggestMaxOutputTokens, DEFAULT_CONFIG.suggestMaxOutputTokens),
425
- suggestTimeoutMs: num(c.suggestTimeoutMs, DEFAULT_CONFIG.suggestTimeoutMs),
426
- ...route
427
- };
428
- }
429
- function unwrapVolatile(value) {
430
- return value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
431
- }
432
- function readDiffView(value, fallback) {
433
- const raw = unwrapVolatile(value);
434
- return raw === "unified" || raw === "split" ? raw : fallback;
435
- }
436
- function readGraphStyle(value, fallback) {
437
- const raw = unwrapVolatile(value);
438
- return raw === "parallel" || raw === "compact" ? raw : fallback;
439
- }
440
- function readBool(value, fallback) {
441
- const raw = unwrapVolatile(value);
442
- return typeof raw === "boolean" ? raw : fallback;
443
- }
444
- var NEG_CACHE_MS = 15e3;
445
- function parsePrimaryRemote(stdout) {
446
- const fetchUrls = /* @__PURE__ */ new Map();
447
- const order = [];
448
- for (const line of stdout.split("\n")) {
449
- const m = line.match(/^(\S+)\t(\S+)\s+\(fetch\)$/);
450
- if (m === null) continue;
451
- const name2 = m[1];
452
- if (!fetchUrls.has(name2)) {
453
- fetchUrls.set(name2, m[2]);
454
- order.push(name2);
455
- }
456
- }
457
- if (order.length === 0) return null;
458
- const name = fetchUrls.has("origin") ? "origin" : order[0];
459
- return parseRemote(name, fetchUrls.get(name));
460
- }
461
- var STATS_FANOUT_LIMIT = 200;
462
- async function resolveBrowseRoot(deps, sessionId) {
463
- const ws = await resolveWorkspace(deps, sessionId);
464
- if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
465
- const err = ws.failure.error;
466
- if ((err.code === "not-a-git-repo" || err.code === "git-unavailable") && "cwd" in err && err.cwd !== void 0 && err.cwd !== "") {
467
- try {
468
- return { ok: true, root: await deps.fs.realpath(err.cwd), isGitRepo: false };
469
- } catch {
470
- return { ok: false, error: { code: "git-error" } };
471
- }
472
- }
473
- return { ok: false, error: mapWorkspaceFailure(ws.failure) };
474
- }
475
- async function resolveWorkspace(deps, sessionId) {
476
- let cwd = deps.sessions.liveCwd(sessionId);
477
- if (cwd === void 0 || cwd === "") {
478
- const meta = await deps.sessions.persistedMeta(sessionId);
479
- cwd = meta?.cwd;
480
- }
481
- if (cwd === void 0 || cwd === "") {
482
- return { ok: false, failure: { ok: false, error: { code: "cwd-unavailable", sessionId } } };
483
- }
484
- const cached = deps.rootCache?.get(cwd);
485
- if (cached !== void 0) return { ok: true, root: cached };
486
- const negAt = deps.rootNegCache?.get(cwd);
487
- if (negAt !== void 0) {
488
- if (Date.now() < negAt) return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
489
- deps.rootNegCache?.delete(cwd);
490
- }
491
- const top = await runCommand(deps.run, ["git", "rev-parse", "--show-toplevel"], cwd, "toplevel", deps.signal);
492
- if ("failure" in top) {
493
- return { ok: false, failure: { ok: false, error: { ...mapRunFailure(top.failure), cwd } } };
494
- }
495
- if (top.run.cancelled) return { ok: false, failure: { ok: false, error: { code: "cancelled" } } };
496
- if (top.run.timedOut) return { ok: false, failure: { ok: false, error: { code: "timeout" } } };
497
- if (top.run.exitCode !== 0) {
498
- deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
499
- return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
500
- }
501
- const raw = top.run.stdout.trim();
502
- if (raw === "") {
503
- deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
504
- return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
505
- }
506
- let root = raw;
507
- try {
508
- root = await deps.fs.realpath(raw);
509
- } catch {
510
- }
511
- deps.rootCache?.set(cwd, root);
512
- return { ok: true, root };
513
- }
514
- async function runCommand(runner, argv, cwd, _label, signal, stdinData) {
515
- try {
516
- const run = await runner.run(argv, { cwd, ...signal ? { signal } : {}, ...stdinData !== void 0 ? { stdinData } : {} });
517
- return { run };
518
- } catch (error) {
519
- return { failure: error };
520
- }
521
- }
522
- function mapRunFailure(failure) {
523
- const message = failure instanceof Error ? failure.message : String(failure);
524
- return { code: "git-unavailable", detail: message };
525
- }
526
- async function detectGitRepo(deps, cwd) {
527
- let dir;
528
- try {
529
- dir = await deps.fs.realpath(cwd);
530
- } catch {
531
- dir = cwd;
532
- }
533
- for (let i = 0; i < 100; i++) {
534
- try {
535
- await deps.fs.stat(join(dir, ".git"));
536
- return true;
537
- } catch {
538
- }
539
- const parent = dirname(dir);
540
- if (parent === dir) break;
541
- dir = parent;
542
- }
543
- return false;
544
- }
545
- function mapWorkspaceFailure(failure) {
546
- const error = failure.error;
547
- const message = "detail" in error ? error.detail : void 0;
548
- return { code: error.code, ...message !== void 0 ? { message } : {} };
549
- }
550
- async function snapshotForSession(deps, config, sessionId) {
551
- const workspace = await resolveWorkspace(deps, sessionId);
552
- if (!workspace.ok) {
553
- const failure = workspace.failure;
554
- if (failure.error.code === "not-a-git-repo") {
555
- return { ok: false, error: { ...failure.error, showInputPill: config.showInputPill } };
556
- }
557
- if (failure.error.code === "git-unavailable") {
558
- const cwd = failure.error.cwd;
559
- const isGitRepo = cwd !== void 0 && cwd !== "" ? await detectGitRepo(deps, cwd) : false;
560
- return { ok: false, error: { ...failure.error, isGitRepo } };
561
- }
562
- return failure;
563
- }
564
- const root = workspace.root;
565
- const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes, worktreeNumRes, stagedNumRes, remoteRes] = await Promise.all([
566
- runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch", deps.signal),
567
- runCommand(deps.run, ["git", "rev-parse", "--short", "HEAD"], root, "head", deps.signal),
568
- runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "status", deps.signal),
569
- runCommand(deps.run, ["git", "rev-list", "--count", "--left-right", "@{upstream}...HEAD"], root, "aheadBehind", deps.signal),
570
- runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal),
571
- runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
572
- runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal),
573
- // `git remote -v` lists every remote's fetch/push URL; parsed into the
574
- // primary remote (origin, else the first) for the left-column status bar.
575
- runCommand(deps.run, ["git", "remote", "-v"], root, "remote", deps.signal)
576
- ]);
577
- const branch = "run" in branchRes && branchRes.run.exitCode === 0 ? branchRes.run.stdout.trim() || null : null;
578
- const unborn = "run" in headRes && headRes.run.exitCode !== 0;
579
- const head = "run" in headRes && headRes.run.exitCode === 0 ? headRes.run.stdout.trim() || null : null;
580
- let allChanges = [];
581
- let statusLossy = false;
582
- if ("run" in statusRes && !statusRes.run.timedOut && statusRes.run.exitCode === 0) {
583
- allChanges = parseStatus(statusRes.run.stdout);
584
- statusLossy = statusRes.run.stdoutLossy;
585
- }
586
- const truncated = allChanges.length > config.maxChanges || statusLossy;
587
- const changes = allChanges.length > config.maxChanges ? allChanges.slice(0, config.maxChanges) : allChanges;
588
- let staged = 0;
589
- let modified = 0;
590
- let untracked = 0;
591
- for (const c of allChanges) {
592
- if (c.status === "untracked") untracked++;
593
- else if (c.staged) staged++;
594
- else modified++;
595
- }
596
- let ahead = 0;
597
- let behind = 0;
598
- const hasUpstream = "run" in aheadBehindRes && aheadBehindRes.run.exitCode === 0;
599
- if (hasUpstream && "run" in aheadBehindRes) {
600
- const parts = aheadBehindRes.run.stdout.trim().split(/\s+/);
601
- behind = Number(parts[0]) || 0;
602
- ahead = Number(parts[1]) || 0;
603
- }
604
- const remote = "run" in remoteRes && remoteRes.run.exitCode === 0 ? parsePrimaryRemote(remoteRes.run.stdout) : null;
605
- let lastCommit = null;
606
- if ("run" in lastCommitRes && lastCommitRes.run.exitCode === 0) {
607
- lastCommit = commitFromFields(lastCommitRes.run.stdout.trim().split(""));
608
- }
609
- const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
610
- const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
611
- const statsDetailed = allChanges.length <= STATS_FANOUT_LIMIT;
612
- let untrackedInsertions = 0;
613
- if (statsDetailed) {
614
- const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
615
- const safeUntracked = untrackedPaths.filter(isSafePath);
616
- if (safeUntracked.length > 0) {
617
- const perFile = await Promise.all(
618
- safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
619
- );
620
- for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
621
- }
622
- }
623
- const lastChangeAt = statsDetailed ? await maxChangeMtime(deps, root, allChanges) : null;
624
- const distinct = new Set(allChanges.map((c) => c.path));
625
- const stats = {
626
- fileCount: distinct.size,
627
- staged,
628
- modified,
629
- untracked,
630
- insertions: wt.insertions + stg.insertions + untrackedInsertions,
631
- deletions: wt.deletions + stg.deletions,
632
- lastChangeAt,
633
- headCommittedAt: lastCommit?.dateIso ?? null,
634
- partial: !statsDetailed
635
- };
636
- const snapshot = {
637
- root,
638
- branch,
639
- head,
640
- unborn,
641
- dirty: allChanges.length > 0,
642
- staged,
643
- modified,
644
- untracked,
645
- ahead,
646
- behind,
647
- hasUpstream,
648
- remote,
649
- lastCommit,
650
- changes,
651
- stats,
652
- truncated,
653
- refreshIntervalMs: config.refreshIntervalMs,
654
- showInputPill: config.showInputPill,
655
- defaultDiffView: config.defaultDiffView,
656
- graphStyle: config.graphStyle,
657
- suggestEnabled: config.suggestEnabled,
658
- checkedAt: Date.now()
659
- };
660
- return { ok: true, value: snapshot };
661
- }
662
- async function maxChangeMtime(deps, root, changes, cap = 200) {
663
- let max = null;
664
- const slice = changes.slice(0, cap);
665
- await Promise.all(slice.map(async (c) => {
666
- if (c.isDirectory) return;
667
- try {
668
- const info = await deps.fs.stat(join(root, c.path));
669
- if (Number.isFinite(info.mtimeMs)) {
670
- max = max === null ? info.mtimeMs : Math.max(max, info.mtimeMs);
671
- }
672
- } catch {
673
- }
674
- }));
675
- return max;
676
- }
677
-
678
- // src/host/actions.ts
679
- import { join as join2, sep } from "node:path";
680
- function withPaths(prefixes, paths) {
681
- if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
682
- for (const path of paths) {
683
- if (!isSafePath(path)) return { error: "invalid-path", message: `unsafe path: ${path}` };
684
- }
685
- return { argv: prefixes.map((prefix) => [...prefix, ...paths]) };
686
- }
687
- function planAction(action, unborn) {
688
- switch (action.kind) {
689
- case "stage":
690
- return withPaths([["git", "add", "--"]], action.paths);
691
- case "stage-all":
692
- return { argv: [["git", "add", "-A"]] };
693
- case "unstage":
694
- return unborn ? withPaths([["git", "rm", "--cached", "-r", "--"]], action.paths) : withPaths([["git", "restore", "--staged", "--"]], action.paths);
695
- case "unstage-all":
696
- return unborn ? { argv: [["git", "rm", "--cached", "-r", "--", "."]] } : { argv: [["git", "restore", "--staged", "--", "."]] };
697
- case "discard":
698
- return withPaths([["git", "restore", "--"]], action.paths);
699
- case "commit": {
700
- const message = action.message.trim();
701
- const amend = action.amend === true;
702
- if (message === "" && !amend) return { error: "empty-message" };
703
- const amendFlag = amend ? ["--amend"] : [];
704
- const msgArgs = message === "" ? ["--no-edit"] : ["-m", message];
705
- if (action.paths === void 0 || action.paths.length === 0) {
706
- return { argv: [["git", "commit", ...amendFlag, ...msgArgs]] };
707
- }
708
- const staged = withPaths([["git", "add", "--"]], action.paths);
709
- if ("error" in staged) return staged;
710
- const commitCmd = ["git", "commit", ...amendFlag, ...msgArgs, "--", ...action.paths];
711
- return { argv: [...staged.argv, commitCmd] };
712
- }
713
- case "branch-checkout":
714
- if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe branch name: ${action.name}` };
715
- return { argv: [["git", "checkout", "--end-of-options", action.name]] };
716
- case "fetch":
717
- return { argv: [["git", "fetch", "--all", "--prune"]] };
718
- case "pull-ff":
719
- return { argv: [["git", "pull", "--ff-only"]] };
720
- case "push":
721
- return { argv: [["git", "push"]] };
722
- case "publish":
723
- if (!isSafeRemoteName(action.remote)) return { error: "invalid-name", message: `unsafe remote name: ${action.remote}` };
724
- return { argv: [["git", "push", "-u", action.remote, "HEAD"]] };
725
- case "tag-create": {
726
- if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
727
- if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
728
- const msg = action.message?.trim() ?? "";
729
- const flags = msg === "" ? [] : ["-a", "-m", msg];
730
- return { argv: [["git", "tag", ...flags, "--end-of-options", action.name, action.commit]] };
731
- }
732
- case "tag-delete":
733
- if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
734
- return { argv: [["git", "tag", "-d", "--end-of-options", action.name]] };
735
- case "stash-push": {
736
- const msg = action.message?.trim() ?? "";
737
- return { argv: [msg === "" ? ["git", "stash", "push"] : ["git", "stash", "push", "-m", msg]] };
738
- }
739
- case "stash-apply":
740
- case "stash-pop":
741
- case "stash-drop": {
742
- if (!Number.isInteger(action.index) || action.index < 0) {
743
- return { error: "invalid-index", message: `invalid stash index: ${action.index}` };
744
- }
745
- const verb = action.kind === "stash-apply" ? "apply" : action.kind === "stash-pop" ? "pop" : "drop";
746
- return { argv: [["git", "stash", verb, "--end-of-options", `stash@{${action.index}}`]] };
747
- }
748
- case "revert": {
749
- if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
750
- return { argv: [["git", "revert", "--no-edit", "--end-of-options", action.commit]] };
751
- }
752
- case "reset": {
753
- if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
754
- if (action.mode !== "soft" && action.mode !== "mixed" && action.mode !== "hard") {
755
- return { error: "invalid-name", message: `invalid reset mode: ${String(action.mode)}` };
756
- }
757
- return { argv: [["git", "reset", `--${action.mode}`, "--end-of-options", action.commit]] };
758
- }
759
- }
760
- }
761
- function isIndexBusy(outcome) {
762
- if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
763
- return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
764
- }
765
- function scrubCredentials(text) {
766
- return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi, "$1");
767
- }
768
- function classifyActionFailure(stdout, stderr, exitCode) {
769
- const combined = stderr + stdout;
770
- const err = scrubCredentials(stderr.trim());
771
- if (/CONFLICT|Merge conflict|needs merge|could not restore untracked/i.test(combined)) {
772
- return { code: "conflict", message: err || "merge conflict" };
773
- }
774
- if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
775
- return { code: "index-busy", message: err };
776
- }
777
- if (/nothing to commit|no changes added/i.test(combined)) {
778
- return { code: "git-error", message: err || "nothing to commit" };
779
- }
780
- if (/No such ref|not a valid reference|is not a stash|no tag|tag .* not found|unknown revision|bad revision|Could not parse object|ambiguous argument/i.test(combined)) {
781
- return { code: "not-found", message: err || "not found" };
782
- }
783
- if (/Authentication failed|could not read (Username|Password)|terminal prompts disabled|Permission denied \(publickey\)|Could not read from remote repository/i.test(combined)) {
784
- return { code: "auth-failed", message: err || "authentication failed" };
785
- }
786
- if (/\[rejected\]|Updates were rejected|failed to push some refs|fetch first|tip of your current branch is behind/i.test(combined)) {
787
- return { code: "push-rejected", message: err || "push rejected (remote has new commits; fetch first)" };
788
- }
789
- if (/Not possible to fast-forward|cannot fast-forward|not a fast-forward|Need to specify how to reconcile divergent branches/i.test(combined)) {
790
- return { code: "not-ff", message: err || "cannot fast-forward (branches diverged)" };
791
- }
792
- if (/would be overwritten by (checkout|merge)|local changes|overwritten by merge|Your local changes/i.test(stderr)) {
793
- return { code: "local-changes-block", message: err };
794
- }
795
- if (/is a merge but no -m|mainline parent/i.test(combined)) {
796
- return { code: "revert-merge", message: err || "cannot revert a merge commit from the panel" };
797
- }
798
- return { code: "git-error", message: err || `git exited ${exitCode}` };
799
- }
800
- async function runAction(deps, config, request) {
801
- const workspace = await resolveWorkspace(deps, request.sessionId);
802
- if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
803
- const root = workspace.root;
804
- const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
805
- const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
806
- const act = request.action;
807
- if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
808
- if (!Number.isInteger(act.index) || act.index < 0) {
809
- return { ok: false, error: { code: "invalid-index", message: `invalid stash index: ${act.index}` } };
810
- }
811
- const probe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", "--end-of-options", `stash@{${act.index}}`], root, "stash-verify", deps.signal);
812
- const sha = "run" in probe && probe.run.exitCode === 0 ? probe.run.stdout.trim() : "";
813
- if (sha === "" || sha !== act.sha) {
814
- return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
815
- }
816
- }
817
- if (request.action.kind === "discard") {
818
- const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
819
- if (removed !== null) {
820
- if (!removed.ok) return removed.result;
821
- if (removed.remainingTracked.length === 0) {
822
- const snapshot2 = await snapshotForSession(deps, config, request.sessionId);
823
- if (!snapshot2.ok) return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
824
- return { ok: true, snapshot: snapshot2.value, output: "" };
825
- }
826
- request = { ...request, action: { kind: "discard", paths: removed.remainingTracked } };
827
- }
828
- }
829
- const plan = planAction(request.action, unborn);
830
- if ("error" in plan) return { ok: false, error: { code: plan.error, ...plan.message ? { message: plan.message } : {} } };
831
- let lastOutput = "";
832
- for (let step = 0; step < plan.argv.length; step += 1) {
833
- const argv = plan.argv[step];
834
- let outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
835
- if (isIndexBusy(outcome)) {
836
- await new Promise((resolve) => setTimeout(resolve, 150));
837
- outcome = await runCommand(deps.run, argv, root, "action-retry", deps.signal);
838
- }
839
- const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
840
- if ("failure" in outcome) {
841
- const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
842
- return { ok: false, error: { code: "git-unavailable", message: message + where } };
843
- }
844
- if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
845
- if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
846
- lastOutput = outcome.run.stdout || outcome.run.stderr;
847
- if (outcome.run.exitCode !== 0) {
848
- const failure = classifyActionFailure(outcome.run.stdout, outcome.run.stderr, outcome.run.exitCode ?? -1);
849
- if (request.action.kind === "revert" && failure.code === "conflict") {
850
- let abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort", deps.signal);
851
- if (isIndexBusy(abort)) {
852
- await new Promise((resolve) => setTimeout(resolve, 150));
853
- abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort-retry", deps.signal);
854
- }
855
- const aborted = "run" in abort && abort.run.exitCode === 0;
856
- return aborted ? { ok: false, error: { code: "revert-conflict", message: failure.message + where } } : { ok: false, error: { code: "revert-stuck", message: failure.message + where } };
857
- }
858
- return { ok: false, error: { code: failure.code, message: failure.message + where } };
859
- }
860
- if (request.action.kind === "stash-push" && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
861
- return { ok: false, error: { code: "git-error", message: (outcome.run.stdout.trim() || "No local changes to save") + where } };
862
- }
863
- }
864
- const snapshot = await snapshotForSession(deps, config, request.sessionId);
865
- if (!snapshot.ok) {
866
- return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
867
- }
868
- return { ok: true, snapshot: snapshot.value, output: lastOutput.trim() };
869
- }
870
- async function discardUntracked(deps, config, root, sessionId, paths) {
871
- const snap = await snapshotForSession(deps, config, sessionId);
872
- if (!snap.ok) return null;
873
- const untrackedSet = new Set(snap.value.changes.filter((c) => c.status === "untracked").map((c) => c.path));
874
- const untracked = paths.filter((p) => untrackedSet.has(p));
875
- if (untracked.length === 0) return null;
876
- const tracked = paths.filter((p) => !untrackedSet.has(p));
877
- let rootReal;
878
- try {
879
- rootReal = await deps.fs.realpath(root);
880
- } catch {
881
- return { ok: false, result: { ok: false, error: { code: "git-error", message: "repository root unavailable" } } };
882
- }
883
- for (const path of untracked) {
884
- if (!isSafePath(path)) return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `unsafe path: ${path}` } } };
885
- const target = join2(root, path);
886
- let targetReal;
887
- try {
888
- targetReal = await deps.fs.realpath(target);
889
- } catch {
890
- continue;
891
- }
892
- if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep)) {
893
- return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `path escapes repository: ${path}` } } };
894
- }
895
- await deps.fs.remove(target).catch(() => {
896
- });
897
- }
898
- return { ok: true, remainingTracked: tracked };
899
- }
900
-
901
- // src/host/queries.ts
902
- import { join as join3, sep as sep2 } from "node:path";
903
-
904
- // src/host/types.ts
905
- var IMAGE_MIME = {
906
- png: "image/png",
907
- jpg: "image/jpeg",
908
- jpeg: "image/jpeg",
909
- gif: "image/gif",
910
- webp: "image/webp",
911
- avif: "image/avif",
912
- bmp: "image/bmp",
913
- svg: "image/svg+xml",
914
- ico: "image/x-icon",
915
- tif: "image/tiff",
916
- tiff: "image/tiff"
917
- };
918
- function imageMimeFor(path) {
919
- const dot = path.lastIndexOf(".");
920
- if (dot < 0) return null;
921
- return IMAGE_MIME[path.slice(dot + 1).toLowerCase()] ?? null;
922
- }
923
-
924
- // src/host/queries.ts
925
- var GRAPH_FORMAT = "--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e";
926
- var VISIBLE_REFS = ["--branches", "--remotes", "--tags", "HEAD"];
927
- function isHexLike(text) {
928
- return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
929
- }
930
- async function runQuery(deps, config, request) {
931
- const q = request.query;
932
- if (q.kind === "dir-list" || q.kind === "file-content") {
933
- const browse = await resolveBrowseRoot(deps, request.sessionId);
934
- if (!browse.ok) return { ok: false, error: browse.error };
935
- try {
936
- return q.kind === "dir-list" ? await queryDirList(deps, browse.root, q, browse.isGitRepo) : await queryFileContent(deps, config, browse.root, q);
937
- } catch (error) {
938
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
939
- }
940
- }
941
- const workspace = await resolveWorkspace(deps, request.sessionId);
942
- if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
943
- const root = workspace.root;
944
- try {
945
- switch (q.kind) {
946
- case "history":
947
- return await queryHistory(deps, root, q);
948
- case "diff":
949
- return await queryDiff(deps, root, q);
950
- case "file-lines":
951
- return await queryFileLines(deps, root, q);
952
- case "image-diff":
953
- return await queryImageDiff(deps, config, root, q);
954
- case "show":
955
- return await queryShow(deps, root, q.ref);
956
- case "branches":
957
- return await queryBranches(deps, root);
958
- case "tags":
959
- return await queryTags(deps, root);
960
- case "stash-list":
961
- return await queryStashList(deps, root);
962
- case "authors":
963
- return await queryAuthors(deps, root);
964
- case "last-commit-message":
965
- return await queryLastCommitMessage(deps, root);
966
- case "worktree-stats":
967
- return await queryWorktreeStats(deps, config, request.sessionId);
968
- case "pull-preview":
969
- return await queryPullPreview(deps, root);
970
- case "push-preview":
971
- return await queryPushPreview(deps, root);
972
- }
973
- } catch (error) {
974
- const message = error instanceof Error ? error.message : String(error);
975
- return { ok: false, error: { code: "git-error", message } };
976
- }
977
- }
978
- async function queryHistory(deps, root, q) {
979
- const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
980
- const skip = Number.isFinite(q.skip) ? Math.max(0, Math.trunc(q.skip)) : 0;
981
- const args = ["git", "log", "--topo-order", "--decorate=full", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
982
- const search = q.search?.trim() ?? "";
983
- const hexJump = search !== "" && isHexLike(search);
984
- const countArgs = ["git", "rev-list", "--count"];
985
- if (!hexJump) {
986
- const filters = [];
987
- if (search !== "") filters.push("-i", "-E", `--grep=${search}`);
988
- if (q.author !== void 0 && q.author !== "") filters.push(`--author=${q.author}`);
989
- if (q.since !== void 0 && q.since !== "") filters.push(`--since=${q.since}`);
990
- if (q.ref !== void 0 && q.ref !== "") {
991
- if (!isSafeRev(q.ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${q.ref}` } };
992
- filters.push("--end-of-options", q.ref);
993
- } else {
994
- filters.push(...VISIBLE_REFS);
995
- }
996
- args.push(...filters);
997
- countArgs.push(...filters);
998
- } else {
999
- args.push("--end-of-options", search);
1000
- }
1001
- const [res, countRes] = await Promise.all([
1002
- runCommand(deps.run, args, root, "history", deps.signal),
1003
- hexJump ? Promise.resolve(null) : runCommand(deps.run, countArgs, root, "history-count", deps.signal)
1004
- ]);
1005
- if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
1006
- if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1007
- if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
1008
- if (res.run.exitCode !== 0) {
1009
- const stderr = res.run.stderr.trim();
1010
- if (hexJump || /unknown revision|bad revision|does not have any commits|ambiguous argument/i.test(stderr)) {
1011
- return { ok: true, value: { kind: "history", commits: [], total: 0 } };
1012
- }
1013
- return { ok: false, error: { code: "git-error", message: stderr || `git exited ${res.run.exitCode}` } };
1014
- }
1015
- const commits = parseGraphLog(res.run.stdout);
1016
- let total = -1;
1017
- if (!hexJump && countRes !== null && "run" in countRes && countRes.run.exitCode === 0) {
1018
- const n = Number(countRes.run.stdout.trim());
1019
- if (Number.isFinite(n)) total = n;
1020
- }
1021
- return { ok: true, value: { kind: "history", commits, total } };
1022
- }
1023
- async function queryDiff(deps, root, q) {
1024
- if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
1025
- const ctx = q.context !== void 0 && Number.isFinite(q.context) ? Math.max(0, Math.min(1e5, Math.floor(q.context))) : 3;
1026
- const unified = `-U${ctx}`;
1027
- let args;
1028
- if (q.base === "staged") {
1029
- args = ["git", "diff", unified, "--cached", "--", q.path];
1030
- } else if (q.base === "commit") {
1031
- if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
1032
- args = ["git", "show", unified, "--end-of-options", q.commit, "--", q.path];
1033
- } else {
1034
- args = ["git", "diff", unified, "--", q.path];
1035
- }
1036
- const res = await runCommand(deps.run, args, root, "diff", deps.signal);
1037
- if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
1038
- if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1039
- if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
1040
- let text = res.run.stdout;
1041
- if (q.base === "worktree" && text.trim() === "") {
1042
- const tracked = await runCommand(deps.run, ["git", "ls-files", "--error-unmatch", "--", q.path], root, "diff-tracked-probe", deps.signal);
1043
- const isUntracked = !("run" in tracked) || tracked.run.exitCode !== 0;
1044
- if (isUntracked) {
1045
- const noIndex = await runCommand(deps.run, ["git", "diff", unified, "--no-index", "--", "/dev/null", q.path], root, "diff-untracked", deps.signal);
1046
- if ("run" in noIndex) text = noIndex.run.stdout;
1047
- }
1048
- }
1049
- return { ok: true, value: { kind: "diff", path: q.path, text } };
1050
- }
1051
- async function queryFileLines(deps, root, q) {
1052
- if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
1053
- const start = Number.isFinite(q.start) ? Math.max(1, Math.floor(q.start)) : 1;
1054
- const end = Number.isFinite(q.end) ? Math.max(start, Math.floor(q.end)) : start;
1055
- let args;
1056
- if (q.base === "commit") {
1057
- if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
1058
- args = ["git", "show", "--end-of-options", `${q.commit}:${q.path}`];
1059
- } else {
1060
- args = ["git", "show", `--end-of-options`, `:${q.path}`];
1061
- if (q.base === "worktree") args = ["git", "cat-file", "-p", `:${q.path}`];
1062
- }
1063
- const res = await runCommand(deps.run, args, root, "file-lines", deps.signal);
1064
- if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
1065
- if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1066
- if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
1067
- if (res.run.exitCode !== 0) {
1068
- if (q.base === "worktree") {
1069
- const wt = await readWorktreeText(deps, root, q.path);
1070
- if (wt !== null) return sliceLines(q.path, wt, start, end);
1071
- }
1072
- return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "no such blob" } };
1073
- }
1074
- return sliceLines(q.path, res.run.stdout, start, end);
1075
- }
1076
- async function readWorktreeText(deps, root, path) {
1077
- try {
1078
- const file = join3(root, path);
1079
- const real = await deps.fs.realpath(file);
1080
- const rootReal = await deps.fs.realpath(root);
1081
- if (real !== rootReal && !real.startsWith(rootReal + sep2)) return null;
1082
- const buf = await deps.fs.readFile(file);
1083
- return buf.toString("utf8");
1084
- } catch {
1085
- return null;
1086
- }
1087
- }
1088
- function sliceLines(path, content, start, end) {
1089
- const all = content.split("\n");
1090
- if (all[all.length - 1] === "") all.pop();
1091
- const from = Math.min(start, all.length + 1);
1092
- const to = Math.min(end, all.length);
1093
- const lines = from <= to ? all.slice(from - 1, to) : [];
1094
- const eof = to >= all.length;
1095
- return { ok: true, value: { kind: "file-lines", path, start: from, lines, eof } };
1096
- }
1097
- async function queryImageDiff(deps, config, root, q) {
1098
- if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
1099
- if (q.base === "commit" && !isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
1100
- const mime = imageMimeFor(q.path);
1101
- if (mime === null) return { ok: true, value: { kind: "image-diff", path: q.path, mime } };
1102
- const oldSpec = q.base === "staged" ? `HEAD:${q.path}` : q.base === "commit" ? `${q.commit}^1:${q.path}` : `:${q.path}`;
1103
- const newSpec = q.base === "staged" ? `:${q.path}` : q.base === "commit" ? `${q.commit}:${q.path}` : null;
1104
- const [oldOid, newOid] = await Promise.all([
1105
- resolveOid(deps, root, oldSpec),
1106
- newSpec === null ? Promise.resolve(void 0) : resolveOid(deps, root, newSpec)
1107
- ]);
1108
- const cap = config.maxBytes;
1109
- const gitDir = oldOid !== void 0 || newOid !== void 0 ? await absoluteGitDir(deps, root) : "";
1110
- const [oldSide, newSide] = await Promise.all([
1111
- oldOid === void 0 ? Promise.resolve(void 0) : blobSide(deps, gitDir, oldOid, cap),
1112
- newOid !== void 0 ? blobSide(deps, gitDir, newOid, cap) : q.base === "worktree" ? worktreeSide(deps, root, q.path, cap) : Promise.resolve(void 0)
1113
- ]);
1114
- const sides = [oldSide, newSide];
1115
- if (sides.some((s) => s !== void 0 && "tooLarge" in s)) {
1116
- return { ok: true, value: { kind: "image-diff", path: q.path, mime, tooLarge: true } };
1117
- }
1118
- const old64 = sides[0] !== void 0 && !("tooLarge" in sides[0]) ? sides[0].data : void 0;
1119
- const new64 = sides[1] !== void 0 && !("tooLarge" in sides[1]) ? sides[1].data : void 0;
1120
- return {
1121
- ok: true,
1122
- value: {
1123
- kind: "image-diff",
1124
- path: q.path,
1125
- mime,
1126
- ...old64 !== void 0 ? { old: `data:${mime};base64,${old64}` } : {},
1127
- ...new64 !== void 0 ? { new: `data:${mime};base64,${new64}` } : {}
1128
- }
1129
- };
1130
- }
1131
- async function resolveOid(deps, root, spec) {
1132
- const res = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", spec], root, "image-oid", deps.signal);
1133
- if (!("run" in res) || res.run.exitCode !== 0) return void 0;
1134
- const oid = res.run.stdout.trim();
1135
- return /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(oid) ? oid : void 0;
1136
- }
1137
- async function absoluteGitDir(deps, root) {
1138
- const res = await runCommand(deps.run, ["git", "rev-parse", "--absolute-git-dir"], root, "git-dir", deps.signal);
1139
- if (!("run" in res) || res.run.exitCode !== 0) throw new Error("git dir unavailable");
1140
- return res.run.stdout.trim();
1141
- }
1142
- async function blobSide(deps, gitDir, oid, cap) {
1143
- const sizeRes = await runCommand(deps.run, ["git", "cat-file", "-s", oid], gitDir, "image-size", deps.signal);
1144
- if (!("run" in sizeRes) || sizeRes.run.exitCode !== 0) return void 0;
1145
- const size = Number(sizeRes.run.stdout.trim());
1146
- if (!Number.isFinite(size)) return void 0;
1147
- if (size > cap) return { tooLarge: true };
1148
- const nameRes = await runCommand(deps.run, ["git", "unpack-file", oid], gitDir, "image-unpack", deps.signal);
1149
- if (!("run" in nameRes) || nameRes.run.exitCode !== 0) return void 0;
1150
- const name = nameRes.run.stdout.trim();
1151
- if (!/^[A-Za-z0-9._-]+$/.test(name)) return void 0;
1152
- const file = join3(gitDir, name);
1153
- try {
1154
- const buf = await deps.fs.readFile(file);
1155
- if (buf.length > cap) return { tooLarge: true };
1156
- return { data: buf.toString("base64") };
1157
- } catch {
1158
- return void 0;
1159
- } finally {
1160
- await deps.fs.remove(file).catch(() => {
1161
- });
1162
- }
1163
- }
1164
- async function worktreeSide(deps, root, path, cap) {
1165
- try {
1166
- const file = join3(root, path);
1167
- const real = await deps.fs.realpath(file);
1168
- const rootReal = await deps.fs.realpath(root);
1169
- if (real !== rootReal && !real.startsWith(rootReal + sep2)) return void 0;
1170
- const info = await deps.fs.stat(file);
1171
- if (info.size > cap) return { tooLarge: true };
1172
- const buf = await deps.fs.readFile(file);
1173
- if (buf.length > cap) return { tooLarge: true };
1174
- return { data: buf.toString("base64") };
1175
- } catch {
1176
- return void 0;
1177
- }
1178
- }
1179
- var DIR_ENTRY_CAP = 2e3;
1180
- async function queryDirList(deps, root, q, isGitRepo) {
1181
- const rel = q.path;
1182
- if (rel !== "" && !isSafePath(rel) || hasGitSegment(rel)) return { ok: false, error: { code: "invalid-path", message: rel } };
1183
- const dir = rel === "" ? root : join3(root, rel);
1184
- const inside = await isInsideRoot(deps, root, dir, true);
1185
- if (!inside) return { ok: false, error: { code: "invalid-path", message: rel } };
1186
- let raw;
1187
- try {
1188
- raw = await deps.fs.readdir(dir);
1189
- } catch (error) {
1190
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "readdir failed" } };
1191
- }
1192
- const filtered = raw.filter((e) => e.name !== ".git");
1193
- filtered.sort((a, b) => a.isDirectory !== b.isDirectory ? a.isDirectory ? -1 : 1 : a.name.localeCompare(b.name));
1194
- const truncated = filtered.length > DIR_ENTRY_CAP;
1195
- const slice = truncated ? filtered.slice(0, DIR_ENTRY_CAP) : filtered;
1196
- const ignored = isGitRepo ? await ignoredEntries(deps, root, rel, slice.map((e) => e.name)) : /* @__PURE__ */ new Set();
1197
- const entries = await Promise.all(slice.map(async (e) => {
1198
- const ignoreFlag = ignored.has(e.name) ? { ignored: true } : {};
1199
- if (e.isDirectory) return { name: e.name, dir: true, ...ignoreFlag };
1200
- let size;
1201
- try {
1202
- size = (await deps.fs.stat(join3(dir, e.name))).size;
1203
- } catch {
1204
- size = void 0;
1205
- }
1206
- return { name: e.name, dir: false, ...size !== void 0 ? { size } : {}, ...ignoreFlag };
1207
- }));
1208
- const path = rel === "" ? "" : rel.replace(/\/+$/, "");
1209
- return { ok: true, value: { kind: "dir-list", path, entries, truncated } };
1210
- }
1211
- async function ignoredEntries(deps, root, parent, names) {
1212
- const ignored = /* @__PURE__ */ new Set();
1213
- const paths = names.map((name) => parent === "" ? name : `${parent}/${name}`);
1214
- for (let index = 0; index < paths.length; ) {
1215
- const batch = [];
1216
- let bytes = 0;
1217
- while (index < paths.length && batch.length < 128) {
1218
- const candidate = paths[index];
1219
- const size = Buffer.byteLength(candidate) + 1;
1220
- if (batch.length > 0 && bytes + size > 32 * 1024) break;
1221
- batch.push(candidate);
1222
- bytes += size;
1223
- index++;
1224
- }
1225
- const result = await runCommand(deps.run, ["git", "check-ignore", "-z", "--stdin"], root, "dir-ignore", deps.signal, `${batch.join("\0")}\0`);
1226
- if ("failure" in result || result.run.timedOut || result.run.cancelled || result.run.stdoutLossy || ![0, 1].includes(result.run.exitCode ?? -1)) continue;
1227
- const matches = new Set(result.run.stdout.split("\0"));
1228
- for (const path of batch) if (matches.has(path)) ignored.add(parent === "" ? path : path.slice(parent.length + 1));
1229
- }
1230
- return ignored;
1231
- }
1232
- async function queryFileContent(deps, config, root, q) {
1233
- if (!isSafePath(q.path) || hasGitSegment(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
1234
- const file = join3(root, q.path);
1235
- const inside = await isInsideRoot(deps, root, file, true);
1236
- if (!inside) return { ok: false, error: { code: "invalid-path", message: q.path } };
1237
- const cap = config.maxBytes;
1238
- let info;
1239
- try {
1240
- info = await deps.fs.stat(file);
1241
- } catch (error) {
1242
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "stat failed" } };
1243
- }
1244
- const mime = imageMimeFor(q.path);
1245
- const tooLarge = { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true } };
1246
- if (info.size > cap) return tooLarge;
1247
- let buf;
1248
- try {
1249
- buf = await deps.fs.readFile(file);
1250
- } catch (error) {
1251
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "read failed" } };
1252
- }
1253
- if (buf.length > cap) return tooLarge;
1254
- if (mime !== null) {
1255
- return { ok: true, value: { kind: "file-content", path: q.path, variant: "image", dataUrl: `data:${mime};base64,${buf.toString("base64")}` } };
1256
- }
1257
- if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary" } };
1258
- const content = buf.toString("utf8");
1259
- const lines = content === "" ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
1260
- return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines } };
1261
- }
1262
- function hasGitSegment(path) {
1263
- return path.split(/[\\/]/).includes(".git");
1264
- }
1265
- async function isInsideRoot(deps, root, path, hideGit = false) {
1266
- try {
1267
- const real = await deps.fs.realpath(path);
1268
- const rootReal = await deps.fs.realpath(root);
1269
- if (real !== rootReal && !real.startsWith(rootReal + sep2)) return false;
1270
- return !hideGit || !hasGitSegment(real.slice(rootReal.length + 1));
1271
- } catch {
1272
- return false;
1273
- }
1274
- }
1275
- function isBinaryBuffer(buf) {
1276
- const n = Math.min(buf.length, 8192);
1277
- for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
1278
- return false;
1279
- }
1280
- async function queryShow(deps, root, ref) {
1281
- if (!isSafeRev(ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${ref}` } };
1282
- const metaFormat = "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b";
1283
- const [metaRes, statRes, numstatRes] = await Promise.all([
1284
- runCommand(deps.run, ["git", "show", "-s", metaFormat, "--end-of-options", ref], root, "show-meta", deps.signal),
1285
- runCommand(deps.run, ["git", "show", "--name-status", "-z", "--format=", "--end-of-options", ref], root, "show-stat", deps.signal),
1286
- runCommand(deps.run, ["git", "show", "--numstat", "--format=", "--end-of-options", ref], root, "show-numstat", deps.signal)
1287
- ]);
1288
- if (!("run" in metaRes)) return { ok: false, error: { code: "git-unavailable" } };
1289
- if (metaRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1290
- if (metaRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
1291
- if (metaRes.run.exitCode !== 0) {
1292
- return { ok: false, error: { code: "git-error", message: metaRes.run.stderr.trim() || "unknown ref" } };
1293
- }
1294
- const parts = metaRes.run.stdout.split("");
1295
- const commit = commitFromFields(parts);
1296
- const body = commit === null ? "" : parts.slice(5).join("").trim();
1297
- const stats = "run" in statRes && statRes.run.exitCode === 0 ? parseNameStatus(statRes.run.stdout) : [];
1298
- const { insertions, deletions } = stats.length > 0 && "run" in numstatRes && numstatRes.run.exitCode === 0 ? sumNumstat(numstatRes.run.stdout) : { insertions: 0, deletions: 0 };
1299
- return { ok: true, value: { kind: "show", ref, commit, body, stats, insertions, deletions } };
1300
- }
1301
- async function queryBranches(deps, root) {
1302
- const fmt = "--format=%(refname:short)%00%(objectname:short)%00%(upstream:track)";
1303
- const [localRes, remoteRes, currentRes, defaultRes] = await Promise.all([
1304
- runCommand(deps.run, ["git", "for-each-ref", "--sort=-committerdate", fmt, "refs/heads"], root, "branches-local", deps.signal),
1305
- runCommand(deps.run, ["git", "for-each-ref", "--sort=-committerdate", fmt, "refs/remotes"], root, "branches-remote", deps.signal),
1306
- runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch-current", deps.signal),
1307
- runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], root, "branch-default", deps.signal)
1308
- ]);
1309
- const local = "run" in localRes && localRes.run.exitCode === 0 ? parseBranches(localRes.run.stdout) : [];
1310
- const remote = "run" in remoteRes && remoteRes.run.exitCode === 0 ? parseBranches(remoteRes.run.stdout).filter((b) => !b.name.endsWith("/HEAD")) : [];
1311
- const current = "run" in currentRes && currentRes.run.exitCode === 0 ? currentRes.run.stdout.trim() || null : null;
1312
- let defaultBranch = null;
1313
- if ("run" in defaultRes && defaultRes.run.exitCode === 0) {
1314
- const raw = defaultRes.run.stdout.trim();
1315
- defaultBranch = raw.replace(/^origin\//, "") || null;
1316
- }
1317
- return { ok: true, value: { kind: "branches", current, defaultBranch, local, remote } };
1318
- }
1319
- async function queryTags(deps, root) {
1320
- const res = await runCommand(deps.run, ["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)%00%(objectname:short)", "refs/tags"], root, "tags", deps.signal);
1321
- const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
1322
- return { ok: true, value: { kind: "tags", tags } };
1323
- }
1324
- async function queryStashList(deps, root) {
1325
- const res = await runCommand(deps.run, ["git", "stash", "list", "-z", "--format=%gd%x1f%H%x1f%gs%x1f%cr"], root, "stash-list", deps.signal);
1326
- const entries = "run" in res && res.run.exitCode === 0 ? parseStashList(res.run.stdout) : [];
1327
- return { ok: true, value: { kind: "stash-list", entries } };
1328
- }
1329
- async function queryAuthors(deps, root) {
1330
- const res = await runCommand(deps.run, ["git", "log", ...VISIBLE_REFS, "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
1331
- const authors = "run" in res && res.run.exitCode === 0 ? [...new Set(res.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== ""))].sort((a, b) => a.localeCompare(b)) : [];
1332
- return { ok: true, value: { kind: "authors", authors } };
1333
- }
1334
- async function queryLastCommitMessage(deps, root) {
1335
- const res = await runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "last-message", deps.signal);
1336
- const message = "run" in res && res.run.exitCode === 0 ? res.run.stdout.replace(/\n+$/, "") : "";
1337
- return { ok: true, value: { kind: "last-commit-message", message } };
1338
- }
1339
- async function queryPullPreview(deps, root) {
1340
- const empty = { kind: "pull-preview", hasUpstream: false, commits: 0, files: 0, insertions: 0, deletions: 0 };
1341
- const commitsRes = await runCommand(deps.run, ["git", "rev-list", "--count", "--end-of-options", "HEAD..@{upstream}"], root, "pull-preview-count", deps.signal);
1342
- if (!("run" in commitsRes) || commitsRes.run.exitCode !== 0) return { ok: true, value: empty };
1343
- const commits = Number(commitsRes.run.stdout.trim()) || 0;
1344
- const diffRes = await runCommand(deps.run, ["git", "diff", "--numstat", "--end-of-options", "HEAD", "@{upstream}"], root, "pull-preview-diff", deps.signal);
1345
- let files = 0;
1346
- let insertions = 0;
1347
- let deletions = 0;
1348
- if ("run" in diffRes && diffRes.run.exitCode === 0) {
1349
- files = diffRes.run.stdout.split("\n").filter((l) => l.trim() !== "").length;
1350
- const sum = sumNumstat(diffRes.run.stdout);
1351
- insertions = sum.insertions;
1352
- deletions = sum.deletions;
1353
- }
1354
- return { ok: true, value: { kind: "pull-preview", hasUpstream: true, commits, files, insertions, deletions } };
1355
- }
1356
- async function queryPushPreview(deps, root) {
1357
- const empty = { kind: "push-preview", hasUpstream: false, commits: 0, files: 0, insertions: 0, deletions: 0 };
1358
- const commitsRes = await runCommand(deps.run, ["git", "rev-list", "--count", "--end-of-options", "@{upstream}..HEAD"], root, "push-preview-count", deps.signal);
1359
- if (!("run" in commitsRes) || commitsRes.run.exitCode !== 0) return { ok: true, value: empty };
1360
- const commits = Number(commitsRes.run.stdout.trim()) || 0;
1361
- const diffRes = await runCommand(deps.run, ["git", "diff", "--numstat", "--end-of-options", "@{upstream}", "HEAD"], root, "push-preview-diff", deps.signal);
1362
- let files = 0;
1363
- let insertions = 0;
1364
- let deletions = 0;
1365
- if ("run" in diffRes && diffRes.run.exitCode === 0) {
1366
- files = diffRes.run.stdout.split("\n").filter((l) => l.trim() !== "").length;
1367
- const sum = sumNumstat(diffRes.run.stdout);
1368
- insertions = sum.insertions;
1369
- deletions = sum.deletions;
1370
- }
1371
- return { ok: true, value: { kind: "push-preview", hasUpstream: true, commits, files, insertions, deletions } };
1372
- }
1373
- async function queryWorktreeStats(deps, config, sessionId) {
1374
- const snapshot = await snapshotForSession(deps, config, sessionId);
1375
- if (!snapshot.ok) return { ok: false, error: { code: "git-error", message: "snapshot failed" } };
1376
- return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
1377
- }
1378
-
1379
- // src/host/suggest.ts
1380
- var SYSTEM_PROMPT = [
1381
- "You are an expert at writing concise, accurate git commit messages.",
1382
- "Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
1383
- "The message must start with a short imperative subject line (72 chars max), optionally followed by a blank line and a brief body explaining what changed and why.",
1384
- "Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
1385
- "Match the language and style of the recent commit message when one is provided; otherwise write in English.",
1386
- "Focus on the most important change; do not mechanically list every file."
1387
- ].join("\n");
1388
- var UNTRACKED_FILE_CAP = 16;
1389
- function resolveRoute(config, agentDefaultModel) {
1390
- if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
1391
- return { provider: config.suggestProvider, model: config.suggestModel };
1392
- }
1393
- return agentDefaultModel?.currentSelection();
1394
- }
1395
- function withDeadline(timeoutMs, parent) {
1396
- const controller = new AbortController();
1397
- let timedOut = false;
1398
- const timer = setTimeout(() => {
1399
- timedOut = true;
1400
- controller.abort();
1401
- }, timeoutMs);
1402
- const onAbort = () => {
1403
- controller.abort();
1404
- };
1405
- if (parent !== void 0) {
1406
- if (parent.aborted) {
1407
- controller.abort();
1408
- } else parent.addEventListener("abort", onAbort, { once: true });
1409
- }
1410
- return {
1411
- signal: controller.signal,
1412
- expired: () => timedOut,
1413
- dispose: () => {
1414
- clearTimeout(timer);
1415
- parent?.removeEventListener("abort", onAbort);
1416
- }
1417
- };
1418
- }
1419
- function truncateUtf8(text, maxBytes) {
1420
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1421
- let lo = 0;
1422
- let hi = text.length;
1423
- while (lo < hi) {
1424
- const mid = lo + hi + 1 >> 1;
1425
- if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
1426
- else hi = mid - 1;
1427
- }
1428
- return text.slice(0, lo);
1429
- }
1430
- function frameChanges(files, diff, recentCommitMessage) {
1431
- return JSON.stringify({
1432
- files,
1433
- diff,
1434
- recentCommitMessage: recentCommitMessage === "" ? null : recentCommitMessage
1435
- });
1436
- }
1437
- function normalizeMessage(raw) {
1438
- let m = raw.trim();
1439
- if (m.startsWith("```")) {
1440
- const firstNl = m.indexOf("\n");
1441
- if (firstNl !== -1) {
1442
- m = m.slice(firstNl + 1);
1443
- m = m.endsWith("```") ? m.slice(0, -3) : m;
1444
- m = m.trim();
1445
- }
1446
- }
1447
- return m.replace(/\n---+\s*$/, "").trim();
1448
- }
1449
- function finishError(reason) {
1450
- if (reason === void 0 || reason.kind === "stop") return void 0;
1451
- if (reason.kind === "max-tokens") return { ok: false, error: { code: "llm-output", message: "model output hit the token limit" } };
1452
- if (reason.kind === "tool-calls") return { ok: false, error: { code: "llm-output", message: "model requested a tool instead of writing a message" } };
1453
- const failure = "failure" in reason ? reason.failure : void 0;
1454
- return { ok: false, error: { code: "llm-error", message: failure?.message ?? `model stream finished abnormally (${reason.kind})` } };
1455
- }
1456
- async function collectDiff(deps, root, hasHead, pathArgs) {
1457
- const cmds = hasHead ? [["git", "diff", "HEAD", ...pathArgs]] : [["git", "diff", "--cached", ...pathArgs], ["git", "diff", ...pathArgs]];
1458
- const runs = await Promise.all(cmds.map((argv) => runCommand(deps.run, argv, root, "suggest-diff", deps.signal)));
1459
- let diff = "";
1460
- for (const res of runs) {
1461
- if ("failure" in res) return { ok: false, error: { code: "git-unavailable", message: "diff failed" } };
1462
- if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1463
- if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
1464
- if (res.run.exitCode !== 0) {
1465
- return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "git diff failed" } };
1466
- }
1467
- diff += res.run.stdout;
1468
- }
1469
- return diff;
1470
- }
1471
- async function appendUntracked(deps, root, files, diff, budget) {
1472
- let text = diff;
1473
- let truncated = false;
1474
- const candidates = files.filter((c) => c.status === "untracked" && !c.isDirectory && isSafePath(c.path));
1475
- for (const c of candidates.slice(0, UNTRACKED_FILE_CAP)) {
1476
- const remaining = budget - Buffer.byteLength(text, "utf8");
1477
- if (remaining <= 0) {
1478
- truncated = true;
1479
- break;
1480
- }
1481
- const res = await runCommand(deps.run, ["git", "diff", "--no-index", "--", "/dev/null", c.path], root, "suggest-untracked", deps.signal);
1482
- if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
1483
- const chunk = res.run.stdout;
1484
- if (chunk === "") continue;
1485
- if (Buffer.byteLength(chunk, "utf8") > remaining) {
1486
- text += truncateUtf8(chunk, remaining) + "\n... [diff truncated]";
1487
- truncated = true;
1488
- break;
1489
- }
1490
- text += chunk;
1491
- }
1492
- return { diff: text, truncated };
1493
- }
1494
- async function runSuggest(deps, config, request) {
1495
- if (!config.suggestEnabled) {
1496
- return { ok: false, error: { code: "suggest-disabled", message: "AI suggest is turned off in the plugin config" } };
1497
- }
1498
- const workspace = await resolveWorkspace(deps, request.sessionId);
1499
- if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
1500
- const root = workspace.root;
1501
- const paths = request.paths ?? [];
1502
- for (const path of paths) {
1503
- if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
1504
- }
1505
- const pathArgs = paths.length > 0 ? ["--", ...paths] : [];
1506
- const pathSet = new Set(paths);
1507
- const [statusRes, headRes, lastRes] = await Promise.all([
1508
- runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "suggest-status", deps.signal),
1509
- runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "suggest-head", deps.signal),
1510
- runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "suggest-last", deps.signal)
1511
- ]);
1512
- if ("failure" in statusRes) return { ok: false, error: { code: "git-unavailable", message: "status failed" } };
1513
- if (statusRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
1514
- if (statusRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
1515
- if (statusRes.run.exitCode !== 0) {
1516
- return { ok: false, error: { code: "git-error", message: statusRes.run.stderr.trim() || "git status failed" } };
1517
- }
1518
- const hasHead = "run" in headRes && headRes.run.exitCode === 0;
1519
- const recentCommitMessage = "run" in lastRes && lastRes.run.exitCode === 0 ? lastRes.run.stdout.replace(/\n+$/, "") : "";
1520
- const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
1521
- const files = pathSet.size > 0 ? all.filter((c) => pathSet.has(c.path)) : all;
1522
- const collected = await collectDiff(deps, root, hasHead, pathArgs);
1523
- if (typeof collected !== "string") return collected;
1524
- if (files.length === 0 && collected.trim() === "") {
1525
- return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
1526
- }
1527
- let diff = collected;
1528
- let truncated = false;
1529
- if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
1530
- diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
1531
- truncated = true;
1532
- }
1533
- const untracked = await appendUntracked(deps, root, files, diff, config.suggestMaxBytes);
1534
- diff = untracked.diff;
1535
- truncated = truncated || untracked.truncated;
1536
- const llm = deps.getLlm?.();
1537
- const route = resolveRoute(config, deps.getAgentDefaultModel?.());
1538
- if (llm === void 0 || route === void 0) {
1539
- return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
1540
- }
1541
- const frame = frameChanges(
1542
- files.map((c) => ({ path: c.path, status: c.status })),
1543
- diff,
1544
- recentCommitMessage
1545
- );
1546
- const deadline = withDeadline(config.suggestTimeoutMs, deps.signal);
1547
- const options = {
1548
- provider: route.provider,
1549
- model: route.model,
1550
- system: SYSTEM_PROMPT,
1551
- messages: [{
1552
- id: "git-panel-suggest",
1553
- role: "user",
1554
- content: [{ type: "text", text: frame }],
1555
- source: { kind: "user" }
1556
- }],
1557
- maxTokens: config.suggestMaxOutputTokens,
1558
- temperature: 0.3,
1559
- signal: deadline.signal
1560
- };
1561
- let output = "";
1562
- let finish;
1563
- try {
1564
- for await (const chunk of llm.stream(options)) {
1565
- if (chunk.type === "text-delta") output += chunk.text;
1566
- else if (chunk.type === "finish") finish = chunk.reason;
1567
- }
1568
- } catch (error) {
1569
- if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
1570
- if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
1571
- return { ok: false, error: { code: "llm-error", message: error instanceof Error ? error.message : "model stream failed" } };
1572
- } finally {
1573
- deadline.dispose();
1574
- }
1575
- if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
1576
- if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
1577
- const terminal = finishError(finish);
1578
- if (terminal !== void 0) return terminal;
1579
- const message = normalizeMessage(output);
1580
- if (message === "") return { ok: false, error: { code: "llm-output", message: "model produced no message text" } };
1581
- return {
1582
- ok: true,
1583
- value: {
1584
- message,
1585
- provider: route.provider,
1586
- model: route.model,
1587
- ...truncated ? { truncated: true } : {}
1588
- }
1589
- };
1590
- }
1591
-
1592
- // src/host/version.ts
1593
- import { readFile as readFile2 } from "node:fs/promises";
1594
- import { dirname as dirname2, join as join4 } from "node:path";
1595
- import { fileURLToPath } from "node:url";
1596
- function manifestPath() {
1597
- return join4(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
1598
- }
1599
- function stringField(value) {
1600
- return typeof value === "string" && value !== "" ? value : void 0;
1601
- }
1602
- function parseRepository(repository) {
1603
- const raw = typeof repository === "string" ? repository : typeof repository === "object" && repository !== null ? stringField(repository.url) : void 0;
1604
- if (raw === void 0) return void 0;
1605
- const fromHost = raw.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?(?:$|[/#?])/i);
1606
- if (fromHost !== null) return { owner: fromHost[1], repo: fromHost[2] };
1607
- const shorthand = raw.match(/^(?:github:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
1608
- if (shorthand !== null) return { owner: shorthand[1], repo: shorthand[2] };
1609
- return void 0;
1610
- }
1611
- function compareVersions(a, b) {
1612
- const split = (value) => {
1613
- const trimmed = value.trim().replace(/^v/i, "");
1614
- const [core, ...rest] = trimmed.split("-");
1615
- const preRaw = rest.join("-");
1616
- return {
1617
- core: core.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part)),
1618
- pre: preRaw === "" ? [] : preRaw.split(".")
1619
- };
1620
- };
1621
- const left = split(a);
1622
- const right = split(b);
1623
- const length = Math.max(left.core.length, right.core.length);
1624
- for (let index = 0; index < length; index += 1) {
1625
- const l = left.core[index] ?? 0;
1626
- const r = right.core[index] ?? 0;
1627
- if (l > r) return 1;
1628
- if (l < r) return -1;
1629
- }
1630
- if (left.pre.length === 0 && right.pre.length === 0) return 0;
1631
- if (left.pre.length === 0) return 1;
1632
- if (right.pre.length === 0) return -1;
1633
- const preLen = Math.max(left.pre.length, right.pre.length);
1634
- for (let i = 0; i < preLen; i += 1) {
1635
- const lp = left.pre[i];
1636
- const rp = right.pre[i];
1637
- if (lp === void 0) return -1;
1638
- if (rp === void 0) return 1;
1639
- const ln = Number.parseInt(lp, 10);
1640
- const rn = Number.parseInt(rp, 10);
1641
- const lNum = Number.isFinite(ln) && String(ln) === lp;
1642
- const rNum = Number.isFinite(rn) && String(rn) === rp;
1643
- if (lNum && rNum) {
1644
- if (ln !== rn) return ln > rn ? 1 : -1;
1645
- } else if (lp !== rp) return lp > rp ? 1 : -1;
1646
- }
1647
- return 0;
1648
- }
1649
- var manifestCache;
1650
- async function readManifest(path) {
1651
- const raw = await readFile2(path, "utf8");
1652
- const parsed = JSON.parse(raw);
1653
- if (typeof parsed !== "object" || parsed === null) throw new Error("package.json is not an object");
1654
- return parsed;
1655
- }
1656
- async function loadManifest(path) {
1657
- if (path !== void 0) return readManifest(path);
1658
- if (manifestCache === void 0) manifestCache = await readManifest(manifestPath());
1659
- return manifestCache;
1660
- }
1661
- async function readVersionInfo(options = {}) {
1662
- const manifest = await loadManifest(options.manifestPath);
1663
- const current = stringField(manifest.version) ?? "0.0.0";
1664
- const repo = parseRepository(manifest.repository);
1665
- const repositoryUrl = repo === void 0 ? void 0 : `https://github.com/${repo.owner}/${repo.repo}`;
1666
- return {
1667
- current,
1668
- ...repositoryUrl ? { repositoryUrl } : {},
1669
- updateAvailable: false,
1670
- checkedRemote: false
1671
- };
1672
- }
1673
- function safeReleaseUrl(url) {
1674
- if (url === void 0) return void 0;
1675
- return /^https:\/\/github\.com\//i.test(url) ? url : void 0;
1676
- }
1677
- var REMOTE_CACHE_MS = 10 * 60 * 1e3;
1678
- var remoteCache;
1679
- async function checkLatestVersion(options = {}) {
1680
- if (options.manifestPath === void 0 && options.fetchFn === void 0 && remoteCache !== void 0 && Date.now() - remoteCache.at < REMOTE_CACHE_MS) {
1681
- return remoteCache.info;
1682
- }
1683
- const base = await readVersionInfo({ manifestPath: options.manifestPath });
1684
- const repo = parseRepository((await loadManifest(options.manifestPath)).repository);
1685
- if (repo === void 0) {
1686
- return { ...base, checkedRemote: false, error: "repository is not configured" };
1687
- }
1688
- const fetchFn = options.fetchFn ?? fetch;
1689
- try {
1690
- const response = await fetchFn(`https://api.github.com/repos/${repo.owner}/${repo.repo}/releases/latest`, {
1691
- headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" },
1692
- signal: AbortSignal.timeout(5e3)
1693
- });
1694
- if (!response.ok) {
1695
- return { ...base, checkedRemote: true, error: `GitHub responded with ${response.status}` };
1696
- }
1697
- const body = await response.json();
1698
- const tag = typeof body === "object" && body !== null ? stringField(body.tag_name) : void 0;
1699
- const htmlUrl = typeof body === "object" && body !== null ? stringField(body.html_url) : void 0;
1700
- if (tag === void 0) {
1701
- return { ...base, checkedRemote: true, error: "GitHub response did not include a release tag" };
1702
- }
1703
- const latest = tag.replace(/^v/i, "");
1704
- const info = {
1705
- ...base,
1706
- checkedRemote: true,
1707
- latest,
1708
- updateAvailable: compareVersions(latest, base.current) > 0,
1709
- releaseUrl: safeReleaseUrl(htmlUrl) ?? (base.repositoryUrl !== void 0 ? `${base.repositoryUrl}/releases/latest` : void 0)
1710
- };
1711
- if (options.manifestPath === void 0 && options.fetchFn === void 0) remoteCache = { at: Date.now(), info };
1712
- return info;
1713
- } catch (error) {
1714
- return { ...base, checkedRemote: true, error: error instanceof Error ? error.message : "Unable to reach GitHub" };
1715
- }
1716
- }
1717
-
1718
- // src/host/index.ts
1719
- var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
1720
- var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _a) {
52
+ import { readFile, readdir, realpath, rm, stat } from "node:fs/promises";
53
+ import { createGitRunner } from "./git.js";
54
+ import { normalizeConfig, snapshotForSession } from "./core.js";
55
+ import { runAction } from "./actions.js";
56
+ import { runQuery } from "./queries.js";
57
+ import { runSuggest } from "./suggest.js";
58
+ import { checkLatestVersion, readVersionInfo } from "./version.js";
59
+ import { normalizeConfig as normalizeConfig2, DEFAULT_CONFIG, snapshotForSession as snapshotForSession2, resolveWorkspace, parsePrimaryRemote } from "./core.js";
60
+ import { createGitRunner as createGitRunner2 } from "./git.js";
61
+ import { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from "./parser.js";
62
+ import { parseRemote, classifyHost } from "./remote.js";
63
+ import { isSafePath, planAction, runAction as runAction2, classifyActionFailure } from "./actions.js";
64
+ import { runQuery as runQuery2 } from "./queries.js";
65
+ import { runSuggest as runSuggest2 } from "./suggest.js";
66
+ import { readVersionInfo as readVersionInfo2, checkLatestVersion as checkLatestVersion2, compareVersions, parseRepository } from "./version.js";
67
+ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _a) {
1721
68
  constructor(ctx, config) {
1722
69
  super(ctx, "gitPanel");
1723
70
  __runInitializers(_init, 5, this);
@@ -1735,7 +82,7 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
1735
82
  const fs = {
1736
83
  realpath,
1737
84
  stat: async (p) => stat(p),
1738
- readFile: readFile3,
85
+ readFile,
1739
86
  readdir: async (p) => (await readdir(p, { withFileTypes: true })).map((e) => ({ name: e.name, isDirectory: e.isDirectory() })),
1740
87
  remove: async (p) => {
1741
88
  await rm(p, { force: true });
@@ -1800,7 +147,7 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
1800
147
  if (signal === void 0) return this.deps;
1801
148
  return { ...this.deps, signal };
1802
149
  }
1803
- };
150
+ }
1804
151
  _init = __decoratorStart(_a);
1805
152
  __decorateElement(_init, 1, "snapshot", _snapshot_dec, GitPanelService);
1806
153
  __decorateElement(_init, 1, "run", _run_dec, GitPanelService);
@@ -1831,14 +178,14 @@ var index_default = GitPanelService;
1831
178
  export {
1832
179
  DEFAULT_CONFIG,
1833
180
  GitPanelService,
1834
- checkLatestVersion,
181
+ checkLatestVersion2 as checkLatestVersion,
1835
182
  classifyActionFailure,
1836
183
  classifyHost,
1837
184
  compareVersions,
1838
- createGitRunner,
185
+ createGitRunner2 as createGitRunner,
1839
186
  index_default as default,
1840
187
  isSafePath,
1841
- normalizeConfig,
188
+ normalizeConfig2 as normalizeConfig,
1842
189
  parseBranches,
1843
190
  parseGraphLog,
1844
191
  parseNameStatus,
@@ -1849,11 +196,11 @@ export {
1849
196
  parseStatus,
1850
197
  parseTags,
1851
198
  planAction,
1852
- readVersionInfo,
199
+ readVersionInfo2 as readVersionInfo,
1853
200
  resolveWorkspace,
1854
- runAction,
1855
- runQuery,
1856
- runSuggest,
1857
- snapshotForSession,
201
+ runAction2 as runAction,
202
+ runQuery2 as runQuery,
203
+ runSuggest2 as runSuggest,
204
+ snapshotForSession2 as snapshotForSession,
1858
205
  sumNumstat
1859
206
  };