@kud/gh-ink 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -311,4 +311,91 @@ declare const writeCache: (key: string, data: {
311
311
  login: string;
312
312
  }) => void;
313
313
 
314
- export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxExtension, type JiraTransition, type RepoHeader, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, clipboard, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, maxViewStart, moveCursor, openInTab, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
314
+ /**
315
+ * A named slice of the reader's repos. Two or more turn on the in-app toggle;
316
+ * one or none means an undivided set and no toggle at all.
317
+ *
318
+ * Named rather than boolean because the distinction was never a property of a
319
+ * repo — it is whatever separation the reader actually keeps, and a boolean
320
+ * leaves a third slice nowhere to go.
321
+ */
322
+ type RepoProfile = {
323
+ /** Stable id — what `--profile=<name>` selects. */
324
+ name: string;
325
+ /** Shown on the toggle. Defaults to `name`. */
326
+ label?: string;
327
+ /** Which repos belong to this profile. */
328
+ match: (repo: string) => boolean;
329
+ /** Where this profile's checkouts live, if it keeps its own directory. */
330
+ checkoutDir?: string;
331
+ };
332
+ type InboxConfig = {
333
+ /**
334
+ * Repo ranking, best first. An entry ending in `/` matches by owner prefix,
335
+ * anything else must equal `owner/name` exactly — so a single repo can outrank
336
+ * the owner that contains it. Repos matching nothing sort last, together.
337
+ */
338
+ repoPriority: readonly string[];
339
+ profiles: readonly RepoProfile[];
340
+ /**
341
+ * Fallback directory holding checkouts, for repos in no profile that names one
342
+ * of its own. Unset means local checkouts are not resolved at all, which is
343
+ * correct for a host that only ever opens things in a browser.
344
+ */
345
+ checkoutDir?: string;
346
+ /**
347
+ * Directory name the on-disk cache lives under, inside the platform cache
348
+ * root. Defaults to this package's name — a library has no business claiming a
349
+ * directory named after whichever tool happens to embed it, and two hosts on
350
+ * one machine would otherwise share a cache keyed only by section name.
351
+ */
352
+ cacheNamespace: string;
353
+ /**
354
+ * How long a cached inbox is trusted before a launch refetches. Every launch
355
+ * past it pays the full query — eight searches with check rollups — so this is
356
+ * the single knob between "always current" and "always slow, and drawing 502s
357
+ * from the API".
358
+ *
359
+ * It can be generous because staleness is bounded from the other end: acting
360
+ * on a row drops the entry outright, and `r` refetches on demand. What the TTL
361
+ * actually governs is how long a change made ELSEWHERE — someone approving
362
+ * your PR while you were not looking — can go unseen, which is a glance
363
+ * arriving late, not a wrong action taken.
364
+ */
365
+ cacheTtlMs: number;
366
+ };
367
+ /** Supply the host's opinions. Call once, before rendering. */
368
+ declare const configureInbox: (config: Partial<InboxConfig>) => void;
369
+ declare const inboxConfig: () => InboxConfig;
370
+ /** Test seam — restores the empty defaults. */
371
+ declare const resetInboxConfig: () => void;
372
+ /** The profile a repo belongs to, or null when none claims it. */
373
+ declare const profileOf: (repo: string) => RepoProfile | null;
374
+ /**
375
+ * Where checkouts are searched, in order: every profile that names its own
376
+ * directory, then the fallback. Deduped, since the ordinary single-directory
377
+ * case points them all at the same place. Empty when nothing is configured —
378
+ * callers treat that as "not checked out locally", which it truthfully is.
379
+ */
380
+ declare const checkoutDirs: () => string[];
381
+
382
+ type RepoFilter = {
383
+ /** Show only repos matching one of these. Empty or absent means show all. */
384
+ include?: readonly string[];
385
+ /** Hide repos matching one of these. Applied after include, and wins. */
386
+ exclude?: readonly string[];
387
+ };
388
+ /**
389
+ * Whether a repo survives the filter. No include list means everything is in;
390
+ * exclude is applied afterwards and always wins, so a reader can take a whole
391
+ * org and drop one repo from it without listing the rest.
392
+ */
393
+ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
394
+ /**
395
+ * Parse a comma-separated pattern list, as a flag supplies it. Blank entries and
396
+ * surrounding whitespace are dropped, so a trailing comma or a quoted list with
397
+ * spaces after the commas both behave.
398
+ */
399
+ declare const parsePatterns: (value: string) => string[];
400
+
401
+ export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxConfig, type InboxExtension, type JiraTransition, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/dist/index.js CHANGED
@@ -606,9 +606,37 @@ var healthLegend = [
606
606
  ["merged", "Merged"],
607
607
  ["closed", "Closed"]
608
608
  ];
