@reviewflow/sdk 6.24.0 → 6.27.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.
@@ -1,14 +1,14 @@
1
1
  import { execFile, spawn } from 'node:child_process';
2
- import { env } from 'node:process';
2
+ import { env, pid } from 'node:process';
3
3
  import { promisify } from 'node:util';
4
- import { lstat } from 'node:fs/promises';
5
- import { join } from 'node:path';
6
- import { existsSync } from 'node:fs';
4
+ import { lstat, readdir } from 'node:fs/promises';
5
+ import { join, dirname, relative, isAbsolute } from 'node:path';
6
+ import { existsSync, realpathSync } from 'node:fs';
7
7
  import { parseSync } from 'oxc-parser';
8
8
  import { builtinModules } from 'node:module';
9
9
  import { setImmediate } from 'node:timers/promises';
10
10
 
11
- const execFileAsync = promisify(execFile);
11
+ const execFileAsync$1 = promisify(execFile);
12
12
  const timeoutMs = 1e4;
13
13
  const networkTimeoutMs = 6e4;
14
14
  const defaultMaxBuffer = 4 * 1024 * 1024;
@@ -31,10 +31,39 @@ const failureReason = (error, timeout) => {
31
31
  return "stderr" in error ? reasonLine(String(error.stderr)) : "";
32
32
  };
33
33
  const isExitOneAnswer = (error) => typeof error === "object" && error !== null && "code" in error && error.code === 1 && "stdout" in error && typeof error.stdout === "string" && error.stdout !== "";
