vde-open 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,18 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
2
- import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-CNsfwB72.js";
3
- import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-yCayIbhf.js";
2
+ import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-__laOomT.js";
3
+ import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-BNZz0CjQ.js";
4
4
  import { n as measureHeap, t as collectGarbage } from "./heap-C3qHm7SE.js";
5
5
  import { constants, existsSync, stat, unwatchFile, watch, watchFile } from "node:fs";
6
- import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
6
+ import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
7
7
  import * as sp from "node:path";
8
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
9
  import { EventEmitter } from "node:events";
10
10
  import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
13
  import * as nativeFs from "fs";
14
14
  import { readdir as readdir$1, readdirSync, realpath as realpath$1, realpathSync, stat as stat$2, statSync } from "fs";
15
- import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, normalize, posix, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
15
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, normalize, posix as posix$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
  import { createRequire } from "module";
18
18
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
@@ -154,7 +154,7 @@ function parseStrictJson(text) {
154
154
  }
155
155
  //#endregion
156
156
  //#region package.json
157
- var version = "0.1.1";
157
+ var version = "0.1.4";
158
158
  //#endregion
159
159
  //#region src/documents/cursor.ts
160
160
  function invalidCursor(reason) {
@@ -186,7 +186,7 @@ function createCursorCodec(secret, now = Date.now) {
186
186
  //#endregion
187
187
  //#region ../../packages/document/src/profiles.ts
188
188
  const MARKDOWN_PARSER_PROFILE = "markdown-tanstack-v1";
189
- const HTML_STATIC_PARSER_PROFILE = "html-static-v1";
189
+ const HTML_STATIC_PARSER_PROFILE = "html-static-v2";
190
190
  //#endregion
191
191
  //#region ../../packages/document/src/source-lines.ts
192
192
  const LF = 10;
@@ -3152,8 +3152,8 @@ function buildFormat(cwd, root, absolute) {
3152
3152
  };
3153
3153
  return (p, isDir) => isDir && p !== "." ? p.slice(0, -1) : p;
3154
3154
  }
3155
- if (absolute) return (p) => posix.relative(cwd, p) || ".";
3156
- return (p) => posix.relative(cwd, `${root}/${p}`) || ".";
3155
+ if (absolute) return (p) => posix$1.relative(cwd, p) || ".";
3156
+ return (p) => posix$1.relative(cwd, `${root}/${p}`) || ".";
3157
3157
  }
3158
3158
  function buildRelative(cwd, root) {
3159
3159
  if (root.startsWith(`${cwd}/`)) {
@@ -3161,7 +3161,7 @@ function buildRelative(cwd, root) {
3161
3161
  return (p) => `${prefix}/${p}`;
3162
3162
  }
3163
3163
  return (p) => {
3164
- const result = posix.relative(cwd, `${root}/${p}`);
3164
+ const result = posix$1.relative(cwd, `${root}/${p}`);
3165
3165
  return p[p.length - 1] === "/" && result !== "" ? `${result}/` : result || ".";
3166
3166
  };
3167
3167
  }
@@ -3216,7 +3216,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3216
3216
  if (pattern[pattern.length - 1] === "/") result = pattern.slice(0, -1);
3217
3217
  if (result[result.length - 1] !== "*" && opts.expandDirectories) result += "/**";
3218
3218
  const escapedCwd = escapePath(cwd);
3219
- result = isAbsolute$1(result.replace(ESCAPING_BACKSLASHES, "")) ? posix.relative(escapedCwd, result) : posix.normalize(result);
3219
+ result = isAbsolute$1(result.replace(ESCAPING_BACKSLASHES, "")) ? posix$1.relative(escapedCwd, result) : posix$1.normalize(result);
3220
3220
  const parentDir = (_PARENT_DIRECTORY$exe = PARENT_DIRECTORY.exec(result)) === null || _PARENT_DIRECTORY$exe === void 0 ? void 0 : _PARENT_DIRECTORY$exe[0];
3221
3221
  const parts = splitPattern(result);
3222
3222
  if (parentDir) {
@@ -3227,7 +3227,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3227
3227
  result = result.slice(0, (n - i - 1) * 3) + result.slice((n - i) * 3 + parts[i + n].length + 1) || ".";
3228
3228
  i++;
3229
3229
  }
3230
- const potentialRoot = posix.join(cwd, parentDir.slice(i * 3));
3230
+ const potentialRoot = posix$1.join(cwd, parentDir.slice(i * 3));
3231
3231
  if (potentialRoot[0] !== "." && props.root.length > potentialRoot.length) {
3232
3232
  props.root = ensureNonDriveRelativePath(potentialRoot);
3233
3233
  props.depthOffset = -n + i;
@@ -3249,7 +3249,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3249
3249
  }
3250
3250
  props.depthOffset = newCommonPath.length;
3251
3251
  props.commonPath = newCommonPath;
3252
- props.root = ensureNonDriveRelativePath(newCommonPath.length > 0 ? posix.join(cwd, ...newCommonPath) : cwd);
3252
+ props.root = ensureNonDriveRelativePath(newCommonPath.length > 0 ? posix$1.join(cwd, ...newCommonPath) : cwd);
3253
3253
  }
3254
3254
  return result;
3255
3255
  }
@@ -3515,6 +3515,550 @@ function isExcludedDirectoryName(name) {
3515
3515
  return isHidden(name) || EXCLUDED_DIRECTORIES.has(name);
3516
3516
  }
3517
3517
  //#endregion