609
- var CACHE_TTL_MS = 12e4;
609
+
610
+ // src/inbox/config.ts
611
+ var EMPTY = {
612
+ repoPriority: [],
613
+ profiles: [],
614
+ cacheNamespace: "gh-ink",
615
+ cacheTtlMs: 6e5
616
+ };
617
+ var current = EMPTY;
618
+ var configureInbox = (config) => {
619
+ current = { ...EMPTY, ...config };
620
+ };
621
+ var inboxConfig = () => current;
622
+ var resetInboxConfig = () => {
623
+ current = EMPTY;
624
+ };
625
+ var profileOf = (repo) => current.profiles.find((p) => p.match(repo)) ?? null;
626
+ var checkoutDirs = () => [
627
+ .../* @__PURE__ */ new Set([
628
+ ...current.profiles.flatMap((p) => p.checkoutDir ?? []),
629
+ ...current.checkoutDir ? [current.checkoutDir] : []
630
+ ])
631
+ ];
632
+
633
+ // src/inbox/cache.ts
634
+ var cacheTtlMs = () => inboxConfig().cacheTtlMs;
610
635
  var CACHE_VERSION = 2;
611
- var cacheDir = () => join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "ambre");
636
+ var cacheDir = () => join(
637
+ process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
638
+ inboxConfig().cacheNamespace
639
+ );
612
640
  var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
