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