3518
+ //#region src/documents/repository.ts
3519
+ const METADATA_LIMIT = 4096;
3520
+ function kindOf(stats) {
3521
+ if (stats.isSymbolicLink()) return "symlink";
3522
+ if (stats.isFile()) return "file";
3523
+ if (stats.isDirectory()) return "directory";
3524
+ return "other";
3525
+ }
3526
+ function notRegularFile(path) {
3527
+ return Object.assign(/* @__PURE__ */ new Error(`${path} is not a regular file.`), { code: "EFTYPE" });
3528
+ }
3529
+ const nodeRepositoryFs = {
3530
+ lstat: async (path) => kindOf(await lstat(path)),
3531
+ stat: async (path) => kindOf(await stat$1(path)),
3532
+ readlink: (path) => readlink(path),
3533
+ realpath: (path) => realpath(path),
3534
+ readSmall: async (path, limit) => {
3535
+ const before = await lstat(path);
3536
+ if (!before.isFile()) throw notRegularFile(path);
3537
+ const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
3538
+ try {
3539
+ const opened = await handle.stat();
3540
+ if (!opened.isFile() || opened.ino !== before.ino || opened.dev !== before.dev) throw notRegularFile(path);
3541
+ return await readBounded(handle, limit);
3542
+ } finally {
3543
+ await handle.close();
3544
+ }
3545
+ }
3546
+ };
3547
+ function createLimiter(limit) {
3548
+ let active = 0;
3549
+ const waiting = [];
3550
+ return {
3551
+ async run(work) {
3552
+ if (active < limit) active += 1;
3553
+ else await new Promise((resolveTurn) => waiting.push(resolveTurn));
3554
+ try {
3555
+ return await work();
3556
+ } finally {
3557
+ const next = waiting.shift();
3558
+ if (next) next();
3559
+ else active -= 1;
3560
+ }
3561
+ },
3562
+ get active() {
3563
+ return active;
3564
+ }
3565
+ };
3566
+ }
3567
+ function limitRepositoryFs(fs, limiter) {
3568
+ return {
3569
+ lstat: (path) => limiter.run(() => fs.lstat(path)),
3570
+ stat: (path) => limiter.run(() => fs.stat(path)),
3571
+ readlink: (path) => limiter.run(() => fs.readlink(path)),
3572
+ realpath: (path) => limiter.run(() => fs.realpath(path)),
3573
+ readSmall: (path, size) => limiter.run(() => fs.readSmall(path, size))
3574
+ };
3575
+ }
3576
+ var Unresolved = class extends Error {
3577
+ reason;
3578
+ constructor(reason) {
3579
+ super(reason);
3580
+ this.reason = reason;
3581
+ }
3582
+ };
3583
+ function failureOf(error, invalid) {
3584
+ if (error instanceof Unresolved) return error;
3585
+ const code = error?.code;
3586
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "EFTYPE") return new Unresolved(invalid);
3587
+ return new Unresolved("unreadable");
3588
+ }
3589
+ const HEX_OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
3590
+ const REFTABLE_HEAD = "refs/heads/.invalid";
3591
+ function isValidRefName(ref) {
3592
+ if (ref === "@" || ref.endsWith("/") || ref.endsWith(".")) return false;
3593
+ if (ref.includes("..") || ref.includes("@{") || ref.includes("//")) return false;
3594
+ if (/[\u0000- \u007f~^:?*[\\]/.test(ref)) return false;
3595
+ return ref.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
3596
+ }
3597
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
3598
+ function parseHead(bytes) {
3599
+ const content = bytes.toString("latin1").replace(/\s+$/, "");
3600
+ if (HEX_OBJECT_ID.test(content)) return null;
3601
+ if (!content.startsWith("ref:")) throw new Unresolved("invalid-git-dir");
3602
+ const ref = content.slice(4).replace(/^\s+/, "");
3603
+ if (ref === REFTABLE_HEAD) return null;
3604
+ if (!ref.startsWith("refs/") || !isValidRefName(ref)) throw new Unresolved("invalid-git-dir");
3605
+ if (!ref.startsWith("refs/heads/")) return null;
3606
+ try {
3607
+ return utf8.decode(Buffer.from(ref.slice(11), "latin1"));
3608
+ } catch {
3609
+ return null;
3610
+ }
3611
+ }
3612
+ function parseLinkFile(bytes, prefix, invalid) {
3613
+ let content;
3614
+ try {
3615
+ content = utf8.decode(bytes);
3616
+ } catch {
3617
+ throw new Unresolved(invalid);
3618
+ }
3619
+ content = content.replace(/\r?\n$/, "");
3620
+ if (!content.startsWith(prefix)) throw new Unresolved(invalid);
3621
+ const path = content.slice(prefix.length);
3622
+ if (path === "" || path.includes("\n") || path.includes("\0")) throw new Unresolved(invalid);
3623
+ return path;
3624
+ }
3625
+ function createRepositoryDetector(options = {}) {
3626
+ const fs = options.fs ?? nodeRepositoryFs;
3627
+ const path = options.platform === "win32" ? win32 : posix;
3628
+ const isWindows = options.platform === "win32";
3629
+ const probes = /* @__PURE__ */ new Map();
3630
+ const checkouts = /* @__PURE__ */ new Map();
3631
+ const segmentsOf = (absolute) => absolute.split(path.sep).filter((part) => part !== "");
3632
+ const relativeSegments = (from, to) => segmentsOf(path.relative(from, to));
3633
+ const permits = (documentRoot, target) => {
3634
+ if (!isWindows) return true;
3635
+ const root = path.parse(target).root;
3636
+ if (/^[A-Za-z]:[\\/]$/.test(root)) return true;
3637
+ return root.toLowerCase() === documentRoot.toLowerCase();
3638
+ };
3639
+ const allowed = (documentRoot, target) => {
3640
+ if (!permits(documentRoot, target)) throw new Unresolved("blocked-path");
3641
+ return target;
3642
+ };
3643
+ const probe = (directory) => {
3644
+ let pending = probes.get(directory);
3645
+ if (!pending) {
3646
+ pending = fs.lstat(path.join(directory, ".git")).then((kind) => kind, (error) => error.code === "ENOENT" || error.code === "ENOTDIR" ? "absent" : "error");
3647
+ probes.set(directory, pending);
3648
+ }
3649
+ return pending;
3650
+ };
3651
+ const readMetadata = async (file, invalid) => {
3652
+ let bytes;
3653
+ try {
3654
+ bytes = await fs.readSmall(file, METADATA_LIMIT);
3655
+ } catch (error) {
3656
+ throw failureOf(error, invalid);
3657
+ }
3658
+ if (bytes === null) throw new Unresolved("limit-exceeded");
3659
+ return bytes;
3660
+ };
3661
+ const realpathOf = async (target, invalid) => {
3662
+ try {
3663
+ return await fs.realpath(target);
3664
+ } catch (error) {
3665
+ throw failureOf(error, invalid);
3666
+ }
3667
+ };
3668
+ const expectDirectory = async (target) => {
3669
+ let kind;
3670
+ try {
3671
+ kind = await fs.stat(target);
3672
+ } catch (error) {
3673
+ throw failureOf(error, "invalid-git-dir");
3674
+ }
3675
+ if (kind !== "directory") throw new Unresolved("invalid-git-dir");
3676
+ };
3677
+ const validated = /* @__PURE__ */ new Map();
3678
+ const validateGitDir = (gitDir) => {
3679
+ let pending = validated.get(gitDir);
3680
+ if (!pending) {
3681
+ pending = (async () => {
3682
+ await expectDirectory(gitDir);
3683
+ const branch = parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3684
+ await expectDirectory(path.join(gitDir, "objects"));
3685
+ await expectDirectory(path.join(gitDir, "refs"));
3686
+ return branch;
3687
+ })();
3688
+ validated.set(gitDir, pending);
3689
+ }
3690
+ return pending;
3691
+ };
3692
+ const repositoryNameSegments = (commonDir) => {
3693
+ const segments = segmentsOf(commonDir);
3694
+ const last = segments.at(-1) ?? "";
3695
+ if (last === ".git") return segments.slice(0, -1);
3696
+ if (last.length > 4 && last.endsWith(".git")) return [...segments.slice(0, -1), last.slice(0, -4)];
3697
+ return segments;
3698
+ };
3699
+ const standalone = (checkoutDir) => ({
3700
+ state: "resolved",
3701
+ id: checkoutDir,
3702
+ nameSegments: segmentsOf(checkoutDir),
3703
+ checkout: {
3704
+ id: checkoutDir,
3705
+ kind: "main"
3706
+ }
3707
+ });
3708
+ const commonDirOf = async (gitDir, documentRoot) => {
3709
+ try {
3710
+ await fs.lstat(path.join(gitDir, "commondir"));
3711
+ } catch (error) {
3712
+ const code = error.code;
3713
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
3714
+ throw failureOf(error, "invalid-git-file");
3715
+ }
3716
+ const written = parseLinkFile(await readMetadata(path.join(gitDir, "commondir"), "invalid-git-file"), "", "invalid-git-file");
3717
+ return realpathOf(allowed(documentRoot, path.resolve(gitDir, written)), "invalid-git-dir");
3718
+ };
3719
+ const resolveGitFile = async (checkoutDir, gitFile, documentRoot) => {
3720
+ const written = parseLinkFile(await readMetadata(gitFile, "invalid-git-file"), "gitdir: ", "invalid-git-file");
3721
+ const gitDir = await realpathOf(allowed(documentRoot, path.resolve(checkoutDir, written)), "invalid-git-file");
3722
+ const commonDir = await commonDirOf(gitDir, documentRoot);
3723
+ if (commonDir === null) {
3724
+ await validateGitDir(gitDir);
3725
+ return standalone(checkoutDir);
3726
+ }
3727
+ await validateGitDir(commonDir);
3728
+ if (path.dirname(gitDir) !== path.join(commonDir, "worktrees")) throw new Unresolved("link-mismatch");
3729
+ const branch = parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3730
+ const backWritten = parseLinkFile(await readMetadata(path.join(gitDir, "gitdir"), "link-mismatch"), "", "link-mismatch");
3731
+ const back = allowed(documentRoot, path.resolve(gitDir, backWritten));
3732
+ if (path.basename(back) !== ".git") throw new Unresolved("link-mismatch");
3733
+ if (await realpathOf(path.dirname(back), "link-mismatch") !== checkoutDir) throw new Unresolved("link-mismatch");
3734
+ return {
3735
+ state: "resolved",
3736
+ id: commonDir,
3737
+ nameSegments: repositoryNameSegments(commonDir),
3738
+ checkout: {
3739
+ id: checkoutDir,
3740
+ kind: "linked",
3741
+ name: path.basename(gitDir),
3742
+ branch
3743
+ }
3744
+ };
3745
+ };
3746
+ const resolveGitLink = async (checkoutDir, gitLink, documentRoot) => {
3747
+ let target;
3748
+ try {
3749
+ target = allowed(documentRoot, path.resolve(checkoutDir, await fs.readlink(gitLink)));
3750
+ } catch (error) {
3751
+ throw failureOf(error, "invalid-git-dir");
3752
+ }
3753
+ let kind;
3754
+ try {
3755
+ kind = await fs.lstat(target);
3756
+ } catch (error) {
3757
+ throw failureOf(error, "invalid-git-dir");
3758
+ }
3759
+ if (kind === "directory") {
3760
+ await validateGitDir(target);
3761
+ return standalone(checkoutDir);
3762
+ }
3763
+ if (kind === "file") {
3764
+ const written = parseLinkFile(await readMetadata(target, "invalid-git-file"), "gitdir: ", "invalid-git-file");
3765
+ const gitDir = await realpathOf(allowed(documentRoot, path.resolve(checkoutDir, written)), "invalid-git-file");
3766
+ const common = await commonDirOf(gitDir, documentRoot);
3767
+ if (common === null) await validateGitDir(gitDir);
3768
+ else {
3769
+ await validateGitDir(common);
3770
+ parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3771
+ }
3772
+ return standalone(checkoutDir);
3773
+ }
3774
+ throw new Unresolved("invalid-git-dir");
3775
+ };
3776
+ const resolveCheckout = (checkoutDir, kind, documentRoot) => {
3777
+ let pending = checkouts.get(checkoutDir);
3778
+ if (!pending) {
3779
+ const gitEntry = path.join(checkoutDir, ".git");
3780
+ const work = async () => {
3781
+ if (kind === "directory") {
3782
+ await validateGitDir(gitEntry);
3783
+ return {
3784
+ state: "resolved",
3785
+ id: gitEntry,
3786
+ nameSegments: repositoryNameSegments(gitEntry),
3787
+ checkout: {
3788
+ id: checkoutDir,
3789
+ kind: "main"
3790
+ }
3791
+ };
3792
+ }
3793
+ if (kind === "file") return resolveGitFile(checkoutDir, gitEntry, documentRoot);
3794
+ if (kind === "symlink") return resolveGitLink(checkoutDir, gitEntry, documentRoot);
3795
+ throw new Unresolved("invalid-git-dir");
3796
+ };
3797
+ pending = work().catch((error) => ({
3798
+ state: "unresolved",
3799
+ reason: failureOf(error, "invalid-git-dir").reason
3800
+ }));
3801
+ checkouts.set(checkoutDir, pending);
3802
+ }
3803
+ return pending;
3804
+ };
3805
+ const detect = async (canonicalPath) => {
3806
+ const documentRoot = path.parse(canonicalPath).root;
3807
+ let directory = path.dirname(canonicalPath);
3808
+ let kind;
3809
+ for (;;) {
3810
+ const found = await probe(directory);
3811
+ if (found === "error") return {
3812
+ state: "unresolved",
3813
+ id: directory,
3814
+ nameSegments: segmentsOf(directory),
3815
+ pathInCheckout: relativeSegments(directory, canonicalPath),
3816
+ reason: "unreadable"
3817
+ };
3818
+ if (found !== "absent") {
3819
+ kind = found;
3820
+ break;
3821
+ }
3822
+ const parent = path.dirname(directory);
3823
+ if (parent === directory || directory === options.stopAt) return { state: "outside" };
3824
+ directory = parent;
3825
+ }
3826
+ const gitEntry = path.join(directory, ".git");
3827
+ if (kind === "directory" && canonicalPath.startsWith(gitEntry + path.sep)) {
3828
+ try {
3829
+ await validateGitDir(gitEntry);
3830
+ } catch (error) {
3831
+ return {
3832
+ state: "unresolved",
3833
+ id: directory,
3834
+ nameSegments: segmentsOf(directory),
3835
+ pathInCheckout: relativeSegments(directory, canonicalPath),
3836
+ reason: failureOf(error, "invalid-git-dir").reason
3837
+ };
3838
+ }
3839
+ return {
3840
+ state: "resolved",
3841
+ id: gitEntry,
3842
+ nameSegments: repositoryNameSegments(gitEntry),
3843
+ checkout: null,
3844
+ pathInCheckout: relativeSegments(directory, canonicalPath)
3845
+ };
3846
+ }
3847
+ const resolution = await resolveCheckout(directory, kind, documentRoot);
3848
+ const pathInCheckout = relativeSegments(directory, canonicalPath);
3849
+ if (resolution.state === "unresolved") return {
3850
+ state: "unresolved",
3851
+ id: directory,
3852
+ nameSegments: segmentsOf(directory),
3853
+ pathInCheckout,
3854
+ reason: resolution.reason ?? "invalid-git-dir"
3855
+ };
3856
+ return {
3857
+ state: "resolved",
3858
+ id: resolution.id,
3859
+ nameSegments: resolution.nameSegments,
3860
+ checkout: resolution.checkout,
3861
+ pathInCheckout
3862
+ };
3863
+ };
3864
+ return { detect };
3865
+ }
3866
+ function supersedes(current, sequence) {
3867
+ return current === void 0 || current.sequence < sequence || current.sequence === sequence && current.value?.state === "pending";
3868
+ }
3869
+ var RepositoryTracker = class {
3870
+ #fs;
3871
+ #platform;
3872
+ #limiter = createLimiter(2);
3873
+ #stopAt;
3874
+ #createDetector;
3875
+ waitMs;
3876
+ lateApplyMs;
3877
+ lateApplyMaxMs;
3878
+ #sequence = 0;
3879
+ #documents = /* @__PURE__ */ new Map();
3880
+ #checkouts = /* @__PURE__ */ new Map();
3881
+ #members = /* @__PURE__ */ new Map();
3882
+ constructor(options = {}) {
3883
+ this.#fs = limitRepositoryFs(options.fs ?? nodeRepositoryFs, this.#limiter);
3884
+ this.#platform = options.platform ?? (process.platform === "win32" ? "win32" : "posix");
3885
+ this.waitMs = options.waitMs ?? 2e3;
3886
+ this.lateApplyMs = options.lateApplyMs ?? 250;
3887
+ this.lateApplyMaxMs = options.lateApplyMaxMs ?? 1e3;
3888
+ this.#stopAt = options.stopAt;
3889
+ this.#createDetector = options.createDetector;
3890
+ }
3891
+ begin(paths) {
3892
+ this.#sequence += 1;
3893
+ const detector = this.#createDetector?.() ?? createRepositoryDetector({
3894
+ fs: this.#fs,
3895
+ platform: this.#platform,
3896
+ ...this.#stopAt === void 0 ? {} : { stopAt: this.#stopAt }
3897
+ });
3898
+ const results = /* @__PURE__ */ new Map();
3899
+ const settled = /* @__PURE__ */ new Map();
3900
+ const work = [...new Set(paths)].map((path) => {
3901
+ const finished = detector.detect(path).then((detection) => {
3902
+ results.set(path, detection);
3903
+ }, () => {
3904
+ results.set(path, {
3905
+ state: "unresolved",
3906
+ id: path,
3907
+ nameSegments: [],
3908
+ pathInCheckout: [],
3909
+ reason: "unreadable"
3910
+ });
3911
+ });
3912
+ settled.set(path, finished);
3913
+ return finished;
3914
+ });
3915
+ return {
3916
+ sequence: this.#sequence,
3917
+ startedAt: Date.now(),
3918
+ results,
3919
+ done: Promise.all(work).then(() => void 0),
3920
+ settled: (path) => settled.get(path) ?? Promise.resolve()
3921
+ };
3922
+ }
3923
+ async wait(batch) {
3924
+ const remaining = batch.startedAt + this.waitMs - Date.now();
3925
+ if (remaining <= 0) return;
3926
+ let timer;
3927
+ await Promise.race([batch.done, new Promise((resolveTimeout) => {
3928
+ timer = setTimeout(resolveTimeout, remaining);
3929
+ })]);
3930
+ clearTimeout(timer);
3931
+ }
3932
+ #compose(stored, checkouts = this.#checkouts) {
3933
+ if (stored === void 0) return { state: "pending" };
3934
+ if (stored.state === "outside") return null;
3935
+ if (stored.state !== "resolved") return stored;
3936
+ if (stored.checkoutId === null) {
3937
+ const { checkoutId: _none, ...rest } = stored;
3938
+ return {
3939
+ ...rest,
3940
+ checkout: null
3941
+ };
3942
+ }
3943
+ const entry = checkouts.get(stored.checkoutId);
3944
+ if (entry === void 0) return { state: "pending" };
3945
+ return {
3946
+ state: "resolved",
3947
+ id: entry.repositoryId,
3948
+ nameSegments: entry.nameSegments,
3949
+ checkout: entry.info,
3950
+ pathInCheckout: stored.pathInCheckout
3951
+ };
3952
+ }
3953
+ view(documentId) {
3954
+ return this.#compose(this.#documents.get(documentId)?.value);
3955
+ }
3956
+ stage(batch, targets, options) {
3957
+ const documents = /* @__PURE__ */ new Map();
3958
+ const checkouts = /* @__PURE__ */ new Map();
3959
+ const unfinished = [];
3960
+ const appliesTo = (current) => supersedes(current, batch.sequence);
3961
+ for (const target of targets) {
3962
+ const current = this.#documents.get(target.documentId);
3963
+ const detection = batch.results.get(target.path);
3964
+ if (detection === void 0) {
3965
+ unfinished.push(target);
3966
+ if (options.pendingWhenUnfinished && current === void 0) documents.set(target.documentId, {
3967
+ sequence: batch.sequence,
3968
+ value: { state: "pending" }
3969
+ });
3970
+ continue;
3971
+ }
3972
+ if (!appliesTo(current)) continue;
3973
+ let value;
3974
+ if (detection.state === "resolved" && detection.checkout !== null) {
3975
+ const { checkout } = detection;
3976
+ value = {
3977
+ state: "resolved",
3978
+ checkoutId: checkout.id,
3979
+ pathInCheckout: detection.pathInCheckout
3980
+ };
3981
+ if (appliesTo(this.#checkouts.get(checkout.id))) checkouts.set(checkout.id, {
3982
+ sequence: batch.sequence,
3983
+ info: checkout,
3984
+ repositoryId: detection.id,
3985
+ nameSegments: detection.nameSegments
3986
+ });
3987
+ } else if (detection.state === "resolved") {
3988
+ const { checkout: _none, ...rest } = detection;
3989
+ value = {
3990
+ ...rest,
3991
+ checkoutId: null
3992
+ };
3993
+ } else value = detection;
3994
+ documents.set(target.documentId, {
3995
+ sequence: batch.sequence,
3996
+ value
3997
+ });
3998
+ }
3999
+ const merged = new Map(this.#checkouts);
4000
+ for (const [id, entry] of checkouts) merged.set(id, entry);
4001
+ const affected = new Set(documents.keys());
4002
+ for (const id of checkouts.keys()) for (const member of this.#members.get(id) ?? []) affected.add(member);
4003
+ let changed = false;
4004
+ for (const documentId of affected) {
4005
+ const before = this.view(documentId);
4006
+ const after = this.#compose(documents.get(documentId)?.value ?? this.#documents.get(documentId)?.value, merged);
4007
+ if (canonicalJson(before) !== canonicalJson(after)) {
4008
+ changed = true;
4009
+ break;
4010
+ }
4011
+ }
4012
+ return {
4013
+ changed,
4014
+ unfinished,
4015
+ documents,
4016
+ checkouts
4017
+ };
4018
+ }
4019
+ apply(staged) {
4020
+ for (const [id, entry] of staged.checkouts) if (supersedes(this.#checkouts.get(id), entry.sequence)) this.#checkouts.set(id, entry);
4021
+ for (const [documentId, entry] of staged.documents) {
4022
+ const current = this.#documents.get(documentId);
4023
+ if (entry.value.state === "pending" && current !== void 0) continue;
4024
+ if (supersedes(current, entry.sequence)) this.#setDocument(documentId, entry);
4025
+ }
4026
+ this.#dropUnused();
4027
+ }
4028
+ #setDocument(documentId, entry) {
4029
+ const before = this.#documents.get(documentId)?.value;
4030
+ if (before?.state === "resolved" && before.checkoutId !== null) this.#members.get(before.checkoutId)?.delete(documentId);
4031
+ this.#documents.set(documentId, entry);
4032
+ if (entry.value.state === "resolved" && entry.value.checkoutId !== null) {
4033
+ let members = this.#members.get(entry.value.checkoutId);
4034
+ if (!members) {
4035
+ members = /* @__PURE__ */ new Set();
4036
+ this.#members.set(entry.value.checkoutId, members);
4037
+ }
4038
+ members.add(documentId);
4039
+ }
4040
+ }
4041
+ forget(documentIds) {
4042
+ for (const documentId of documentIds) {
4043
+ const before = this.#documents.get(documentId)?.value;
4044
+ if (before?.state === "resolved" && before.checkoutId !== null) this.#members.get(before.checkoutId)?.delete(documentId);
4045
+ this.#documents.delete(documentId);
4046
+ }
4047
+ this.#dropUnused();
4048
+ }
4049
+ #dropUnused() {
4050
+ for (const [id, members] of this.#members) if (members.size === 0) this.#members.delete(id);
4051
+ for (const id of this.#checkouts.keys()) if (!this.#members.has(id)) this.#checkouts.delete(id);
4052
+ }
4053
+ retainedCounts() {
4054
+ return {
4055
+ repositoryDocuments: this.#documents.size,
4056
+ repositoryCheckouts: this.#checkouts.size,
4057
+ repositoryOperations: this.#limiter.active
4058
+ };
4059
+ }
4060
+ };
4061
+ //#endregion
3518
4062
  //#region src/documents/revision.ts
3519
4063
  function sha256Hex(bytes) {
3520
4064
  return createHash("sha256").update(bytes).digest("hex");
@@ -3544,7 +4088,7 @@ const MISSING_RETRY_INTERVAL_MS = 100;
3544
4088
  function profileOf(format) {
3545
4089
  return format === "markdown" ? MARKDOWN_PARSER_PROFILE : HTML_STATIC_PARSER_PROFILE;
3546
4090
  }
3547
- function toSummary(record, order, searchState = "excluded", pendingRequestIds = [], interactiveAllowed = false) {
4091
+ function toSummary(record, order, searchState = "excluded", pendingRequestIds = [], interactiveAllowed = false, repository = null) {
3548
4092
  return {
3549
4093
  documentId: record.documentId,
3550
4094
  key: record.key,
@@ -3553,6 +4097,8 @@ function toSummary(record, order, searchState = "excluded", pendingRequestIds =
3553
4097
  title: record.title,
3554
4098
  displayPath: record.displayPath,
3555
4099
  pathSegments: record.pathSegments,
4100
+ canonicalPath: record.sourceKind === "file" ? record.canonicalPath : null,
4101
+ repository,
3556
4102
  revision: record.currentRevision,
3557
4103
  sourceState: record.sourceState,
3558
4104
  searchState,
@@ -3627,6 +4173,7 @@ var DocumentService = class {
3627
4173
  #interactive = /* @__PURE__ */ new Map();
3628
4174
  #interactiveGenerations = 0;
3629
4175
  #linkConfirmations = /* @__PURE__ */ new Map();
4176
+ #repositories;
3630
4177
  constructor(options) {
3631
4178
  this.#store = options.store;
3632
4179
  this.#cursors = options.cursors;
@@ -3636,6 +4183,68 @@ var DocumentService = class {
3636
4183
  this.#readSource = options.readSource ?? readSourceFile;
3637
4184
  this.#scan = options.scan ?? ((kind, text) => Promise.resolve(scanReferences(kind, text)));
3638
4185
  this.#searchStateOf = options.searchStateOf ?? (() => "excluded");
4186
+ this.#repositories = new RepositoryTracker(options.repositories);
4187
+ }
4188
+ async initializeRepositories() {
4189
+ const state = this.#store.payload;
4190
+ const targets = this.#trackedFiles(state, state.openOrder);
4191
+ if (targets.length === 0) return;
4192
+ const batch = this.#repositories.begin(targets.map((target) => target.path));
4193
+ await this.#repositories.wait(batch);
4194
+ const staged = this.#repositories.stage(batch, targets, { pendingWhenUnfinished: true });
4195
+ this.#repositories.apply(staged);
4196
+ this.#applyWhenDone(batch, staged.unfinished);
4197
+ }
4198
+ #trackedFiles(state, documentIds) {
4199
+ const targets = [];
4200
+ for (const documentId of documentIds) {
4201
+ const record = state.documents[documentId];
4202
+ if (record?.isOpen && record.sourceKind === "file" && record.canonicalPath !== null) targets.push({
4203
+ documentId,
4204
+ path: record.canonicalPath,
4205
+ epoch: this.openEpoch(documentId)
4206
+ });
4207
+ }
4208
+ return targets;
4209
+ }
4210
+ async #applyBatch(batch, targets) {
4211
+ let staged = null;
4212
+ await this.#store.transaction((tx) => {
4213
+ const live = targets.filter((target) => tx.state.documents[target.documentId]?.isOpen === true && this.openEpoch(target.documentId) === target.epoch);
4214
+ const next = this.#repositories.stage(batch, live, { pendingWhenUnfinished: false });
4215
+ if (next.changed) tx.state.catalogVersion += 1;
4216
+ staged = next;
4217
+ });
4218
+ const applied = staged;
4219
+ if (applied === null) return [];
4220
+ this.#repositories.apply(applied);
4221
+ if (applied.changed) this.#emit({ type: "catalog-changed" });
4222
+ return applied.unfinished;
4223
+ }
4224
+ #applyWhenDone(batch, targets) {
4225
+ if (targets.length === 0) return;
4226
+ let ready = [];
4227
+ let timer = null;
4228
+ let delay = this.#repositories.lateApplyMs;
4229
+ const flush = () => {
4230
+ timer = null;
4231
+ const now = ready;
4232
+ ready = [];
4233
+ delay = Math.min(delay * 2, this.#repositories.lateApplyMaxMs);
4234
+ this.#applyBatch(batch, now).catch(() => void 0);
4235
+ };
4236
+ for (const target of targets) batch.settled(target.path).then(() => {
4237
+ ready.push(target);
4238
+ timer ??= setTimeout(flush, delay);
4239
+ });
4240
+ }
4241
+ async #redetect(documentIds) {
4242
+ const state = this.#store.payload;
4243
+ const targets = this.#trackedFiles(state, documentIds.filter((documentId) => state.documents[documentId]?.sourceState !== "missing"));
4244
+ if (targets.length === 0) return;
4245
+ const batch = this.#repositories.begin(targets.map((target) => target.path));
4246
+ await this.#repositories.wait(batch);
4247
+ this.#applyWhenDone(batch, await this.#applyBatch(batch, targets));
3639
4248
  }
3640
4249
  async #withPathLocks(paths, work) {
3641
4250
  const releases = [];
@@ -3807,7 +4416,8 @@ var DocumentService = class {
3807
4416
  pathQueues: this.#pathQueues.size,
3808
4417
  openEpochs: this.#openEpochs.size,
3809
4418
  interactive: this.#interactive.size,
3810
- linkConfirmations: this.#linkConfirmations.size
4419
+ linkConfirmations: this.#linkConfirmations.size,
4420
+ ...this.#repositories.retainedCounts()
3811
4421
  };
3812
4422
  }
3813
4423
  async open(rawParams) {
@@ -3856,15 +4466,18 @@ var DocumentService = class {
3856
4466
  const settings = await this.#assetSettings(params);
3857
4467
  if (settings.explicit !== void 0 && resolved.unique.size !== 1) throw new VdeError("E_INVALID_ARGUMENT", "--asset can only be specified when opening a single document.", { documents: resolved.unique.size });
3858
4468
  if (params.htmlMode !== void 0 && ![...resolved.unique.values()].some((entry) => entry.format === "html")) throw new VdeError("E_INVALID_ARGUMENT", "--html-mode applies to HTML documents.");
4469
+ const batch = this.#repositories.begin(resolved.unique.keys());
3859
4470
  return this.#withPathLocks(resolved.unique.keys(), async () => {
3860
4471
  const incoming = await this.#readCandidates(resolved, settings, warnings);
3861
4472
  if (incoming.length === 0 && watchTargets.length === 0) throw new VdeError("E_PATH_NOT_FOUND", "No documents found.", { paths: params.paths });
3862
- return this.#commitOpen(params, incoming, watchTargets, settings, warnings);
4473
+ await this.#repositories.wait(batch);
4474
+ return this.#commitOpen(params, incoming, watchTargets, settings, warnings, batch);
3863
4475
  });
3864
4476
  }
3865
- async #commitOpen(params, incoming, watchTargets, settings, warnings) {
4477
+ async #commitOpen(params, incoming, watchTargets, settings, warnings, batch) {
3866
4478
  if ((params.title !== void 0 || params.key !== void 0) && incoming.length !== 1) throw new VdeError("E_INVALID_ARGUMENT", "--title and --key can only be specified when opening a single document.", { documents: incoming.length });
3867
4479
  const events = [];
4480
+ let repositories = null;
3868
4481
  const result = await this.#store.transaction((tx) => {
3869
4482
  const state = tx.state;
3870
4483
  const timestamp = this.#now().toISOString();
@@ -3908,9 +4521,11 @@ var DocumentService = class {
3908
4521
  rules.push(rule);
3909
4522
  }
3910
4523
  assertOpenLimits(state);
3911
- if (created + updated > 0) state.catalogVersion += 1;
4524
+ if (batch !== void 0) repositories = this.#repositories.stage(batch, this.#trackedFiles(state, touched), { pendingWhenUnfinished: true });
4525
+ const repositoryChanged = repositories?.changed === true;
4526
+ if (created + updated > 0 || repositoryChanged) state.catalogVersion += 1;
3912
4527
  if (state.activeDocumentId === null) state.activeDocumentId = state.openOrder[0] ?? null;
3913
- if (openSetChanged) events.unshift({ type: "catalog-changed" });
4528
+ if (openSetChanged || repositoryChanged) events.unshift({ type: "catalog-changed" });
3914
4529
  return {
3915
4530
  data: {
3916
4531
  documents: touched.map((documentId) => this.#summary(state, documentId)),
@@ -3923,6 +4538,11 @@ var DocumentService = class {
3923
4538
  warnings
3924
4539
  };
3925
4540
  });
4541
+ const staged = repositories;
4542
+ if (staged !== null && batch !== void 0) {
4543
+ this.#repositories.apply(staged);
4544
+ this.#applyWhenDone(batch, staged.unfinished);
4545
+ }
3926
4546
  for (const item of incoming) this.#record(item);
3927
4547
  const permissionChanged = [];
3928
4548
  result.data.documents.forEach((summary, index) => {
@@ -4185,7 +4805,7 @@ var DocumentService = class {
4185
4805
  };
4186
4806
  }
4187
4807
  #summary(state, documentId) {
4188
- return toSummary(state.documents[documentId], state.openOrder.indexOf(documentId), this.#searchStateOf(documentId), pendingRequestIdsOf(state, documentId), this.interactiveAllowed(documentId));
4808
+ return toSummary(state.documents[documentId], state.openOrder.indexOf(documentId), this.#searchStateOf(documentId), pendingRequestIdsOf(state, documentId), this.interactiveAllowed(documentId), state.documents[documentId]?.sourceKind === "file" ? this.#repositories.view(documentId) : null);
4189
4809
  }
4190
4810
  #findExisting(state, item, key) {
4191
4811
  if (item.sourceKind === "file") return Object.values(state.documents).find((record) => record.sourceKind === "file" && record.canonicalPath === item.canonicalPath);
@@ -4520,6 +5140,7 @@ var DocumentService = class {
4520
5140
  };
4521
5141
  });
4522
5142
  for (const documentId of result.data.closed) this.#openEpochs.set(documentId, this.openEpoch(documentId) + 1);
5143
+ this.#repositories.forget(result.data.closed);
4523
5144
  if (changed) this.#emit({ type: "catalog-changed" });
4524
5145
  for (const event of cancelled) this.#emit(event);
4525
5146
  return result;
@@ -4597,6 +5218,7 @@ var DocumentService = class {
4597
5218
  const targets = raw === void 0 ? this.#store.payload.openOrder : [this.#requireOpen(documentIdSchema.parse(raw)).documentId];
4598
5219
  const changed = [];
4599
5220
  for (const documentId of targets) if ((await this.refreshFromDisk(documentId)).changed) changed.push(documentId);
5221
+ await this.#redetect(targets);
4600
5222
  const state = this.#store.payload;
4601
5223
  return {
4602
5224
  data: {
@@ -4732,6 +5354,7 @@ var DocumentService = class {
4732
5354
  const before = this.#store.payload;
4733
5355
  const rule = before.watchRules.find((candidate) => candidate.watchId === watchId);
4734
5356
  if (!rule) return [];
5357
+ const batch = this.#repositories.begin(candidates.keys());
4735
5358
  const isOpenPath = (state, path) => Object.values(state.documents).some((record) => record.isOpen && record.sourceKind === "file" && record.canonicalPath === path);
4736
5359
  let totalBytes = openSourceBytes(before);
4737
5360
  const discovered = [];
@@ -4771,7 +5394,9 @@ var DocumentService = class {
4771
5394
  }
4772
5395
  }
4773
5396
  if (discovered.length === 0) return [];
5397
+ await this.#repositories.wait(batch);
4774
5398
  const appliedItems = [];
5399
+ let repositories = null;
4775
5400
  const registered = await this.#store.transaction((tx) => {
4776
5401
  const state = tx.state;
4777
5402
  const current = state.watchRules.find((candidate) => candidate.watchId === watchId);
@@ -4787,10 +5412,16 @@ var DocumentService = class {
4787
5412
  }
4788
5413
  if (added.length === 0) return [];
4789
5414
  assertOpenLimits(state);
5415
+ repositories = this.#repositories.stage(batch, this.#trackedFiles(state, added), { pendingWhenUnfinished: true });
4790
5416
  state.catalogVersion += 1;
4791
5417
  if (state.activeDocumentId === null) state.activeDocumentId = state.openOrder[0] ?? null;
4792
5418
  return added;
4793
5419
  });
5420
+ const staged = repositories;
5421
+ if (staged !== null) {
5422
+ this.#repositories.apply(staged);
5423
+ this.#applyWhenDone(batch, staged.unfinished);
5424
+ }
4794
5425
  for (const item of appliedItems) this.#record(item);
4795
5426
  if (registered.length > 0) this.#emit({ type: "catalog-changed" });
4796
5427
  return registered;
@@ -5871,7 +6502,8 @@ function createRenderService(options) {
5871
6502
  role
5872
6503
  })),
5873
6504
  links: output.links,
5874
- diagnostics: output.diagnostics
6505
+ diagnostics: output.diagnostics,
6506
+ headingTargets: output.headingTargets
5875
6507
  };
5876
6508
  snapshots.set(cacheKey, snapshot);
5877
6509
  while (snapshots.size > SNAPSHOT_CACHE_SIZE) {
@@ -5967,6 +6599,7 @@ function createRenderService(options) {
5967
6599
  target: null,
5968
6600
  count: 1
5969
6601
  }, ...snapshot.diagnostics] : snapshot.diagnostics,
6602
+ headingTargets: snapshot.headingTargets,
5970
6603
  bridge: bridge === null ? null : {
5971
6604
  instanceId: bridge.instanceId,
5972
6605
  requestId: bridge.requestId
@@ -13198,6 +13831,7 @@ async function startDaemon(options) {
13198
13831
  scan: (kind, text) => parser.scan(kind, text),
13199
13832
  emit
13200
13833
  });
13834
+ await documents.initializeRepositories();
13201
13835
  feedback = new FeedbackService({
13202
13836
  store: openedStore,
13203
13837
  documents,