613
641
  var readCache = (key) => {
614
642
  try {
@@ -620,7 +648,7 @@ var readCache = (key) => {
620
648
  return null;
621
649
  }
622
650
  };
623
- var isFresh = (cached, now = Date.now()) => !!cached && now - cached.at < CACHE_TTL_MS;
651
+ var isFresh = (cached, now = Date.now()) => !!cached && now - cached.at < cacheTtlMs();
624
652
  var writeCache = (key, data) => {
625
653
  try {
626
654
  mkdirSync(cacheDir(), { recursive: true });
@@ -840,14 +868,11 @@ var explainItem = (item, login) => {
840
868
  ];
841
869
  };
842
870
  var repoPriority = (repo) => {
843
- const profile = process.env.OS_PROFILE ?? "";
844
- if (profile === "work") {
845
- if (repo === "theorchard/orchardgo") return 0;
846
- if (repo.startsWith("theorchard/")) return 1;
847
- if (repo.startsWith("kud/")) return 2;
848
- return 3;
849
- }
850
- return repo.startsWith("kud/") ? 0 : 1;
871
+ const order = inboxConfig().repoPriority;
872
+ const i = order.findIndex(
873
+ (p) => p.endsWith("/") ? repo.startsWith(p) : repo === p
874
+ );
875
+ return i === -1 ? order.length : i;
851
876
  };
852
877
  var sortItems = (items) => [...items].sort((a, b) => {
853
878
  const pd = repoPriority(a.repo) - repoPriority(b.repo);
@@ -996,11 +1021,11 @@ var reposInSections = (sections) => [
996
1021
  sections.flatMap((s) => s.items).filter((i) => i.kind === "pr" || i.kind === "issue").map((i) => i.repo)
997
1022
  )
998
1023
  ].sort();
999
- var moveCursor = (items, current, dir) => {
1000
- let next = current + dir;
1024
+ var moveCursor = (items, current2, dir) => {
1025
+ let next = current2 + dir;
1001
1026
  while (next >= 0 && next < items.length && (items[next].kind === "repo-header" || items[next].kind === "subgroup-header"))
1002
1027
  next += dir;
1003
- if (next < 0 || next >= items.length) return current;
1028
+ if (next < 0 || next >= items.length) return current2;
1004
1029
  return next;
1005
1030
  };
1006
1031
  var itemLines = (item, isFirst) => (item.kind === "repo-header" || item.kind === "subgroup-header") && !isFirst ? 2 : 1;
@@ -1049,11 +1074,8 @@ var clipboard = (text) => {
1049
1074
  };
1050
1075
  var buildCheckoutCmd = async (repoFull, branch, login) => {
1051
1076
  const [repoOwner, repoName] = repoFull.split("/");
1052
- const projects = process.env.PROJECTS_DIR ?? `${process.env.HOME}/Projects`;
1053
- const profile = process.env.OS_PROFILE ?? "";
1054
- const isWorkRepo = profile === "work" && (repoFull.startsWith("theorchard/") || repoFull.startsWith("kud/") && (repoName ?? "").startsWith("theorchard-"));
1055
- const cloneBase = profile === "work" ? `${projects}/${isWorkRepo ? "work" : "home"}` : projects;
1056
- const searchDirs = profile === "work" ? [`${projects}/work`, `${projects}/home`] : [projects];
1077
+ const searchDirs = checkoutDirs();
1078
+ const cloneBase = profileOf(repoFull)?.checkoutDir ?? inboxConfig().checkoutDir ?? "";
1057
1079
  let repoPath = "";
1058
1080
  outer: for (const searchDir of searchDirs) {
1059
1081
  if (!existsSync(searchDir)) continue;
@@ -1108,9 +1130,7 @@ var buildCheckoutCmd = async (repoFull, branch, login) => {
1108
1130
  return cmd;
1109
1131
  };
1110
1132
  var resolveRepoPath = async (repoFull) => {
1111
- const projects = process.env.PROJECTS_DIR ?? `${process.env.HOME}/Projects`;
1112
- const profile = process.env.OS_PROFILE ?? "";
1113
- const searchDirs = profile === "work" ? [`${projects}/work`, `${projects}/home`] : [projects];
1133
+ const searchDirs = checkoutDirs();
1114
1134
  for (const searchDir of searchDirs) {
1115
1135
  if (!existsSync(searchDir)) continue;
1116
1136
  let entries;
@@ -2936,4 +2956,18 @@ var App = ({
2936
2956
  ] });
2937
2957
  };
2938
2958
 
2939
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, clipboard, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, maxViewStart, moveCursor, openInTab, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
2959
+ // src/inbox/filter.ts
2960
+ var toRegExp = (pattern) => {
2961
+ const full = pattern.includes("/") ? pattern : `${pattern}/*`;
2962
+ const escaped = full.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2963
+ return new RegExp(`^${escaped.replace(/\*/g, "[^/]*")}$`, "i");
2964
+ };
2965
+ var matchesAny = (repo, patterns) => patterns.some((p) => toRegExp(p).test(repo));
2966
+ var matchesFilter = (repo, filter) => {
2967
+ const { include = [], exclude = [] } = filter;
2968
+ if (include.length > 0 && !matchesAny(repo, include)) return false;
2969
+ return !matchesAny(repo, exclude);
2970
+ };
2971
+ var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
2972
+
2973
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Ink components for rendering GitHub PR review comments and health — controlled, presentation-only, built on @kud/ink-ui and fed by @kud/gh.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",