@youtyan/code-viewer 0.1.17 → 0.1.19

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.
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // web-src/server/preview.ts
4
- import { closeSync, constants, existsSync as existsSync3, lstatSync as lstatSync3, openSync, readFileSync as readFileSync2, realpathSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
4
+ import {
5
+ closeSync,
6
+ constants,
7
+ existsSync as existsSync3,
8
+ lstatSync as lstatSync3,
9
+ openSync,
10
+ readFileSync as readFileSync2,
11
+ realpathSync,
12
+ statSync,
13
+ unlinkSync,
14
+ watch,
15
+ writeFileSync
16
+ } from "node:fs";
5
17
  import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
6
18
 
7
19
  // web-src/routes.ts
@@ -70,7 +82,13 @@ function startDevAssetReload(options) {
70
82
  }
71
83
 
72
84
  // web-src/server/git.ts
73
- import { existsSync, lstatSync as lstatSync2, readdirSync, readFileSync } from "node:fs";
85
+ import {
86
+ existsSync,
87
+ lstatSync as lstatSync2,
88
+ readdirSync,
89
+ readFileSync
90
+ } from "node:fs";
91
+ import { join as join2 } from "node:path";
74
92
 
75
93
  // web-src/server/runtime.ts
76
94
  import { spawn, spawnSync } from "node:child_process";
@@ -223,9 +241,11 @@ async function writeWebResponse(res, response) {
223
241
  }
224
242
 
225
243
  // web-src/server/git.ts
226
- import { join as join2 } from "node:path";
227
244
  var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
228
245
  var WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
246
+ var DEFAULT_REF_COMMIT_LIMIT = 100;
247
+ var MAX_REF_COMMIT_LIMIT = 500;
248
+ var COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI";
229
249
  var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
230
250
  "node_modules",
231
251
  ".venv",
@@ -279,11 +299,19 @@ function catFileBlobStream(oid, cwd) {
279
299
  }
280
300
  function objectSize(ref, path, cwd) {
281
301
  const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
282
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
302
+ return {
303
+ code: res.code,
304
+ size: Number(res.stdout.trim()) || 0,
305
+ stderr: res.stderr
306
+ };
283
307
  }
284
308
  function objectByteSize(oid, cwd) {
285
309
  const res = run(["git", "cat-file", "-s", oid], cwd);
286
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
310
+ return {
311
+ code: res.code,
312
+ size: Number(res.stdout.trim()) || 0,
313
+ stderr: res.stderr
314
+ };
287
315
  }
288
316
  function objectId(ref, path, cwd) {
289
317
  const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
@@ -304,7 +332,12 @@ function verifyTreeRef(ref, cwd) {
304
332
  return res.code === 0;
305
333
  }
306
334
  function refs(cwd) {
307
- const out = { branches: [], tags: [], commits: [], current: "" };
335
+ const out = {
336
+ branches: [],
337
+ tags: [],
338
+ commits: [],
339
+ current: ""
340
+ };
308
341
  const branches = run([
309
342
  "git",
310
343
  "for-each-ref",
@@ -317,17 +350,104 @@ function refs(cwd) {
317
350
  out.branches = branches.stdout.split(`
318
351
  `).filter((line) => line && line !== "origin/HEAD");
319
352
  }
320
- const tags = run(["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", "refs/tags"], cwd);
353
+ const tags = run([
354
+ "git",
355
+ "for-each-ref",
356
+ "--sort=-creatordate",
357
+ "--format=%(refname:short)",
358
+ "refs/tags"
359
+ ], cwd);
321
360
  if (tags.code === 0)
322
361
  out.tags = tags.stdout.split(`
323
362
  `).filter(Boolean);
324
- const commits = run(["git", "log", "-50", "--format=%h\t%s\t%an\t%ar"], cwd);
325
- if (commits.code === 0)
326
- out.commits = commits.stdout.split(`
327
- `).filter(Boolean);
363
+ out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
328
364
  out.current = currentBranch(cwd) || "";
329
365
  return out;
330
366
  }
367
+ function clampCommitLimit(max) {
368
+ return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
369
+ }
370
+ function parseCommitLog(stdout) {
371
+ const parts = stdout.split("\x00");
372
+ const commits = [];
373
+ for (let index = 0;index < parts.length; ) {
374
+ if (!parts[index]) {
375
+ index++;
376
+ continue;
377
+ }
378
+ const sha = parts[index++] || "";
379
+ const subject = parts[index++] || "";
380
+ const author = parts[index++] || "";
381
+ const when = parts[index++] || "";
382
+ if (sha)
383
+ commits.push({ sha, subject, author, when });
384
+ }
385
+ return commits;
386
+ }
387
+ function commitLogArgs(limit) {
388
+ return [
389
+ "git",
390
+ "log",
391
+ "--all",
392
+ "-z",
393
+ `--max-count=${limit}`,
394
+ `--format=${COMMIT_FORMAT}`
395
+ ];
396
+ }
397
+ function mergeCommitResults(limit, ...groups) {
398
+ const seen = new Set;
399
+ const merged = [];
400
+ for (const commits of groups) {
401
+ for (const commit of commits) {
402
+ if (!commit.sha || seen.has(commit.sha))
403
+ continue;
404
+ seen.add(commit.sha);
405
+ merged.push(commit);
406
+ if (merged.length >= limit)
407
+ return merged;
408
+ }
409
+ }
410
+ return merged;
411
+ }
412
+ function runCommitLog(cwd, args) {
413
+ const commits = run(args, cwd);
414
+ return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
415
+ }
416
+ function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
417
+ const limit = clampCommitLimit(max);
418
+ const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
419
+ const hashMatches = [];
420
+ if (/^[0-9a-f]{4,40}$/i.test(trimmed)) {
421
+ const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
422
+ const single = run([
423
+ "git",
424
+ "log",
425
+ "-z",
426
+ "-1",
427
+ `--format=${COMMIT_FORMAT}`,
428
+ verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
429
+ ], cwd);
430
+ if (single.code === 0 && single.stdout.trim()) {
431
+ hashMatches.push(...parseCommitLog(single.stdout));
432
+ }
433
+ }
434
+ if (!trimmed) {
435
+ return runCommitLog(cwd, commitLogArgs(limit));
436
+ }
437
+ const subjectMatches = runCommitLog(cwd, [
438
+ ...commitLogArgs(limit),
439
+ "--regexp-ignore-case",
440
+ "--fixed-strings",
441
+ `--grep=${trimmed}`
442
+ ]);
443
+ const authorMatches = runCommitLog(cwd, [
444
+ ...commitLogArgs(limit),
445
+ "--regexp-ignore-case",
446
+ "--fixed-strings",
447
+ `--author=${trimmed}`
448
+ ]);
449
+ return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
450
+ }
331
451
  function nameStatus(args, cwd) {
332
452
  const res = run([
333
453
  "git",
@@ -354,7 +474,12 @@ function nameStatus(args, cwd) {
354
474
  const oldPath = parts[i++] || "";
355
475
  const path = parts[i++] || "";
356
476
  if (path)
357
- files.push({ status: kind, old_path: oldPath, path, similarity: Number(status.slice(1)) || undefined });
477
+ files.push({
478
+ status: kind,
479
+ old_path: oldPath,
480
+ path,
481
+ similarity: Number(status.slice(1)) || undefined
482
+ });
358
483
  } else {
359
484
  const path = parts[i++] || "";
360
485
  if (path)
@@ -500,7 +625,11 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
500
625
  continue;
501
626
  walk(full, entryPath, depth + 1);
502
627
  } else if (entry.isFile() || entry.isSymbolicLink()) {
503
- if (!pushRecursiveEntry({ name: entry.name, path: entryPath, type: "blob" }))
628
+ if (!pushRecursiveEntry({
629
+ name: entry.name,
630
+ path: entryPath,
631
+ type: "blob"
632
+ }))
504
633
  return;
505
634
  }
506
635
  }
@@ -533,7 +662,11 @@ function gitTreeEntries(ref, path, cwd, recursive) {
533
662
  if (!match)
534
663
  return null;
535
664
  const entryPath = match[2];
536
- return { name: entryPath.split("/").pop() || entryPath, path: entryPath, type: match[1] };
665
+ return {
666
+ name: entryPath.split("/").pop() || entryPath,
667
+ path: entryPath,
668
+ type: match[1]
669
+ };
537
670
  }).filter((entry) => !!entry);
538
671
  if (recursive)
539
672
  entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -554,7 +687,11 @@ function worktreeFiles(cwd) {
554
687
  function listTree(ref, path, cwd, options = {}) {
555
688
  const base = normalizeTreePath(path);
556
689
  if (ref === "worktree") {
557
- return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames), stderr: "" };
690
+ return {
691
+ code: 0,
692
+ entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
693
+ stderr: ""
694
+ };
558
695
  }
559
696
  const direct = gitTreeEntries(ref, base, cwd, false);
560
697
  if (direct.code !== 0 || !options.recursive)
@@ -562,7 +699,11 @@ function listTree(ref, path, cwd, options = {}) {
562
699
  const recursive = gitTreeEntries(ref, base, cwd, true);
563
700
  if (recursive.code !== 0)
564
701
  return recursive;
565
- return { code: 0, entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries), stderr: "" };
702
+ return {
703
+ code: 0,
704
+ entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
705
+ stderr: ""
706
+ };
566
707
  }
567
708
  function untrackedMeta(cwd) {
568
709
  return untracked(cwd).map((path) => {
@@ -577,7 +718,14 @@ function untrackedMeta(cwd) {
577
718
  lines = data.toString("utf8").split(`
578
719
  `).length - 1;
579
720
  }
580
- return { path, status: "A", additions: binary ? 0 : lines, deletions: 0, binary, untracked: true };
721
+ return {
722
+ path,
723
+ status: "A",
724
+ additions: binary ? 0 : lines,
725
+ deletions: 0,
726
+ binary,
727
+ untracked: true
728
+ };
581
729
  });
582
730
  }
583
731
  function fileMeta(args, cwd, includeUntracked = false) {
@@ -978,7 +1126,12 @@ function buildFileSearchList(ref, generation, entries) {
978
1126
  }
979
1127
  function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
980
1128
  const safePaths = paths.length ? paths : ["."];
981
- const omitGlobs = omitDirNames.flatMap((name) => ["--glob", `!${name}/**`, "--glob", `!**/${name}/**`]);
1129
+ const omitGlobs = omitDirNames.flatMap((name) => [
1130
+ "--glob",
1131
+ `!${name}/**`,
1132
+ "--glob",
1133
+ `!**/${name}/**`
1134
+ ]);
982
1135
  const args = [
983
1136
  "rg",
984
1137
  "--no-config",
@@ -1017,7 +1170,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
1017
1170
  const preview = parsed[4];
1018
1171
  if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
1019
1172
  continue;
1020
- matches.push({ path, line: lineNo, column, preview: preview.slice(0, 500) });
1173
+ matches.push({
1174
+ path,
1175
+ line: lineNo,
1176
+ column,
1177
+ preview: preview.slice(0, 500)
1178
+ });
1021
1179
  }
1022
1180
  return matches;
1023
1181
  }
@@ -1076,6 +1234,7 @@ var cliArgs = DEFAULT_ARGS;
1076
1234
  var listenPort = 0;
1077
1235
  var allowUpload = false;
1078
1236
  var uploadAllowedByCli = false;
1237
+ var openAfterStart = false;
1079
1238
  var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
1080
1239
  var scopeOmitDirCliOverride = null;
1081
1240
  var rgAvailableCache = null;
@@ -1129,7 +1288,7 @@ Examples:
1129
1288
  }
1130
1289
  listenPort = parsed;
1131
1290
  } else if (arg === "--open") {
1132
- setTimeout(() => openBrowser(`http://127.0.0.1:${server.port}/`), 0);
1291
+ openAfterStart = true;
1133
1292
  } else if (arg === "--allow-upload") {
1134
1293
  allowUpload = true;
1135
1294
  uploadAllowedByCli = true;
@@ -1139,7 +1298,10 @@ Examples:
1139
1298
  console.error("--scope-omit-dir requires a directory name");
1140
1299
  process.exit(1);
1141
1300
  }
1142
- scopeOmitDirCliOverride = normalizeScopeOmitDirNames([...scopeOmitDirCliOverride || [], next]);
1301
+ scopeOmitDirCliOverride = normalizeScopeOmitDirNames([
1302
+ ...scopeOmitDirCliOverride || [],
1303
+ next
1304
+ ]);
1143
1305
  } else {
1144
1306
  rest.push(arg);
1145
1307
  }
