opencode-db-search 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -283,6 +283,112 @@ function kvBlock(pairs: Array<[string, string]>): void {
283
283
  }
284
284
  }
285
285
 
286
+ /**
287
+ * Word-wrap text to fit within a given width.
288
+ * Breaks at spaces; falls back to hard-break for words exceeding width.
289
+ */
290
+ function wordWrap(text: string, width: number): string[] {
291
+ if (width <= 0) return [text];
292
+ const words = text.split(/\s+/);
293
+ const lines: string[] = [];
294
+ let line = "";
295
+ for (const word of words) {
296
+ if (word === "") continue;
297
+ if (line.length === 0) {
298
+ // Hard-break words longer than width
299
+ if (visibleLen(word) > width) {
300
+ let remaining = word;
301
+ while (visibleLen(remaining) > width) {
302
+ lines.push(remaining.slice(0, width));
303
+ remaining = remaining.slice(width);
304
+ }
305
+ line = remaining;
306
+ } else {
307
+ line = word;
308
+ }
309
+ } else if (visibleLen(line) + 1 + visibleLen(word) <= width) {
310
+ line += " " + word;
311
+ } else {
312
+ lines.push(line);
313
+ if (visibleLen(word) > width) {
314
+ let remaining = word;
315
+ while (visibleLen(remaining) > width) {
316
+ lines.push(remaining.slice(0, width));
317
+ remaining = remaining.slice(width);
318
+ }
319
+ line = remaining;
320
+ } else {
321
+ line = word;
322
+ }
323
+ }
324
+ }
325
+ if (line) lines.push(line);
326
+ return lines.length > 0 ? lines : [""];
327
+ }
328
+
329
+ /**
330
+ * Render a search result card with box-drawing borders.
331
+ */
332
+ function card(opts: {
333
+ id: string;
334
+ badge: string;
335
+ title: string;
336
+ subtitle?: string;
337
+ body: string[];
338
+ width?: number;
339
+ maxBodyLines?: number;
340
+ }): void {
341
+ const w = opts.width || 80;
342
+ const inner = w - 4; // "│ " + content + " │"
343
+ const maxLines = opts.maxBodyLines || 6;
344
+ const dim = (s: string) => ansi("2", s);
345
+
346
+ // Top border: ┌─ id ──...── badge ─┐
347
+ const idStr = ` ${opts.id} `;
348
+ const badgeStr = ` ${opts.badge} `;
349
+ const fill = w - 2 - idStr.length - badgeStr.length; // -2 for ┌ and ┐
350
+ const topLine =
351
+ dim(BOX.tl + BOX.h) +
352
+ ansi("1;36", idStr) +
353
+ dim(BOX.h.repeat(Math.max(1, fill))) +
354
+ dim(badgeStr) +
355
+ dim(BOX.h + BOX.tr);
356
+ console.log(topLine);
357
+
358
+ // Body line renderer
359
+ const printLine = (text: string) => {
360
+ const padded = pad(text, inner);
361
+ // Truncate if pad somehow exceeds inner width
362
+ const display = visibleLen(padded) > inner ? trunc(padded, inner) : padded;
363
+ console.log(`${dim(BOX.v)} ${display} ${dim(BOX.v)}`);
364
+ };
365
+
366
+ // Title (bold)
367
+ printLine(ansi("1", trunc(opts.title, inner)));
368
+
369
+ // Subtitle (dimmed)
370
+ if (opts.subtitle) {
371
+ printLine(ansi("2", trunc(opts.subtitle, inner)));
372
+ }
373
+
374
+ // Blank separator before body
375
+ if (opts.body.length > 0) {
376
+ printLine("");
377
+ let printed = 0;
378
+ for (const line of opts.body) {
379
+ if (printed >= maxLines) {
380
+ printLine(ansi("2", "…"));
381
+ break;
382
+ }
383
+ printLine(line);
384
+ printed++;
385
+ }
386
+ }
387
+
388
+ // Bottom border
389
+ console.log(dim(BOX.bl + BOX.h.repeat(w - 2) + BOX.br));
390
+ }
391
+
286
392
  function size(bytes: number): string {
287
393
  if (bytes < 1024) return `${bytes} B`;
288
394
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -953,7 +1059,7 @@ function cmdSearch(db: Database) {
953
1059
  )
954
1060
  .all(...w.params, limit) as any[];
955
1061
 
956
- const radius = Math.max(20, Math.floor(width / 2));
1062
+ const radius = 250;
957
1063
  for (const r of rows) {
958
1064
  const pv = partPreview(r.part_data);
959
1065
  const idx = pv.toLowerCase().indexOf(query.toLowerCase());
@@ -968,7 +1074,7 @@ function cmdSearch(db: Database) {
968
1074
  title: r.title,
969
1075
  directory: r.directory,
970
1076
  match: "part",
971
- preview: ctx || pv.slice(0, width),
1077
+ preview: ctx || pv.slice(0, 500),
972
1078
  updated: r.time_updated,
973
1079
  created: r.time_created,
974
1080
  });
@@ -986,13 +1092,26 @@ function cmdSearch(db: Database) {
986
1092
  console.log(`No results for "${query}".`);
987
1093
  return;
988
1094
  }
1095
+
1096
+ const cardWidth = Math.min(width, termWidth());
1097
+ const cardInner = cardWidth - 4;
989
1098
  for (const r of final) {
990
- const dir = r.directory ? trunc(r.directory, 50) : "";
991
- console.log(
992
- `${ansi("36", r.session)} ${ansi("2", r.match)} ${trunc(r.title || "", 40)}`,
993
- );
994
- if (dir) console.log(` ${ansi("2", dir)}`);
995
- console.log(` ${highlight(trunc(r.preview, width), query)}`);
1099
+ // For title matches, the title is already shown — skip redundant preview
1100
+ const body =
1101
+ r.match === "title"
1102
+ ? []
1103
+ : wordWrap(r.preview, cardInner).map((line) => highlight(line, query));
1104
+ card({
1105
+ id: r.session,
1106
+ badge: r.match,
1107
+ title:
1108
+ r.match === "title"
1109
+ ? highlight(r.title || "(untitled)", query)
1110
+ : r.title || "(untitled)",
1111
+ subtitle: r.directory || undefined,
1112
+ body,
1113
+ width: cardWidth,
1114
+ });
996
1115
  console.log();
997
1116
  }
998
1117
  console.log(`${final.length} result(s)`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-db-search",
4
- "version": "1.0.2",
4
+ "version": "1.1.0",
5
5
  "description": "Search and inspect the OpenCode SQLite database from the command line",
6
6
  "type": "module",
7
7
  "bin": {