@youtyan/code-viewer 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -759,6 +759,16 @@ import {
759
759
  statSync
760
760
  } from "node:fs";
761
761
  import { join as join2 } from "node:path";
762
+ function normalizeBlameRef(ref, base) {
763
+ const rawRef = ref || "worktree";
764
+ if (base === "worktree" && rawRef !== "worktree") {
765
+ return { base: "HEAD", ref: rawRef };
766
+ }
767
+ if (base === "HEAD" && rawRef === "worktree") {
768
+ return { base: "HEAD", ref: "HEAD" };
769
+ }
770
+ return { base, ref: rawRef };
771
+ }
762
772
  function run(args, cwd) {
763
773
  return runSync(args, cwd);
764
774
  }
@@ -1063,6 +1073,13 @@ function commitHistory(cwd, options) {
1063
1073
  const skip = Math.max(0, Math.floor(options.skip) || 0);
1064
1074
  const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
1065
1075
  const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
1076
+ const pathFilter = (options.path || "").trim();
1077
+ const pathArgs = [];
1078
+ if (pathFilter && !pathFilter.includes("\x00") && !pathFilter.startsWith("-")) {
1079
+ if (!pathFilter.endsWith("/"))
1080
+ pathArgs.push("--follow");
1081
+ pathArgs.push("--", pathFilter);
1082
+ }
1066
1083
  const res = run([
1067
1084
  "git",
1068
1085
  "log",
@@ -1072,7 +1089,8 @@ function commitHistory(cwd, options) {
1072
1089
  `--format=${HISTORY_FORMAT}`,
1073
1090
  ...filterArgs,
1074
1091
  verified.stdout.trim(),
1075
- ...pathspec
1092
+ ...pathspec,
1093
+ ...pathArgs
1076
1094
  ], cwd);
1077
1095
  if (res.code !== 0)
1078
1096
  return { commits: [], hasMore: false, error: "git log failed" };
@@ -1173,6 +1191,130 @@ function numstatZ(args, cwd) {
1173
1191
  function isToolInternalPath(path) {
1174
1192
  return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
1175
1193
  }
1194
+ function syntheticUncommittedBlameFromWorktree(cwd, path) {
1195
+ const filePath = join2(cwd, path);
1196
+ try {
1197
+ const stat = statSync(filePath);
1198
+ if (!stat.isFile())
1199
+ return { lines: [], commits: {}, error: "not a file" };
1200
+ const text = readFileSync(filePath, "utf8");
1201
+ const normalized = text.replace(/\r\n/g, `
1202
+ `).replace(/\r/g, `
1203
+ `);
1204
+ const lineCount = normalized.length ? normalized.endsWith(`
1205
+ `) ? normalized.length - 1 === 0 ? 1 : normalized.split(`
1206
+ `).length - 1 : normalized.split(`
1207
+ `).length : 1;
1208
+ const lines = [];
1209
+ for (let i = 1;i <= lineCount; i++) {
1210
+ lines.push({ lineNo: i, sha: BLAME_ZERO_SHA, isUncommitted: true });
1211
+ }
1212
+ return {
1213
+ lines,
1214
+ commits: {
1215
+ [BLAME_ZERO_SHA]: {
1216
+ sha: BLAME_ZERO_SHA,
1217
+ author: "Not Committed Yet",
1218
+ authorMail: "",
1219
+ authorTime: 0,
1220
+ summary: "Working tree",
1221
+ isUncommitted: true
1222
+ }
1223
+ },
1224
+ isUntracked: true,
1225
+ isSynthetic: true
1226
+ };
1227
+ } catch (err) {
1228
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
1229
+ return { lines: [], commits: {}, error: "file not found" };
1230
+ }
1231
+ return { lines: [], commits: {}, error: "file not readable" };
1232
+ }
1233
+ }
1234
+ function blame(cwd, options) {
1235
+ const path = options.path;
1236
+ if (!path || path.includes("\x00") || path.startsWith("-")) {
1237
+ return { lines: [], commits: {}, error: "invalid path" };
1238
+ }
1239
+ const normalized = normalizeBlameRef(options.ref, options.base);
1240
+ const args = ["git", "blame", "--porcelain"];
1241
+ if (normalized.base === "HEAD") {
1242
+ if (normalized.ref.startsWith("-") || normalized.ref.includes("\x00"))
1243
+ return { lines: [], commits: {}, error: "invalid ref" };
1244
+ args.push(normalized.ref);
1245
+ }
1246
+ args.push("--", path);
1247
+ const res = run(args, cwd);
1248
+ if (res.code !== 0) {
1249
+ if (normalized.base === "worktree") {
1250
+ return syntheticUncommittedBlameFromWorktree(cwd, path);
1251
+ }
1252
+ return {
1253
+ lines: [],
1254
+ commits: {},
1255
+ error: res.stderr.trim() || "blame failed"
1256
+ };
1257
+ }
1258
+ const lines = [];
1259
+ const commits = {};
1260
+ const rawLines = res.stdout.split(`
1261
+ `);
1262
+ let i = 0;
1263
+ while (i < rawLines.length) {
1264
+ const headerLine = rawLines[i];
1265
+ if (!headerLine) {
1266
+ i++;
1267
+ continue;
1268
+ }
1269
+ const headerMatch = /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(headerLine);
1270
+ if (!headerMatch) {
1271
+ i++;
1272
+ continue;
1273
+ }
1274
+ const sha = headerMatch[1];
1275
+ const finalLine = Number(headerMatch[3]);
1276
+ i++;
1277
+ let commit = commits[sha];
1278
+ if (!commit) {
1279
+ commit = {
1280
+ sha,
1281
+ author: "",
1282
+ authorMail: "",
1283
+ authorTime: 0,
1284
+ summary: "",
1285
+ isUncommitted: sha === BLAME_ZERO_SHA
1286
+ };
1287
+ commits[sha] = commit;
1288
+ }
1289
+ while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
1290
+ const metaLine = rawLines[i++];
1291
+ if (!metaLine)
1292
+ continue;
1293
+ const sp = metaLine.indexOf(" ");
1294
+ const key = sp >= 0 ? metaLine.slice(0, sp) : metaLine;
1295
+ const val = sp >= 0 ? metaLine.slice(sp + 1) : "";
1296
+ if (key === "author" && !commit.author)
1297
+ commit.author = val;
1298
+ else if (key === "author-mail" && !commit.authorMail)
1299
+ commit.authorMail = val.replace(/^</, "").replace(/>$/, "");
1300
+ else if (key === "author-time" && !commit.authorTime)
1301
+ commit.authorTime = Number(val) || 0;
1302
+ else if (key === "summary" && !commit.summary)
1303
+ commit.summary = val;
1304
+ }
1305
+ if (i < rawLines.length && rawLines[i].startsWith("\t"))
1306
+ i++;
1307
+ if (Number.isFinite(finalLine) && finalLine > 0) {
1308
+ lines.push({
1309
+ lineNo: finalLine,
1310
+ sha,
1311
+ isUncommitted: sha === BLAME_ZERO_SHA
1312
+ });
1313
+ }
1314
+ }
1315
+ lines.sort((a, b) => a.lineNo - b.lineNo);
1316
+ return { lines, commits };
1317
+ }
1176
1318
  function untracked(cwd, path = "") {
1177
1319
  const args = ["git", "ls-files", "--others", "--exclude-standard"];
1178
1320
  if (path)
@@ -1509,7 +1651,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
1509
1651
  lineTruncated
1510
1652
  };
1511
1653
  }
