get-tbd 0.2.2 → 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.
Files changed (86) hide show
  1. package/README.md +38 -6
  2. package/dist/bin.mjs +4036 -1074
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +2505 -1585
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-BJz1m9eN.mjs → config-B1w3pAkY.mjs} +142 -279
  7. package/dist/config-B1w3pAkY.mjs.map +1 -0
  8. package/dist/config-DXhifxOw.mjs +3 -0
  9. package/dist/doc-cache-CamtXfi4.mjs +1035 -0
  10. package/dist/doc-cache-CamtXfi4.mjs.map +1 -0
  11. package/dist/doc-cache-CiBwpDTf.mjs +3 -0
  12. package/dist/doc-fork-BMjqzfAs.mjs +3 -0
  13. package/dist/doc-fork-CoPi1G1N.mjs +304 -0
  14. package/dist/doc-fork-CoPi1G1N.mjs.map +1 -0
  15. package/dist/docref-C7g0sjvL.mjs +167 -0
  16. package/dist/docref-C7g0sjvL.mjs.map +1 -0
  17. package/dist/docs/README.md +38 -6
  18. package/dist/docs/SKILL.md +21 -5
  19. package/dist/docs/guidelines/backward-compatibility-rules.md +1 -0
  20. package/dist/docs/guidelines/bun-monorepo-patterns.md +1 -0
  21. package/dist/docs/guidelines/cli-agent-skill-patterns.md +228 -52
  22. package/dist/docs/guidelines/commit-conventions.md +1 -0
  23. package/dist/docs/guidelines/common-doc-guidelines.md +1 -0
  24. package/dist/docs/guidelines/convex-limits-best-practices.md +1 -0
  25. package/dist/docs/guidelines/convex-rules.md +1 -0
  26. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -0
  27. package/dist/docs/guidelines/error-handling-rules.md +4 -0
  28. package/dist/docs/guidelines/general-coding-rules.md +3 -1
  29. package/dist/docs/guidelines/general-comment-rules.md +3 -1
  30. package/dist/docs/guidelines/general-eng-agent-principles.md +127 -0
  31. package/dist/docs/guidelines/general-tdd-guidelines.md +7 -0
  32. package/dist/docs/guidelines/general-testing-rules.md +5 -0
  33. package/dist/docs/guidelines/golden-testing-guidelines.md +1 -0
  34. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +1 -0
  35. package/dist/docs/guidelines/python-cli-patterns.md +5 -0
  36. package/dist/docs/guidelines/python-modern-guidelines.md +4 -0
  37. package/dist/docs/guidelines/python-rules.md +7 -0
  38. package/dist/docs/guidelines/release-notes-guidelines.md +1 -0
  39. package/dist/docs/guidelines/supply-chain-hardening.md +3 -2
  40. package/dist/docs/guidelines/tbd-sync-troubleshooting.md +25 -2
  41. package/dist/docs/guidelines/typescript-cli-tool-rules.md +4 -3
  42. package/dist/docs/guidelines/typescript-code-coverage.md +7 -4
  43. package/dist/docs/guidelines/typescript-rules.md +8 -9
  44. package/dist/docs/guidelines/typescript-sorting-patterns.md +2 -1
  45. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +6 -5
  46. package/dist/docs/references/docmap-format.md +64 -0
  47. package/dist/docs/references/docref-format.md +71 -0
  48. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +17 -4
  49. package/dist/docs/shortcuts/standard/new-shortcut.md +17 -3
  50. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -1
  51. package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -1
  52. package/dist/docs/shortcuts/standard/suggest-upstream-improvements.md +69 -0
  53. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +1 -1
  54. package/dist/docs/shortcuts/standard/welcome-user.md +28 -0
  55. package/dist/docs/shortcuts/system/shortcut-explanation.md +16 -1
  56. package/dist/docs/shortcuts/system/skill-baseline.md +21 -5
  57. package/dist/docs/tbd-design.md +144 -3
  58. package/dist/docs/tbd-docs.md +169 -5
  59. package/dist/docs/tbd-prime.md +0 -1
  60. package/dist/docs/templates/qa-playbook.md +3 -1
  61. package/dist/docs/templates/research-brief.md +2 -2
  62. package/dist/fork-manifest-ByU7U2do.mjs +253 -0
  63. package/dist/fork-manifest-ByU7U2do.mjs.map +1 -0
  64. package/dist/fork-manifest-C7lGRq-6.mjs +3 -0
  65. package/dist/{id-mapping-mtoSP9Qt.mjs → id-mapping-D0iitY-F.mjs} +1 -1
  66. package/dist/{id-mapping-687_UEsy.mjs → id-mapping-DAIeLKzm.mjs} +8 -200
  67. package/dist/id-mapping-DAIeLKzm.mjs.map +1 -0
  68. package/dist/index.d.mts +90 -13
  69. package/dist/index.mjs +3 -3
  70. package/dist/lockfile-BR0laSDy.mjs +198 -0
  71. package/dist/lockfile-BR0laSDy.mjs.map +1 -0
  72. package/dist/paths-C1DpnZJW.mjs +405 -0
  73. package/dist/paths-C1DpnZJW.mjs.map +1 -0
  74. package/dist/{schemas-f0EcuAVu.mjs → schemas-lCwRk30L.mjs} +19 -6
  75. package/dist/schemas-lCwRk30L.mjs.map +1 -0
  76. package/dist/{src-BpvcrLnq.mjs → src-CxKOynr1.mjs} +3 -3
  77. package/dist/src-CxKOynr1.mjs.map +1 -0
  78. package/dist/tbd +4036 -1074
  79. package/dist/yaml-utils-BPy991by.mjs.map +1 -1
  80. package/package.json +1 -1
  81. package/dist/config-BJz1m9eN.mjs.map +0 -1
  82. package/dist/config-DlCUMyCG.mjs +0 -3
  83. package/dist/docs/guidelines/general-eng-assistant-rules.md +0 -59
  84. package/dist/id-mapping-687_UEsy.mjs.map +0 -1
  85. package/dist/schemas-f0EcuAVu.mjs.map +0 -1
  86. package/dist/src-BpvcrLnq.mjs.map +0 -1
@@ -0,0 +1,1035 @@
1
+ import { d as updateLocalState, f as writeConfig, l as readLocalState, n as findTbdRoot, s as readConfig } from "./config-B1w3pAkY.mjs";
2
+ import { F as gitSafeEnv, S as TBD_DOCS_DIR, a as CHARS_PER_TOKEN } from "./paths-C1DpnZJW.mjs";
3
+ import { a as tryParseDocRef } from "./docref-C7g0sjvL.mjs";
4
+ import matter from "gray-matter";
5
+ import { execFile } from "node:child_process";
6
+ import { access, mkdir, readFile, readdir, rm } from "node:fs/promises";
7
+ import { basename, dirname, join } from "node:path";
8
+ import { writeFile } from "atomically";
9
+ import { promisify } from "node:util";
10
+ import { fileURLToPath } from "node:url";
11
+ import prettyBytes from "pretty-bytes";
12
+ import prettyMs from "pretty-ms";
13
+
14
+ //#region src/file/github-fetch.ts
15
+ /**
16
+ * GitHub URL utilities and content fetching with `gh` CLI fallback.
17
+ *
18
+ * Consolidates all GitHub-specific URL handling and fetching logic:
19
+ * - GitHub blob URL → raw URL conversion
20
+ * - Direct HTTP fetch with timeout
21
+ * - Fallback to `gh api` on 403 errors (common in restricted environments)
22
+ *
23
+ * This module is reusable across doc-sync, doc-add, and any future code
24
+ * that needs to fetch content from GitHub.
25
+ */
26
+ const execFileAsync = promisify(execFile);
27
+ /**
28
+ * Regular expression to match GitHub blob URLs.
29
+ *
30
+ * Matches patterns like:
31
+ * https://github.com/{owner}/{repo}/blob/{ref}/{path}
32
+ */
33
+ const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/;
34
+ /**
35
+ * Regular expression to match raw.githubusercontent.com URLs.
36
+ *
37
+ * Matches patterns like:
38
+ * https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path}
39
+ */
40
+ const RAW_GITHUB_RE = /^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/;
41
+ /**
42
+ * Convert a GitHub blob URL to a raw.githubusercontent.com URL.
43
+ *
44
+ * If the URL is already a raw URL or not a GitHub blob URL, returns it unchanged.
45
+ *
46
+ * @example
47
+ * githubBlobToRawUrl('https://github.com/org/repo/blob/main/docs/file.md')
48
+ * // => 'https://raw.githubusercontent.com/org/repo/main/docs/file.md'
49
+ *
50
+ * @example
51
+ * githubBlobToRawUrl('https://raw.githubusercontent.com/org/repo/main/docs/file.md')
52
+ * // => 'https://raw.githubusercontent.com/org/repo/main/docs/file.md' (unchanged)
53
+ */
54
+ function githubBlobToRawUrl(url) {
55
+ const match = GITHUB_BLOB_RE.exec(url);
56
+ if (!match) return url;
57
+ const [, owner, repo, ref, path] = match;
58
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`;
59
+ }
60
+ /**
61
+ * Parse a raw.githubusercontent.com URL into its components.
62
+ *
63
+ * @returns The parsed components, or null if not a raw GitHub URL
64
+ */
65
+ function parseRawGitHubUrl(url) {
66
+ const match = RAW_GITHUB_RE.exec(url);
67
+ if (!match) return null;
68
+ return {
69
+ owner: match[1],
70
+ repo: match[2],
71
+ ref: match[3],
72
+ path: match[4]
73
+ };
74
+ }
75
+ /** Default timeout for URL fetches in milliseconds */
76
+ const DEFAULT_FETCH_TIMEOUT = 3e4;
77
+ /**
78
+ * Fetch content from a URL via direct HTTP.
79
+ *
80
+ * @throws If the request fails or returns a non-OK status
81
+ */
82
+ async function directFetch(url, options) {
83
+ const timeout = options?.timeout ?? DEFAULT_FETCH_TIMEOUT;
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => {
86
+ controller.abort();
87
+ }, timeout);
88
+ try {
89
+ const response = await fetch(url, {
90
+ signal: controller.signal,
91
+ headers: {
92
+ "User-Agent": "get-tbd/1.0",
93
+ Accept: "text/plain, text/markdown, */*"
94
+ }
95
+ });
96
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
97
+ return await response.text();
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ }
102
+ /**
103
+ * Fetch content from a GitHub raw URL using `gh api`.
104
+ *
105
+ * Converts raw.githubusercontent.com URLs to GitHub API content endpoints
106
+ * and decodes the base64-encoded response.
107
+ *
108
+ * @throws If the URL is not a GitHub URL or gh CLI fails
109
+ */
110
+ async function ghCliFetch(rawUrl) {
111
+ const parsed = parseRawGitHubUrl(rawUrl);
112
+ if (parsed) try {
113
+ const { stdout } = await execFileAsync("gh", [
114
+ "api",
115
+ `/repos/${parsed.owner}/${parsed.repo}/contents/${parsed.path}?ref=${parsed.ref}`,
116
+ "--jq",
117
+ ".content",
118
+ "-H",
119
+ "Accept: application/vnd.github.v3+json"
120
+ ]);
121
+ const base64Content = stdout.trim();
122
+ return Buffer.from(base64Content, "base64").toString("utf-8");
123
+ } catch (error) {
124
+ throw new Error(`Failed to fetch via gh CLI: ${error instanceof Error ? error.message : String(error)}`);
125
+ }
126
+ try {
127
+ const { stdout } = await execFileAsync("gh", ["api", rawUrl]);
128
+ return stdout;
129
+ } catch (error) {
130
+ throw new Error(`Failed to fetch via gh CLI: ${error instanceof Error ? error.message : String(error)}`);
131
+ }
132
+ }
133
+ /**
134
+ * Fetch content from a URL, falling back to `gh` CLI on 403 errors.
135
+ *
136
+ * GitHub.com and raw.githubusercontent.com may block requests from
137
+ * certain environments (e.g., CI runners, corporate proxies). When
138
+ * a 403 is received, we retry using `gh api` which authenticates
139
+ * via the user's GitHub CLI token.
140
+ *
141
+ * This function also auto-converts GitHub blob URLs to raw URLs.
142
+ *
143
+ * @returns The fetched content and whether gh CLI was used
144
+ * @throws If both direct fetch and gh CLI fallback fail
145
+ */
146
+ async function fetchWithGhFallback(url, options) {
147
+ const rawUrl = githubBlobToRawUrl(url);
148
+ try {
149
+ return {
150
+ content: await directFetch(rawUrl, options),
151
+ usedGhCli: false
152
+ };
153
+ } catch (error) {
154
+ if (!(error instanceof Error && error.message.includes("HTTP 403"))) throw error;
155
+ }
156
+ return {
157
+ content: await ghCliFetch(rawUrl),
158
+ usedGhCli: true
159
+ };
160
+ }
161
+ /**
162
+ * Resolve a git docref (github:o/r@ref//path, gitlab:...) to a raw-content URL.
163
+ * The ref is required for sync determinism; callers enforce that.
164
+ */
165
+ function gitDocRefToRawUrl(ref) {
166
+ const rev = ref.ref ?? "main";
167
+ if (ref.host === "gitlab") return `https://gitlab.com/${ref.owner}/${ref.repo}/-/raw/${rev}/${ref.path}`;
168
+ return `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${rev}/${ref.path}`;
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/file/doc-sync.ts
173
+ /**
174
+ * DocSync - Sync documentation files from configured sources.
175
+ *
176
+ * Syncs docs from internal bundled sources and external URLs to .tbd/docs/.
177
+ *
178
+ * See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
179
+ */
180
+ /**
181
+ * Group docs_cache.files entries by source root: one group per git repo+ref
182
+ * (so failures isolate per source and revisions are captured once), with
183
+ * internal, local, and ad-hoc URL entries in origin-keyed groups.
184
+ */
185
+ function groupSourceEntries(config) {
186
+ const groups = /* @__PURE__ */ new Map();
187
+ for (const [destPath, sourceStr] of Object.entries(config)) {
188
+ let key = "url";
189
+ let gitSource;
190
+ if (sourceStr.startsWith("internal:")) key = "internal";
191
+ else {
192
+ const parsed = tryParseDocRef(sourceStr);
193
+ if (parsed?.kind === "git") {
194
+ key = `${parsed.host}:${parsed.owner}/${parsed.repo}@${parsed.ref ?? "main"}`;
195
+ gitSource = {
196
+ host: parsed.host,
197
+ owner: parsed.owner,
198
+ repo: parsed.repo,
199
+ ref: parsed.ref
200
+ };
201
+ } else if (parsed?.kind === "local") key = "local";
202
+ else try {
203
+ key = new URL(sourceStr).origin;
204
+ } catch {
205
+ key = "url";
206
+ }
207
+ }
208
+ let group = groups.get(key);
209
+ if (!group) {
210
+ group = {
211
+ key,
212
+ gitSource,
213
+ entries: []
214
+ };
215
+ groups.set(key, group);
216
+ }
217
+ group.entries.push([destPath, sourceStr]);
218
+ }
219
+ return [...groups.values()];
220
+ }
221
+ /**
222
+ * Capture the current revision of a git source's ref (one ls-remote per
223
+ * group), for provenance. Best-effort: offline or missing git returns null.
224
+ */
225
+ async function captureGitRevision(src) {
226
+ const repoUrl = src.host === "gitlab" ? `https://gitlab.com/${src.owner}/${src.repo}.git` : `https://github.com/${src.owner}/${src.repo}.git`;
227
+ try {
228
+ const { execFile } = await import("node:child_process");
229
+ const { promisify } = await import("node:util");
230
+ const sha = (await promisify(execFile)("git", [
231
+ "ls-remote",
232
+ repoUrl,
233
+ src.ref ?? "HEAD"
234
+ ], {
235
+ timeout: 1e4,
236
+ env: gitSafeEnv()
237
+ })).stdout.split(" ")[0]?.trim();
238
+ return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ /** Prefix for internal bundled doc sources */
244
+ const INTERNAL_SOURCE_PREFIX = "internal:";
245
+ /**
246
+ * Syncs documentation files from configured sources.
247
+ *
248
+ * Supports:
249
+ * - Internal bundled docs (using internal: prefix)
250
+ * - External URLs (raw.githubusercontent.com, etc.)
251
+ */
252
+ var DocSync = class {
253
+ docsDir;
254
+ /**
255
+ * Create a new DocSync instance.
256
+ *
257
+ * @param tbdRoot - The tbd project root directory (parent of .tbd/)
258
+ * @param config - The doc_cache configuration mapping dest paths to sources
259
+ */
260
+ constructor(tbdRoot, config) {
261
+ this.tbdRoot = tbdRoot;
262
+ this.config = config;
263
+ this.docsDir = join(tbdRoot, TBD_DOCS_DIR);
264
+ }
265
+ /**
266
+ * Parse a source string into a DocSource.
267
+ *
268
+ * @example
269
+ * parseSource('internal:shortcuts/standard/code-review-and-commit.md')
270
+ * // => { type: 'internal', location: 'shortcuts/standard/code-review-and-commit.md' }
271
+ *
272
+ * @example
273
+ * parseSource('https://raw.githubusercontent.com/org/repo/main/file.md')
274
+ * // => { type: 'url', location: 'https://...' }
275
+ */
276
+ parseSource(source) {
277
+ if (source.startsWith(INTERNAL_SOURCE_PREFIX)) return {
278
+ type: "internal",
279
+ location: source.slice(9)
280
+ };
281
+ const parsed = tryParseDocRef(source);
282
+ if (parsed?.kind === "git") return {
283
+ type: "url",
284
+ location: gitDocRefToRawUrl(parsed)
285
+ };
286
+ if (parsed?.kind === "local") return {
287
+ type: "local",
288
+ location: parsed.path
289
+ };
290
+ return {
291
+ type: "url",
292
+ location: source
293
+ };
294
+ }
295
+ /**
296
+ * Fetch content from a source.
297
+ *
298
+ * @throws If the source cannot be fetched
299
+ */
300
+ async fetchContent(source) {
301
+ if (source.type === "internal") return this.fetchInternalContent(source.location);
302
+ if (source.type === "local") return await readFile(join(this.tbdRoot, source.location), "utf-8");
303
+ return this.fetchUrlContent(source.location);
304
+ }
305
+ /**
306
+ * Fetch content from an internal bundled doc.
307
+ */
308
+ async fetchInternalContent(location) {
309
+ const basePaths = getDocsBasePath();
310
+ for (const basePath of basePaths) {
311
+ const fullPath = join(basePath, location);
312
+ try {
313
+ await access(fullPath);
314
+ return await readFile(fullPath, "utf-8");
315
+ } catch {}
316
+ }
317
+ throw new Error(`Internal doc not found: ${location}`);
318
+ }
319
+ /**
320
+ * Fetch content from a URL (with gh CLI fallback on 403).
321
+ */
322
+ async fetchUrlContent(url) {
323
+ const { content } = await fetchWithGhFallback(url);
324
+ return content;
325
+ }
326
+ /**
327
+ * Get the current state of the docs directory.
328
+ * Returns a set of relative paths that exist in .tbd/docs/.
329
+ */
330
+ async getCurrentState() {
331
+ const paths = /* @__PURE__ */ new Set();
332
+ try {
333
+ await access(this.docsDir);
334
+ } catch {
335
+ return paths;
336
+ }
337
+ await this.scanDirectory(this.docsDir, "", paths);
338
+ return paths;
339
+ }
340
+ /**
341
+ * Recursively scan a directory and add all .md file paths to the set.
342
+ */
343
+ async scanDirectory(baseDir, prefix, paths) {
344
+ try {
345
+ const dirEntries = await readdir(baseDir, { withFileTypes: true });
346
+ for (const entry of dirEntries) {
347
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
348
+ if (entry.isDirectory()) await this.scanDirectory(join(baseDir, entry.name), relativePath, paths);
349
+ else if (entry.isFile() && entry.name.endsWith(".md")) paths.add(relativePath);
350
+ }
351
+ } catch {}
352
+ }
353
+ /**
354
+ * Sync all docs from config to .tbd/docs/.
355
+ *
356
+ * - Downloads/copies new docs
357
+ * - Updates docs whose source has changed
358
+ * - Removes docs no longer in config
359
+ *
360
+ * @param options - Sync options (dryRun, silent)
361
+ */
362
+ async sync(options = {}) {
363
+ const result = {
364
+ sourceRevisions: {},
365
+ added: [],
366
+ updated: [],
367
+ removed: [],
368
+ errors: [],
369
+ success: true
370
+ };
371
+ const currentPaths = await this.getCurrentState();
372
+ const configPaths = new Set(Object.keys(this.config));
373
+ const groups = groupSourceEntries(this.config);
374
+ const failedGroups = /* @__PURE__ */ new Set();
375
+ for (const group of groups) if (group.gitSource) {
376
+ const revision = await captureGitRevision(group.gitSource);
377
+ if (revision) result.sourceRevisions[group.key] = revision;
378
+ }
379
+ for (const group of groups) for (const [destPath, sourceStr] of group.entries) {
380
+ if (failedGroups.has(group.key)) {
381
+ result.errors.push({
382
+ path: destPath,
383
+ error: `skipped: source group ${group.key} unreachable`
384
+ });
385
+ result.success = false;
386
+ continue;
387
+ }
388
+ try {
389
+ const source = this.parseSource(sourceStr);
390
+ const content = await this.fetchContent(source);
391
+ const fullPath = join(this.docsDir, destPath);
392
+ let exists = false;
393
+ let existingContent = "";
394
+ try {
395
+ existingContent = await readFile(fullPath, "utf-8");
396
+ exists = true;
397
+ } catch {}
398
+ if (!exists) {
399
+ if (!options.dryRun) {
400
+ await mkdir(dirname(fullPath), { recursive: true });
401
+ await writeFile(fullPath, content);
402
+ }
403
+ result.added.push(destPath);
404
+ } else if (existingContent !== content) {
405
+ if (!options.dryRun) await writeFile(fullPath, content);
406
+ result.updated.push(destPath);
407
+ }
408
+ } catch (err) {
409
+ const message = err.message;
410
+ result.errors.push({
411
+ path: destPath,
412
+ error: message
413
+ });
414
+ result.success = false;
415
+ if (group.gitSource && /fetch|network|ENOTFOUND|ETIMEDOUT|ECONNREFUSED|HTTP/i.test(message)) failedGroups.add(group.key);
416
+ }
417
+ }
418
+ for (const existingPath of currentPaths) if (!configPaths.has(existingPath)) try {
419
+ if (!options.dryRun) await rm(join(this.docsDir, existingPath));
420
+ result.removed.push(existingPath);
421
+ } catch (err) {
422
+ result.errors.push({
423
+ path: existingPath,
424
+ error: `Failed to remove: ${err.message}`
425
+ });
426
+ }
427
+ return result;
428
+ }
429
+ /**
430
+ * Get a status of what would change without actually making changes.
431
+ */
432
+ async status() {
433
+ return this.sync({ dryRun: true });
434
+ }
435
+ };
436
+ /**
437
+ * Get base docs paths (with fallbacks for development).
438
+ * Matches the logic in setup.ts.
439
+ */
440
+ function getDocsBasePath() {
441
+ const __dirname = dirname(fileURLToPath(import.meta.url));
442
+ return [join(__dirname, "docs"), join(__dirname, "..", "..", "docs")];
443
+ }
444
+ /**
445
+ * Generate default doc_cache config by scanning bundled docs.
446
+ *
447
+ * This creates a config entry for each .md file found in the bundled
448
+ * docs directories (shortcuts, guidelines, templates).
449
+ *
450
+ * @returns A doc_cache config mapping destination paths to internal: sources
451
+ */
452
+ async function generateDefaultDocCacheConfig() {
453
+ const config = {};
454
+ const basePaths = getDocsBasePath();
455
+ let docsDir = null;
456
+ for (const path of basePaths) try {
457
+ await access(path);
458
+ docsDir = path;
459
+ break;
460
+ } catch {}
461
+ if (!docsDir) return config;
462
+ const scanDirs = [
463
+ {
464
+ subdir: "shortcuts/system",
465
+ prefix: "shortcuts/system"
466
+ },
467
+ {
468
+ subdir: "shortcuts/standard",
469
+ prefix: "shortcuts/standard"
470
+ },
471
+ {
472
+ subdir: "guidelines",
473
+ prefix: "guidelines"
474
+ },
475
+ {
476
+ subdir: "templates",
477
+ prefix: "templates"
478
+ },
479
+ {
480
+ subdir: "references",
481
+ prefix: "references"
482
+ }
483
+ ];
484
+ config[`references/tbd-docs.md`] = `${INTERNAL_SOURCE_PREFIX}tbd-docs.md`;
485
+ config[`references/tbd-design.md`] = `${INTERNAL_SOURCE_PREFIX}tbd-design.md`;
486
+ for (const { subdir, prefix } of scanDirs) {
487
+ const fullDir = join(docsDir, subdir);
488
+ try {
489
+ const entries = await readdir(fullDir, { withFileTypes: true });
490
+ for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
491
+ const relativePath = `${prefix}/${entry.name}`;
492
+ config[relativePath] = `${INTERNAL_SOURCE_PREFIX}${relativePath}`;
493
+ }
494
+ } catch {}
495
+ }
496
+ return config;
497
+ }
498
+ /**
499
+ * Merge user's doc_cache config with default bundled docs.
500
+ *
501
+ * This ensures:
502
+ * - New bundled docs from tbd updates are added to existing configs
503
+ * - User's custom sources (URLs, etc.) are preserved
504
+ * - User's overrides of bundled docs are respected
505
+ *
506
+ * @param userConfig - The user's existing doc_cache config (may be undefined/empty)
507
+ * @param defaults - The default config from generateDefaultDocCacheConfig()
508
+ * @returns Merged config with defaults as base, user config overlaid
509
+ */
510
+ function mergeDocCacheConfig(userConfig, defaults) {
511
+ return {
512
+ ...defaults,
513
+ ...userConfig
514
+ };
515
+ }
516
+ /**
517
+ * Check if docs are stale based on last sync time and configured hours.
518
+ *
519
+ * @param lastSyncAt - ISO timestamp of last sync (or undefined if never synced)
520
+ * @param autoSyncHours - Hours between auto-syncs (0 = disabled)
521
+ * @returns true if docs should be synced
522
+ */
523
+ function isDocsStale(lastSyncAt, autoSyncHours) {
524
+ if (autoSyncHours <= 0) return false;
525
+ if (!lastSyncAt) return true;
526
+ const lastSync = new Date(lastSyncAt).getTime();
527
+ return (Date.now() - lastSync) / (1e3 * 60 * 60) >= autoSyncHours;
528
+ }
529
+ /**
530
+ * Check if an internal bundled doc exists.
531
+ *
532
+ * @param location - The internal doc path (without 'internal:' prefix)
533
+ * @returns true if the doc exists in any of the bundled doc paths
534
+ */
535
+ async function internalDocExists(location) {
536
+ const basePaths = getDocsBasePath();
537
+ for (const basePath of basePaths) {
538
+ const fullPath = join(basePath, location);
539
+ try {
540
+ await access(fullPath);
541
+ return true;
542
+ } catch {}
543
+ }
544
+ return false;
545
+ }
546
+ /**
547
+ * Prune entries from config that point to non-existent internal sources.
548
+ *
549
+ * This handles the case where a bundled doc is removed in a tbd update -
550
+ * the stale config entry is automatically cleaned up.
551
+ *
552
+ * @param config - The doc_cache config to prune
553
+ * @returns Object with pruned config and list of removed entries
554
+ */
555
+ async function pruneStaleInternals(config) {
556
+ const result = {};
557
+ const pruned = [];
558
+ for (const [dest, source] of Object.entries(config)) {
559
+ if (source.startsWith(INTERNAL_SOURCE_PREFIX)) {
560
+ if (!await internalDocExists(source.slice(9))) {
561
+ pruned.push(dest);
562
+ continue;
563
+ }
564
+ }
565
+ result[dest] = source;
566
+ }
567
+ return {
568
+ config: result,
569
+ pruned
570
+ };
571
+ }
572
+ /**
573
+ * Deep equality check for config objects.
574
+ */
575
+ function configsEqual(a, b) {
576
+ const keysA = Object.keys(a).sort();
577
+ const keysB = Object.keys(b).sort();
578
+ if (keysA.length !== keysB.length) return false;
579
+ for (const key of keysA) if (!Object.hasOwn(b, key) || a[key] !== b[key]) return false;
580
+ return true;
581
+ }
582
+ /**
583
+ * Sync docs with merged defaults and auto-pruning.
584
+ *
585
+ * This is the single entry point for all doc sync operations that need
586
+ * to pick up new bundled docs from tbd upgrades.
587
+ *
588
+ * Steps:
589
+ * 1. Read current config
590
+ * 2. Generate defaults from bundled docs
591
+ * 3. Merge: defaults as base, user config overlays
592
+ * 4. Prune entries with missing internal sources
593
+ * 5. Sync files to .tbd/docs/
594
+ * 6. Write config if changed
595
+ * 7. Update last_doc_sync_at in state
596
+ *
597
+ * @param tbdRoot - The tbd project root directory
598
+ * @param options - Sync options (quiet, dryRun)
599
+ * @returns Sync result with added/updated/removed/pruned counts
600
+ */
601
+ async function syncDocsWithDefaults(tbdRoot, options = {}) {
602
+ const config = await readConfig(tbdRoot);
603
+ const originalFiles = config.docs_cache?.files ?? {};
604
+ const { config: prunedConfig, pruned } = await pruneStaleInternals(mergeDocCacheConfig(originalFiles, await generateDefaultDocCacheConfig()));
605
+ const syncResult = await new DocSync(tbdRoot, prunedConfig).sync({ dryRun: options.dryRun });
606
+ const configChanged = !configsEqual(prunedConfig, originalFiles);
607
+ if (configChanged && !options.dryRun) {
608
+ const lookupPath = config.docs_cache?.lookup_path ?? [".tbd/docs/shortcuts/system", ".tbd/docs/shortcuts/standard"];
609
+ config.docs_cache = {
610
+ ...config.docs_cache,
611
+ lookup_path: lookupPath,
612
+ files: prunedConfig
613
+ };
614
+ await writeConfig(tbdRoot, config);
615
+ }
616
+ if (!options.dryRun) await updateLocalState(tbdRoot, { last_doc_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
617
+ return {
618
+ added: syncResult.added,
619
+ updated: syncResult.updated,
620
+ removed: syncResult.removed,
621
+ pruned,
622
+ configChanged,
623
+ errors: syncResult.errors,
624
+ success: syncResult.success
625
+ };
626
+ }
627
+
628
+ //#endregion
629
+ //#region src/lib/format-utils.ts
630
+ /**
631
+ * Human-readable formatting utilities for tbd CLI.
632
+ *
633
+ * Uses sindresorhus libraries for consistent, well-tested formatting:
634
+ * - pretty-bytes: Byte sizes (1337 -> "1.34 kB")
635
+ * - pretty-ms: Durations (3600000 -> "1h")
636
+ */
637
+ /**
638
+ * Estimate token count from text content.
639
+ * Uses CHARS_PER_TOKEN (~3.5) for markdown/code content.
640
+ */
641
+ function estimateTokens(text) {
642
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
643
+ }
644
+ /**
645
+ * Format token count: "~1.2k tok" or "~450 tok"
646
+ */
647
+ function formatTokens(tokens) {
648
+ if (tokens >= 1e3) return `~${(tokens / 1e3).toFixed(1)}k tok`;
649
+ return `~${tokens} tok`;
650
+ }
651
+ /**
652
+ * Format doc size for list display: "(1.8 kB, ~450 tok)"
653
+ */
654
+ function formatDocSize(sizeBytes, approxTokens) {
655
+ return `(${prettyBytes(sizeBytes)}, ${formatTokens(approxTokens)})`;
656
+ }
657
+ /**
658
+ * Format relative time from Date: "2d ago", "3h ago", "5m ago"
659
+ * Uses compact format for concise display.
660
+ */
661
+ function formatTimeAgo(date) {
662
+ const ms = Date.now() - date.getTime();
663
+ if (ms < 0) return "just now";
664
+ if (ms < 6e4) return "just now";
665
+ return `${prettyMs(ms, { compact: true })} ago`;
666
+ }
667
+ /**
668
+ * Format relative time from ISO timestamp string.
669
+ * Returns null if timestamp is invalid.
670
+ */
671
+ function formatTimestampAgo(timestamp) {
672
+ if (!timestamp) return null;
673
+ const date = new Date(timestamp);
674
+ if (isNaN(date.getTime())) return null;
675
+ return formatTimeAgo(date);
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/file/doc-cache.ts
680
+ /**
681
+ * DocCache - Path-ordered markdown document cache with lookup.
682
+ *
683
+ * Provides document lookups for the `tbd shortcut` command, supporting
684
+ * both exact matching by filename and fuzzy matching against metadata.
685
+ *
686
+ * Also provides auto-sync functionality when docs are stale (per spec).
687
+ *
688
+ * See: docs/project/specs/active/plan-2026-01-22-doc-cache-abstraction.md
689
+ * See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
690
+ */
691
+ /** Score for exact filename match (with or without .md extension) */
692
+ const SCORE_EXACT_MATCH = 1;
693
+ /** Score when query is a prefix of the filename */
694
+ const SCORE_PREFIX_MATCH = .9;
695
+ /** Score when filename contains all query words */
696
+ const SCORE_CONTAINS_ALL = .8;
697
+ /** Base score for partial word matches (multiplied by matched/total ratio) */
698
+ const SCORE_PARTIAL_BASE = .7;
699
+ /** Minimum score threshold to return a fuzzy match result */
700
+ const SCORE_MIN_THRESHOLD = .5;
701
+ /**
702
+ * Path-ordered markdown document cache.
703
+ *
704
+ * Loads all .md files from configured paths in order, with earlier paths
705
+ * taking precedence (like shell $PATH). Supports exact lookup by filename
706
+ * and fuzzy search across filename + frontmatter metadata.
707
+ */
708
+ var DocCache = class {
709
+ /** Active docs (first occurrence of each name) */
710
+ docs = [];
711
+ /** All docs including shadowed ones */
712
+ allDocs = [];
713
+ /** Track names we've seen for shadow detection */
714
+ seenNames = /* @__PURE__ */ new Set();
715
+ /** Whether the cache has been loaded */
716
+ loaded = false;
717
+ /**
718
+ * Create a new DocCache.
719
+ *
720
+ * @param paths - Ordered array of directory paths to search (relative to baseDir)
721
+ * @param baseDir - Base directory for resolving relative paths (default: cwd)
722
+ */
723
+ constructor(paths, baseDir = process.cwd()) {
724
+ this.paths = paths;
725
+ this.baseDir = baseDir;
726
+ }
727
+ /**
728
+ * Load all documents from configured paths.
729
+ *
730
+ * Reads all .md files from each path in order. Documents with the same
731
+ * name in later paths are shadowed (tracked but not returned by default).
732
+ *
733
+ * If auto-sync is enabled and docs are stale, triggers a sync first.
734
+ *
735
+ * @param options - Load options (quiet: suppress auto-sync output)
736
+ */
737
+ async load(options) {
738
+ if (this.loaded) return;
739
+ await this.checkAutoSync(options?.quiet ?? false);
740
+ for (const relativePath of this.paths) {
741
+ const dirPath = join(this.baseDir, relativePath);
742
+ await this.loadDirectory(dirPath, relativePath);
743
+ }
744
+ this.loaded = true;
745
+ }
746
+ /**
747
+ * Check if docs are stale and auto-sync if needed.
748
+ * Respects the quiet option - only silent when explicitly requested.
749
+ *
750
+ * Uses syncDocsWithDefaults() to ensure auto-sync also picks up new bundled
751
+ * docs from tbd upgrades, not just existing config entries.
752
+ *
753
+ * @param quiet - If true, suppress sync output
754
+ */
755
+ async checkAutoSync(quiet) {
756
+ try {
757
+ const tbdRoot = await findTbdRoot(this.baseDir);
758
+ if (!tbdRoot) return;
759
+ const config = await readConfig(tbdRoot);
760
+ const state = await readLocalState(tbdRoot);
761
+ const autoSyncHours = config.settings?.doc_auto_sync_hours ?? 24;
762
+ if (!isDocsStale(state.last_doc_sync_at, autoSyncHours)) return;
763
+ await syncDocsWithDefaults(tbdRoot, { quiet });
764
+ } catch {}
765
+ }
766
+ /**
767
+ * Load documents from a single directory.
768
+ */
769
+ async loadDirectory(dirPath, sourceDir) {
770
+ let entries;
771
+ try {
772
+ entries = await readdir(dirPath);
773
+ } catch {
774
+ return;
775
+ }
776
+ for (const entry of entries) {
777
+ if (!entry.endsWith(".md")) continue;
778
+ const filePath = join(dirPath, entry);
779
+ const name = basename(entry, ".md");
780
+ try {
781
+ const content = await readFile(filePath, "utf-8");
782
+ const doc = {
783
+ path: filePath,
784
+ name,
785
+ frontmatter: this.parseFrontmatterData(content),
786
+ content,
787
+ sourceDir,
788
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
789
+ approxTokens: estimateTokens(content)
790
+ };
791
+ this.allDocs.push(doc);
792
+ if (!this.seenNames.has(name)) {
793
+ this.docs.push(doc);
794
+ this.seenNames.add(name);
795
+ }
796
+ } catch (error) {
797
+ console.warn(`Failed to load shortcut ${filePath}: ${error.message}`);
798
+ }
799
+ }
800
+ }
801
+ /**
802
+ * Parse YAML frontmatter from content and return typed data.
803
+ * Uses gray-matter for consistent frontmatter parsing.
804
+ */
805
+ parseFrontmatterData(content) {
806
+ if (!matter.test(content)) return;
807
+ try {
808
+ const parsed = matter(content).data;
809
+ return {
810
+ title: typeof parsed.title === "string" ? parsed.title : void 0,
811
+ description: typeof parsed.description === "string" ? parsed.description : void 0,
812
+ category: typeof parsed.category === "string" ? parsed.category : void 0,
813
+ tags: Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : void 0
814
+ };
815
+ } catch {
816
+ return;
817
+ }
818
+ }
819
+ /**
820
+ * Get a document by exact name match.
821
+ *
822
+ * @param name - Filename to match (with or without .md extension)
823
+ * @returns Match with score SCORE_EXACT_MATCH, or null if not found
824
+ */
825
+ get(name) {
826
+ const lookupName = name.endsWith(".md") ? name.slice(0, -3) : name;
827
+ const doc = this.docs.find((d) => d.name === lookupName);
828
+ if (!doc) return null;
829
+ return {
830
+ doc,
831
+ score: SCORE_EXACT_MATCH
832
+ };
833
+ }
834
+ /**
835
+ * Search for documents matching a query.
836
+ *
837
+ * Performs fuzzy matching against filename, title, and description.
838
+ * Returns matches sorted by score descending.
839
+ *
840
+ * @param query - Search query string
841
+ * @param limit - Maximum number of results (default: 10)
842
+ * @returns Array of matches sorted by score descending
843
+ */
844
+ search(query, limit = 10) {
845
+ const matches = [];
846
+ for (const doc of this.docs) {
847
+ const score = this.calculateScore(doc, query);
848
+ if (score >= SCORE_MIN_THRESHOLD) matches.push({
849
+ doc,
850
+ score
851
+ });
852
+ }
853
+ matches.sort((a, b) => {
854
+ if (b.score !== a.score) return b.score - a.score;
855
+ return a.doc.name.localeCompare(b.doc.name);
856
+ });
857
+ return matches.slice(0, limit);
858
+ }
859
+ /**
860
+ * Calculate relevance score for a document against a query.
861
+ */
862
+ calculateScore(doc, query) {
863
+ const queryLower = query.toLowerCase().trim();
864
+ if (queryLower.length === 0) return 0;
865
+ const nameLower = doc.name.toLowerCase();
866
+ const titleLower = doc.frontmatter?.title?.toLowerCase() ?? "";
867
+ const descLower = doc.frontmatter?.description?.toLowerCase() ?? "";
868
+ if (nameLower === queryLower) return SCORE_EXACT_MATCH;
869
+ if (nameLower.startsWith(queryLower)) return SCORE_PREFIX_MATCH;
870
+ const queryWords = queryLower.split(/\s+/).filter((w) => w.length > 0);
871
+ const searchableText = `${nameLower} ${titleLower} ${descLower}`;
872
+ if (queryWords.every((word) => searchableText.includes(word)) && queryWords.length > 0) return SCORE_CONTAINS_ALL;
873
+ const matchedWords = queryWords.filter((word) => searchableText.includes(word));
874
+ if (matchedWords.length > 0) return SCORE_PARTIAL_BASE * (matchedWords.length / queryWords.length);
875
+ return 0;
876
+ }
877
+ /**
878
+ * List all documents.
879
+ *
880
+ * @param includeAll - If true, include shadowed documents
881
+ * @returns Array of cached documents
882
+ */
883
+ list(includeAll = false) {
884
+ return includeAll ? this.allDocs : this.docs;
885
+ }
886
+ /**
887
+ * Check if a document is shadowed by an earlier path.
888
+ *
889
+ * @param doc - Document to check
890
+ * @returns True if this doc is shadowed (not the first with this name)
891
+ */
892
+ isShadowed(doc) {
893
+ return this.docs.find((d) => d.name === doc.name) !== doc;
894
+ }
895
+ /**
896
+ * Check if the cache has been loaded.
897
+ */
898
+ isLoaded() {
899
+ return this.loaded;
900
+ }
901
+ };
902
+ /**
903
+ * Marker comments for shortcut directory section in skill files.
904
+ * Used for incremental updates without overwriting user content.
905
+ */
906
+ const SHORTCUT_DIRECTORY_BEGIN = "<!-- BEGIN SHORTCUT DIRECTORY -->";
907
+ const SHORTCUT_DIRECTORY_END = "<!-- END SHORTCUT DIRECTORY -->";
908
+ const GUIDELINE_GROUPS = [
909
+ {
910
+ heading: "General engineering",
911
+ note: "Read all of these for any engineering work (writing or reviewing code).",
912
+ match: (n) => n.startsWith("general-") || n === "error-handling-rules" || n === "backward-compatibility-rules" || n === "commit-conventions" || n.includes("tdd") || n.includes("testing") || n.includes("golden")
913
+ },
914
+ {
915
+ heading: "TypeScript & JS ecosystem",
916
+ note: "Also load these when working in TypeScript or JavaScript.",
917
+ match: (n) => n.startsWith("typescript-") || n.endsWith("monorepo-patterns") || n.startsWith("electron-")
918
+ },
919
+ {
920
+ heading: "Python",
921
+ note: "Also load these when working in Python.",
922
+ match: (n) => n.startsWith("python-")
923
+ },
924
+ {
925
+ heading: "Convex",
926
+ note: "Also load these when working with Convex.",
927
+ match: (n) => n.startsWith("convex-")
928
+ },
929
+ {
930
+ heading: "Docs, process & tooling",
931
+ match: () => true
932
+ }
933
+ ];
934
+ /**
935
+ * Build table rows from docs (shared helper for shortcuts and guidelines).
936
+ */
937
+ function buildTableRows(docs, skipNames = []) {
938
+ const sortedDocs = [...docs].sort((a, b) => a.name.localeCompare(b.name));
939
+ const rows = [];
940
+ for (const doc of sortedDocs) {
941
+ if (skipNames.includes(doc.name)) continue;
942
+ const name = doc.name;
943
+ const escapedDescription = (doc.frontmatter?.description ?? "").replace(/\|/g, "\\|");
944
+ rows.push(`| ${name} | ${escapedDescription} |`);
945
+ }
946
+ return rows;
947
+ }
948
+ /**
949
+ * Generate a formatted markdown directory of shortcuts and guidelines.
950
+ *
951
+ * The output includes:
952
+ * 1. Marker comments for incremental updates
953
+ * 2. Available Shortcuts section with name and description
954
+ * 3. Available Guidelines section, grouped by area (General engineering first, then
955
+ * language/framework groups), with name and description (if provided)
956
+ *
957
+ * @param shortcuts - Array of shortcut CachedDoc objects
958
+ * @param guidelines - Optional array of guideline CachedDoc objects
959
+ * @returns Formatted markdown string with shortcuts and guidelines directory
960
+ *
961
+ * @example
962
+ * const directory = generateShortcutDirectory(shortcutDocs, guidelineDocs);
963
+ * // Returns:
964
+ * // <!-- BEGIN SHORTCUT DIRECTORY -->
965
+ * // ## Available Shortcuts
966
+ * // | Name | Description |
967
+ * // | --- | --- |
968
+ * // | code-review-and-commit | Run pre-commit checks, review changes, and commit code |
969
+ * // ...
970
+ * // ## Available Guidelines
971
+ * // ### General engineering
972
+ * // | Name | Description |
973
+ * // | --- | --- |
974
+ * // | error-handling-rules | Rules for handling errors... |
975
+ * // ...
976
+ * // ### TypeScript & JS ecosystem
977
+ * // | typescript-rules | TypeScript coding rules and best practices |
978
+ * // ...
979
+ * // <!-- END SHORTCUT DIRECTORY -->
980
+ */
981
+ function generateShortcutDirectory(shortcuts, guidelines = []) {
982
+ const lines = [SHORTCUT_DIRECTORY_BEGIN];
983
+ const shortcutRows = buildTableRows(shortcuts, [
984
+ "skill-baseline",
985
+ "skill-brief",
986
+ "skill-minimal",
987
+ "shortcut-explanation"
988
+ ]);
989
+ lines.push("## Available Shortcuts");
990
+ lines.push("");
991
+ lines.push("Run `tbd shortcut <name>` to use any of these shortcuts:");
992
+ lines.push("");
993
+ if (shortcutRows.length === 0) lines.push("No shortcuts available. Create shortcuts in `.tbd/docs/shortcuts/standard/`.");
994
+ else {
995
+ lines.push("| Name | Description |");
996
+ lines.push("| --- | --- |");
997
+ lines.push(...shortcutRows);
998
+ }
999
+ if (guidelines.length > 0) {
1000
+ const grouped = GUIDELINE_GROUPS.map((group) => ({
1001
+ group,
1002
+ docs: []
1003
+ }));
1004
+ const catchAll = grouped[grouped.length - 1];
1005
+ for (const doc of guidelines) (grouped.find((g) => g.group.match(doc.name)) ?? catchAll)?.docs.push(doc);
1006
+ if (grouped.some((g) => g.docs.length > 0)) {
1007
+ lines.push("");
1008
+ lines.push("## Available Guidelines");
1009
+ lines.push("");
1010
+ lines.push("Run `tbd guidelines <name>` to apply any of these guidelines.");
1011
+ lines.push("Load the **General engineering** group first, then the language or framework group.");
1012
+ for (const { group, docs } of grouped) {
1013
+ const rows = buildTableRows(docs);
1014
+ if (rows.length === 0) continue;
1015
+ lines.push("");
1016
+ lines.push(`### ${group.heading}`);
1017
+ if (group.note) {
1018
+ lines.push("");
1019
+ lines.push(`*${group.note}*`);
1020
+ }
1021
+ lines.push("");
1022
+ lines.push("| Name | Description |");
1023
+ lines.push("| --- | --- |");
1024
+ lines.push(...rows);
1025
+ }
1026
+ }
1027
+ }
1028
+ lines.push("");
1029
+ lines.push(SHORTCUT_DIRECTORY_END);
1030
+ return lines.join("\n");
1031
+ }
1032
+
1033
+ //#endregion
1034
+ export { SCORE_PARTIAL_BASE as a, formatDocSize as c, fetchWithGhFallback as d, gitDocRefToRawUrl as f, SCORE_MIN_THRESHOLD as i, formatTimestampAgo as l, SCORE_CONTAINS_ALL as n, SCORE_PREFIX_MATCH as o, SCORE_EXACT_MATCH as r, generateShortcutDirectory as s, DocCache as t, syncDocsWithDefaults as u };
1035
+ //# sourceMappingURL=doc-cache-CamtXfi4.mjs.map