@toon-protocol/client-mcp 0.13.1 → 0.13.2

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.
@@ -367,12 +367,13 @@ var STATUS_APPLIED_KIND = 1631;
367
367
  var STATUS_CLOSED_KIND = 1632;
368
368
  var STATUS_DRAFT_KIND = 1633;
369
369
 
370
- // ../../node_modules/.pnpm/@toon-protocol+core@1.6.0_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/dist/nip34/index.js
370
+ // ../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/dist/nip34/index.js
371
371
  var REPOSITORY_ANNOUNCEMENT_KIND2 = 30617;
372
372
  var PATCH_KIND = 1617;
373
373
  var ISSUE_KIND = 1621;
374
374
 
375
- // ../rig/dist/chunk-HPSOQP7Q.js
375
+ // ../rig/dist/chunk-JBB7HBQC.js
376
+ import { decode as decodeToon } from "@toon-format/toon";
376
377
  var REPOSITORY_STATE_KIND = 30618;
377
378
  var COMMENT_KIND = 1622;
378
379
  function buildRepoAnnouncement(repoId, name, description) {
@@ -432,12 +433,15 @@ function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, b
432
433
  created_at: Math.floor(Date.now() / 1e3)
433
434
  };
434
435
  }
435
- function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "") {
436
+ function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "", description) {
436
437
  const tags = [
437
438
  ["a", `${REPOSITORY_ANNOUNCEMENT_KIND2}:${repoOwnerPubkey}:${repoId}`],
438
439
  ["p", repoOwnerPubkey],
439
440
  ["subject", title]
440
441
  ];
442
+ if (description !== void 0 && description !== "") {
443
+ tags.push(["description", description]);
444
+ }
441
445
  for (const commit of commits) {
442
446
  tags.push(["commit", commit.sha]);
443
447
  tags.push(["parent-commit", commit.parentSha]);
@@ -464,966 +468,966 @@ function buildStatus(targetEventId, statusKind, targetPubkey) {
464
468
  created_at: Math.floor(Date.now() / 1e3)
465
469
  };
466
470
  }
467
-
468
- // ../rig/dist/chunk-X2CZPPDM.js
469
- var MAX_OBJECT_SIZE = 95 * 1024;
470
-
471
- // ../rig/dist/chunk-LFGDLD6J.js
472
- import { execFile, spawn } from "child_process";
473
- import { promisify } from "util";
474
- var execFileAsync = promisify(execFile);
475
- var MAX_BUFFER = 256 * 1024 * 1024;
476
- var GitError = class extends Error {
477
- constructor(message, exitCode, stderr) {
478
- super(message);
479
- this.exitCode = exitCode;
480
- this.stderr = stderr;
481
- this.name = "GitError";
482
- }
483
- exitCode;
484
- stderr;
485
- };
486
- var FULL_SHA_RE = /^[0-9a-f]{40}$/;
487
- var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
488
- function isValidRevision(rev) {
489
- if (rev.length === 0 || rev.length > 1024) return false;
490
- if (!REV_TOKEN_RE.test(rev)) return false;
491
- if (rev.includes("..")) return false;
492
- if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
493
- return true;
471
+ var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
472
+ var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
473
+ var SHA_CACHE_MAX_SIZE = 1e4;
474
+ var shaToTxIdCache = /* @__PURE__ */ new Map();
475
+ function isValidGitSha(sha) {
476
+ return /^[0-9a-f]{40}$/i.test(sha);
494
477
  }
495
- function assertRevision(rev, what) {
496
- if (!isValidRevision(rev)) {
497
- throw new Error(
498
- `${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
499
- );
478
+ function sanitizeGraphQLValue(value) {
479
+ return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
480
+ }
481
+ function shaCacheKey(sha, repo) {
482
+ return `${sha}:${repo}`;
483
+ }
484
+ function seedShaCache(mappings) {
485
+ const entries = mappings instanceof Map ? mappings.entries() : mappings;
486
+ for (const [key2, txId] of entries) {
487
+ if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
488
+ const firstKey = shaToTxIdCache.keys().next().value;
489
+ if (firstKey !== void 0) {
490
+ shaToTxIdCache.delete(firstKey);
491
+ }
492
+ }
493
+ shaToTxIdCache.set(key2, txId);
500
494
  }
501
495
  }
502
- function assertFullSha(sha, what) {
503
- if (!FULL_SHA_RE.test(sha)) {
504
- throw new Error(
505
- `${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
506
- );
496
+ var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
497
+ function isValidArweaveTxId(txId) {
498
+ return ARWEAVE_TX_ID_RE.test(txId);
499
+ }
500
+ async function resolveGitSha(sha, repo) {
501
+ if (!isValidGitSha(sha)) {
502
+ return null;
503
+ }
504
+ const cacheKey = shaCacheKey(sha, repo);
505
+ const cached = shaToTxIdCache.get(cacheKey);
506
+ if (cached !== void 0) {
507
+ return cached;
508
+ }
509
+ const safeSha = sanitizeGraphQLValue(sha);
510
+ const safeRepo = sanitizeGraphQLValue(repo);
511
+ const query = `query {
512
+ transactions(tags: [
513
+ { name: "Git-SHA", values: ["${safeSha}"] },
514
+ { name: "Repo", values: ["${safeRepo}"] }
515
+ ]) {
516
+ edges { node { id } }
517
+ }
518
+ }`;
519
+ try {
520
+ const response = await fetch(ARWEAVE_GRAPHQL_URL, {
521
+ method: "POST",
522
+ headers: { "Content-Type": "application/json" },
523
+ body: JSON.stringify({ query }),
524
+ signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS)
525
+ });
526
+ if (!response.ok) {
527
+ return null;
528
+ }
529
+ const json = await response.json();
530
+ const edges = json.data?.transactions?.edges;
531
+ if (!edges || edges.length === 0) {
532
+ return null;
533
+ }
534
+ const txId = edges[0]?.node?.id;
535
+ if (!txId || !isValidArweaveTxId(txId)) {
536
+ return null;
537
+ }
538
+ if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
539
+ const firstKey = shaToTxIdCache.keys().next().value;
540
+ if (firstKey !== void 0) {
541
+ shaToTxIdCache.delete(firstKey);
542
+ }
543
+ }
544
+ shaToTxIdCache.set(cacheKey, txId);
545
+ return txId;
546
+ } catch {
547
+ return null;
507
548
  }
508
549
  }
509
- function assertRange(range, what) {
510
- const parts = range.split(/\.{2,3}/);
511
- const separators = range.match(/\.{2,3}/g) ?? [];
512
- const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
513
- if (!ok) {
550
+ function isValidRelayUrl(url) {
551
+ return /^wss?:\/\//i.test(url);
552
+ }
553
+ var WS_OPEN = 1;
554
+ function defaultWebSocketFactory2(url) {
555
+ const ctor = globalThis.WebSocket;
556
+ if (!ctor) {
514
557
  throw new Error(
515
- `${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
558
+ "No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
516
559
  );
517
560
  }
561
+ return new ctor(url);
518
562
  }
519
- var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
520
- var BatchParser = class {
521
- buf = Buffer.alloc(0);
522
- pending = null;
523
- objects = [];
524
- missing = [];
525
- push(chunk) {
526
- this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
527
- this.drain();
563
+ function decodeEventPayload(payload) {
564
+ if (payload !== null && typeof payload === "object") {
565
+ return payload;
528
566
  }
529
- /** True when no partially-parsed record remains. */
530
- isComplete() {
531
- return this.pending === null && this.buf.length === 0;
567
+ if (typeof payload !== "string") {
568
+ return null;
532
569
  }
533
- drain() {
534
- for (; ; ) {
535
- if (this.pending) {
536
- const needed = this.pending.size + 1;
537
- if (this.buf.length < needed) return;
538
- const body = Buffer.from(this.buf.subarray(0, this.pending.size));
539
- this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
540
- this.buf = this.buf.subarray(needed);
541
- this.pending = null;
542
- continue;
543
- }
544
- const nl = this.buf.indexOf(10);
545
- if (nl === -1) return;
546
- const header = this.buf.subarray(0, nl).toString("utf-8");
547
- this.buf = this.buf.subarray(nl + 1);
548
- const [name, second, third] = header.split(" ");
549
- if (name && second === "missing" && third === void 0) {
550
- this.missing.push(name);
551
- continue;
552
- }
553
- if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
554
- const size = Number.parseInt(third, 10);
555
- if (Number.isSafeInteger(size) && size >= 0) {
556
- this.pending = { sha: name, type: second, size };
557
- continue;
570
+ try {
571
+ const parsed = JSON.parse(payload);
572
+ if (parsed !== null && typeof parsed === "object") {
573
+ return parsed;
574
+ }
575
+ } catch {
576
+ }
577
+ try {
578
+ return decodeToon(payload);
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+ function queryRelay(relayUrl, filter, timeoutMs, webSocketFactory) {
584
+ return new Promise((resolve2, reject) => {
585
+ const events = [];
586
+ const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
587
+ let ws;
588
+ let timeoutHandle;
589
+ let settled = false;
590
+ const settle = (outcome, error) => {
591
+ if (settled) return;
592
+ settled = true;
593
+ clearTimeout(timeoutHandle);
594
+ try {
595
+ if (ws.readyState === WS_OPEN) {
596
+ ws.send(JSON.stringify(["CLOSE", subId]));
558
597
  }
598
+ ws.close();
599
+ } catch {
559
600
  }
560
- throw new GitError(
561
- `unexpected cat-file --batch header: ${JSON.stringify(header)}`,
562
- void 0,
563
- ""
601
+ if (outcome === "reject") {
602
+ reject(error);
603
+ } else {
604
+ resolve2(events);
605
+ }
606
+ };
607
+ if (!isValidRelayUrl(relayUrl)) {
608
+ reject(
609
+ new Error(
610
+ `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
611
+ )
564
612
  );
613
+ return;
565
614
  }
566
- }
567
- };
568
- var GitRepoReader = class {
569
- constructor(repoPath) {
570
- this.repoPath = repoPath;
571
- }
572
- repoPath;
573
- /** Run git with argument-array safety; resolves stdout as UTF-8. */
574
- async git(args, opts = {}) {
575
615
  try {
576
- const { stdout } = await execFileAsync("git", args, {
577
- cwd: this.repoPath,
578
- maxBuffer: MAX_BUFFER,
579
- encoding: "utf-8"
580
- });
581
- return { stdout, exitCode: 0 };
616
+ ws = webSocketFactory(relayUrl);
582
617
  } catch (err) {
583
- const e = err;
584
- const exitCode = typeof e.code === "number" ? e.code : void 0;
585
- if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
586
- return { stdout: e.stdout ?? "", exitCode };
587
- }
588
- throw new GitError(
589
- `git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
590
- exitCode,
591
- (e.stderr ?? "").trim()
592
- );
618
+ reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
619
+ return;
593
620
  }
594
- }
595
- /**
596
- * List all branches and tags plus the symbolic HEAD.
597
- *
598
- * Annotated tags report the tag object's SHA/type with the peeled target
599
- * in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
600
- */
601
- async listRefs() {
602
- const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
603
- const [refsRes, headRes] = await Promise.all([
604
- this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
605
- // Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
606
- this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
607
- ]);
608
- const refs = [];
609
- for (const line of refsRes.stdout.split("\n")) {
610
- if (!line) continue;
611
- const [refname, sha, objecttype, peeled] = line.split("\0");
612
- if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
613
- throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
621
+ timeoutHandle = setTimeout(() => {
622
+ settle("resolve");
623
+ }, timeoutMs);
624
+ ws.addEventListener("open", () => {
625
+ ws.send(JSON.stringify(["REQ", subId, filter]));
626
+ });
627
+ ws.addEventListener("message", (msgEvent) => {
628
+ try {
629
+ const msg = JSON.parse(String(msgEvent.data));
630
+ if (!Array.isArray(msg) || msg.length < 2) return;
631
+ const msgType = msg[0];
632
+ if (msgType === "EVENT" && msg[1] === subId && msg[2] !== void 0) {
633
+ const event = decodeEventPayload(msg[2]);
634
+ if (event) events.push(event);
635
+ } else if (msgType === "EOSE" && msg[1] === subId) {
636
+ settle("resolve");
637
+ }
638
+ } catch {
614
639
  }
615
- refs.push({
616
- refname,
617
- sha,
618
- type: objecttype,
619
- ...peeled ? { peeledSha: peeled } : {}
620
- });
640
+ });
641
+ ws.addEventListener("error", (event) => {
642
+ const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
643
+ settle(
644
+ "reject",
645
+ new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)
646
+ );
647
+ });
648
+ ws.addEventListener("close", () => {
649
+ settle("resolve");
650
+ });
651
+ });
652
+ }
653
+ function latestReplaceable(events) {
654
+ let winner = null;
655
+ for (const event of events) {
656
+ if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
657
+ winner = event;
621
658
  }
622
- const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
623
- return { head, refs };
624
- }
625
- /**
626
- * SHAs of every object reachable from `want` but not from `have`
627
- * (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
628
- *
629
- * Haves that don't exist locally (e.g. remote tips we never fetched) are
630
- * filtered out first via one `cat-file --batch-check` pass — rev-list
631
- * would otherwise die on them.
632
- */
633
- async objectsBetween(want, have) {
634
- const objects = await this.objectsBetweenWithPaths(want, have);
635
- return objects.map((o) => o.sha);
636
659
  }
637
- /**
638
- * Like {@link objectsBetween} but keeps the path each object was reached
639
- * by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
640
- * trees) used by push planning to report actionable oversize errors.
641
- */
642
- async objectsBetweenWithPaths(want, have) {
643
- for (const w of want) assertRevision(w, "want");
644
- for (const h of have) assertRevision(h, "have");
645
- if (want.length === 0) return [];
646
- const knownHaves = await this.filterExisting(have);
647
- const args = ["rev-list", "--objects", ...want];
648
- if (knownHaves.length > 0) args.push("--not", ...knownHaves);
649
- args.push("--");
650
- const { stdout } = await this.git(args);
651
- const objects = [];
652
- for (const line of stdout.split("\n")) {
653
- if (!line) continue;
654
- const spaceIdx = line.indexOf(" ");
655
- if (spaceIdx === -1) {
656
- objects.push({ sha: line });
657
- } else {
658
- const path = line.slice(spaceIdx + 1);
659
- objects.push({
660
- sha: line.slice(0, spaceIdx),
661
- ...path ? { path } : {}
662
- });
660
+ return winner;
661
+ }
662
+ function getTagValue(tags, name) {
663
+ const tag = tags.find((t) => t[0] === name);
664
+ return tag?.[1];
665
+ }
666
+ var MAX_REFS_PER_EVENT = 1e3;
667
+ var SYMREF_PREFIX = "ref: ";
668
+ function parseRefsEvent(event) {
669
+ const refs = /* @__PURE__ */ new Map();
670
+ const shaToTxId = /* @__PURE__ */ new Map();
671
+ let headSymref = null;
672
+ for (const tag of event.tags) {
673
+ const [tagName, v1, v2] = tag;
674
+ if (tagName === "r" && v1 && v2) {
675
+ if (v1 === "HEAD" && v2.startsWith(SYMREF_PREFIX)) {
676
+ headSymref = v2.slice(SYMREF_PREFIX.length);
677
+ continue;
663
678
  }
679
+ if (refs.size >= MAX_REFS_PER_EVENT) continue;
680
+ refs.set(v1, v2);
681
+ } else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
682
+ headSymref = v1.slice(SYMREF_PREFIX.length);
683
+ } else if (tagName === "arweave" && v1 && v2) {
684
+ shaToTxId.set(v1, v2);
664
685
  }
665
- return objects;
666
686
  }
667
- /** Run git feeding `input` on stdin; resolves collected stdout bytes. */
668
- runWithStdin(args, input) {
669
- return new Promise((resolve2, reject) => {
670
- const child = spawn("git", args, {
671
- cwd: this.repoPath,
672
- stdio: ["pipe", "pipe", "pipe"]
673
- });
674
- const out = [];
675
- let stderr = "";
676
- child.stdout.on("data", (chunk) => out.push(chunk));
677
- child.stderr.on("data", (chunk) => {
678
- stderr += chunk.toString("utf-8");
679
- });
680
- child.on("error", (err) => {
681
- reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
682
- });
683
- child.on("close", (code) => {
684
- if (code !== 0) {
685
- return reject(
686
- new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
687
- );
688
- }
689
- resolve2(Buffer.concat(out));
690
- });
691
- child.stdin.on("error", () => {
692
- });
693
- child.stdin.write(input);
694
- child.stdin.end();
695
- });
687
+ return { refs, headSymref, shaToTxId };
688
+ }
689
+ async function fetchRemoteState(options) {
690
+ const {
691
+ relayUrls,
692
+ ownerPubkey,
693
+ repoId,
694
+ timeoutMs = 1e4,
695
+ resolveSha = resolveGitSha,
696
+ webSocketFactory = defaultWebSocketFactory2
697
+ } = options;
698
+ if (relayUrls.length === 0) {
699
+ throw new Error("fetchRemoteState: relayUrls must not be empty");
696
700
  }
697
- /** Of the given revisions, keep only those resolvable locally. */
698
- async filterExisting(revs) {
699
- if (revs.length === 0) return [];
700
- const { missing } = await this.batchCheck(revs);
701
- const missingSet = new Set(missing);
702
- return revs.filter((r) => !missingSet.has(r));
701
+ if (!ownerPubkey) {
702
+ throw new Error("fetchRemoteState: ownerPubkey is required");
703
703
  }
704
- /** One `cat-file --batch-check` pass; returns names reported missing. */
705
- async batchCheck(names) {
706
- const stdout = await this.runWithStdin(
707
- ["cat-file", "--batch-check"],
708
- names.join("\n") + "\n"
709
- );
710
- const missing = [];
711
- for (const line of stdout.toString("utf-8").split("\n")) {
712
- if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
704
+ if (!repoId) {
705
+ throw new Error("fetchRemoteState: repoId is required");
706
+ }
707
+ const filter = {
708
+ kinds: [REPOSITORY_ANNOUNCEMENT_KIND2, REPOSITORY_STATE_KIND],
709
+ authors: [ownerPubkey],
710
+ "#d": [repoId]
711
+ };
712
+ const results = await Promise.allSettled(
713
+ relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
714
+ );
715
+ const failures = [];
716
+ const byId = /* @__PURE__ */ new Map();
717
+ for (const result of results) {
718
+ if (result.status === "rejected") {
719
+ failures.push(String(result.reason?.message ?? result.reason));
720
+ continue;
721
+ }
722
+ for (const event of result.value) {
723
+ if (event.pubkey !== ownerPubkey) continue;
724
+ if (getTagValue(event.tags, "d") !== repoId) continue;
725
+ if (typeof event.id === "string" && !byId.has(event.id)) {
726
+ byId.set(event.id, event);
727
+ }
713
728
  }
714
- return { missing };
715
729
  }
716
- /**
717
- * Object metadata (type + body size) for a batch of SHAs via one
718
- * `cat-file --batch-check` pass no bodies are read. Missing objects are
719
- * reported, not thrown.
720
- */
721
- async statObjects(shas) {
722
- for (const sha of shas) assertFullSha(sha, "sha");
723
- if (shas.length === 0) return { objects: [], missing: [] };
724
- const stdout = await this.runWithStdin(
725
- ["cat-file", "--batch-check"],
726
- shas.join("\n") + "\n"
730
+ if (failures.length === relayUrls.length) {
731
+ throw new Error(
732
+ `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join("; ")}`
727
733
  );
728
- const objects = [];
729
- const missing = [];
730
- for (const line of stdout.toString("utf-8").split("\n")) {
731
- if (!line) continue;
732
- if (line.endsWith(" missing")) {
733
- missing.push(line.slice(0, -" missing".length));
734
- continue;
735
- }
736
- const [sha, type, sizeStr] = line.split(" ");
737
- const size = Number.parseInt(sizeStr ?? "", 10);
738
- if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
739
- throw new GitError(
740
- `unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
741
- void 0,
742
- ""
743
- );
734
+ }
735
+ const events = [...byId.values()];
736
+ const refsEvent = latestReplaceable(
737
+ events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
738
+ );
739
+ const announceEvent = latestReplaceable(
740
+ events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
741
+ );
742
+ const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
743
+ refs: /* @__PURE__ */ new Map(),
744
+ headSymref: null,
745
+ shaToTxId: /* @__PURE__ */ new Map()
746
+ };
747
+ if (shaToTxId.size > 0) {
748
+ seedShaCache(
749
+ [...shaToTxId].map(
750
+ ([sha, txId]) => [shaCacheKey(sha, repoId), txId]
751
+ )
752
+ );
753
+ }
754
+ const name = announceEvent ? getTagValue(announceEvent.tags, "name") ?? null : null;
755
+ const description = announceEvent ? getTagValue(announceEvent.tags, "description") ?? announceEvent.content : null;
756
+ const relays = [];
757
+ if (announceEvent) {
758
+ for (const tag of announceEvent.tags) {
759
+ if (tag[0] === "relays") {
760
+ relays.push(...tag.slice(1).filter((url) => url.length > 0));
744
761
  }
745
- objects.push({ sha, type, size });
746
762
  }
747
- return { objects, missing };
748
763
  }
749
- /**
750
- * Read raw object bodies via a single streaming `git cat-file --batch`
751
- * child process. Bodies may be binary and are parsed size-driven across
752
- * chunk boundaries. Missing objects are reported, not thrown.
753
- */
754
- async readObjects(shas) {
755
- for (const sha of shas) assertFullSha(sha, "sha");
756
- if (shas.length === 0) return { objects: [], missing: [] };
757
- const parser = new BatchParser();
758
- await new Promise((resolve2, reject) => {
759
- const child = spawn("git", ["cat-file", "--batch"], {
760
- cwd: this.repoPath,
761
- stdio: ["pipe", "pipe", "pipe"]
762
- });
763
- let stderr = "";
764
- let parseError = null;
765
- child.stderr.on("data", (chunk) => {
766
- stderr += chunk.toString("utf-8");
767
- });
768
- child.stdout.on("data", (chunk) => {
769
- if (parseError) return;
770
- try {
771
- parser.push(chunk);
772
- } catch (err) {
773
- parseError = err;
774
- child.kill();
775
- }
776
- });
777
- child.on("error", (err) => {
778
- reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
779
- });
780
- child.on("close", (code) => {
781
- if (parseError) return reject(parseError);
782
- if (code !== 0) {
783
- return reject(
784
- new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
785
- );
786
- }
787
- if (!parser.isComplete()) {
788
- return reject(
789
- new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
790
- );
791
- }
792
- resolve2();
793
- });
794
- child.stdin.on("error", () => {
795
- });
796
- child.stdin.write(shas.join("\n") + "\n");
797
- child.stdin.end();
798
- });
799
- return { objects: parser.objects, missing: parser.missing };
764
+ const resolveMissing = async (shas) => {
765
+ const resolved = /* @__PURE__ */ new Map();
766
+ const missing = [];
767
+ for (const sha of new Set(shas)) {
768
+ const known = shaToTxId.get(sha);
769
+ if (known !== void 0) {
770
+ resolved.set(sha, known);
771
+ } else {
772
+ missing.push(sha);
773
+ }
774
+ }
775
+ const lookups = await Promise.all(
776
+ missing.map(
777
+ async (sha) => [sha, await resolveSha(sha, repoId)]
778
+ )
779
+ );
780
+ for (const [sha, txId] of lookups) {
781
+ if (txId) resolved.set(sha, txId);
782
+ }
783
+ return resolved;
784
+ };
785
+ return {
786
+ announced: announceEvent !== null,
787
+ refs,
788
+ headSymref,
789
+ shaToTxId,
790
+ refsEvent,
791
+ announceEvent,
792
+ name,
793
+ description,
794
+ relays,
795
+ resolveMissing
796
+ };
797
+ }
798
+
799
+ // ../rig/dist/chunk-X2CZPPDM.js
800
+ var MAX_OBJECT_SIZE = 95 * 1024;
801
+
802
+ // ../rig/dist/chunk-PS5QOT62.js
803
+ import { execFile, spawn } from "child_process";
804
+ import { promisify } from "util";
805
+ import { execFile as execFile2, spawn as spawn2 } from "child_process";
806
+ import { promisify as promisify2 } from "util";
807
+ var execFileAsync = promisify(execFile);
808
+ var MAX_BUFFER = 256 * 1024 * 1024;
809
+ var GitError = class extends Error {
810
+ constructor(message, exitCode, stderr) {
811
+ super(message);
812
+ this.exitCode = exitCode;
813
+ this.stderr = stderr;
814
+ this.name = "GitError";
800
815
  }
801
- /**
802
- * `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
803
- * of `b` (fast-forward check / force detection). Exit codes other than
804
- * 0/1 (e.g. unknown revisions) throw.
805
- */
806
- async isAncestor(a, b) {
807
- assertRevision(a, "ancestor candidate");
808
- assertRevision(b, "descendant candidate");
809
- const { exitCode } = await this.git(
810
- ["merge-base", "--is-ancestor", a, b],
811
- { allowExitCodes: [1] }
816
+ exitCode;
817
+ stderr;
818
+ };
819
+ var FULL_SHA_RE = /^[0-9a-f]{40}$/;
820
+ var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
821
+ function isValidRevision(rev) {
822
+ if (rev.length === 0 || rev.length > 1024) return false;
823
+ if (!REV_TOKEN_RE.test(rev)) return false;
824
+ if (rev.includes("..")) return false;
825
+ if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
826
+ return true;
827
+ }
828
+ function assertRevision(rev, what) {
829
+ if (!isValidRevision(rev)) {
830
+ throw new Error(
831
+ `${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
812
832
  );
813
- return exitCode === 0;
814
833
  }
815
- /**
816
- * `git format-patch --stdout <range>` — the full mbox-formatted patch
817
- * series text (empty string when the range selects no commits).
818
- */
819
- async formatPatch(range) {
820
- assertRange(range, "range");
821
- const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
822
- return stdout;
834
+ }
835
+ function assertFullSha(sha, what) {
836
+ if (!FULL_SHA_RE.test(sha)) {
837
+ throw new Error(
838
+ `${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
839
+ );
823
840
  }
824
- /**
825
- * Parent SHAs for a batch of commit SHAs via one
826
- * `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
827
- * empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
828
- * pairs for exactly the commits a format-patch series carries.
829
- */
830
- async commitParents(shas) {
831
- for (const sha of shas) assertFullSha(sha, "sha");
832
- if (shas.length === 0) return /* @__PURE__ */ new Map();
833
- const { stdout } = await this.git([
834
- "rev-list",
835
- "--no-walk=unsorted",
836
- "--parents",
837
- ...shas,
838
- "--"
839
- ]);
840
- const parents = /* @__PURE__ */ new Map();
841
- for (const line of stdout.split("\n")) {
842
- if (!line) continue;
843
- const [sha, ...rest] = line.split(" ");
844
- if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
845
- throw new GitError(
846
- `unexpected rev-list --parents line: ${JSON.stringify(line)}`,
847
- void 0,
848
- ""
849
- );
850
- }
851
- parents.set(sha, rest);
852
- }
853
- return parents;
841
+ }
842
+ function assertRange(range, what) {
843
+ const parts = range.split(/\.{2,3}/);
844
+ const separators = range.match(/\.{2,3}/g) ?? [];
845
+ const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
846
+ if (!ok) {
847
+ throw new Error(
848
+ `${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
849
+ );
854
850
  }
855
- /**
856
- * Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
857
- * Throws {@link GitError} when the name doesn't resolve.
858
- */
859
- async resolveRef(name) {
860
- assertRevision(name, "ref name");
861
- const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
862
- const sha = stdout.trim();
863
- if (!FULL_SHA_RE.test(sha)) {
851
+ }
852
+ var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
853
+ var BatchParser = class {
854
+ buf = Buffer.alloc(0);
855
+ pending = null;
856
+ objects = [];
857
+ missing = [];
858
+ push(chunk) {
859
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
860
+ this.drain();
861
+ }
862
+ /** True when no partially-parsed record remains. */
863
+ isComplete() {
864
+ return this.pending === null && this.buf.length === 0;
865
+ }
866
+ drain() {
867
+ for (; ; ) {
868
+ if (this.pending) {
869
+ const needed = this.pending.size + 1;
870
+ if (this.buf.length < needed) return;
871
+ const body = Buffer.from(this.buf.subarray(0, this.pending.size));
872
+ this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
873
+ this.buf = this.buf.subarray(needed);
874
+ this.pending = null;
875
+ continue;
876
+ }
877
+ const nl = this.buf.indexOf(10);
878
+ if (nl === -1) return;
879
+ const header = this.buf.subarray(0, nl).toString("utf-8");
880
+ this.buf = this.buf.subarray(nl + 1);
881
+ const [name, second, third] = header.split(" ");
882
+ if (name && second === "missing" && third === void 0) {
883
+ this.missing.push(name);
884
+ continue;
885
+ }
886
+ if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
887
+ const size = Number.parseInt(third, 10);
888
+ if (Number.isSafeInteger(size) && size >= 0) {
889
+ this.pending = { sha: name, type: second, size };
890
+ continue;
891
+ }
892
+ }
864
893
  throw new GitError(
865
- `rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
894
+ `unexpected cat-file --batch header: ${JSON.stringify(header)}`,
866
895
  void 0,
867
896
  ""
868
897
  );
869
898
  }
870
- return sha;
871
- }
872
- };
873
- var NonFastForwardError = class extends Error {
874
- constructor(refs) {
875
- super(
876
- `non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
877
- );
878
- this.refs = refs;
879
- this.name = "NonFastForwardError";
880
899
  }
881
- refs;
882
900
  };
883
- var OversizeObjectsError = class extends Error {
884
- constructor(objects) {
885
- super(
886
- `${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
887
- );
888
- this.objects = objects;
889
- this.name = "OversizeObjectsError";
901
+ var GitRepoReader = class {
902
+ constructor(repoPath) {
903
+ this.repoPath = repoPath;
890
904
  }
891
- objects;
892
- };
893
- async function planPush(options) {
894
- const { repoReader, remoteState, feeRates, repoId, force = false } = options;
895
- const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
896
- const { head, refs: localRefs } = await repoReader.listRefs();
897
- const localByName = new Map(localRefs.map((r) => [r.refname, r]));
898
- let selected;
899
- if (options.refs !== void 0) {
900
- selected = options.refs.map((name) => {
901
- const ref = localByName.get(name);
902
- if (!ref) {
903
- throw new Error(
904
- `ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
905
- );
906
- }
907
- return ref;
908
- });
909
- } else {
910
- selected = localRefs;
911
- }
912
- const refUpdates = [];
913
- const rejected = [];
914
- for (const ref of selected) {
915
- const remoteSha = remoteState.refs.get(ref.refname) ?? null;
916
- if (remoteSha === null) {
917
- refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
918
- continue;
919
- }
920
- if (remoteSha === ref.sha) {
921
- refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
922
- continue;
923
- }
924
- let fastForward = false;
905
+ repoPath;
906
+ /** Run git with argument-array safety; resolves stdout as UTF-8. */
907
+ async git(args, opts = {}) {
925
908
  try {
926
- fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
927
- } catch (err) {
928
- if (!(err instanceof GitError)) throw err;
929
- fastForward = false;
930
- }
931
- if (fastForward) {
932
- refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
933
- } else if (force) {
934
- refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
935
- } else {
936
- rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
937
- }
938
- }
939
- if (rejected.length > 0) throw new NonFastForwardError(rejected);
940
- const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
941
- const wantTips = [...new Set(updates.map((u) => u.localSha))];
942
- const haveTips = [...new Set(remoteState.refs.values())];
943
- const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
944
- const knownShaToTxId = new Map(remoteState.shaToTxId);
945
- let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
946
- if (candidates.length > 0) {
947
- const resolved = await resolveMissing(candidates.map((o) => o.sha));
948
- for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
949
- candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
950
- }
951
- const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
952
- const { objects: stats, missing } = await repoReader.statObjects(
953
- candidates.map((c) => c.sha)
954
- );
955
- if (missing.length > 0) {
956
- throw new Error(
957
- `objects vanished from the local repository during planning: ${missing.join(", ")}`
958
- );
959
- }
960
- const oversize = [];
961
- for (const stat2 of stats) {
962
- if (stat2.size > MAX_OBJECT_SIZE) {
963
- const path = pathBySha.get(stat2.sha);
964
- oversize.push({ ...stat2, ...path ? { path } : {} });
965
- }
966
- }
967
- if (oversize.length > 0) throw new OversizeObjectsError(oversize);
968
- const tipShas = new Set(updates.map((u) => u.localSha));
969
- const planned = stats.map((stat2) => {
970
- const path = pathBySha.get(stat2.sha);
971
- return { ...stat2, ...path ? { path } : {}, isRefTip: tipShas.has(stat2.sha) };
972
- });
973
- const objects = [
974
- ...planned.filter((o) => !o.isRefTip),
975
- ...planned.filter((o) => o.isRefTip)
976
- ];
977
- const newRefsMap = new Map(remoteState.refs);
978
- for (const update of updates) newRefsMap.set(update.refname, update.localSha);
979
- const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
980
- const newRefs = {};
981
- const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
982
- if (headSymref && headSha) newRefs[headSymref] = headSha;
983
- for (const [refname, sha] of newRefsMap) {
984
- if (refname !== headSymref) newRefs[refname] = sha;
985
- }
986
- const announceNeeded = !remoteState.announced;
987
- const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
988
- const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
989
- const eventCount = 1 + (announceNeeded ? 1 : 0);
990
- const eventFees = BigInt(eventCount) * feeRates.eventFee;
991
- return {
992
- repoId,
993
- refUpdates,
994
- newRefs,
995
- headSymref,
996
- objects,
997
- knownShaToTxId,
998
- announceNeeded,
999
- announcement: {
1000
- name: options.announcement?.name ?? repoId,
1001
- description: options.announcement?.description ?? ""
1002
- },
1003
- estimate: {
1004
- objectCount: objects.length,
1005
- totalObjectBytes,
1006
- uploadFee,
1007
- eventCount,
1008
- eventFees,
1009
- totalFee: uploadFee + eventFees
1010
- }
1011
- };
1012
- }
1013
- var READ_BATCH_SIZE = 100;
1014
- async function executePush(options) {
1015
- const { plan, publisher, remoteState, repoReader, relayUrls } = options;
1016
- const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
1017
- const resultBySha = /* @__PURE__ */ new Map();
1018
- let totalFeePaid = 0n;
1019
- const pending = [];
1020
- for (const object of plan.objects) {
1021
- const knownTxId = merged.get(object.sha);
1022
- if (knownTxId !== void 0) {
1023
- resultBySha.set(object.sha, {
1024
- sha: object.sha,
1025
- txId: knownTxId,
1026
- feePaid: 0n,
1027
- skipped: true
909
+ const { stdout } = await execFileAsync("git", args, {
910
+ cwd: this.repoPath,
911
+ maxBuffer: MAX_BUFFER,
912
+ encoding: "utf-8"
1028
913
  });
1029
- } else {
1030
- pending.push(object);
1031
- }
1032
- }
1033
- for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
1034
- const batch = pending.slice(i, i + READ_BATCH_SIZE);
1035
- const { objects: read, missing } = await repoReader.readObjects(
1036
- batch.map((o) => o.sha)
1037
- );
1038
- if (missing.length > 0) {
1039
- throw new Error(
1040
- `objects vanished from the local repository during push: ${missing.join(", ")}`
914
+ return { stdout, exitCode: 0 };
915
+ } catch (err) {
916
+ const e = err;
917
+ const exitCode = typeof e.code === "number" ? e.code : void 0;
918
+ if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
919
+ return { stdout: e.stdout ?? "", exitCode };
920
+ }
921
+ throw new GitError(
922
+ `git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
923
+ exitCode,
924
+ (e.stderr ?? "").trim()
1041
925
  );
1042
926
  }
1043
- const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
1044
- for (const object of batch) {
1045
- const body = bodyBySha.get(object.sha);
1046
- if (!body) {
1047
- throw new Error(
1048
- `internal: cat-file returned no body for ${object.sha}`
1049
- );
927
+ }
928
+ /**
929
+ * List all branches and tags plus the symbolic HEAD.
930
+ *
931
+ * Annotated tags report the tag object's SHA/type with the peeled target
932
+ * in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
933
+ */
934
+ async listRefs() {
935
+ const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
936
+ const [refsRes, headRes] = await Promise.all([
937
+ this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
938
+ // Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
939
+ this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
940
+ ]);
941
+ const refs = [];
942
+ for (const line of refsRes.stdout.split("\n")) {
943
+ if (!line) continue;
944
+ const [refname, sha, objecttype, peeled] = line.split("\0");
945
+ if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
946
+ throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
1050
947
  }
1051
- const receipt = await publisher.uploadGitObject({
1052
- sha: object.sha,
1053
- type: object.type,
1054
- body,
1055
- repoId: plan.repoId
1056
- });
1057
- merged.set(object.sha, receipt.txId);
1058
- totalFeePaid += receipt.feePaid;
1059
- resultBySha.set(object.sha, {
1060
- sha: object.sha,
1061
- txId: receipt.txId,
1062
- feePaid: receipt.feePaid,
1063
- skipped: false
948
+ refs.push({
949
+ refname,
950
+ sha,
951
+ type: objecttype,
952
+ ...peeled ? { peeledSha: peeled } : {}
1064
953
  });
1065
954
  }
955
+ const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
956
+ return { head, refs };
1066
957
  }
1067
- let announceReceipt = null;
1068
- if (plan.announceNeeded && !remoteState.announced) {
1069
- const announceEvent = buildRepoAnnouncement(
1070
- plan.repoId,
1071
- plan.announcement.name,
1072
- plan.announcement.description
1073
- );
1074
- announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
1075
- totalFeePaid += announceReceipt.feePaid;
958
+ /**
959
+ * SHAs of every object reachable from `want` but not from `have`
960
+ * (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
961
+ *
962
+ * Haves that don't exist locally (e.g. remote tips we never fetched) are
963
+ * filtered out first via one `cat-file --batch-check` pass — rev-list
964
+ * would otherwise die on them.
965
+ */
966
+ async objectsBetween(want, have) {
967
+ const objects = await this.objectsBetweenWithPaths(want, have);
968
+ return objects.map((o) => o.sha);
1076
969
  }
1077
- const refsEvent = buildRepoRefs(
1078
- plan.repoId,
1079
- plan.newRefs,
1080
- Object.fromEntries(merged)
1081
- );
1082
- const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
1083
- totalFeePaid += refsReceipt.feePaid;
1084
- const uploads = plan.objects.map((o) => {
1085
- const step = resultBySha.get(o.sha);
1086
- if (!step) {
1087
- throw new Error(`internal: no upload result recorded for ${o.sha}`);
1088
- }
1089
- return step;
1090
- });
1091
- return {
1092
- uploads,
1093
- announceReceipt,
1094
- refsReceipt,
1095
- arweaveMap: merged,
1096
- totalFeePaid
1097
- };
1098
- }
1099
-
1100
- // ../rig/dist/chunk-G4W4MH6G.js
1101
- import { decode as decodeToon } from "@toon-format/toon";
1102
- var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
1103
- var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
1104
- var SHA_CACHE_MAX_SIZE = 1e4;
1105
- var shaToTxIdCache = /* @__PURE__ */ new Map();
1106
- function isValidGitSha(sha) {
1107
- return /^[0-9a-f]{40}$/i.test(sha);
1108
- }
1109
- function sanitizeGraphQLValue(value) {
1110
- return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
1111
- }
1112
- function shaCacheKey(sha, repo) {
1113
- return `${sha}:${repo}`;
1114
- }
1115
- function seedShaCache(mappings) {
1116
- const entries = mappings instanceof Map ? mappings.entries() : mappings;
1117
- for (const [key2, txId] of entries) {
1118
- if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
1119
- const firstKey = shaToTxIdCache.keys().next().value;
1120
- if (firstKey !== void 0) {
1121
- shaToTxIdCache.delete(firstKey);
970
+ /**
971
+ * Like {@link objectsBetween} but keeps the path each object was reached
972
+ * by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
973
+ * trees) — used by push planning to report actionable oversize errors.
974
+ */
975
+ async objectsBetweenWithPaths(want, have) {
976
+ for (const w of want) assertRevision(w, "want");
977
+ for (const h of have) assertRevision(h, "have");
978
+ if (want.length === 0) return [];
979
+ const knownHaves = await this.filterExisting(have);
980
+ const args = ["rev-list", "--objects", ...want];
981
+ if (knownHaves.length > 0) args.push("--not", ...knownHaves);
982
+ args.push("--");
983
+ const { stdout } = await this.git(args);
984
+ const objects = [];
985
+ for (const line of stdout.split("\n")) {
986
+ if (!line) continue;
987
+ const spaceIdx = line.indexOf(" ");
988
+ if (spaceIdx === -1) {
989
+ objects.push({ sha: line });
990
+ } else {
991
+ const path = line.slice(spaceIdx + 1);
992
+ objects.push({
993
+ sha: line.slice(0, spaceIdx),
994
+ ...path ? { path } : {}
995
+ });
1122
996
  }
1123
997
  }
1124
- shaToTxIdCache.set(key2, txId);
1125
- }
1126
- }
1127
- var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
1128
- function isValidArweaveTxId(txId) {
1129
- return ARWEAVE_TX_ID_RE.test(txId);
1130
- }
1131
- async function resolveGitSha(sha, repo) {
1132
- if (!isValidGitSha(sha)) {
1133
- return null;
1134
- }
1135
- const cacheKey = shaCacheKey(sha, repo);
1136
- const cached = shaToTxIdCache.get(cacheKey);
1137
- if (cached !== void 0) {
1138
- return cached;
1139
- }
1140
- const safeSha = sanitizeGraphQLValue(sha);
1141
- const safeRepo = sanitizeGraphQLValue(repo);
1142
- const query = `query {
1143
- transactions(tags: [
1144
- { name: "Git-SHA", values: ["${safeSha}"] },
1145
- { name: "Repo", values: ["${safeRepo}"] }
1146
- ]) {
1147
- edges { node { id } }
998
+ return objects;
1148
999
  }
1149
- }`;
1150
- try {
1151
- const response = await fetch(ARWEAVE_GRAPHQL_URL, {
1152
- method: "POST",
1153
- headers: { "Content-Type": "application/json" },
1154
- body: JSON.stringify({ query }),
1155
- signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS)
1000
+ /** Run git feeding `input` on stdin; resolves collected stdout bytes. */
1001
+ runWithStdin(args, input) {
1002
+ return new Promise((resolve2, reject) => {
1003
+ const child = spawn("git", args, {
1004
+ cwd: this.repoPath,
1005
+ stdio: ["pipe", "pipe", "pipe"]
1006
+ });
1007
+ const out = [];
1008
+ let stderr = "";
1009
+ child.stdout.on("data", (chunk) => out.push(chunk));
1010
+ child.stderr.on("data", (chunk) => {
1011
+ stderr += chunk.toString("utf-8");
1012
+ });
1013
+ child.on("error", (err) => {
1014
+ reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
1015
+ });
1016
+ child.on("close", (code) => {
1017
+ if (code !== 0) {
1018
+ return reject(
1019
+ new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
1020
+ );
1021
+ }
1022
+ resolve2(Buffer.concat(out));
1023
+ });
1024
+ child.stdin.on("error", () => {
1025
+ });
1026
+ child.stdin.write(input);
1027
+ child.stdin.end();
1156
1028
  });
1157
- if (!response.ok) {
1158
- return null;
1159
- }
1160
- const json = await response.json();
1161
- const edges = json.data?.transactions?.edges;
1162
- if (!edges || edges.length === 0) {
1163
- return null;
1164
- }
1165
- const txId = edges[0]?.node?.id;
1166
- if (!txId || !isValidArweaveTxId(txId)) {
1167
- return null;
1168
- }
1169
- if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
1170
- const firstKey = shaToTxIdCache.keys().next().value;
1171
- if (firstKey !== void 0) {
1172
- shaToTxIdCache.delete(firstKey);
1173
- }
1174
- }
1175
- shaToTxIdCache.set(cacheKey, txId);
1176
- return txId;
1177
- } catch {
1178
- return null;
1179
- }
1180
- }
1181
- function isValidRelayUrl(url) {
1182
- return /^wss?:\/\//i.test(url);
1183
- }
1184
- var WS_OPEN = 1;
1185
- function defaultWebSocketFactory2(url) {
1186
- const ctor = globalThis.WebSocket;
1187
- if (!ctor) {
1188
- throw new Error(
1189
- "No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
1190
- );
1191
- }
1192
- return new ctor(url);
1193
- }
1194
- function decodeEventPayload(payload) {
1195
- if (payload !== null && typeof payload === "object") {
1196
- return payload;
1197
1029
  }
1198
- if (typeof payload !== "string") {
1199
- return null;
1030
+ /** Of the given revisions, keep only those resolvable locally. */
1031
+ async filterExisting(revs) {
1032
+ if (revs.length === 0) return [];
1033
+ const { missing } = await this.batchCheck(revs);
1034
+ const missingSet = new Set(missing);
1035
+ return revs.filter((r) => !missingSet.has(r));
1200
1036
  }
1201
- try {
1202
- const parsed = JSON.parse(payload);
1203
- if (parsed !== null && typeof parsed === "object") {
1204
- return parsed;
1037
+ /** One `cat-file --batch-check` pass; returns names reported missing. */
1038
+ async batchCheck(names) {
1039
+ const stdout = await this.runWithStdin(
1040
+ ["cat-file", "--batch-check"],
1041
+ names.join("\n") + "\n"
1042
+ );
1043
+ const missing = [];
1044
+ for (const line of stdout.toString("utf-8").split("\n")) {
1045
+ if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
1205
1046
  }
1206
- } catch {
1207
- }
1208
- try {
1209
- return decodeToon(payload);
1210
- } catch {
1211
- return null;
1047
+ return { missing };
1212
1048
  }
1213
- }
1214
- function queryRelay(relayUrl, filter, timeoutMs, webSocketFactory) {
1215
- return new Promise((resolve2, reject) => {
1216
- const events = [];
1217
- const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1218
- let ws;
1219
- let timeoutHandle;
1220
- let settled = false;
1221
- const settle = (outcome, error) => {
1222
- if (settled) return;
1223
- settled = true;
1224
- clearTimeout(timeoutHandle);
1225
- try {
1226
- if (ws.readyState === WS_OPEN) {
1227
- ws.send(JSON.stringify(["CLOSE", subId]));
1228
- }
1229
- ws.close();
1230
- } catch {
1049
+ /**
1050
+ * Object metadata (type + body size) for a batch of SHAs via one
1051
+ * `cat-file --batch-check` pass no bodies are read. Missing objects are
1052
+ * reported, not thrown.
1053
+ */
1054
+ async statObjects(shas) {
1055
+ for (const sha of shas) assertFullSha(sha, "sha");
1056
+ if (shas.length === 0) return { objects: [], missing: [] };
1057
+ const stdout = await this.runWithStdin(
1058
+ ["cat-file", "--batch-check"],
1059
+ shas.join("\n") + "\n"
1060
+ );
1061
+ const objects = [];
1062
+ const missing = [];
1063
+ for (const line of stdout.toString("utf-8").split("\n")) {
1064
+ if (!line) continue;
1065
+ if (line.endsWith(" missing")) {
1066
+ missing.push(line.slice(0, -" missing".length));
1067
+ continue;
1231
1068
  }
1232
- if (outcome === "reject") {
1233
- reject(error);
1234
- } else {
1235
- resolve2(events);
1069
+ const [sha, type, sizeStr] = line.split(" ");
1070
+ const size = Number.parseInt(sizeStr ?? "", 10);
1071
+ if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
1072
+ throw new GitError(
1073
+ `unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
1074
+ void 0,
1075
+ ""
1076
+ );
1236
1077
  }
1237
- };
1238
- if (!isValidRelayUrl(relayUrl)) {
1239
- reject(
1240
- new Error(
1241
- `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
1242
- )
1243
- );
1244
- return;
1245
- }
1246
- try {
1247
- ws = webSocketFactory(relayUrl);
1248
- } catch (err) {
1249
- reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
1250
- return;
1078
+ objects.push({ sha, type, size });
1251
1079
  }
1252
- timeoutHandle = setTimeout(() => {
1253
- settle("resolve");
1254
- }, timeoutMs);
1255
- ws.addEventListener("open", () => {
1256
- ws.send(JSON.stringify(["REQ", subId, filter]));
1257
- });
1258
- ws.addEventListener("message", (msgEvent) => {
1259
- try {
1260
- const msg = JSON.parse(String(msgEvent.data));
1261
- if (!Array.isArray(msg) || msg.length < 2) return;
1262
- const msgType = msg[0];
1263
- if (msgType === "EVENT" && msg[1] === subId && msg[2] !== void 0) {
1264
- const event = decodeEventPayload(msg[2]);
1265
- if (event) events.push(event);
1266
- } else if (msgType === "EOSE" && msg[1] === subId) {
1267
- settle("resolve");
1080
+ return { objects, missing };
1081
+ }
1082
+ /**
1083
+ * Read raw object bodies via a single streaming `git cat-file --batch`
1084
+ * child process. Bodies may be binary and are parsed size-driven across
1085
+ * chunk boundaries. Missing objects are reported, not thrown.
1086
+ */
1087
+ async readObjects(shas) {
1088
+ for (const sha of shas) assertFullSha(sha, "sha");
1089
+ if (shas.length === 0) return { objects: [], missing: [] };
1090
+ const parser = new BatchParser();
1091
+ await new Promise((resolve2, reject) => {
1092
+ const child = spawn("git", ["cat-file", "--batch"], {
1093
+ cwd: this.repoPath,
1094
+ stdio: ["pipe", "pipe", "pipe"]
1095
+ });
1096
+ let stderr = "";
1097
+ let parseError = null;
1098
+ child.stderr.on("data", (chunk) => {
1099
+ stderr += chunk.toString("utf-8");
1100
+ });
1101
+ child.stdout.on("data", (chunk) => {
1102
+ if (parseError) return;
1103
+ try {
1104
+ parser.push(chunk);
1105
+ } catch (err) {
1106
+ parseError = err;
1107
+ child.kill();
1268
1108
  }
1269
- } catch {
1270
- }
1271
- });
1272
- ws.addEventListener("error", (event) => {
1273
- const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
1274
- settle(
1275
- "reject",
1276
- new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)
1277
- );
1278
- });
1279
- ws.addEventListener("close", () => {
1280
- settle("resolve");
1109
+ });
1110
+ child.on("error", (err) => {
1111
+ reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
1112
+ });
1113
+ child.on("close", (code) => {
1114
+ if (parseError) return reject(parseError);
1115
+ if (code !== 0) {
1116
+ return reject(
1117
+ new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
1118
+ );
1119
+ }
1120
+ if (!parser.isComplete()) {
1121
+ return reject(
1122
+ new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
1123
+ );
1124
+ }
1125
+ resolve2();
1126
+ });
1127
+ child.stdin.on("error", () => {
1128
+ });
1129
+ child.stdin.write(shas.join("\n") + "\n");
1130
+ child.stdin.end();
1281
1131
  });
1282
- });
1283
- }
1284
- function latestReplaceable(events) {
1285
- let winner = null;
1286
- for (const event of events) {
1287
- if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
1288
- winner = event;
1289
- }
1132
+ return { objects: parser.objects, missing: parser.missing };
1290
1133
  }
1291
- return winner;
1292
- }
1293
- function getTagValue(tags, name) {
1294
- const tag = tags.find((t) => t[0] === name);
1295
- return tag?.[1];
1296
- }
1297
- var MAX_REFS_PER_EVENT = 1e3;
1298
- var SYMREF_PREFIX = "ref: ";
1299
- function parseRefsEvent(event) {
1300
- const refs = /* @__PURE__ */ new Map();
1301
- const shaToTxId = /* @__PURE__ */ new Map();
1302
- let headSymref = null;
1303
- for (const tag of event.tags) {
1304
- const [tagName, v1, v2] = tag;
1305
- if (tagName === "r" && v1 && v2) {
1306
- if (v1 === "HEAD" && v2.startsWith(SYMREF_PREFIX)) {
1307
- headSymref = v2.slice(SYMREF_PREFIX.length);
1308
- continue;
1134
+ /**
1135
+ * `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
1136
+ * of `b` (fast-forward check / force detection). Exit codes other than
1137
+ * 0/1 (e.g. unknown revisions) throw.
1138
+ */
1139
+ async isAncestor(a, b) {
1140
+ assertRevision(a, "ancestor candidate");
1141
+ assertRevision(b, "descendant candidate");
1142
+ const { exitCode } = await this.git(
1143
+ ["merge-base", "--is-ancestor", a, b],
1144
+ { allowExitCodes: [1] }
1145
+ );
1146
+ return exitCode === 0;
1147
+ }
1148
+ /**
1149
+ * `git format-patch --stdout <range>` the full mbox-formatted patch
1150
+ * series text (empty string when the range selects no commits).
1151
+ */
1152
+ async formatPatch(range) {
1153
+ assertRange(range, "range");
1154
+ const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
1155
+ return stdout;
1156
+ }
1157
+ /**
1158
+ * Parent SHAs for a batch of commit SHAs via one
1159
+ * `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
1160
+ * empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
1161
+ * pairs for exactly the commits a format-patch series carries.
1162
+ */
1163
+ async commitParents(shas) {
1164
+ for (const sha of shas) assertFullSha(sha, "sha");
1165
+ if (shas.length === 0) return /* @__PURE__ */ new Map();
1166
+ const { stdout } = await this.git([
1167
+ "rev-list",
1168
+ "--no-walk=unsorted",
1169
+ "--parents",
1170
+ ...shas,
1171
+ "--"
1172
+ ]);
1173
+ const parents = /* @__PURE__ */ new Map();
1174
+ for (const line of stdout.split("\n")) {
1175
+ if (!line) continue;
1176
+ const [sha, ...rest] = line.split(" ");
1177
+ if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
1178
+ throw new GitError(
1179
+ `unexpected rev-list --parents line: ${JSON.stringify(line)}`,
1180
+ void 0,
1181
+ ""
1182
+ );
1309
1183
  }
1310
- if (refs.size >= MAX_REFS_PER_EVENT) continue;
1311
- refs.set(v1, v2);
1312
- } else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
1313
- headSymref = v1.slice(SYMREF_PREFIX.length);
1314
- } else if (tagName === "arweave" && v1 && v2) {
1315
- shaToTxId.set(v1, v2);
1184
+ parents.set(sha, rest);
1316
1185
  }
1186
+ return parents;
1317
1187
  }
1318
- return { refs, headSymref, shaToTxId };
1319
- }
1320
- async function fetchRemoteState(options) {
1321
- const {
1322
- relayUrls,
1323
- ownerPubkey,
1324
- repoId,
1325
- timeoutMs = 1e4,
1326
- resolveSha = resolveGitSha,
1327
- webSocketFactory = defaultWebSocketFactory2
1328
- } = options;
1329
- if (relayUrls.length === 0) {
1330
- throw new Error("fetchRemoteState: relayUrls must not be empty");
1188
+ /**
1189
+ * Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
1190
+ * Throws {@link GitError} when the name doesn't resolve.
1191
+ */
1192
+ async resolveRef(name) {
1193
+ assertRevision(name, "ref name");
1194
+ const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
1195
+ const sha = stdout.trim();
1196
+ if (!FULL_SHA_RE.test(sha)) {
1197
+ throw new GitError(
1198
+ `rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
1199
+ void 0,
1200
+ ""
1201
+ );
1202
+ }
1203
+ return sha;
1331
1204
  }
1332
- if (!ownerPubkey) {
1333
- throw new Error("fetchRemoteState: ownerPubkey is required");
1205
+ };
1206
+ var execFileAsync2 = promisify2(execFile2);
1207
+ var NonFastForwardError = class extends Error {
1208
+ constructor(refs) {
1209
+ super(
1210
+ `non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
1211
+ );
1212
+ this.refs = refs;
1213
+ this.name = "NonFastForwardError";
1334
1214
  }
1335
- if (!repoId) {
1336
- throw new Error("fetchRemoteState: repoId is required");
1215
+ refs;
1216
+ };
1217
+ var OversizeObjectsError = class extends Error {
1218
+ constructor(objects) {
1219
+ super(
1220
+ `${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
1221
+ );
1222
+ this.objects = objects;
1223
+ this.name = "OversizeObjectsError";
1224
+ }
1225
+ objects;
1226
+ };
1227
+ async function planPush(options) {
1228
+ const { repoReader, remoteState, feeRates, repoId, force = false } = options;
1229
+ const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
1230
+ const { head, refs: localRefs } = await repoReader.listRefs();
1231
+ const localByName = new Map(localRefs.map((r) => [r.refname, r]));
1232
+ let selected;
1233
+ if (options.refs !== void 0) {
1234
+ selected = options.refs.map((name) => {
1235
+ const ref = localByName.get(name);
1236
+ if (!ref) {
1237
+ throw new Error(
1238
+ `ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
1239
+ );
1240
+ }
1241
+ return ref;
1242
+ });
1243
+ } else {
1244
+ selected = localRefs;
1337
1245
  }
1338
- const filter = {
1339
- kinds: [REPOSITORY_ANNOUNCEMENT_KIND2, REPOSITORY_STATE_KIND],
1340
- authors: [ownerPubkey],
1341
- "#d": [repoId]
1342
- };
1343
- const results = await Promise.allSettled(
1344
- relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
1345
- );
1346
- const failures = [];
1347
- const byId = /* @__PURE__ */ new Map();
1348
- for (const result of results) {
1349
- if (result.status === "rejected") {
1350
- failures.push(String(result.reason?.message ?? result.reason));
1246
+ const refUpdates = [];
1247
+ const rejected = [];
1248
+ for (const ref of selected) {
1249
+ const remoteSha = remoteState.refs.get(ref.refname) ?? null;
1250
+ if (remoteSha === null) {
1251
+ refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
1351
1252
  continue;
1352
1253
  }
1353
- for (const event of result.value) {
1354
- if (event.pubkey !== ownerPubkey) continue;
1355
- if (getTagValue(event.tags, "d") !== repoId) continue;
1356
- if (typeof event.id === "string" && !byId.has(event.id)) {
1357
- byId.set(event.id, event);
1358
- }
1254
+ if (remoteSha === ref.sha) {
1255
+ refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
1256
+ continue;
1257
+ }
1258
+ let fastForward = false;
1259
+ try {
1260
+ fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
1261
+ } catch (err) {
1262
+ if (!(err instanceof GitError)) throw err;
1263
+ fastForward = false;
1264
+ }
1265
+ if (fastForward) {
1266
+ refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
1267
+ } else if (force) {
1268
+ refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
1269
+ } else {
1270
+ rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
1359
1271
  }
1360
1272
  }
1361
- if (failures.length === relayUrls.length) {
1362
- throw new Error(
1363
- `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join("; ")}`
1364
- );
1273
+ if (rejected.length > 0) throw new NonFastForwardError(rejected);
1274
+ const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
1275
+ const wantTips = [...new Set(updates.map((u) => u.localSha))];
1276
+ const haveTips = [...new Set(remoteState.refs.values())];
1277
+ const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
1278
+ const knownShaToTxId = new Map(remoteState.shaToTxId);
1279
+ let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
1280
+ if (candidates.length > 0) {
1281
+ const resolved = await resolveMissing(candidates.map((o) => o.sha));
1282
+ for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
1283
+ candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
1365
1284
  }
1366
- const events = [...byId.values()];
1367
- const refsEvent = latestReplaceable(
1368
- events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
1369
- );
1370
- const announceEvent = latestReplaceable(
1371
- events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
1285
+ const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
1286
+ const { objects: stats, missing } = await repoReader.statObjects(
1287
+ candidates.map((c) => c.sha)
1372
1288
  );
1373
- const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
1374
- refs: /* @__PURE__ */ new Map(),
1375
- headSymref: null,
1376
- shaToTxId: /* @__PURE__ */ new Map()
1377
- };
1378
- if (shaToTxId.size > 0) {
1379
- seedShaCache(
1380
- [...shaToTxId].map(
1381
- ([sha, txId]) => [shaCacheKey(sha, repoId), txId]
1382
- )
1289
+ if (missing.length > 0) {
1290
+ throw new Error(
1291
+ `objects vanished from the local repository during planning: ${missing.join(", ")}`
1383
1292
  );
1384
1293
  }
1385
- const name = announceEvent ? getTagValue(announceEvent.tags, "name") ?? null : null;
1386
- const description = announceEvent ? getTagValue(announceEvent.tags, "description") ?? announceEvent.content : null;
1387
- const relays = [];
1388
- if (announceEvent) {
1389
- for (const tag of announceEvent.tags) {
1390
- if (tag[0] === "relays") {
1391
- relays.push(...tag.slice(1).filter((url) => url.length > 0));
1392
- }
1294
+ const oversize = [];
1295
+ for (const stat2 of stats) {
1296
+ if (stat2.size > MAX_OBJECT_SIZE) {
1297
+ const path = pathBySha.get(stat2.sha);
1298
+ oversize.push({ ...stat2, ...path ? { path } : {} });
1393
1299
  }
1394
1300
  }
1395
- const resolveMissing = async (shas) => {
1396
- const resolved = /* @__PURE__ */ new Map();
1397
- const missing = [];
1398
- for (const sha of new Set(shas)) {
1399
- const known = shaToTxId.get(sha);
1400
- if (known !== void 0) {
1401
- resolved.set(sha, known);
1402
- } else {
1403
- missing.push(sha);
1301
+ if (oversize.length > 0) throw new OversizeObjectsError(oversize);
1302
+ const tipShas = new Set(updates.map((u) => u.localSha));
1303
+ const planned = stats.map((stat2) => {
1304
+ const path = pathBySha.get(stat2.sha);
1305
+ return { ...stat2, ...path ? { path } : {}, isRefTip: tipShas.has(stat2.sha) };
1306
+ });
1307
+ const objects = [
1308
+ ...planned.filter((o) => !o.isRefTip),
1309
+ ...planned.filter((o) => o.isRefTip)
1310
+ ];
1311
+ const newRefsMap = new Map(remoteState.refs);
1312
+ for (const update of updates) newRefsMap.set(update.refname, update.localSha);
1313
+ const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
1314
+ const newRefs = {};
1315
+ const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
1316
+ if (headSymref && headSha) newRefs[headSymref] = headSha;
1317
+ for (const [refname, sha] of newRefsMap) {
1318
+ if (refname !== headSymref) newRefs[refname] = sha;
1319
+ }
1320
+ const announceNeeded = !remoteState.announced;
1321
+ const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
1322
+ const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
1323
+ const eventCount = 1 + (announceNeeded ? 1 : 0);
1324
+ const eventFees = BigInt(eventCount) * feeRates.eventFee;
1325
+ return {
1326
+ repoId,
1327
+ refUpdates,
1328
+ newRefs,
1329
+ headSymref,
1330
+ objects,
1331
+ knownShaToTxId,
1332
+ announceNeeded,
1333
+ announcement: {
1334
+ name: options.announcement?.name ?? repoId,
1335
+ description: options.announcement?.description ?? ""
1336
+ },
1337
+ estimate: {
1338
+ objectCount: objects.length,
1339
+ totalObjectBytes,
1340
+ uploadFee,
1341
+ eventCount,
1342
+ eventFees,
1343
+ totalFee: uploadFee + eventFees
1344
+ }
1345
+ };
1346
+ }
1347
+ var READ_BATCH_SIZE = 100;
1348
+ async function executePush(options) {
1349
+ const { plan, publisher, remoteState, repoReader, relayUrls } = options;
1350
+ const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
1351
+ const resultBySha = /* @__PURE__ */ new Map();
1352
+ let totalFeePaid = 0n;
1353
+ const pending = [];
1354
+ for (const object of plan.objects) {
1355
+ const knownTxId = merged.get(object.sha);
1356
+ if (knownTxId !== void 0) {
1357
+ resultBySha.set(object.sha, {
1358
+ sha: object.sha,
1359
+ txId: knownTxId,
1360
+ feePaid: 0n,
1361
+ skipped: true
1362
+ });
1363
+ } else {
1364
+ pending.push(object);
1365
+ }
1366
+ }
1367
+ for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
1368
+ const batch = pending.slice(i, i + READ_BATCH_SIZE);
1369
+ const { objects: read, missing } = await repoReader.readObjects(
1370
+ batch.map((o) => o.sha)
1371
+ );
1372
+ if (missing.length > 0) {
1373
+ throw new Error(
1374
+ `objects vanished from the local repository during push: ${missing.join(", ")}`
1375
+ );
1376
+ }
1377
+ const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
1378
+ for (const object of batch) {
1379
+ const body = bodyBySha.get(object.sha);
1380
+ if (!body) {
1381
+ throw new Error(
1382
+ `internal: cat-file returned no body for ${object.sha}`
1383
+ );
1404
1384
  }
1385
+ const receipt = await publisher.uploadGitObject({
1386
+ sha: object.sha,
1387
+ type: object.type,
1388
+ body,
1389
+ repoId: plan.repoId
1390
+ });
1391
+ merged.set(object.sha, receipt.txId);
1392
+ totalFeePaid += receipt.feePaid;
1393
+ resultBySha.set(object.sha, {
1394
+ sha: object.sha,
1395
+ txId: receipt.txId,
1396
+ feePaid: receipt.feePaid,
1397
+ skipped: false
1398
+ });
1405
1399
  }
1406
- const lookups = await Promise.all(
1407
- missing.map(
1408
- async (sha) => [sha, await resolveSha(sha, repoId)]
1409
- )
1400
+ }
1401
+ let announceReceipt = null;
1402
+ if (plan.announceNeeded && !remoteState.announced) {
1403
+ const announceEvent = buildRepoAnnouncement(
1404
+ plan.repoId,
1405
+ plan.announcement.name,
1406
+ plan.announcement.description
1410
1407
  );
1411
- for (const [sha, txId] of lookups) {
1412
- if (txId) resolved.set(sha, txId);
1408
+ announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
1409
+ totalFeePaid += announceReceipt.feePaid;
1410
+ }
1411
+ const refsEvent = buildRepoRefs(
1412
+ plan.repoId,
1413
+ plan.newRefs,
1414
+ Object.fromEntries(merged)
1415
+ );
1416
+ const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
1417
+ totalFeePaid += refsReceipt.feePaid;
1418
+ const uploads = plan.objects.map((o) => {
1419
+ const step = resultBySha.get(o.sha);
1420
+ if (!step) {
1421
+ throw new Error(`internal: no upload result recorded for ${o.sha}`);
1413
1422
  }
1414
- return resolved;
1415
- };
1423
+ return step;
1424
+ });
1416
1425
  return {
1417
- announced: announceEvent !== null,
1418
- refs,
1419
- headSymref,
1420
- shaToTxId,
1421
- refsEvent,
1422
- announceEvent,
1423
- name,
1424
- description,
1425
- relays,
1426
- resolveMissing
1426
+ uploads,
1427
+ announceReceipt,
1428
+ refsReceipt,
1429
+ arweaveMap: merged,
1430
+ totalFeePaid
1427
1431
  };
1428
1432
  }
1429
1433
 
@@ -3672,7 +3676,9 @@ var ClientRunner = class {
3672
3676
  req.title,
3673
3677
  req.commits ?? [],
3674
3678
  req.branch,
3675
- content
3679
+ content,
3680
+ // PR body → `description` tag; never the content (git am safety, #280).
3681
+ typeof req.description === "string" && req.description !== "" ? req.description : void 0
3676
3682
  );
3677
3683
  return this.gitPublishSigned(event);
3678
3684
  }
@@ -4322,4 +4328,4 @@ export {
4322
4328
  PublishRejectedError,
4323
4329
  registerRoutes
4324
4330
  };
4325
- //# sourceMappingURL=chunk-YVLSWU46.js.map
4331
+ //# sourceMappingURL=chunk-BINI3PEC.js.map