34
+ const runUninterruptible = async (cwd, args) => new Promise((resolve, reject) => {
35
+ const child = spawn("git", args, {
36
+ cwd,
37
+ env,
38
+ detached: true,
39
+ stdio: ["ignore", "pipe", "pipe"]
40
+ });
41
+ let stdout = "";
42
+ let stderr = "";
43
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
44
+ stdout += chunk;
45
+ });
46
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
47
+ stderr += chunk;
48
+ });
49
+ child.on("error", reject);
50
+ child.on("close", (code) => {
51
+ if (code === 0) {
52
+ resolve(stdout);
53
+ return;
54
+ }
55
+ reject(
56
+ Object.assign(new Error(`git exited with ${String(code)}`), {
57
+ stderr
58
+ })
59
+ );
60
+ });
61
+ });
34
62
  const git = async (cwd, args, options = {}) => {
35
63
  const timeout = options.network ? networkTimeoutMs : timeoutMs;
36
64
  try {
37
- const { stdout } = await execFileAsync("git", args, {
65
+ if (options.uninterruptible) return await runUninterruptible(cwd, args);
66
+ const { stdout } = await execFileAsync$1("git", args, {
38
67
  cwd,
39
68
  timeout,
40
69
  maxBuffer: options.maxBuffer ?? defaultMaxBuffer,
@@ -403,7 +432,693 @@ const groupDiffPaths = (isPackage, paths) => {
403
432
  if (group === void 0) groups.set(name, [path]);
404
433
  else group.push(path);
405
434
  }
406
- return [...groups].toSorted(([left], [right]) => Number(left !== "") - Number(right !== "")).map(([name, groupPaths]) => ({ name, paths: groupPaths }));
435
+ return [...groups].toSorted(([left], [right]) => Number(left === "") - Number(right === "")).map(([name, groupPaths]) => ({ name, paths: groupPaths }));
436
+ };
437
+
438
+ const readHeadBranch = async (cwd) => {
439
+ const branch = await gitOrNull(cwd, [
440
+ "symbolic-ref",
441
+ "--quiet",
442
+ "--short",
443
+ "HEAD"
444
+ ]);
445
+ return branch === null || branch.trim() === "" ? null : branch.trim();
446
+ };
447
+ const readHeadSha = async (cwd, ref = "HEAD") => {
448
+ const sha = await git(cwd, ["rev-parse", ref]);
449
+ return sha.trim();
450
+ };
451
+ const readDefaultBranch = async (cwd) => {
452
+ const ref = await gitOrNull(cwd, [
453
+ "symbolic-ref",
454
+ "--quiet",
455
+ "--short",
456
+ "refs/remotes/origin/HEAD"
457
+ ]);
458
+ const name = ref?.trim().replace(/^origin\//, "") ?? "";
459
+ return name === "" ? null : name;
460
+ };
461
+ const readCommit = async (cwd, ref) => {
462
+ const sha = await gitOrNull(cwd, [
463
+ "rev-parse",
464
+ "--verify",
465
+ "--quiet",
466
+ `${ref}^{commit}`
467
+ ]);
468
+ return sha === null ? null : sha.trim();
469
+ };
470
+ const resolveReadHead = async (cwd, branch) => {
471
+ if (branch === void 0) {
472
+ return { ref: "HEAD", branch: await readHeadBranch(cwd) };
473
+ }
474
+ const ref = `refs/heads/${branch}`;
475
+ if (await readCommit(cwd, ref) === null) {
476
+ throw new Error(`no local branch ${branch} in ${cwd}`);
477
+ }
478
+ return { ref, branch };
479
+ };
480
+ const resolveDiffBase = async (cwd, name, head = "HEAD") => {
481
+ const candidates = [
482
+ { ref: `origin/${name}`, fullRef: `refs/remotes/origin/${name}` },
483
+ { ref: name, fullRef: `refs/heads/${name}` }
484
+ ];
485
+ for (const { ref, fullRef } of candidates) {
486
+ const sha = await readCommit(cwd, fullRef);
487
+ if (sha === null) continue;
488
+ const mergeBase = await gitOrNull(cwd, ["merge-base", sha, head]);
489
+ if (mergeBase === null) {
490
+ throw new Error(`${head} shares no history with ${ref}`);
491
+ }
492
+ return { base: { name, ref, sha }, mergeBase: mergeBase.trim() };
493
+ }
494
+ throw new Error(
495
+ `neither origin/${name} nor ${name} exists in ${cwd}: fetch the repository, or check its default branch`
496
+ );
497
+ };
498
+ const readScopeStart = async ({
499
+ cwd,
500
+ scope,
501
+ baseName,
502
+ head
503
+ }) => {
504
+ if (scope === "uncommitted") return null;
505
+ const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
506
+ return resolveDiffBase(cwd, name, head);
507
+ };
508
+ const readCheckoutBranchDiff = async ({
509
+ cwd,
510
+ baseName,
511
+ scope,
512
+ branch: branchName,
513
+ whitespace = "show",
514
+ limits
515
+ }) => {
516
+ if (branchName !== void 0 && scope === "uncommitted") {
517
+ throw new Error(
518
+ `${branchName} is not checked out here: it has nothing uncommitted`
519
+ );
520
+ }
521
+ const head = await resolveReadHead(cwd, branchName);
522
+ const [start, headSha] = await Promise.all([
523
+ readScopeStart({ cwd, scope, baseName, head: head.ref }),
524
+ readHeadSha(cwd, head.ref)
525
+ ]);
526
+ const { paths, ...diff } = await readBranchDiff({
527
+ cwd,
528
+ mergeBase: start?.mergeBase ?? headSha,
529
+ head: head.ref,
530
+ scope: scope === "uncommitted" ? "workingTree" : "committed",
531
+ whitespace,
532
+ limits
533
+ });
534
+ return {
535
+ scope,
536
+ whitespace,
537
+ branch: head.branch,
538
+ headSha,
539
+ base: start?.base ?? null,
540
+ mergeBase: start?.mergeBase ?? null,
541
+ ...diff,
542
+ groups: groupDiffPaths(packagesOnDisk(cwd), paths)
543
+ };
544
+ };
545
+
546
+ const defaultRecentCommits = 30;
547
+ const fieldSeparator$1 = "";
548
+ const recordSeparator = "";
549
+ const logFormat = ["%H", "%P", "%an", "%cI", "%s", "%b"].join("%x1f");
550
+ const parseLog = (output) => output.split(recordSeparator).map((record) => record.replace(/^\n/, "")).filter((record) => record !== "").map((record) => {
551
+ const [
552
+ sha = "",
553
+ parents = "",
554
+ author = "",
555
+ committedAt = "",
556
+ headline = "",
557
+ body = ""
558
+ ] = record.split(fieldSeparator$1);
559
+ return {
560
+ sha,
561
+ parents: parents === "" ? [] : parents.split(" "),
562
+ author,
563
+ committedAt,
564
+ headline,
565
+ body: body.trim()
566
+ };
567
+ });
568
+ const readBranchStart = async ({
569
+ cwd,
570
+ baseName,
571
+ limit,
572
+ head
573
+ }) => {
574
+ const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
575
+ try {
576
+ const { base, mergeBase } = await resolveDiffBase(cwd, name, head);
577
+ const range = `${mergeBase}..${head}`;
578
+ const [listed, count] = await Promise.all([
579
+ git(cwd, ["rev-list", `--max-count=${String(limit)}`, range]),
580
+ git(cwd, ["rev-list", "--count", range])
581
+ ]);
582
+ return {
583
+ base,
584
+ onBranch: new Set(listed.split("\n").filter((sha) => sha !== "")),
585
+ count: Number(count.trim())
586
+ };
587
+ } catch {
588
+ return null;
589
+ }
590
+ };
591
+ const readUnpushed = async (cwd, limit, head) => {
592
+ const remotes = await git(cwd, ["remote"]);
593
+ if (remotes.trim() === "") return null;
594
+ const listed = await gitOrNull(cwd, [
595
+ "rev-list",
596
+ `--max-count=${String(limit)}`,
597
+ head,
598
+ "--not",
599
+ "--remotes"
600
+ ]);
601
+ return new Set((listed ?? "").split("\n").filter((sha) => sha !== ""));
602
+ };
603
+ const readRecentCommits = async ({
604
+ cwd,
605
+ limit = defaultRecentCommits,
606
+ baseName,
607
+ branch: branchName
608
+ }) => {
609
+ const head = await resolveReadHead(cwd, branchName);
610
+ const [start, unpushed, log] = await Promise.all([
611
+ readBranchStart({ cwd, baseName, limit, head: head.ref }),
612
+ readUnpushed(cwd, limit, head.ref),
613
+ gitOrNull(cwd, [
614
+ "log",
615
+ "--no-color",
616
+ `--max-count=${String(limit)}`,
617
+ `--format=${logFormat}%x1e`,
618
+ head.ref
619
+ ])
620
+ ]);
621
+ const { branch } = head;
622
+ const commits = parseLog(log ?? "").map((commit) => ({
623
+ oid: commit.sha,
624
+ parentOid: commit.parents[0] ?? null,
625
+ merge: commit.parents.length > 1,
626
+ headline: commit.headline,
627
+ body: commit.body,
628
+ author: { login: commit.author, avatarUrl: null },
629
+ committedAt: commit.committedAt,
630
+ onBranch: start?.onBranch.has(commit.sha) ?? false,
631
+ pushed: unpushed === null ? null : !unpushed.has(commit.sha)
632
+ }));
633
+ return {
634
+ branch,
635
+ base: start?.base ?? null,
636
+ onBranchCount: start?.count ?? null,
637
+ commits
638
+ };
639
+ };
640
+ const emptyTree = async (cwd) => {
641
+ const tree = await git(cwd, ["hash-object", "-t", "tree", "/dev/null"]);
642
+ return tree.trim();
643
+ };
644
+ const readCommitDiff = async ({
645
+ cwd,
646
+ sha,
647
+ whitespace = "show",
648
+ limits
649
+ }) => {
650
+ const listed = await git(cwd, [
651
+ "rev-list",
652
+ "--parents",
653
+ "--max-count=1",
654
+ `${sha}^{commit}`
655
+ ]);
656
+ const [commit = sha, parentSha = null] = listed.trim().split(" ");
657
+ const { paths, patch, omitted } = await readBranchDiff({
658
+ cwd,
659
+ mergeBase: parentSha ?? await emptyTree(cwd),
660
+ head: commit,
661
+ scope: "committed",
662
+ whitespace,
663
+ limits
664
+ });
665
+ return {
666
+ sha: commit,
667
+ parentSha,
668
+ whitespace,
669
+ patch,
670
+ omitted,
671
+ groups: groupDiffPaths(packagesOnDisk(cwd), paths)
672
+ };
673
+ };
674
+
675
+ const upstreamGone = (upstream) => `${upstream} no longer exists on the remote`;
676
+ const pullRefusal = (status) => {
677
+ if (status.branch === null) return "a detached HEAD has nothing to pull into";
678
+ if (status.upstream === null) {
679
+ return `${status.branch} tracks no remote branch`;
680
+ }
681
+ if (status.behind === null) return upstreamGone(status.upstream);
682
+ if (status.behind === 0) {
683
+ return `${status.branch} is not behind ${status.upstream}`;
684
+ }
685
+ return null;
686
+ };
687
+ const pushRefusal = (status, hasOrigin) => {
688
+ if (status.branch === null) return "a detached HEAD has no branch to push";
689
+ if (status.upstream === null) {
690
+ return hasOrigin ? null : "this repository has no origin remote";
691
+ }
692
+ if (status.ahead === null) return upstreamGone(status.upstream);
693
+ if (status.ahead === 0) {
694
+ return `${status.branch} has nothing to push to ${status.upstream}`;
695
+ }
696
+ return null;
697
+ };
698
+
699
+ const headerValue = (line, key) => line.startsWith(`# ${key} `) ? line.slice(`# ${key} `.length) : null;
700
+ const parseStatusV2 = (stdout) => {
701
+ const status = {
702
+ branch: null,
703
+ hasCommit: true,
704
+ upstream: null,
705
+ ahead: null,
706
+ behind: null,
707
+ modified: 0,
708
+ untracked: 0
709
+ };
710
+ for (const line of stdout.split("\n")) {
711
+ if (line.startsWith("# ")) {
712
+ const oid = headerValue(line, "branch.oid");
713
+ if (oid !== null) status.hasCommit = oid !== "(initial)";
714
+ const head = headerValue(line, "branch.head");
715
+ if (head !== null) status.branch = head === "(detached)" ? null : head;
716
+ const upstream = headerValue(line, "branch.upstream");
717
+ if (upstream !== null) status.upstream = upstream;
718
+ const ab = headerValue(line, "branch.ab");
719
+ const counts = ab === null ? null : /^\+(\d+) -(\d+)$/.exec(ab);
720
+ if (counts) {
721
+ status.ahead = Number(counts[1]);
722
+ status.behind = Number(counts[2]);
723
+ }
724
+ } else if (line.startsWith("? ")) {
725
+ status.untracked += 1;
726
+ } else if (/^[12u] /.test(line)) {
727
+ status.modified += 1;
728
+ }
729
+ }
730
+ return status;
731
+ };
732
+ const readHeadCommit = async (cwd) => {
733
+ const stdout = await gitOrNull(cwd, [
734
+ "log",
735
+ "-1",
736
+ "--format=%h%x00%s%x00%cI",
737
+ "--no-patch"
738
+ ]);
739
+ const [shortSha, subject, committedAt] = (stdout?.trim() ?? "").split("\0");
740
+ if (!shortSha || subject === void 0 || !committedAt) return null;
741
+ return {
742
+ shortSha,
743
+ subject,
744
+ committedAt: new Date(committedAt).toISOString()
745
+ };
746
+ };
747
+ const readCheckoutStatus = async (cwd) => {
748
+ const { hasCommit, ...status } = parseStatusV2(
749
+ await git(cwd, ["status", "--porcelain=v2", "--branch"])
750
+ );
751
+ return {
752
+ path: cwd,
753
+ ...status,
754
+ commit: hasCommit ? await readHeadCommit(cwd) : null
755
+ };
756
+ };
757
+ const messageOf$1 = (error) => error instanceof Error ? error.message : String(error);
758
+ const hasOriginRemote = async (cwd) => await gitOrNull(cwd, ["remote", "get-url", "origin"]) !== null;
759
+ const fetchFirst = async (cwd, fetch) => {
760
+ if (fetch === "never") return null;
761
+ const upstreamSha = await gitOrNull(cwd, [
762
+ "rev-parse",
763
+ "--verify",
764
+ "--quiet",
765
+ "@{upstream}"
766
+ ]);
767
+ if (upstreamSha === null) return null;
768
+ if (fetch !== "always" && upstreamSha.trim() === fetch.unlessUpstreamAt) {
769
+ return null;
770
+ }
771
+ try {
772
+ await git(cwd, ["fetch", "--quiet"], { network: true });
773
+ return null;
774
+ } catch (error) {
775
+ return messageOf$1(error);
776
+ }
777
+ };
778
+ const readCheckoutSync = async ({
779
+ cwd,
780
+ fetch = "never"
781
+ }) => {
782
+ const fetchError = await fetchFirst(cwd, fetch);
783
+ const [status, hasOrigin] = await Promise.all([
784
+ readCheckoutStatus(cwd),
785
+ hasOriginRemote(cwd)
786
+ ]);
787
+ return { status, hasOrigin, fetchError };
788
+ };
789
+ const pullCheckout = async (cwd) => {
790
+ const refusal = pullRefusal(await readCheckoutStatus(cwd));
791
+ if (refusal !== null) throw new Error(refusal);
792
+ await git(cwd, ["pull", "--quiet", "--ff-only"], { network: true });
793
+ };
794
+ const pushCheckout = async (cwd) => {
795
+ const [status, hasOrigin] = await Promise.all([
796
+ readCheckoutStatus(cwd),
797
+ hasOriginRemote(cwd)
798
+ ]);
799
+ const refusal = pushRefusal(status, hasOrigin);
800
+ if (refusal !== null) throw new Error(refusal);
801
+ await git(
802
+ cwd,
803
+ status.upstream === null && status.branch !== null ? ["push", "--quiet", "--set-upstream", "origin", status.branch] : ["push", "--quiet"],
804
+ { network: true }
805
+ );
806
+ };
807
+
808
+ const zeroSha = /^0+$/;
809
+ const parseWorktreeList = (output) => output.split(/\n{2,}/).map((block) => block.split("\n").filter((line) => line !== "")).filter((lines) => lines[0]?.startsWith("worktree ") === true).map((lines) => {
810
+ const value = (key) => {
811
+ const line = lines.find(
812
+ (candidate) => candidate === key || candidate.startsWith(`${key} `)
813
+ );
814
+ return line === void 0 ? null : line.slice(key.length + 1);
815
+ };
816
+ const head = value("HEAD");
817
+ const branch = value("branch");
818
+ return {
819
+ path: value("worktree") ?? "",
820
+ branch: branch === null ? null : branch.replace(/^refs\/heads\//, ""),
821
+ headSha: head === null || zeroSha.test(head) ? null : head,
822
+ bare: value("bare") !== null,
823
+ lock: value("locked"),
824
+ prunable: value("prunable") !== null
825
+ };
826
+ });
827
+ const readWorktrees = async (cwd) => {
828
+ const listed = parseWorktreeList(
829
+ await git(cwd, ["worktree", "list", "--porcelain"])
830
+ );
831
+ return Promise.all(
832
+ listed.map((worktree, index) => ({ ...worktree, main: index === 0 })).filter(({ bare }) => !bare).map(async ({ bare: _bare, ...worktree }) => ({
833
+ ...worktree,
834
+ status: worktree.prunable ? null : await readCheckoutStatus(worktree.path).catch(() => null)
835
+ }))
836
+ );
837
+ };
838
+ const fieldSeparator = "\0";
839
+ const branchFormat = [
840
+ "%(refname:lstrip=2)",
841
+ "%(objectname)",
842
+ "%(upstream:short)",
843
+ "%(upstream:track)",
844
+ "%(committerdate:iso-strict)",
845
+ "%(worktreepath)",
846
+ "%(subject)"
847
+ ].join("%00");
848
+ const parseTrack = (track, hasUpstream) => {
849
+ if (!hasUpstream) return { ahead: null, behind: null, gone: false };
850
+ if (track === "[gone]") return { ahead: null, behind: null, gone: true };
851
+ const count = (word) => Number(new RegExp(`${word} (\\d+)`).exec(track)?.[1] ?? 0);
852
+ return { ahead: count("ahead"), behind: count("behind"), gone: false };
853
+ };
854
+ const readMergedBranches = async (cwd, start) => {
855
+ if (start === null) return /* @__PURE__ */ new Set();
856
+ const listed = await git(cwd, [
857
+ "for-each-ref",
858
+ `--merged=${start.base.sha}`,
859
+ "--format=%(refname:lstrip=2)",
860
+ "refs/heads"
861
+ ]);
862
+ return new Set(
863
+ listed.split("\n").filter((name) => name !== "" && name !== start.base.name)
864
+ );
865
+ };
866
+ const readBase = async (cwd, baseName) => {
867
+ const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
868
+ return resolveDiffBase(cwd, name).catch(() => null);
869
+ };
870
+ const readBranches = async ({
871
+ cwd,
872
+ baseName
873
+ }) => {
874
+ const start = await readBase(cwd, baseName);
875
+ const [listing, merged, worktrees] = await Promise.all([
876
+ gitOrNull(cwd, [
877
+ "for-each-ref",
878
+ "--sort=-committerdate",
879
+ `--format=${branchFormat}`,
880
+ "refs/heads"
881
+ ]),
882
+ readMergedBranches(cwd, start),
883
+ readWorktrees(cwd)
884
+ ]);
885
+ const branches = (listing ?? "").split("\n").filter((line) => line !== "").map((line) => {
886
+ const [
887
+ name = "",
888
+ sha = "",
889
+ upstream = "",
890
+ track = "",
891
+ committedAt = "",
892
+ worktreePath = "",
893
+ subject = ""
894
+ ] = line.split(fieldSeparator);
895
+ const { ahead, behind, gone } = parseTrack(track, upstream !== "");
896
+ return {
897
+ name,
898
+ sha,
899
+ upstream: upstream === "" ? null : upstream,
900
+ ahead,
901
+ behind,
902
+ upstreamGone: gone,
903
+ merged: merged.has(name),
904
+ worktreePath: worktreePath === "" ? null : worktreePath,
905
+ lastCommit: {
906
+ subject,
907
+ committedAt: new Date(committedAt).toISOString()
908
+ }
909
+ };
910
+ });
911
+ return { base: start?.base ?? null, worktrees, branches };
912
+ };
913
+
914
+ const fileCount = (count, kind) => count === 0 ? null : `${String(count)} ${kind} file${count === 1 ? "" : "s"}`;
915
+ const mainWorktreeRefusal = "This is the main checkout, which holds the repository: it is never removed.";
916
+ const dirtyRefusal = ({
917
+ modified,
918
+ untracked
919
+ }) => {
920
+ const parts = [
921
+ fileCount(modified, "modified"),
922
+ fileCount(untracked, "untracked")
923
+ ].filter((part) => part !== null);
924
+ if (parts.length === 0) return null;
925
+ return `${parts.join(" and ")}: git would refuse to remove this worktree. Commit or discard them first.`;
926
+ };
927
+ const unpushedRefusal = (count) => {
928
+ if (count === 0) return null;
929
+ const subject = count === 1 ? "1 commit exists" : `${String(count)} commits exist`;
930
+ return `${subject} only in this clone, on no remote branch and in no pull request. Push them first.`;
931
+ };
932
+ const lockRefusal = (lock) => {
933
+ if (lock === null) return null;
934
+ const reason = lock === "" ? "" : ` (${lock})`;
935
+ return `git holds this worktree locked${reason}, usually by a Claude Code process still running in it. Exit it first.`;
936
+ };
937
+ const processesRefusal = (count) => {
938
+ if (count === null || count === 0) return null;
939
+ const subject = count === 1 ? "1 process runs" : `${String(count)} processes run`;
940
+ return `${subject} inside this worktree (a shell, an editor or an agent): removing it would pull the directory out from under them. Exit them first.`;
941
+ };
942
+ const worktreeRemovalRefusals = (status) => {
943
+ if (status.main) return [mainWorktreeRefusal];
944
+ return [
945
+ dirtyRefusal(status),
946
+ unpushedRefusal(status.unpushedCommits),
947
+ lockRefusal(status.lock),
948
+ processesRefusal(status.processesInside)
949
+ ].filter((refusal) => refusal !== null);
950
+ };
951
+ const worktreeRemovalRefusal = (status) => worktreeRemovalRefusals(status)[0] ?? null;
952
+
953
+ const execFileAsync = promisify(execFile);
954
+ const realpathOrSelf = (path) => {
955
+ try {
956
+ return realpathSync.native(path);
957
+ } catch {
958
+ return path;
959
+ }
960
+ };
961
+ const isInsideDirectory = (directory, path) => {
962
+ const relativePath = relative(directory, path);
963
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
964
+ };
965
+ const readGitDirectory = async (worktree, flag) => {
966
+ const directory = await git(worktree, [
967
+ "rev-parse",
968
+ "--path-format=absolute",
969
+ flag
970
+ ]);
971
+ return realpathOrSelf(directory.trim());
972
+ };
973
+ const readBranch = async (worktree) => {
974
+ const branch = await gitOrNull(worktree, [
975
+ "rev-parse",
976
+ "--abbrev-ref",
977
+ "HEAD"
978
+ ]);
979
+ const name = branch?.trim() ?? "";
980
+ return name === "" || name === "HEAD" ? null : name;
981
+ };
982
+ const countChanges = async (worktree) => {
983
+ const porcelain = await git(worktree, ["status", "--porcelain"]);
984
+ const lines = porcelain.split("\n").filter((line) => line.trim() !== "");
985
+ const untracked = lines.filter((line) => line.startsWith("??")).length;
986
+ return { modified: lines.length - untracked, untracked };
987
+ };
988
+ const hasCommit$1 = async (worktree, sha) => await gitOrNull(worktree, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
989
+ const countUnpushedCommits = async (worktree, keptShas) => {
990
+ const present = await Promise.all(
991
+ keptShas.filter((sha) => /^[\da-f]{7,64}$/i.test(sha)).map(async (sha) => await hasCommit$1(worktree, sha) ? sha : null)
992
+ );
993
+ const count = await gitOrNull(worktree, [
994
+ "rev-list",
995
+ "--count",
996
+ "HEAD",
997
+ "--not",
998
+ "--remotes",
999
+ ...present.filter((sha) => sha !== null)
1000
+ ]);
1001
+ return count === null ? 0 : Number(count.trim());
1002
+ };
1003
+ const readLock = async (worktree) => {
1004
+ const listed = parseWorktreeList(
1005
+ await git(worktree, ["worktree", "list", "--porcelain"])
1006
+ );
1007
+ const own = listed.find(
1008
+ ({ path }) => realpathOrSelf(path) === realpathOrSelf(worktree)
1009
+ );
1010
+ return own?.lock ?? null;
1011
+ };
1012
+ const countProcessesInside = async (directory) => {
1013
+ const stdout = await execFileAsync("lsof", ["-n", "-d", "cwd", "-F", "pn"], {
1014
+ timeout: 1e4,
1015
+ maxBuffer: 16 * 1024 * 1024
1016
+ }).then(
1017
+ ({ stdout: answer }) => answer,
1018
+ (error) => {
1019
+ if (typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" && error.stdout !== "") {
1020
+ return error.stdout;
1021
+ }
1022
+ return null;
1023
+ }
1024
+ );
1025
+ if (stdout === null) return null;
1026
+ const root = realpathOrSelf(directory);
1027
+ const inside = /* @__PURE__ */ new Set();
1028
+ let processId = "";
1029
+ for (const line of stdout.split("\n")) {
1030
+ if (line.startsWith("p")) processId = line.slice(1);
1031
+ else if (line.startsWith("n") && processId !== String(pid) && isInsideDirectory(root, line.slice(1))) {
1032
+ inside.add(processId);
1033
+ }
1034
+ }
1035
+ return inside.size;
1036
+ };
1037
+ const ignoredPrefix = "!! ";
1038
+ const listFiles = async (worktree, relativeDirectory) => {
1039
+ const entries = await readdir(join(worktree, relativeDirectory), {
1040
+ recursive: true,
1041
+ withFileTypes: true
1042
+ }).catch(() => []);
1043
+ return entries.filter((entry) => entry.isFile()).map((entry) => relative(worktree, join(entry.parentPath, entry.name))).toSorted();
1044
+ };
1045
+ const listLocalOnlyIgnored = async (worktree, mainCheckout) => {
1046
+ const porcelain = await git(worktree, [
1047
+ "status",
1048
+ "--porcelain",
1049
+ "--ignored",
1050
+ "-z"
1051
+ ]);
1052
+ const ignored = porcelain.split("\0").filter((entry) => entry.startsWith(ignoredPrefix)).map((entry) => entry.slice(ignoredPrefix.length));
1053
+ const localOnly = await Promise.all(
1054
+ ignored.map(async (relativePath) => {
1055
+ const entry = await lstat(join(worktree, relativePath)).catch(() => null);
1056
+ if (entry === null || entry.isSymbolicLink()) return [];
1057
+ if (existsSync(join(mainCheckout, relativePath))) return [];
1058
+ return entry.isDirectory() ? listFiles(worktree, relativePath) : [relativePath];
1059
+ })
1060
+ );
1061
+ return localOnly.flat();
1062
+ };
1063
+ const readWorktreeRemovalStatus = async ({
1064
+ worktree,
1065
+ keptShas = []
1066
+ }) => {
1067
+ const [gitDirectory, commonDirectory] = await Promise.all([
1068
+ readGitDirectory(worktree, "--git-dir"),
1069
+ readGitDirectory(worktree, "--git-common-dir")
1070
+ ]);
1071
+ const [branch, changes, unpushedCommits, lock, ignored] = await Promise.all([
1072
+ readBranch(worktree),
1073
+ countChanges(worktree),
1074
+ countUnpushedCommits(worktree, keptShas),
1075
+ readLock(worktree),
1076
+ listLocalOnlyIgnored(worktree, dirname(commonDirectory))
1077
+ ]);
1078
+ const processesInside = await countProcessesInside(worktree);
1079
+ return {
1080
+ path: worktree,
1081
+ main: gitDirectory === commonDirectory,
1082
+ branch,
1083
+ ...changes,
1084
+ unpushedCommits,
1085
+ lock,
1086
+ processesInside,
1087
+ localOnlyIgnored: ignored
1088
+ };
1089
+ };
1090
+ const prepareWorktreeRemoval = async ({
1091
+ deleteBranch,
1092
+ ...params
1093
+ }) => {
1094
+ const status = await readWorktreeRemovalStatus(params);
1095
+ const refusal = worktreeRemovalRefusal(status);
1096
+ if (refusal !== null) throw new Error(refusal);
1097
+ return {
1098
+ path: status.path,
1099
+ commonDirectory: await readGitDirectory(
1100
+ params.worktree,
1101
+ "--git-common-dir"
1102
+ ),
1103
+ branch: deleteBranch ? status.branch : null
1104
+ };
1105
+ };
1106
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
1107
+ const completeWorktreeRemoval = async ({
1108
+ path,
1109
+ commonDirectory,
1110
+ branch
1111
+ }) => {
1112
+ await git(commonDirectory, ["worktree", "remove", path], {
1113
+ uninterruptible: true
1114
+ });
1115
+ if (branch === null) return { path, branch, branchError: null };
1116
+ try {
1117
+ await git(commonDirectory, ["branch", "-d", branch]);
1118
+ return { path, branch, branchError: null };
1119
+ } catch (error) {
1120
+ return { path, branch, branchError: messageOf(error) };
1121
+ }
407
1122
  };
408
1123
 
409
1124
  const cEscapes = {
@@ -782,7 +1497,7 @@ const orderPackages = (names, dependsOn) => {
782
1497
  const isDependedOnByRemaining = (name) => [...remaining].some(
783
1498
  (other) => other !== name && dependencies.get(other).has(name)
784
1499
  );
785
- const ordered = names.includes("") ? [""] : [];
1500
+ const ordered = [];
786
1501
  while (remaining.size > 0) {
787
1502
  const sorted = [...remaining].toSorted(
788
1503
  (left, right) => left.localeCompare(right)
@@ -791,6 +1506,7 @@ const orderPackages = (names, dependsOn) => {
791
1506
  ordered.push(next);
792
1507
  remaining.delete(next);
793
1508
  }
1509
+ if (names.includes("")) ordered.push("");
794
1510
  return ordered;
795
1511
  };
796
1512
  const orderForReading = ({
@@ -1788,5 +2504,5 @@ const readImpacts = async ({
1788
2504
  };
1789
2505
  };
1790
2506
 
1791
- export { createBindingsCache, defaultDiffLimits, git, gitFailedCode, gitOrNull, groupDiffPaths, maxTypescriptFiles, noPackages, packagesAtTree, packagesOnDisk, parseNumstat, readBranchDiff, readFileAtSha, readImpacts, readPullRequestDiff, readReadingOrder, reasonLine };
2507
+ export { completeWorktreeRemoval, countProcessesInside, createBindingsCache, defaultDiffLimits, defaultRecentCommits, git, gitFailedCode, gitOrNull, groupDiffPaths, maxTypescriptFiles, noPackages, packagesAtTree, packagesOnDisk, parseNumstat, parseStatusV2, parseTrack, parseWorktreeList, prepareWorktreeRemoval, pullCheckout, pushCheckout, readBranchDiff, readBranches, readCheckoutBranchDiff, readCheckoutStatus, readCheckoutSync, readCommitDiff, readDefaultBranch, readFileAtSha, readHeadBranch, readHeadSha, readImpacts, readPullRequestDiff, readReadingOrder, readRecentCommits, readWorktreeRemovalStatus, readWorktrees, reasonLine, resolveDiffBase };
1792
2508
  //# sourceMappingURL=git-node.mjs.map