@@ -1168,7 +1330,10 @@ function json(data, init = {}) {
1168
1330
  function text(body, status = 200) {
1169
1331
  return new Response(body, {
1170
1332
  status,
1171
- headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }
1333
+ headers: {
1334
+ "Content-Type": "text/plain; charset=utf-8",
1335
+ "Cache-Control": "no-store"
1336
+ }
1172
1337
  });
1173
1338
  }
1174
1339
  function requestAllowed(req) {
@@ -1192,11 +1357,26 @@ function staticFile(pathname) {
1192
1357
  "/app.js": ["app.js", "application/javascript; charset=utf-8"],
1193
1358
  "/mermaid.js": ["mermaid.js", "application/javascript; charset=utf-8"],
1194
1359
  "/shiki.js": ["shiki.js", "application/javascript; charset=utf-8"],
1195
- "/vendor/diff2html/diff2html.min.css": ["vendor/diff2html/diff2html.min.css", "text/css; charset=utf-8"],
1196
- "/vendor/diff2html/diff2html-ui.min.js": ["vendor/diff2html/diff2html-ui.min.js", "application/javascript; charset=utf-8"],
1197
- "/vendor/highlight.js/highlight.min.js": ["vendor/highlight.js/highlight.min.js", "application/javascript; charset=utf-8"],
1198
- "/vendor/highlight.js/styles/github.min.css": ["vendor/highlight.js/styles/github.min.css", "text/css; charset=utf-8"],
1199
- "/vendor/highlight.js/styles/github-dark.min.css": ["vendor/highlight.js/styles/github-dark.min.css", "text/css; charset=utf-8"]
1360
+ "/vendor/diff2html/diff2html.min.css": [
1361
+ "vendor/diff2html/diff2html.min.css",
1362
+ "text/css; charset=utf-8"
1363
+ ],
1364
+ "/vendor/diff2html/diff2html-ui.min.js": [
1365
+ "vendor/diff2html/diff2html-ui.min.js",
1366
+ "application/javascript; charset=utf-8"
1367
+ ],
1368
+ "/vendor/highlight.js/highlight.min.js": [
1369
+ "vendor/highlight.js/highlight.min.js",
1370
+ "application/javascript; charset=utf-8"
1371
+ ],
1372
+ "/vendor/highlight.js/styles/github.min.css": [
1373
+ "vendor/highlight.js/styles/github.min.css",
1374
+ "text/css; charset=utf-8"
1375
+ ],
1376
+ "/vendor/highlight.js/styles/github-dark.min.css": [
1377
+ "vendor/highlight.js/styles/github-dark.min.css",
1378
+ "text/css; charset=utf-8"
1379
+ ]
1200
1380
  };
1201
1381
  for (const spaPath of [...APP_ENTRY_PATHS, ...SPA_PATHS]) {
1202
1382
  map[spaPath] = ["index.html", "text/html; charset=utf-8"];
@@ -1266,7 +1446,14 @@ function buildQuery(params) {
1266
1446
  }
1267
1447
  function fileToMeta(file, range, extraQs) {
1268
1448
  const sizeClass = classify(file);
1269
- const q = { path: file.path, old_path: file.old_path, status: file.status, from: range.from, to: range.to, ...extraQs };
1449
+ const q = {
1450
+ path: file.path,
1451
+ old_path: file.old_path,
1452
+ status: file.status,
1453
+ from: range.from,
1454
+ to: range.to,
1455
+ ...extraQs
1456
+ };
1270
1457
  if (file.untracked)
1271
1458
  Object.assign(q, { untracked: "1" });
1272
1459
  const previewQ = { ...q, mode: "preview", max_hunks: PREVIEW_HUNKS_DEFAULT };
@@ -1326,7 +1513,14 @@ function computePayload(extras, range) {
1326
1513
  }, { files: meta.length, additions: 0, deletions: 0 });
1327
1514
  const toWorktree = !range.to || range.to === "worktree";
1328
1515
  const label = refs2.length ? `${refs2.join(" .. ")}${toWorktree && refs2.length === 1 ? " .. worktree" : ""}` : cliArgs.join(" ");
1329
- return { files: meta, totals, range: label || "HEAD", project: basename2(cwd), branch: currentBranch(cwd) || undefined, generation };
1516
+ return {
1517
+ files: meta,
1518
+ totals,
1519
+ range: label || "HEAD",
1520
+ project: basename2(cwd),
1521
+ branch: currentBranch(cwd) || undefined,
1522
+ generation
1523
+ };
1330
1524
  }
1331
1525
  function handleDiffJson(url) {
1332
1526
  const extras = [];
@@ -1334,7 +1528,10 @@ function handleDiffJson(url) {
1334
1528
  extras.push("-w");
1335
1529
  if (url.searchParams.get("ignore_blank") === "1")
1336
1530
  extras.push("--ignore-blank-lines");
1337
- const range = { from: url.searchParams.get("from") || "", to: url.searchParams.get("to") || "" };
1531
+ const range = {
1532
+ from: url.searchParams.get("from") || "",
1533
+ to: url.searchParams.get("to") || ""
1534
+ };
1338
1535
  const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}`;
1339
1536
  if (url.searchParams.get("nocache") === "1") {
1340
1537
  const payload2 = computePayload(extras, range);
@@ -1348,15 +1545,33 @@ function handleDiffJson(url) {
1348
1545
  }
1349
1546
  const body2 = JSON.stringify(payload2);
1350
1547
  setTimedCacheEntry(metaCache, key, { body: body2, sig });
1351
- return new Response(body2, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1548
+ return new Response(body2, {
1549
+ headers: {
1550
+ "Content-Type": "application/json; charset=utf-8",
1551
+ "Cache-Control": "no-store"
1552
+ }
1553
+ });
1352
1554
  }
1353
1555
  const cached = metaCache.get(key);
1354
1556
  if (cacheFresh(cached))
1355
- return new Response(cached.body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1557
+ return new Response(cached.body, {
1558
+ headers: {
1559
+ "Content-Type": "application/json; charset=utf-8",
1560
+ "Cache-Control": "no-store"
1561
+ }
1562
+ });
1356
1563
  const payload = computePayload(extras, range);
1357
1564
  const body = JSON.stringify(payload);
1358
- setTimedCacheEntry(metaCache, key, { body, sig: JSON.stringify({ ...payload, generation: undefined }) });
1359
- return new Response(body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1565
+ setTimedCacheEntry(metaCache, key, {
1566
+ body,
1567
+ sig: JSON.stringify({ ...payload, generation: undefined })
1568
+ });
1569
+ return new Response(body, {
1570
+ headers: {
1571
+ "Content-Type": "application/json; charset=utf-8",
1572
+ "Cache-Control": "no-store"
1573
+ }
1574
+ });
1360
1575
  }
1361
1576
  function safePath(path) {
1362
1577
  if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\x00"))
@@ -1369,7 +1584,9 @@ function safeRepoPath(path) {
1369
1584
  function normalizeScopeOmitDirNames(names) {
1370
1585
  if (!Array.isArray(names))
1371
1586
  return [];
1372
- return [...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))].sort((a, b) => a.localeCompare(b));
1587
+ return [
1588
+ ...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
1589
+ ].sort((a, b) => a.localeCompare(b));
1373
1590
  }
1374
1591
  function parseScopeOmitDirNamesQuery(value) {
1375
1592
  const names = value ? value.split(",") : [];
@@ -1498,7 +1715,10 @@ function handleTree(url) {
1498
1715
  const recursive = url.searchParams.get("recursive") === "1";
1499
1716
  if (invalidScopeOmitDirNamesQuery(url))
1500
1717
  return text("invalid omit dirs", 400);
1501
- const entries = listTree(target, path, cwd, { recursive, omitDirNames: scopeOmitDirNamesFromQuery(url) }).entries;
1718
+ const entries = listTree(target, path, cwd, {
1719
+ recursive,
1720
+ omitDirNames: scopeOmitDirNamesFromQuery(url)
1721
+ }).entries;
1502
1722
  return json({
1503
1723
  ref: target,
1504
1724
  path,
@@ -1531,7 +1751,10 @@ function handleFiles(url) {
1531
1751
  if (cached && cached.generation === generation)
1532
1752
  return json(cached.body);
1533
1753
  const ref = target || "worktree";
1534
- const entries = listTree(ref, "", cwd, { recursive: true, omitDirNames }).entries;
1754
+ const entries = listTree(ref, "", cwd, {
1755
+ recursive: true,
1756
+ omitDirNames
1757
+ }).entries;
1535
1758
  const body = buildFileSearchList(ref, generation, entries);
1536
1759
  fileListCache.set(key, { generation, body });
1537
1760
  return json(body);
@@ -1584,12 +1807,27 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
1584
1807
  const proc = runSync(args, cwd, { timeout: 5000 });
1585
1808
  const stdout = proc.stdout;
1586
1809
  const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
1587
- return { ref: "worktree", engine: "rg", truncated: matches2.length >= max, matches: matches2 };
1810
+ return {
1811
+ ref: "worktree",
1812
+ engine: "rg",
1813
+ truncated: matches2.length >= max,
1814
+ matches: matches2
1815
+ };
1588
1816
  }
1589
1817
  if (regex)
1590
- return { ref: "worktree", engine: "fallback", truncated: false, matches: [] };
1818
+ return {
1819
+ ref: "worktree",
1820
+ engine: "fallback",
1821
+ truncated: false,
1822
+ matches: []
1823
+ };
1591
1824
  const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
1592
- return { ref: "worktree", engine: "fallback", truncated: matches.length >= max, matches };
1825
+ return {
1826
+ ref: "worktree",
1827
+ engine: "fallback",
1828
+ truncated: matches.length >= max,
1829
+ matches
1830
+ };
1593
1831
  }
1594
1832
  function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
1595
1833
  const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
@@ -1624,13 +1862,24 @@ function handleGrep(url) {
1624
1862
  const paths = parseGrepPaths(url, omitDirNames);
1625
1863
  const regex = url.searchParams.get("regex") === "1";
1626
1864
  if (!query.trim())
1627
- return json({ ref, engine: ref === "worktree" ? "fallback" : "git", truncated: false, matches: [] });
1865
+ return json({
1866
+ ref,
1867
+ engine: ref === "worktree" ? "fallback" : "git",
1868
+ truncated: false,
1869
+ matches: []
1870
+ });
1628
1871
  if (ref === "worktree" || ref === "")
1629
1872
  return json(grepWorktree(query, max, paths, regex, omitDirNames));
1630
1873
  if (!verifyTreeRef(ref, cwd))
1631
1874
  return text("invalid target", 400);
1632
1875
  return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
1633
1876
  }
1877
+ function handleRefCommits(url) {
1878
+ const query = url.searchParams.get("q") || "";
1879
+ const parsedMax = Number(url.searchParams.get("max") || "");
1880
+ const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
1881
+ return json({ commits: refCommits(cwd, query, max) });
1882
+ }
1634
1883
  function handleFileDiff(url) {
1635
1884
  const path = url.searchParams.get("path") || "";
1636
1885
  if (!safePath(path))
@@ -1641,7 +1890,10 @@ function handleFileDiff(url) {
1641
1890
  if (url.searchParams.get("ignore_blank") === "1")
1642
1891
  extras.push("--ignore-blank-lines");
1643
1892
  const isUntracked = url.searchParams.get("untracked") === "1";
1644
- const range = { from: url.searchParams.get("from") || "", to: url.searchParams.get("to") || "" };
1893
+ const range = {
1894
+ from: url.searchParams.get("from") || "",
1895
+ to: url.searchParams.get("to") || ""
1896
+ };
1645
1897
  if (isSameWorktreeRange(range)) {
1646
1898
  return json({
1647
1899
  path,
@@ -1661,7 +1913,15 @@ function handleFileDiff(url) {
1661
1913
  const oldPath = url.searchParams.get("old_path");
1662
1914
  let cacheKey;
1663
1915
  try {
1664
- cacheKey = fileDiffCacheKey({ path, oldPath, isUntracked, range, extras, args, cwd });
1916
+ cacheKey = fileDiffCacheKey({
1917
+ path,
1918
+ oldPath,
1919
+ isUntracked,
1920
+ range,
1921
+ extras,
1922
+ args,
1923
+ cwd
1924
+ });
1665
1925
  } catch {
1666
1926
  return text("invalid diff range", 400);
1667
1927
  }
@@ -1850,7 +2110,16 @@ async function handleFileRange(url) {
1850
2110
  if (!full)
1851
2111
  return text("no file", 404);
1852
2112
  const result = await collectIndexedWorktreeLineRange(full, start, end);
1853
- const body = { path, ref, start, end, lines: result.lines, total: result.total, complete: result.complete, generation };
2113
+ const body = {
2114
+ path,
2115
+ ref,
2116
+ start,
2117
+ end,
2118
+ lines: result.lines,
2119
+ total: result.total,
2120
+ complete: result.complete,
2121
+ generation
2122
+ };
1854
2123
  return json(body);
1855
2124
  } else {
1856
2125
  if (!verifyTreeRef(ref, cwd))
@@ -1864,7 +2133,16 @@ async function handleFileRange(url) {
1864
2133
  const result = await collectIndexedGitBlobLineRange(path, oid.oid, size.size, start, end);
1865
2134
  if (!result)
1866
2135
  return text("cannot read ref", 500);
1867
- const body = { path, ref, start, end, lines: result.lines, total: result.total, complete: result.complete, generation };
2136
+ const body = {
2137
+ path,
2138
+ ref,
2139
+ start,
2140
+ end,
2141
+ lines: result.lines,
2142
+ total: result.total,
2143
+ complete: result.complete,
2144
+ generation
2145
+ };
1868
2146
  return json(body);
1869
2147
  }
1870
2148
  }
@@ -1898,7 +2176,11 @@ function handleRawFile(req, url) {
1898
2176
  if (rangeResult?.kind === "unsatisfiable") {
1899
2177
  return new Response(null, {
1900
2178
  status: 416,
1901
- headers: { ...rawFileHeaders(path, size), "Content-Range": `bytes */${size}`, "Content-Length": "0" }
2179
+ headers: {
2180
+ ...rawFileHeaders(path, size),
2181
+ "Content-Range": `bytes */${size}`,
2182
+ "Content-Length": "0"
2183
+ }
1902
2184
  });
1903
2185
  }
1904
2186
  if (rangeResult?.kind === "range") {
@@ -1916,7 +2198,9 @@ function handleRawFile(req, url) {
1916
2198
  }
1917
2199
  if (req.method === "HEAD")
1918
2200
  return new Response(null, { headers: rawFileHeaders(path, size) });
1919
- return new Response(fileReadableStream(full), { headers: rawFileHeaders(path, size) });
2201
+ return new Response(fileReadableStream(full), {
2202
+ headers: rawFileHeaders(path, size)
2203
+ });
1920
2204
  }
1921
2205
  }
1922
2206
  function rawFileSize(path, ref) {
@@ -2080,7 +2364,11 @@ async function handleUploadFiles(req) {
2080
2364
  fileCache.clear();
2081
2365
  metaCache.clear();
2082
2366
  sendSse("update");
2083
- return json({ ok: true, files: uploads.map((upload) => upload.name), generation });
2367
+ return json({
2368
+ ok: true,
2369
+ files: uploads.map((upload) => upload.name),
2370
+ generation
2371
+ });
2084
2372
  }
2085
2373
  function openOsPath(path) {
2086
2374
  const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
@@ -2164,6 +2452,8 @@ var server = await startServer({
2164
2452
  return handleFiles(url);
2165
2453
  if (url.pathname === "/_grep")
2166
2454
  return handleGrep(url);
2455
+ if (url.pathname === "/_commits")
2456
+ return handleRefCommits(url);
2167
2457
  if (url.pathname === "/file_diff")
2168
2458
  return handleFileDiff(url);
2169
2459
  if (url.pathname === "/file_range")
@@ -2224,6 +2514,9 @@ data: ok
2224
2514
  return text("not found", 404);
2225
2515
  }
2226
2516
  });
2517
+ if (openAfterStart) {
2518
+ openBrowser(`http://127.0.0.1:${server.port}/`);
2519
+ }
2227
2520
  startDevAssetReload({
2228
2521
  enabled: process.env.CODE_VIEWER_DEV === "1",
2229
2522
  webRoot: WEB_ROOT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,12 +32,13 @@
32
32
  "check": "bun run typecheck",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "check:bundle": "bun build --target=browser --format=iife --outfile=/tmp/code-viewer-app.js web-src/app.ts && cmp /tmp/code-viewer-app.js web/app.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-mermaid.js web-src/mermaid-entry.ts && cmp /tmp/code-viewer-mermaid.js web/mermaid.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-shiki.js web-src/shiki-entry.ts && cmp /tmp/code-viewer-shiki.js web/shiki.js",
35
+ "check:format": "biome format .",
35
36
  "dev": "bun run web-src/server/dev.ts",
36
37
  "preview": "bun run web-src/server/dev.ts",
37
38
  "preview:raw": "bun run web-src/server/preview.ts",
38
39
  "test": "bun test",
39
40
  "lint": "biome lint web-src/server package.json biome.jsonc",
40
- "verify": "bun run check && bun run lint && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
41
+ "verify": "bun run check && bun run lint && bun run check:format && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
41
42
  "pack:dry": "npm pack --dry-run",
42
43
  "prepack": "bun run build",
43
44
  "prepublishOnly": "bun run verify"