1512
- var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
1654
+ var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
1513
1655
  var init_git = __esm(() => {
1514
1656
  init_runtime();
1515
1657
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
@@ -2775,7 +2917,7 @@ __export(exports_skill_cli, {
2775
2917
  SKILL_HELP: () => SKILL_HELP,
2776
2918
  AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
2777
2919
  });
2778
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
2920
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
2779
2921
  import { homedir as homedir2 } from "node:os";
2780
2922
  import { join as join5, resolve } from "node:path";
2781
2923
  function parseAgentList(value) {
@@ -2831,25 +2973,34 @@ function parseSkillArgs(argv) {
2831
2973
  }
2832
2974
  return { ok: true, args: { kind: "install", agents, global, cwd } };
2833
2975
  }
2976
+ function discoverBundledSkills(skillsRoot) {
2977
+ if (!existsSync4(skillsRoot))
2978
+ return [];
2979
+ return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync4(join5(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
2980
+ }
2834
2981
  function installSkill(args, deps) {
2835
- if (!existsSync4(join5(deps.sourceDir, "SKILL.md"))) {
2982
+ const skills = discoverBundledSkills(deps.skillsRoot);
2983
+ if (skills.length === 0) {
2836
2984
  return {
2837
2985
  ok: false,
2838
- error: `bundled skill not found at ${deps.sourceDir}`
2986
+ error: `no bundled skills found under ${deps.skillsRoot}`
2839
2987
  };
2840
2988
  }
2841
2989
  const base = args.global ? deps.homeDir : resolve(args.cwd ?? deps.projectDir);
2842
2990
  const results = [];
2843
2991
  for (const agent of args.agents) {
2844
- const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
2845
- const action = existsSync4(target) ? "updated" : "installed";
2846
- try {
2847
- mkdirSync2(target, { recursive: true });
2848
- cpSync(deps.sourceDir, target, { recursive: true });
2849
- } catch (error) {
2850
- return { ok: false, error: String(error) };
2992
+ for (const skill of skills) {
2993
+ const sourceDir = join5(deps.skillsRoot, skill);
2994
+ const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", skill);
2995
+ const action = existsSync4(target) ? "updated" : "installed";
2996
+ try {
2997
+ mkdirSync2(target, { recursive: true });
2998
+ cpSync(sourceDir, target, { recursive: true });
2999
+ } catch (error) {
3000
+ return { ok: false, error: String(error) };
3001
+ }
3002
+ results.push({ agent, skill, action, target });
2851
3003
  }
2852
- results.push({ agent, action, target });
2853
3004
  }
2854
3005
  return { ok: true, results };
2855
3006
  }
@@ -2865,7 +3016,7 @@ function runSkillCli(argv) {
2865
3016
  return;
2866
3017
  }
2867
3018
  const result = installSkill(parsed.args, {
2868
- sourceDir: join5(ROOT, "skills", SKILL_NAME),
3019
+ skillsRoot: join5(ROOT, "skills"),
2869
3020
  homeDir: homedir2(),
2870
3021
  projectDir: process.cwd()
2871
3022
  });
@@ -2874,13 +3025,13 @@ function runSkillCli(argv) {
2874
3025
  process.exit(1);
2875
3026
  }
2876
3027
  for (const entry of result.results) {
2877
- console.log(`${entry.action} (${entry.agent}): ${entry.target}`);
3028
+ console.log(`${entry.action} (${entry.agent}/${entry.skill}): ${entry.target}`);
2878
3029
  }
2879
3030
  if (result.results.some((entry) => entry.action === "installed")) {
2880
- console.log("Re-run the same command anytime to update the skill.");
3031
+ console.log("Re-run the same command anytime to update the skills.");
2881
3032
  }
2882
3033
  }
2883
- var SKILL_NAME = "code-viewer-annotate", AGENT_SKILL_DIRS, AGENT_NAMES, SKILL_HELP;
3034
+ var AGENT_SKILL_DIRS, AGENT_NAMES, SKILL_HELP;
2884
3035
  var init_skill_cli = __esm(() => {
2885
3036
  init_root();
2886
3037
  AGENT_SKILL_DIRS = {
@@ -2891,15 +3042,16 @@ var init_skill_cli = __esm(() => {
2891
3042
  agents: ".agents"
2892
3043
  };
2893
3044
  AGENT_NAMES = Object.keys(AGENT_SKILL_DIRS);
2894
- SKILL_HELP = `code-viewer skill — manage the bundled agent skill
3045
+ SKILL_HELP = `code-viewer skill — manage the bundled agent skills
2895
3046
 
2896
3047
  Usage:
2897
3048
  code-viewer skill install [--agent <list>] [--global] [--cwd <dir>]
2898
3049
 
2899
- Installs the ${SKILL_NAME} skill (SKILL.md for AI coding agents) into the
2900
- skills directory of each selected agent in the current project, or into the
2901
- home directory equivalents with --global. Running install again overwrites
2902
- the files, so the same command also updates an existing installation.
3050
+ Installs every bundled skill (each SKILL.md directory under the package's
3051
+ skills/) into the skills directory of each selected agent in the current
3052
+ project, or into the home directory equivalents with --global. Running
3053
+ install again overwrites the files, so the same command also updates an
3054
+ existing installation.
2903
3055
 
2904
3056
  Options:
2905
3057
  --agent <list> comma separated agents: ${AGENT_NAMES.join(", ")}, or all
@@ -4006,12 +4158,41 @@ function sanitizeDbUiPrefs(raw) {
4006
4158
  }
4007
4159
  return Object.keys(out).length > 0 ? out : undefined;
4008
4160
  }
4161
+ function sanitizeDbUiExpandedTables(raw) {
4162
+ if (!isRecord(raw))
4163
+ return;
4164
+ const out = {};
4165
+ let scopeCount = 0;
4166
+ for (const [scopeRaw, tablesRaw] of Object.entries(raw)) {
4167
+ if (scopeCount >= MAX_DB_UI_EXPANDED_SCOPES)
4168
+ break;
4169
+ const scope = safeObjectKey(scopeRaw);
4170
+ if (!scope)
4171
+ continue;
4172
+ const tables = normalizeStringList(tablesRaw, {
4173
+ maxItems: MAX_DB_UI_TABLES,
4174
+ maxLen: MAX_KEY_LEN,
4175
+ sort: true
4176
+ });
4177
+ if (!tables || tables.length === 0)
4178
+ continue;
4179
+ out[scope] = tables;
4180
+ scopeCount++;
4181
+ }
4182
+ return Object.keys(out).length > 0 ? out : undefined;
4183
+ }
4009
4184
  function sanitizeDbUiState(raw) {
4010
4185
  if (!isRecord(raw))
4011
4186
  return emptyDbUiState();
4012
4187
  const prefs = sanitizeDbUiPrefs(raw.prefs);
4188
+ const expandedTables = sanitizeDbUiExpandedTables(raw.expandedTables);
4013
4189
  if (!isRecord(raw.columnWidths)) {
4014
- return prefs ? { ...emptyDbUiState(), prefs } : emptyDbUiState();
4190
+ const out2 = emptyDbUiState();
4191
+ if (expandedTables)
4192
+ out2.expandedTables = expandedTables;
4193
+ if (prefs)
4194
+ out2.prefs = prefs;
4195
+ return out2;
4015
4196
  }
4016
4197
  const columnWidths = {};
4017
4198
  let dbCount = 0;
@@ -4052,6 +4233,8 @@ function sanitizeDbUiState(raw) {
4052
4233
  dbCount++;
4053
4234
  }
4054
4235
  const out = { version: 1, columnWidths };
4236
+ if (expandedTables)
4237
+ out.expandedTables = expandedTables;
4055
4238
  if (prefs)
4056
4239
  out.prefs = prefs;
4057
4240
  return out;
@@ -4069,12 +4252,30 @@ function mergeDbUiPrefs(current, patch) {
4069
4252
  }
4070
4253
  return Object.keys(next).length > 0 ? next : undefined;
4071
4254
  }
4255
+ function mergeDbUiExpandedTables(current, patch) {
4256
+ if (!isRecord(patch))
4257
+ return current;
4258
+ const next = { ...current ?? {} };
4259
+ for (const [scope, tablesRaw] of Object.entries(patch)) {
4260
+ if (tablesRaw === null) {
4261
+ delete next[scope];
4262
+ continue;
4263
+ }
4264
+ next[scope] = tablesRaw;
4265
+ }
4266
+ return sanitizeDbUiExpandedTables(next);
4267
+ }
4072
4268
  function mergeDbUiState(current, patch) {
4073
4269
  if (!isRecord(patch))
4074
4270
  return current;
4075
4271
  const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
4272
+ const mergedExpandedTables = "expandedTables" in patch ? mergeDbUiExpandedTables(current.expandedTables, patch.expandedTables) : current.expandedTables;
4076
4273
  if (!isRecord(patch.columnWidths)) {
4077
4274
  const merged = { ...current, version: 1 };
4275
+ if (mergedExpandedTables)
4276
+ merged.expandedTables = mergedExpandedTables;
4277
+ else
4278
+ delete merged.expandedTables;
4078
4279
  if (mergedPrefs)
4079
4280
  merged.prefs = mergedPrefs;
4080
4281
  else
@@ -4114,6 +4315,7 @@ function mergeDbUiState(current, patch) {
4114
4315
  ...current,
4115
4316
  ...patch,
4116
4317
  columnWidths,
4318
+ expandedTables: mergedExpandedTables,
4117
4319
  prefs: mergedPrefs,
4118
4320
  version: 1
4119
4321
  });
@@ -4145,7 +4347,7 @@ async function patchDbUiState(root, patch) {
4145
4347
  return { state: next, result: next };
4146
4348
  });
4147
4349
  }
4148
- var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
4350
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
4149
4351
  var init_state_store = __esm(() => {
4150
4352
  init_json_store();
4151
4353
  DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
@@ -5258,9 +5460,12 @@ function createDockerAdapter(config) {
5258
5460
  const tableLiteral = table.replace(/'/g, "''");
5259
5461
  if (config.kind === "postgresql") {
5260
5462
  const schemaLiteral = postgresSchemaLiteral();
5261
- return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END FROM information_schema.columns c LEFT JOIN (SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name WHERE tc.table_schema = ${schemaLiteral} AND tc.table_name = '${tableLiteral}' AND tc.constraint_type = 'PRIMARY KEY') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
5463
+ return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum LEFT JOIN (SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name WHERE tc.table_schema = ${schemaLiteral} AND tc.table_name = '${tableLiteral}' AND tc.constraint_type = 'PRIMARY KEY') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
5262
5464
  }
5263
- return `SELECT column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
5465
+ return `SELECT column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
5466
+ }
5467
+ function columnCommentFromInfoValue(value) {
5468
+ return value ? value : null;
5264
5469
  }
5265
5470
  function columnsFromInfoRows(rows) {
5266
5471
  if (config.kind === "postgresql") {
@@ -5269,7 +5474,8 @@ function createDockerAdapter(config) {
5269
5474
  type: row[1],
5270
5475
  nullable: row[2] === "YES",
5271
5476
  primaryKey: row[4] === "YES",
5272
- defaultValue: row[3] === "" ? null : row[3]
5477
+ defaultValue: row[3] === "" ? null : row[3],
5478
+ comment: columnCommentFromInfoValue(row[5])
5273
5479
  }));
5274
5480
  }
5275
5481
  return rows.map((row) => ({
@@ -5277,7 +5483,8 @@ function createDockerAdapter(config) {
5277
5483
  type: row[1],
5278
5484
  nullable: row[2] === "YES",
5279
5485
  primaryKey: row[4] === "PRI",
5280
- defaultValue: row[3] === "NULL" ? null : row[3]
5486
+ defaultValue: row[3] === "NULL" ? null : row[3],
5487
+ comment: columnCommentFromInfoValue(row[5])
5281
5488
  }));
5282
5489
  }
5283
5490
  async function fetchColumnsAsyncUncached(table, signal) {
@@ -5376,10 +5583,10 @@ function createDockerAdapter(config) {
5376
5583
  let sql;
5377
5584
  if (config.kind === "postgresql") {
5378
5585
  const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
5379
- sql = `SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ${postgresSchemaLiteral()} AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5586
+ sql = `SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum WHERE c.table_schema = ${postgresSchemaLiteral()} AND c.table_name IN (${inList}) ORDER BY c.table_name, c.ordinal_position`;
5380
5587
  } else {
5381
5588
  const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
5382
- sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5589
+ sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5383
5590
  }
5384
5591
  try {
5385
5592
  const queryResult = await execAsync(sql, signal);
@@ -5415,7 +5622,8 @@ function createDockerAdapter(config) {
5415
5622
  type: row[2],
5416
5623
  nullable: row[3] === "YES",
5417
5624
  primaryKey: pkCols.has(row[1]),
5418
- defaultValue: row[4] === "" ? null : row[4]
5625
+ defaultValue: row[4] === "" ? null : row[4],
5626
+ comment: columnCommentFromInfoValue(row[5])
5419
5627
  }));
5420
5628
  } else {
5421
5629
  cols = rows.map((row) => ({
@@ -5423,7 +5631,8 @@ function createDockerAdapter(config) {
5423
5631
  type: row[2],
5424
5632
  nullable: row[3] === "YES",
5425
5633
  primaryKey: row[5] === "PRI",
5426
- defaultValue: row[4] === "NULL" ? null : row[4]
5634
+ defaultValue: row[4] === "NULL" ? null : row[4],
5635
+ comment: columnCommentFromInfoValue(row[6])
5427
5636
  }));
5428
5637
  }
5429
5638
  columnCache.set(tbl, cols);
@@ -5516,12 +5725,7 @@ function createDockerAdapter(config) {
5516
5725
  async getFilteredTablePageWithMeta(table, options, signal) {
5517
5726
  const id = tableIdentifier(table);
5518
5727
  const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
5519
- let columns;
5520
- try {
5521
- columns = await columnsPromise;
5522
- } catch (err) {
5523
- throw err;
5524
- }
5728
+ const columns = await columnsPromise;
5525
5729
  const columnNames = columns.map((column) => column.name);
5526
5730
  const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
5527
5731
  const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
@@ -11597,7 +11801,7 @@ function fileToMeta(file, range, extraQs) {
11597
11801
  untracked: file.untracked || false
11598
11802
  };
11599
11803
  }
11600
- function computePayload(extras, range) {
11804
+ function computePayload(extras, range, pathFilter = "") {
11601
11805
  if (isSameWorktreeRange(range)) {
11602
11806
  return {
11603
11807
  files: [],
@@ -11613,8 +11817,9 @@ function computePayload(extras, range) {
11613
11817
  const files = fileMeta(fullArgs, cwd, false);
11614
11818
  if (includeUntracked(range, refs2))
11615
11819
  files.push(...untrackedMeta(cwd));
11616
- files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
11617
- files.forEach((file, i) => {
11820
+ const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
11821
+ filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
11822
+ filteredFiles.forEach((file, i) => {
11618
11823
  file.order = i + 1;
11619
11824
  });
11620
11825
  const extraQs = {};
@@ -11624,7 +11829,7 @@ function computePayload(extras, range) {
11624
11829
  if (e === "--ignore-blank-lines")
11625
11830
  extraQs.ignore_blank = "1";
11626
11831
  }
11627
- const meta = files.map((file) => fileToMeta(file, range, extraQs));
11832
+ const meta = filteredFiles.map((file) => fileToMeta(file, range, extraQs));
11628
11833
  const totals = meta.reduce((acc, file) => {
11629
11834
  acc.additions += file.additions || 0;
11630
11835
  acc.deletions += file.deletions || 0;
@@ -11651,9 +11856,12 @@ function handleDiffJson(url) {
11651
11856
  from: url.searchParams.get("from") || "",
11652
11857
  to: url.searchParams.get("to") || ""
11653
11858
  };
11654
- const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}`;
11859
+ const path = url.searchParams.get("path") || "";
11860
+ if (path && !safePath(path))
11861
+ return text("invalid path", 400);
11862
+ const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}|${path}`;
11655
11863
  if (url.searchParams.get("nocache") === "1") {
11656
- const payload2 = computePayload(extras, range);
11864
+ const payload2 = computePayload(extras, range, path);
11657
11865
  const sig = JSON.stringify({ ...payload2, generation: undefined });
11658
11866
  const cached2 = metaCache.get(key);
11659
11867
  if (!cached2 || cached2.sig !== sig) {
@@ -11679,7 +11887,7 @@ function handleDiffJson(url) {
11679
11887
  "Cache-Control": "no-store"
11680
11888
  }
11681
11889
  });
11682
- const payload = computePayload(extras, range);
11890
+ const payload = computePayload(extras, range, path);
11683
11891
  const body = JSON.stringify(payload);
11684
11892
  setTimedCacheEntry(metaCache, key, {
11685
11893
  body,
@@ -12096,15 +12304,109 @@ function handleLog(url) {
12096
12304
  const ref = url.searchParams.get("ref") || "HEAD";
12097
12305
  const skip = Number(url.searchParams.get("skip") || "0");
12098
12306
  const limit = Number(url.searchParams.get("limit") || "50");
12307
+ const path = url.searchParams.get("path") || "";
12308
+ if (path && !safePath(path))
12309
+ return text("invalid path", 400);
12099
12310
  const result = commitHistory(cwd, {
12100
12311
  ref,
12101
12312
  skip: Number.isFinite(skip) ? skip : 0,
12102
12313
  limit: Number.isFinite(limit) ? limit : 50,
12103
- query: url.searchParams.get("q") || ""
12314
+ query: url.searchParams.get("q") || "",
12315
+ ...path ? { path } : {}
12104
12316
  });
12105
12317
  if (result.error)
12106
12318
  return text(result.error, 400);
12107
- return json2({ commits: result.commits, hasMore: result.hasMore });
12319
+ const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
12320
+ let commits = result.commits;
12321
+ let hasWorktree = false;
12322
+ if (wantsWorktreeHead) {
12323
+ const status = runSync([
12324
+ "git",
12325
+ "-c",
12326
+ "core.quotepath=false",
12327
+ "status",
12328
+ "--porcelain=v1",
12329
+ "-z",
12330
+ "--untracked-files=normal",
12331
+ "--",
12332
+ path
12333
+ ], cwd);
12334
+ if (status.code === 0 && status.stdout.length > 0) {
12335
+ const parts = status.stdout.split("\x00").filter(Boolean);
12336
+ if (parts.length > 0) {
12337
+ hasWorktree = true;
12338
+ commits = [
12339
+ {
12340
+ sha: "worktree",
12341
+ subject: "未コミット変更 (Working tree)",
12342
+ author: "",
12343
+ when: "",
12344
+ parents: [],
12345
+ body: ""
12346
+ },
12347
+ ...commits
12348
+ ];
12349
+ }
12350
+ }
12351
+ }
12352
+ return json2({
12353
+ commits,
12354
+ hasMore: result.hasMore,
12355
+ generation,
12356
+ ...hasWorktree ? { hasWorktree: true } : {}
12357
+ });
12358
+ }
12359
+ function blamePathKey(p) {
12360
+ try {
12361
+ const st = statSync3(join13(cwd, p));
12362
+ return `${st.mtimeMs}:${st.size}`;
12363
+ } catch {
12364
+ return "missing";
12365
+ }
12366
+ }
12367
+ function rememberBlame(key, value) {
12368
+ if (blameCache.has(key))
12369
+ blameCache.delete(key);
12370
+ blameCache.set(key, value);
12371
+ while (blameCache.size > BLAME_CACHE_MAX) {
12372
+ const oldest = blameCache.keys().next();
12373
+ if (oldest.done)
12374
+ break;
12375
+ blameCache.delete(oldest.value);
12376
+ }
12377
+ }
12378
+ function handleFileBlame(url) {
12379
+ const path = url.searchParams.get("path") || "";
12380
+ if (!safePath(path))
12381
+ return text("invalid path", 400);
12382
+ const ref = url.searchParams.get("ref") || "worktree";
12383
+ if (!ref || ref.startsWith("-") || ref.includes("\x00"))
12384
+ return text("invalid ref", 400);
12385
+ const rawBase = url.searchParams.get("base");
12386
+ const requestedBase = rawBase === "HEAD" ? "HEAD" : rawBase === "worktree" ? "worktree" : ref === "worktree" ? "worktree" : "HEAD";
12387
+ const normalized = normalizeBlameRef(ref, requestedBase);
12388
+ const { base } = normalized;
12389
+ let cacheKey;
12390
+ if (base === "worktree") {
12391
+ cacheKey = `worktree|${path}|${blamePathKey(path)}`;
12392
+ } else {
12393
+ const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
12394
+ if (resolved.code !== 0)
12395
+ return text("unknown ref", 400);
12396
+ cacheKey = `HEAD|${path}|${resolved.stdout.trim()}`;
12397
+ }
12398
+ const cached = blameCache.get(cacheKey);
12399
+ if (cached) {
12400
+ if (blameCache.has(cacheKey)) {
12401
+ blameCache.delete(cacheKey);
12402
+ blameCache.set(cacheKey, cached);
12403
+ }
12404
+ return json2({ ...cached, base, ref, generation });
12405
+ }
12406
+ const result = blame(cwd, { path, ref: normalized.ref, base });
12407
+ if (!result.error)
12408
+ rememberBlame(cacheKey, result);
12409
+ return json2({ ...result, base, ref, generation });
12108
12410
  }
12109
12411
  function handleFileDiff(url) {
12110
12412
  const path = url.searchParams.get("path") || "";
@@ -13113,7 +13415,7 @@ function restartWorktreeWatch() {
13113
13415
  }
13114
13416
  worktreeWatch = startScopedWorktreeWatch();
13115
13417
  }
13116
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13418
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13117
13419
  var init_preview = __esm(async () => {
13118
13420
  init_routes();
13119
13421
  init_annotations();
@@ -13181,6 +13483,7 @@ var init_preview = __esm(async () => {
13181
13483
  sseClients = new Set;
13182
13484
  sseKeepalives = new Map;
13183
13485
  fileCache = new Map;
13486
+ blameCache = new Map;
13184
13487
  metaCache = new Map;
13185
13488
  fileListCache = new Map;
13186
13489
  lineIndexCache = new Map;
@@ -13213,6 +13516,8 @@ var init_preview = __esm(async () => {
13213
13516
  return handleRefCommits(url);
13214
13517
  if (url.pathname === "/_log")
13215
13518
  return handleLog(url);
13519
+ if (url.pathname === "/_file_blame")
13520
+ return handleFileBlame(url);
13216
13521
  if (url.pathname === "/file_diff")
13217
13522
  return handleFileDiff(url);
13218
13523
  if (url.pathname === "/file_range")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.2.10",
3
+ "version": "0.3.0",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {