@toon-protocol/rig 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,16 @@
1
1
  import {
2
2
  CI_ADVERTISEMENT_KIND,
3
+ CI_JOB_RESULT_KIND,
4
+ CI_SERVICE_REQUEST_KIND,
5
+ CI_SERVICE_STOP_KIND,
3
6
  CI_STATUS_USAGE,
7
+ CI_WORKFLOW_PROGRESS_KIND,
8
+ CI_WORKFLOW_RESULT_KIND,
4
9
  GitRepoReader,
5
10
  InvalidRelayUrlError,
6
11
  MultiUrlRemoteError,
7
12
  NotAGitRepositoryError,
13
+ PrMergeError,
8
14
  RESERVED_SECRET_NAMES,
9
15
  SECRET_NAME_RE,
10
16
  UnconfiguredRepoAddressError,
@@ -52,13 +58,13 @@ import {
52
58
  updateRef,
53
59
  writeGitObjects,
54
60
  writeToonConfig
55
- } from "./chunk-J6GNB54J.js";
61
+ } from "./chunk-DGF7WCRW.js";
56
62
  import {
57
63
  PREFERRED_GATEWAY,
58
64
  mirrorGatewaysFor,
59
65
  readGateways,
60
66
  shaResolverFor
61
- } from "./chunk-P5ZKJN6B.js";
67
+ } from "./chunk-UXRWMKXV.js";
62
68
  import {
63
69
  COMMENT_KIND,
64
70
  DEFAULT_KEYSTORE_PASSWORD,
@@ -71,18 +77,23 @@ import {
71
77
  buildStatus,
72
78
  clientConfigPath,
73
79
  clientKeystorePath,
80
+ clientStateDir,
74
81
  commentBelongsToThread,
82
+ decodeEventPayload,
75
83
  defaultWebSocketFactory,
76
84
  fetchRemoteState,
77
85
  firstTagValue,
86
+ isPatchRevision,
78
87
  isValidEvmPayoutAddress,
79
88
  linkKeystoreInClientConfig,
80
89
  parseMaintainers,
81
90
  queryRelay,
82
91
  readClientConfigFile,
83
92
  resolveAccountIndex,
84
- resolveIdentity
85
- } from "./chunk-ITC7ESTZ.js";
93
+ resolveIdentity,
94
+ revisionRootId,
95
+ selectRevision
96
+ } from "./chunk-OVZJ5P6E.js";
86
97
  import {
87
98
  ChannelMapStore,
88
99
  channelStatus,
@@ -311,7 +322,7 @@ function loadPaidSession(deps, relayUrl) {
311
322
  });
312
323
  }
313
324
  var defaultLoadStandalone = async (options) => {
314
- const mod = await import("./standalone-mode-IETLYVCE.js");
325
+ const mod = await import("./standalone-mode-E7TNMWGO.js");
315
326
  return mod.createStandaloneContext(options);
316
327
  };
317
328
  function identityReport(ctx) {
@@ -565,9 +576,23 @@ async function runPush(args, deps) {
565
576
  } else {
566
577
  identity = session.identity;
567
578
  }
568
- if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
579
+ const foreignOwner = toonConfig.owner && toonConfig.owner.toLowerCase() !== identity.pubkey.toLowerCase() ? toonConfig.owner : void 0;
580
+ let maintainerRemote;
581
+ if (session.path === "standalone" && foreignOwner) {
582
+ const ownerState = await session.ctx.fetchRemote({
583
+ ownerPubkey: foreignOwner,
584
+ repoId,
585
+ relayUrls: relaysUsed
586
+ });
587
+ if (ownerState.announced && authorizedAuthors(foreignOwner, ownerState.announceEvent?.tags).has(
588
+ identity.pubkey.toLowerCase()
589
+ )) {
590
+ maintainerRemote = ownerState;
591
+ }
592
+ }
593
+ if (!maintainerRemote && foreignOwner) {
569
594
  io.err(
570
- `warning: git config toon.owner (${toonConfig.owner.slice(0, 8)}\u2026) differs from the active identity (${identity.pubkey.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's. Re-run \`rig init\` to adopt the active identity.`
595
+ `warning: git config toon.owner (${foreignOwner.slice(0, 8)}\u2026) differs from the active identity (${identity.pubkey.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's. Re-run \`rig init\` to adopt the active identity.`
571
596
  );
572
597
  }
573
598
  const announcement = relaysUsed[0] === void 0 ? void 0 : {
@@ -584,7 +609,7 @@ async function runPush(args, deps) {
584
609
  let rigPageSkipDetail;
585
610
  if (session.path === "standalone") {
586
611
  const ctx = session.ctx;
587
- const remoteState = await ctx.fetchRemote({
612
+ const remoteState = maintainerRemote ?? await ctx.fetchRemote({
588
613
  ownerPubkey: ctx.ownerPubkey,
589
614
  repoId,
590
615
  relayUrls: relaysUsed
@@ -614,6 +639,8 @@ async function runPush(args, deps) {
614
639
  rigPageSkipDetail = "skipped (--no-rig-page)";
615
640
  } else if (relaysUsed[0] === void 0) {
616
641
  rigPageSkipDetail = "skipped (no relay resolved for the pointer)";
642
+ } else if (maintainerRemote) {
643
+ rigPageSkipDetail = "skipped (the owner's rig page is the owner's to refresh)";
617
644
  } else if (!ctx.publisher.uploadBlob) {
618
645
  rigPageSkipDetail = "skipped (the active publisher cannot upload raw blobs)";
619
646
  } else {
@@ -2280,9 +2307,9 @@ async function runCiTrigger(args, deps) {
2280
2307
  });
2281
2308
  }
2282
2309
  async function gitShowBytes(repoRoot, commit, path) {
2283
- const { execFile } = await import("child_process");
2310
+ const { execFile: execFile2 } = await import("child_process");
2284
2311
  return new Promise((resolve2, reject) => {
2285
- execFile(
2312
+ execFile2(
2286
2313
  "git",
2287
2314
  ["show", `${commit}:${path}`],
2288
2315
  { cwd: repoRoot, encoding: "buffer", maxBuffer: 16 * 1024 * 1024 },
@@ -2493,7 +2520,7 @@ async function runCi(args, deps) {
2493
2520
  case "status":
2494
2521
  return runCiStatus(rest, deps);
2495
2522
  case "serve":
2496
- return (await import("./ci-serve-MPJ2WPCC.js")).runCiServe(rest, deps);
2523
+ return (await import("./ci-serve-R7ZQ7GJQ.js")).runCiServe(rest, deps);
2497
2524
  case "--help":
2498
2525
  case "-h":
2499
2526
  case "help":
@@ -2764,16 +2791,257 @@ async function runClone(args, deps) {
2764
2791
 
2765
2792
  // src/cli/events.ts
2766
2793
  import { readFile } from "fs/promises";
2767
- import { parseArgs as parseArgs8 } from "util";
2794
+ import { parseArgs as parseArgs10 } from "util";
2768
2795
  import {
2769
- STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2,
2796
+ PATCH_KIND as PATCH_KIND3,
2797
+ REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND3,
2798
+ STATUS_APPLIED_KIND as STATUS_APPLIED_KIND3,
2770
2799
  STATUS_CLOSED_KIND as STATUS_CLOSED_KIND2,
2771
2800
  STATUS_DRAFT_KIND as STATUS_DRAFT_KIND2,
2772
2801
  STATUS_OPEN_KIND as STATUS_OPEN_KIND2
2773
2802
  } from "@toon-protocol/core/nip34";
2774
2803
 
2775
- // src/cli/tracker.ts
2804
+ // src/cli/merge.ts
2805
+ import { execFile } from "child_process";
2806
+ import { mkdtemp as mkdtemp2, rm as rm2, writeFile } from "fs/promises";
2807
+ import { tmpdir } from "os";
2808
+ import { join as join4 } from "path";
2809
+ import { parseArgs as parseArgs9 } from "util";
2810
+ import { promisify } from "util";
2811
+ import {
2812
+ PATCH_KIND as PATCH_KIND2,
2813
+ REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2,
2814
+ STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2
2815
+ } from "@toon-protocol/core/nip34";
2816
+
2817
+ // src/cli/fetch.ts
2776
2818
  import { parseArgs as parseArgs7 } from "util";
2819
+ var FETCH_USAGE = `Usage: rig fetch [remote] [options]
2820
+
2821
+ Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
2822
+ payments, no identity needed). Reads the kind:30618 repository state from the
2823
+ remote's relay, downloads only the git objects this repository is missing
2824
+ (SHA-1 verified), and updates the remote-tracking refs
2825
+ (refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
2826
+
2827
+ No merge happens: integrate with the git passthrough, e.g.
2828
+ \`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
2829
+ \`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
2830
+
2831
+ The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
2832
+ or \`rig clone\` writes; --repo-id/--owner override.
2833
+
2834
+ Options:
2835
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
2836
+ --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
2837
+ --relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
2838
+ --gateway <url> gateway to read objects through FIRST, on its raw-bytes
2839
+ route <url>/raw/<txId>, ahead of the public gateway
2840
+ list \u2014 name a private, local or air-gapped gateway to
2841
+ fetch objects the public ones do not carry
2842
+ (env RIG_ARWEAVE_GATEWAY also sets it)
2843
+ --concurrency <n> parallel gateway downloads (default 8)
2844
+ --json machine-readable result envelope
2845
+ -h, --help show this help`;
2846
+ var WS_URL_RE3 = /^wss?:\/\//i;
2847
+ function localRefFor(refname, remote) {
2848
+ if (refname.startsWith("refs/heads/")) {
2849
+ return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
2850
+ }
2851
+ if (refname.startsWith("refs/tags/")) return refname;
2852
+ return null;
2853
+ }
2854
+ async function resolveLocalRef(repoRoot, refname) {
2855
+ try {
2856
+ const out = await runGit(repoRoot, [
2857
+ "rev-parse",
2858
+ "--verify",
2859
+ "--quiet",
2860
+ refname
2861
+ ]);
2862
+ const sha = out.trim();
2863
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
2864
+ } catch {
2865
+ return null;
2866
+ }
2867
+ }
2868
+ async function runFetch(args, deps) {
2869
+ const { io } = deps;
2870
+ let json = false;
2871
+ let remote = "origin";
2872
+ let relayFlag;
2873
+ let repoIdFlag;
2874
+ let ownerFlag;
2875
+ let gatewayFlag;
2876
+ let concurrency;
2877
+ try {
2878
+ const { values, positionals } = parseArgs7({
2879
+ args,
2880
+ options: {
2881
+ json: { type: "boolean", default: false },
2882
+ relay: { type: "string" },
2883
+ "repo-id": { type: "string" },
2884
+ owner: { type: "string" },
2885
+ gateway: { type: "string" },
2886
+ concurrency: { type: "string" },
2887
+ help: { type: "boolean", short: "h", default: false }
2888
+ },
2889
+ allowPositionals: true
2890
+ });
2891
+ if (values.help) {
2892
+ io.out(FETCH_USAGE);
2893
+ return 0;
2894
+ }
2895
+ json = values.json === true;
2896
+ if (positionals.length > 1) {
2897
+ throw new Error(
2898
+ `expected at most one [remote] argument, got ${positionals.length}`
2899
+ );
2900
+ }
2901
+ if (positionals[0] !== void 0) remote = positionals[0];
2902
+ relayFlag = values.relay;
2903
+ repoIdFlag = values["repo-id"];
2904
+ ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
2905
+ gatewayFlag = values.gateway;
2906
+ if (values.concurrency !== void 0) {
2907
+ concurrency = Number.parseInt(values.concurrency, 10);
2908
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
2909
+ throw new Error("--concurrency must be a positive integer");
2910
+ }
2911
+ }
2912
+ } catch (err) {
2913
+ io.err(err instanceof Error ? err.message : String(err));
2914
+ io.err(FETCH_USAGE);
2915
+ return 2;
2916
+ }
2917
+ try {
2918
+ const repoRoot = await resolveRepoRoot(deps.cwd);
2919
+ const toonConfig = await readToonConfig(repoRoot);
2920
+ const repoId = repoIdFlag ?? toonConfig.repoId;
2921
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2922
+ const owner = ownerFlag ?? toonConfig.owner;
2923
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
2924
+ let relayUrl;
2925
+ if (relayFlag !== void 0) {
2926
+ relayUrl = relayFlag;
2927
+ } else {
2928
+ const urls = await getGitRemoteUrls(repoRoot, remote);
2929
+ if (urls.length === 0) throw new UnknownRemoteError(remote);
2930
+ if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
2931
+ relayUrl = urls[0];
2932
+ }
2933
+ if (!WS_URL_RE3.test(relayUrl)) {
2934
+ throw new InvalidRelayUrlError(
2935
+ relayUrl,
2936
+ `remote ${JSON.stringify(remote)}`
2937
+ );
2938
+ }
2939
+ const remoteState = await fetchRemoteState({
2940
+ relayUrls: [relayUrl],
2941
+ ownerPubkey: owner,
2942
+ repoId,
2943
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
2944
+ // The GraphQL fallback for SHAs the map does not cover asks the SAME
2945
+ // permaweb the objects are read from (#183).
2946
+ resolveSha: deps.resolveSha ?? shaResolverFor(gatewayFlag, deps.env)
2947
+ });
2948
+ const planned = [];
2949
+ for (const [refname, sha] of remoteState.refs) {
2950
+ if (!isSafeRefname(refname)) {
2951
+ io.err(
2952
+ `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
2953
+ );
2954
+ continue;
2955
+ }
2956
+ const localRef = localRefFor(refname, remote);
2957
+ if (localRef === null) {
2958
+ io.err(
2959
+ `warning: skipping ref outside refs/heads and refs/tags: ${refname}`
2960
+ );
2961
+ continue;
2962
+ }
2963
+ planned.push({ refname, localRef, newSha: sha });
2964
+ }
2965
+ const reader = new GitRepoReader(repoRoot);
2966
+ const tips = [...new Set(planned.map((p) => p.newSha))];
2967
+ const presentLocally = new Set(await reader.listAllObjectShas());
2968
+ const collected = await collectRepoObjects({
2969
+ tips,
2970
+ shaToTxId: remoteState.shaToTxId,
2971
+ resolveMissing: (shas) => remoteState.resolveMissing(shas),
2972
+ presentLocally,
2973
+ // `--gateway` / RIG_ARWEAVE_GATEWAY first, public list behind it (#176).
2974
+ gateways: readGateways(gatewayFlag, deps.env),
2975
+ ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
2976
+ ...concurrency !== void 0 ? { concurrency } : {}
2977
+ });
2978
+ if (collected.missing.length > 0) {
2979
+ throw new MissingRemoteObjectsError(
2980
+ collected.missing,
2981
+ `cannot fetch from ${relayUrl}`
2982
+ );
2983
+ }
2984
+ const written = await writeGitObjects(repoRoot, collected.objects.values());
2985
+ const updates = [];
2986
+ for (const { refname, localRef, newSha } of planned) {
2987
+ const oldSha = await resolveLocalRef(repoRoot, localRef);
2988
+ if (oldSha === newSha) {
2989
+ updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
2990
+ continue;
2991
+ }
2992
+ let kind;
2993
+ if (oldSha === null) {
2994
+ kind = "new";
2995
+ } else {
2996
+ kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
2997
+ }
2998
+ await updateRef(repoRoot, localRef, newSha);
2999
+ updates.push({ refname, localRef, oldSha, newSha, kind });
3000
+ }
3001
+ const moved = updates.filter((u) => u.kind !== "up-to-date");
3002
+ if (json) {
3003
+ io.emitJson({
3004
+ command: "fetch",
3005
+ repoAddr: { ownerPubkey: owner, repoId },
3006
+ remote,
3007
+ relay: relayUrl,
3008
+ objectsDownloaded: written,
3009
+ updates,
3010
+ executed: true
3011
+ });
3012
+ return 0;
3013
+ }
3014
+ if (moved.length === 0) {
3015
+ io.out("Already up to date.");
3016
+ return 0;
3017
+ }
3018
+ io.out(`From ${relayUrl}`);
3019
+ for (const u of moved) {
3020
+ const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
3021
+ const src = short(u.refname);
3022
+ const dst = short(u.localRef);
3023
+ if (u.kind === "new") {
3024
+ const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
3025
+ io.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
3026
+ } else if (u.kind === "forced") {
3027
+ io.out(
3028
+ ` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
3029
+ );
3030
+ } else {
3031
+ io.out(
3032
+ ` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
3033
+ );
3034
+ }
3035
+ }
3036
+ io.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
3037
+ return 0;
3038
+ } catch (err) {
3039
+ return emitCliError(io, json, "fetch", err);
3040
+ }
3041
+ }
3042
+
3043
+ // src/cli/tracker.ts
3044
+ import { parseArgs as parseArgs8 } from "util";
2777
3045
  import {
2778
3046
  ISSUE_KIND,
2779
3047
  PATCH_KIND,
@@ -2859,7 +3127,7 @@ function withTargetAuthor(authorized, targetAuthorPubkey) {
2859
3127
  }
2860
3128
  var RELAY_TIMEOUT_MS = 1e4;
2861
3129
  var HEX64_RE2 = /^[0-9a-f]{64}$/;
2862
- var WS_URL_RE3 = /^wss?:\/\//i;
3130
+ var WS_URL_RE4 = /^wss?:\/\//i;
2863
3131
  function defaultWebSocketFactory2(url) {
2864
3132
  const ctor = globalThis.WebSocket;
2865
3133
  if (!ctor) {
@@ -2943,7 +3211,7 @@ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2943
3211
  repoRoot,
2944
3212
  toonRelays: toonConfig.relays
2945
3213
  });
2946
- const wsRelays = resolved.relays.filter((url) => WS_URL_RE3.test(url));
3214
+ const wsRelays = resolved.relays.filter((url) => WS_URL_RE4.test(url));
2947
3215
  if (wsRelays.length === 0) {
2948
3216
  throw new InvalidRelayUrlError(
2949
3217
  resolved.relays[0] ?? "",
@@ -3031,7 +3299,7 @@ async function fetchItems(ctx, kind) {
3031
3299
  )
3032
3300
  ]);
3033
3301
  const items = [...itemEvents.values()].filter(
3034
- (e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag)
3302
+ (e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag) && !(kind === PATCH_KIND && isPatchRevision(e))
3035
3303
  );
3036
3304
  const statuses = new Map(aStatusEvents);
3037
3305
  if (items.length > 0) {
@@ -3065,6 +3333,12 @@ async function fetchItem(ctx, eventId, expectedKind) {
3065
3333
  if (!event) {
3066
3334
  throw new Error(`event ${eventId} not found on ${ctx.relays.join(", ")}`);
3067
3335
  }
3336
+ if (event.kind === PATCH_KIND && isPatchRevision(event)) {
3337
+ const rootId = revisionRootId(event);
3338
+ if (rootId !== void 0 && rootId !== eventId) {
3339
+ return fetchItem(ctx, rootId, expectedKind);
3340
+ }
3341
+ }
3068
3342
  if (event.kind !== expectedKind) {
3069
3343
  const hint = event.kind === PATCH_KIND ? " (it is a kind:1617 patch \u2014 use `rig pr show`)" : event.kind === ISSUE_KIND ? " (it is a kind:1621 issue \u2014 use `rig issue show`)" : "";
3070
3344
  throw new Error(
@@ -3105,18 +3379,51 @@ async function fetchItem(ctx, eventId, expectedKind) {
3105
3379
  parsedAddr.ownerPubkey,
3106
3380
  parsedAddr.repoId
3107
3381
  ) : /* @__PURE__ */ new Set();
3108
- return {
3109
- item: parseTrackerItem(
3382
+ const item = parseTrackerItem(
3383
+ event,
3384
+ deriveStatus(
3385
+ eventId,
3386
+ statusEvents.values(),
3387
+ withTargetAuthor(authorized, event.pubkey)
3388
+ )
3389
+ );
3390
+ let revisions;
3391
+ if (event.kind === PATCH_KIND) {
3392
+ const revisionEvents = await queryAll(
3393
+ ctx.relays,
3394
+ [{ kinds: [PATCH_KIND], "#e": [eventId] }],
3395
+ ctx.webSocketFactory
3396
+ );
3397
+ const { latest, earlier } = selectRevision(
3110
3398
  event,
3111
- deriveStatus(
3112
- eventId,
3113
- statusEvents.values(),
3114
- withTargetAuthor(authorized, event.pubkey)
3115
- )
3116
- ),
3117
- comments,
3118
- repoATag: repoATag2
3119
- };
3399
+ [...revisionEvents.values()].filter((e) => e.kind === PATCH_KIND),
3400
+ authorized
3401
+ );
3402
+ const describe = (e) => ({
3403
+ eventId: e.id,
3404
+ authorPubkey: e.pubkey,
3405
+ createdAt: e.created_at
3406
+ });
3407
+ revisions = { latest: describe(latest), earlier: earlier.map(describe) };
3408
+ if (latest.id !== event.id) {
3409
+ const shown = parseTrackerItem(latest, item.status);
3410
+ item.content = shown.content;
3411
+ item.title = shown.title;
3412
+ item.commitShas = shown.commitShas ?? [];
3413
+ delete item.branch;
3414
+ delete item.description;
3415
+ if (shown.branch !== void 0) item.branch = shown.branch;
3416
+ if (shown.description !== void 0) {
3417
+ item.description = shown.description;
3418
+ }
3419
+ }
3420
+ }
3421
+ return {
3422
+ item,
3423
+ ...revisions ? { revisions } : {},
3424
+ comments,
3425
+ repoATag: repoATag2
3426
+ };
3120
3427
  }
3121
3428
  function parseRepoATag(aTag) {
3122
3429
  if (!aTag) return null;
@@ -3131,7 +3438,7 @@ async function runList(args, deps, spec) {
3131
3438
  const { io } = deps;
3132
3439
  let flags;
3133
3440
  try {
3134
- const { values, positionals } = parseArgs7({
3441
+ const { values, positionals } = parseArgs8({
3135
3442
  args,
3136
3443
  options: TRACKER_OPTIONS,
3137
3444
  allowPositionals: true
@@ -3196,7 +3503,7 @@ async function runShow(args, deps, spec) {
3196
3503
  let flags;
3197
3504
  let eventId;
3198
3505
  try {
3199
- const { values, positionals } = parseArgs7({
3506
+ const { values, positionals } = parseArgs8({
3200
3507
  args,
3201
3508
  options: TRACKER_OPTIONS,
3202
3509
  allowPositionals: true
@@ -3229,6 +3536,10 @@ async function runShow(args, deps, spec) {
3229
3536
  relays: ctx.relays,
3230
3537
  repoATag: shown.repoATag,
3231
3538
  [spec.jsonKey]: shown.item,
3539
+ ...shown.revisions ? {
3540
+ latestRevision: shown.revisions.latest,
3541
+ earlierRevisions: shown.revisions.earlier
3542
+ } : {},
3232
3543
  comments: shown.comments
3233
3544
  });
3234
3545
  return 0;
@@ -3245,6 +3556,20 @@ async function runShow(args, deps, spec) {
3245
3556
  io.out(`Commits: ${item.commitShas.join(", ")}`);
3246
3557
  }
3247
3558
  if (shown.repoATag !== null) io.out(`Repo: ${shown.repoATag}`);
3559
+ if (shown.revisions) {
3560
+ const { latest, earlier } = shown.revisions;
3561
+ if (earlier.length > 0) {
3562
+ io.out(
3563
+ `Showing: ${latest.eventId} (latest revision, by ${latest.authorPubkey.slice(0, 8)}, ${isoDate(latest.createdAt)})`
3564
+ );
3565
+ io.out("Earlier revisions (oldest first):");
3566
+ for (const r of shown.revisions.earlier) {
3567
+ io.out(
3568
+ ` ${r.eventId} (${r.authorPubkey.slice(0, 8)}, ${isoDate(r.createdAt)})`
3569
+ );
3570
+ }
3571
+ }
3572
+ }
3248
3573
  if (item.description !== void 0) {
3249
3574
  io.out("");
3250
3575
  io.out("Body:");
@@ -3306,6 +3631,435 @@ function runPrShow(args, deps) {
3306
3631
  });
3307
3632
  }
3308
3633
 
3634
+ // src/cli/merge.ts
3635
+ var execFileAsync = promisify(execFile);
3636
+ var HEX64_RE3 = /^[0-9a-f]{64}$/;
3637
+ var PR_MERGE_USAGE = `Usage: rig pr merge <patch-id> [--target <branch>] [options]
3638
+
3639
+ Apply the latest authorized revision of a patch with \`git am\` onto the
3640
+ honored tip of the target branch, push the result, and publish an applied
3641
+ status (kind:1631) naming the new commit SHAs \u2014 in that order, so the status
3642
+ never claims a merge that did not happen. A paid command; writes are permanent
3643
+ and non-refundable. Only the owner or a declared maintainer may merge.
3644
+
3645
+ If the patch does not apply, nothing is spent and your working tree is left as
3646
+ it was. If the push succeeds but the status publish fails, the merge is
3647
+ reported as a partial success; retry with \`rig pr status <patch-id> applied\`.
3648
+
3649
+ Options:
3650
+ --target <branch> branch to merge onto (default: the patch's branch-name
3651
+ tag, else the repo's default branch)
3652
+ --yes skip the confirmation prompt
3653
+ --json machine-readable output (without --yes: estimate only)
3654
+ --relay <url> ad-hoc relay override (bypasses git remotes)
3655
+ --remote <name> git remote to use (default: origin)
3656
+ --repo-id <id> repository id (default: git config toon.repoid)
3657
+ --owner <pubkey> repository owner (default: git config toon.owner)`;
3658
+ function parseMergeArgs(args) {
3659
+ const { values, positionals } = parseArgs9({
3660
+ args,
3661
+ options: {
3662
+ target: { type: "string" },
3663
+ yes: { type: "boolean", default: false },
3664
+ json: { type: "boolean", default: false },
3665
+ relay: { type: "string", multiple: true },
3666
+ remote: { type: "string" },
3667
+ "repo-id": { type: "string" },
3668
+ owner: { type: "string" },
3669
+ standalone: { type: "boolean", default: false },
3670
+ "no-daemon": { type: "boolean", default: false },
3671
+ help: { type: "boolean", short: "h", default: false }
3672
+ },
3673
+ allowPositionals: true
3674
+ });
3675
+ if (values.help) return "help";
3676
+ if (positionals.length !== 1) {
3677
+ throw new Error("expected exactly one argument: <patch-id>");
3678
+ }
3679
+ const patchId = positionals[0];
3680
+ if (!HEX64_RE3.test(patchId)) {
3681
+ throw new Error("<patch-id> must be a 64-character lowercase hex event id");
3682
+ }
3683
+ if (values.owner !== void 0 && !HEX64_RE3.test(values.owner)) {
3684
+ throw new Error("--owner must be a 64-character lowercase hex pubkey");
3685
+ }
3686
+ const flags = {
3687
+ patchId,
3688
+ yes: values.yes === true,
3689
+ json: values.json === true,
3690
+ relay: values.relay ?? []
3691
+ };
3692
+ if (values.target !== void 0) flags.target = values.target;
3693
+ if (values.remote !== void 0) flags.remote = values.remote;
3694
+ if (values["repo-id"] !== void 0) flags.repoId = values["repo-id"];
3695
+ if (values.owner !== void 0) flags.owner = values.owner;
3696
+ return flags;
3697
+ }
3698
+ async function git(cwd, args, env) {
3699
+ const { stdout } = await execFileAsync("git", args, {
3700
+ cwd,
3701
+ encoding: "utf-8",
3702
+ maxBuffer: 64 * 1024 * 1024,
3703
+ ...env ? { env } : {}
3704
+ });
3705
+ return stdout.trim();
3706
+ }
3707
+ async function gitOk(cwd, args) {
3708
+ try {
3709
+ await git(cwd, args);
3710
+ return true;
3711
+ } catch {
3712
+ return false;
3713
+ }
3714
+ }
3715
+ function gitFailure(err) {
3716
+ const e = err;
3717
+ return (e.stderr || e.stdout || e.message || String(err)).trim();
3718
+ }
3719
+ var OverlayReader = class extends GitRepoReader {
3720
+ constructor(repoPath, overlay) {
3721
+ super(repoPath);
3722
+ this.overlay = overlay;
3723
+ }
3724
+ overlay;
3725
+ async listRefs() {
3726
+ const base = await super.listRefs();
3727
+ const refs = base.refs.filter(
3728
+ (r) => r.refname !== this.overlay.refname
3729
+ );
3730
+ refs.push({
3731
+ refname: this.overlay.refname,
3732
+ sha: this.overlay.sha,
3733
+ type: "commit"
3734
+ });
3735
+ return { ...base, refs };
3736
+ }
3737
+ };
3738
+ async function applyInIsolation(repoRoot, tip, patchText, committer) {
3739
+ const scratch = await mkdtemp2(join4(tmpdir(), "rig-merge-"));
3740
+ const worktree = join4(scratch, "wt");
3741
+ try {
3742
+ await git(repoRoot, ["worktree", "add", "--detach", worktree, tip]);
3743
+ const patchFile = join4(scratch, "patch.mbox");
3744
+ await writeFile(patchFile, patchText, "utf-8");
3745
+ const env = { ...process.env };
3746
+ if (committer) {
3747
+ env["GIT_COMMITTER_NAME"] = committer.name;
3748
+ env["GIT_COMMITTER_EMAIL"] = committer.email;
3749
+ }
3750
+ try {
3751
+ await git(worktree, ["am", "--quiet", patchFile], env);
3752
+ } catch (err) {
3753
+ await git(worktree, ["am", "--abort"]).catch(() => void 0);
3754
+ throw new PrMergeError(
3755
+ "apply_conflict",
3756
+ `the patch does not apply cleanly onto ${tip.slice(0, 7)} \u2014 ask the contributor for a revision (\`rig pr create --revises\`). Nothing was pushed, published, or paid, and your working tree is unchanged.
3757
+ ` + gitFailure(err)
3758
+ );
3759
+ }
3760
+ const newHead = await git(worktree, ["rev-parse", "HEAD"]);
3761
+ const listed = await git(worktree, [
3762
+ "rev-list",
3763
+ "--reverse",
3764
+ `${tip}..${newHead}`
3765
+ ]);
3766
+ const commits = listed === "" ? [] : listed.split("\n");
3767
+ if (commits.length === 0) {
3768
+ throw new PrMergeError(
3769
+ "nothing_to_apply",
3770
+ "the patch applied no commits (already merged?) \u2014 nothing was pushed, published, or paid."
3771
+ );
3772
+ }
3773
+ return { newHead, commits };
3774
+ } finally {
3775
+ await git(repoRoot, ["worktree", "remove", "--force", worktree]).catch(
3776
+ () => void 0
3777
+ );
3778
+ await git(repoRoot, ["worktree", "prune"]).catch(() => void 0);
3779
+ await rm2(scratch, { recursive: true, force: true });
3780
+ }
3781
+ }
3782
+ var BRANCH_PREFIX = "refs/heads/";
3783
+ async function runPrMerge(args, deps) {
3784
+ const { io } = deps;
3785
+ let flags;
3786
+ try {
3787
+ const parsed = parseMergeArgs(args);
3788
+ if (parsed === "help") {
3789
+ io.out(PR_MERGE_USAGE);
3790
+ return 0;
3791
+ }
3792
+ flags = parsed;
3793
+ } catch (err) {
3794
+ io.err(err instanceof Error ? err.message : String(err));
3795
+ io.err(PR_MERGE_USAGE);
3796
+ return 2;
3797
+ }
3798
+ deps = withForcedStandalone(deps);
3799
+ const webSocketFactory = deps.webSocketFactory ?? defaultWebSocketFactory;
3800
+ let stop;
3801
+ try {
3802
+ const repoRoot = await resolveRepoRoot(deps.cwd);
3803
+ const toonConfig = await readToonConfig(repoRoot);
3804
+ const repoId = flags.repoId ?? toonConfig.repoId;
3805
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
3806
+ const resolved = await resolveRelays({
3807
+ relayFlags: flags.relay,
3808
+ remoteName: flags.remote,
3809
+ repoRoot,
3810
+ toonRelays: toonConfig.relays
3811
+ });
3812
+ if (resolved.nudge !== void 0) io.err(resolved.nudge);
3813
+ const relays = resolved.relays;
3814
+ if (relays.length > 1) {
3815
+ io.err(singleRelayRefusal(resolved, "Nothing was pushed or paid."));
3816
+ return 1;
3817
+ }
3818
+ const relay = relays[0];
3819
+ const session = await loadPaidSession(deps, relay);
3820
+ if (session.path !== "standalone") {
3821
+ throw new Error("internal: rig pr merge requires the standalone session");
3822
+ }
3823
+ const ctx = session.ctx;
3824
+ stop = () => ctx.stop();
3825
+ const identity = identityReport(ctx);
3826
+ const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
3827
+ const addr = { ownerPubkey: owner, repoId };
3828
+ const aTag = `${REPOSITORY_ANNOUNCEMENT_KIND2}:${owner}:${repoId}`;
3829
+ const lookup = async (id) => (await queryAll(relays, [{ ids: [id] }], webSocketFactory)).get(id);
3830
+ let root = await lookup(flags.patchId);
3831
+ if (root && root.kind === PATCH_KIND2 && isPatchRevision(root)) {
3832
+ const rootId = revisionRootId(root);
3833
+ if (rootId !== void 0) root = await lookup(rootId);
3834
+ }
3835
+ if (!root || root.kind !== PATCH_KIND2) {
3836
+ throw new PrMergeError(
3837
+ "patch_not_found",
3838
+ `patch ${flags.patchId} not found as a kind:1617 patch on ${relay} \u2014 nothing was pushed or paid`
3839
+ );
3840
+ }
3841
+ if (!root.tags.some((t) => t[0] === "a" && t[1] === aTag)) {
3842
+ throw new PrMergeError(
3843
+ "patch_not_found",
3844
+ `patch ${root.id} does not belong to ${aTag} \u2014 nothing was pushed or paid`
3845
+ );
3846
+ }
3847
+ const remote = await ctx.fetchRemote({
3848
+ ownerPubkey: owner,
3849
+ repoId,
3850
+ relayUrls: relays
3851
+ });
3852
+ const authorized = authorizedAuthors(
3853
+ owner,
3854
+ remote.announceEvent?.tags ?? []
3855
+ );
3856
+ const revisionEvents = await queryAll(
3857
+ relays,
3858
+ [{ kinds: [PATCH_KIND2], "#e": [root.id] }],
3859
+ webSocketFactory
3860
+ );
3861
+ const { latest } = selectRevision(
3862
+ root,
3863
+ [...revisionEvents.values()].filter((e) => e.kind === PATCH_KIND2),
3864
+ authorized
3865
+ );
3866
+ if (!authorized.has(identity.pubkey.toLowerCase())) {
3867
+ throw new PrMergeError(
3868
+ "not_authorized",
3869
+ `only the owner or a declared maintainer can merge \u2014 ${identity.pubkey.slice(0, 8)}\u2026 is neither for ${aTag}. Readers would ignore the pushed state. Nothing was pushed or paid.`
3870
+ );
3871
+ }
3872
+ if (!remote.announced) {
3873
+ throw new PrMergeError(
3874
+ "target_not_found",
3875
+ `${aTag} is not announced on ${relay} \u2014 nothing to merge onto`
3876
+ );
3877
+ }
3878
+ const branch = (flags.target ?? firstTagValue(latest.tags, "branch-name") ?? remote.headSymref ?? "main").replace(/^refs\/heads\//, "");
3879
+ const refname = `${BRANCH_PREFIX}${branch}`;
3880
+ const tip = remote.refs.get(refname);
3881
+ if (tip === void 0) {
3882
+ throw new PrMergeError(
3883
+ "target_not_found",
3884
+ `branch ${JSON.stringify(branch)} is not in the honored repo state (${[...remote.refs.keys()].join(", ") || "no refs"}) \u2014 pass --target. Nothing was pushed or paid.`
3885
+ );
3886
+ }
3887
+ if (!await gitOk(repoRoot, ["cat-file", "-e", `${tip}^{commit}`])) {
3888
+ await runFetch(
3889
+ ["--relay", relay, "--owner", owner, "--repo-id", repoId],
3890
+ { ...deps, io: { ...io, out: () => void 0 } }
3891
+ );
3892
+ if (!await gitOk(repoRoot, ["cat-file", "-e", `${tip}^{commit}`])) {
3893
+ throw new PrMergeError(
3894
+ "tip_unavailable",
3895
+ `the honored tip ${tip.slice(0, 7)} of ${branch} could not be fetched \u2014 run \`rig fetch\` and retry. Nothing was pushed or paid.`
3896
+ );
3897
+ }
3898
+ }
3899
+ const reader = new GitRepoReader(repoRoot);
3900
+ const localRefs = (await reader.listRefs()).refs;
3901
+ const localSha = localRefs.find((r) => r.refname === refname)?.sha;
3902
+ if (localSha !== void 0 && localSha !== tip && !await reader.isAncestor(localSha, tip)) {
3903
+ throw new PrMergeError(
3904
+ "local_branch_diverged",
3905
+ `local ${branch} (${localSha.slice(0, 7)}) is not behind the honored tip (${tip.slice(0, 7)}) \u2014 push or reset it first. Nothing was pushed or paid.`
3906
+ );
3907
+ }
3908
+ const hasCommitter = await gitOk(repoRoot, ["config", "user.email"]);
3909
+ const applied = await applyInIsolation(
3910
+ repoRoot,
3911
+ tip,
3912
+ latest.content,
3913
+ hasCommitter ? void 0 : {
3914
+ name: hexToNpub(identity.pubkey),
3915
+ email: `${hexToNpub(identity.pubkey)}@nostr`
3916
+ }
3917
+ );
3918
+ const overlay = new OverlayReader(repoRoot, {
3919
+ refname,
3920
+ sha: applied.newHead
3921
+ });
3922
+ const feeRates = await ctx.publisher.getFeeRates();
3923
+ const pushPlan = await planPush({
3924
+ repoReader: overlay,
3925
+ remoteState: remote,
3926
+ feeRates,
3927
+ repoId,
3928
+ refs: [refname],
3929
+ force: false
3930
+ });
3931
+ const plan = serializePushPlan(pushPlan);
3932
+ const total = BigInt(plan.estimate.totalFee) + feeRates.eventFee;
3933
+ if (!flags.json) {
3934
+ for (const line of renderPlan(plan)) io.out(line);
3935
+ io.out(
3936
+ ` status kind:1631 applied (${applied.commits.length} commit${applied.commits.length === 1 ? "" : "s"}) ${feeRates.eventFee}`
3937
+ );
3938
+ io.out(` total with status ${total}`);
3939
+ io.out(renderIdentityLine(identity));
3940
+ }
3941
+ if (!flags.yes) {
3942
+ if (flags.json) {
3943
+ io.emitJson({
3944
+ command: "pr merge",
3945
+ repoAddr: addr,
3946
+ identity,
3947
+ executed: false,
3948
+ patchId: root.id,
3949
+ revisionId: latest.id,
3950
+ target: refname,
3951
+ mergedShas: applied.commits,
3952
+ plan,
3953
+ totalFee: total.toString(),
3954
+ hint: "estimate only \u2014 re-run with --yes to push and publish (permanent, non-refundable)"
3955
+ });
3956
+ return 0;
3957
+ }
3958
+ if (!io.isInteractive) {
3959
+ io.err(
3960
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
3961
+ );
3962
+ return 1;
3963
+ }
3964
+ const proceed = await io.confirm(
3965
+ `Proceed with paid merge (total ${total} base units)? [y/N] `
3966
+ );
3967
+ if (!proceed) {
3968
+ io.err("aborted \u2014 nothing was pushed or published.");
3969
+ return 1;
3970
+ }
3971
+ }
3972
+ const pushResult = await executePush({
3973
+ plan: pushPlan,
3974
+ publisher: ctx.publisher,
3975
+ remoteState: remote,
3976
+ repoReader: overlay,
3977
+ relayUrls: relays
3978
+ });
3979
+ const result = serializePushResult(pushPlan, pushResult);
3980
+ const localNote = await advanceLocalBranch(
3981
+ repoRoot,
3982
+ reader,
3983
+ refname,
3984
+ applied.newHead,
3985
+ localSha
3986
+ );
3987
+ if (localNote !== void 0) io.err(localNote);
3988
+ let statusEventId;
3989
+ let statusError;
3990
+ try {
3991
+ const receipt = await ctx.publisher.publishEvent(
3992
+ buildStatus(owner, repoId, root.id, STATUS_APPLIED_KIND2, root.pubkey, {
3993
+ commits: applied.commits,
3994
+ revisionEventId: latest.id
3995
+ }),
3996
+ relays
3997
+ );
3998
+ statusEventId = receipt.eventId;
3999
+ } catch (err) {
4000
+ statusError = err instanceof Error ? err.message : String(err);
4001
+ }
4002
+ const retryHint = `rig pr status ${root.id} applied`;
4003
+ if (flags.json) {
4004
+ io.emitJson({
4005
+ command: "pr merge",
4006
+ repoAddr: addr,
4007
+ identity,
4008
+ executed: true,
4009
+ patchId: root.id,
4010
+ revisionId: latest.id,
4011
+ target: refname,
4012
+ mergedShas: applied.commits,
4013
+ result,
4014
+ ...statusEventId !== void 0 ? { statusEventId } : {},
4015
+ ...statusError !== void 0 ? {
4016
+ partial: true,
4017
+ error: "status_publish_failed",
4018
+ detail: statusError,
4019
+ hint: `the push succeeded but the applied status was not published \u2014 retry with \`${retryHint}\``
4020
+ } : {}
4021
+ });
4022
+ } else {
4023
+ for (const line of renderResult(result)) io.out(line);
4024
+ if (statusEventId !== void 0) {
4025
+ io.out(
4026
+ `Merged ${applied.commits.length} commit(s) onto ${branch}; applied status ${statusEventId}`
4027
+ );
4028
+ }
4029
+ }
4030
+ if (statusError !== void 0) {
4031
+ io.err(
4032
+ `partial success: the push succeeded but publishing the applied status failed: ${statusError}`
4033
+ );
4034
+ io.err(`retry the status with: ${retryHint}`);
4035
+ return 1;
4036
+ }
4037
+ return 0;
4038
+ } catch (err) {
4039
+ return emitCliError(io, flags.json, "pr merge", err);
4040
+ } finally {
4041
+ if (stop) await stop().catch(() => void 0);
4042
+ }
4043
+ }
4044
+ async function advanceLocalBranch(repoRoot, reader, refname, newHead, oldSha) {
4045
+ try {
4046
+ const { head } = await reader.listRefs();
4047
+ if (head === refname) {
4048
+ await git(repoRoot, ["merge", "--ff-only", newHead]);
4049
+ } else {
4050
+ await git(repoRoot, [
4051
+ "update-ref",
4052
+ refname,
4053
+ newHead,
4054
+ oldSha ?? "0".repeat(40)
4055
+ ]);
4056
+ }
4057
+ return void 0;
4058
+ } catch (err) {
4059
+ return `warning: the push succeeded but local ${refname} could not be moved to ${newHead.slice(0, 7)}: ${gitFailure(err)} \u2014 run \`rig fetch\``;
4060
+ }
4061
+ }
4062
+
3309
4063
  // src/cli/events.ts
3310
4064
  var defaultReadStdin2 = async () => {
3311
4065
  if (process.stdin.isTTY) return "";
@@ -3372,7 +4126,7 @@ comments published by this version.
3372
4126
  Options:
3373
4127
  --body <text> comment body (Markdown) [required]
3374
4128
  ${COMMON_FLAGS_USAGE2}`;
3375
- var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
4129
+ var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [--revises <patch-id>] [options]
3376
4130
 
3377
4131
  Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
3378
4132
  a paid publish; writes are permanent and non-refundable. --range runs
@@ -3386,6 +4140,12 @@ The PR body (--body/--body-file) is carried in a dedicated \`description\`
3386
4140
  tag, not in the event content \u2014 the content stays pure format-patch output so
3387
4141
  \`rig pr show \u2026 | git am\` keeps working.
3388
4142
 
4143
+ --revises publishes the patch as a NIP-34 revision of an existing root patch
4144
+ (\`t root-revision\` plus a NIP-10 reply to it), keeping the original thread,
4145
+ comments and status. Readers honor a revision only from the root's author or a
4146
+ declared maintainer; otherwise rig warns and still publishes. A revision is
4147
+ always published through the embedded (standalone) path.
4148
+
3389
4149
  Options:
3390
4150
  --title <title> patch/PR title (subject tag) [required]
3391
4151
  --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
@@ -3394,6 +4154,7 @@ Options:
3394
4154
  --body <text> PR description (Markdown; description tag)
3395
4155
  --body-file <path> read the PR description from a file
3396
4156
  --branch <name> branch name (branch-name tag)
4157
+ --revises <patch-id> publish as a revision of this root patch (64-hex id)
3397
4158
  ${COMMON_FLAGS_USAGE2}`;
3398
4159
  var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
3399
4160
 
@@ -3420,14 +4181,16 @@ Options:
3420
4181
  ${COMMON_FLAGS_USAGE2}`;
3421
4182
  var PR_USAGE = `${PR_CREATE_USAGE}
3422
4183
 
4184
+ ${PR_MERGE_USAGE}
4185
+
3423
4186
  ${PR_STATUS_USAGE}
3424
4187
 
3425
4188
  ${PR_LIST_USAGE}
3426
4189
 
3427
4190
  ${PR_SHOW_USAGE}`;
3428
- var HEX64_RE3 = /^[0-9a-f]{64}$/;
4191
+ var HEX64_RE4 = /^[0-9a-f]{64}$/;
3429
4192
  function assertHex64(value, what) {
3430
- if (!HEX64_RE3.test(value)) {
4193
+ if (!HEX64_RE4.test(value)) {
3431
4194
  throw new Error(
3432
4195
  `${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
3433
4196
  );
@@ -3624,7 +4387,7 @@ async function runIssue(args, deps) {
3624
4387
  let bodyFile;
3625
4388
  let labels;
3626
4389
  try {
3627
- const { values } = parseArgs8({
4390
+ const { values } = parseArgs10({
3628
4391
  args: rest,
3629
4392
  options: {
3630
4393
  ...COMMON_OPTIONS2,
@@ -3720,7 +4483,7 @@ async function resolveCommentTarget(deps, targetEventId, relayUrl) {
3720
4483
  const rootEventId = firstTagValue(target.tags, "E");
3721
4484
  const rootKind = Number(firstTagValue(target.tags, "K"));
3722
4485
  const rootAuthor = firstTagValue(target.tags, "P");
3723
- if (rootEventId === void 0 || !HEX64_RE3.test(rootEventId) || !Number.isSafeInteger(rootKind) || rootAuthor === void 0 || !HEX64_RE3.test(rootAuthor)) {
4486
+ if (rootEventId === void 0 || !HEX64_RE4.test(rootEventId) || !Number.isSafeInteger(rootKind) || rootAuthor === void 0 || !HEX64_RE4.test(rootAuthor)) {
3724
4487
  throw new Error(
3725
4488
  `kind:1111 comment ${targetEventId} carries no usable NIP-22 root scope (E/K/P tags) \u2014 cannot derive the thread root to reply into`
3726
4489
  );
@@ -3744,7 +4507,7 @@ async function runComment(args, deps) {
3744
4507
  let targetEventId;
3745
4508
  let body;
3746
4509
  try {
3747
- const { values, positionals } = parseArgs8({
4510
+ const { values, positionals } = parseArgs10({
3748
4511
  args,
3749
4512
  options: {
3750
4513
  ...COMMON_OPTIONS2,
@@ -3825,6 +4588,8 @@ async function runPr(args, deps) {
3825
4588
  switch (sub) {
3826
4589
  case "create":
3827
4590
  return runPrCreate(rest, deps);
4591
+ case "merge":
4592
+ return runPrMerge(rest, deps);
3828
4593
  case "status":
3829
4594
  return runStatusCommand(rest, deps, PR_STATUS_SPEC);
3830
4595
  // FREE read subcommands (#278): relay queries only, no payment path.
@@ -3839,12 +4604,75 @@ async function runPr(args, deps) {
3839
4604
  return 0;
3840
4605
  default:
3841
4606
  io.err(
3842
- sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
4607
+ sub === void 0 ? "missing subcommand: rig pr <create|merge|status|list|show>" : `unknown rig pr subcommand: ${sub}`
3843
4608
  );
3844
4609
  io.err(PR_USAGE);
3845
4610
  return 2;
3846
4611
  }
3847
4612
  }
4613
+ async function resolveRevisedRoot(deps, rootId, relayUrl, addr) {
4614
+ if (relayUrl === void 0) {
4615
+ throw new Error(
4616
+ "--revises needs the root patch from a relay; pass --relay <url> or configure a remote"
4617
+ );
4618
+ }
4619
+ const found = await queryRelay(
4620
+ relayUrl,
4621
+ { ids: [rootId] },
4622
+ TARGET_LOOKUP_TIMEOUT_MS,
4623
+ deps.webSocketFactory ?? defaultWebSocketFactory
4624
+ );
4625
+ const root = found.find((e) => e.id === rootId);
4626
+ if (!root) {
4627
+ throw new Error(
4628
+ `patch ${rootId} not found on ${relayUrl} \u2014 nothing was published or paid`
4629
+ );
4630
+ }
4631
+ if (root.kind !== PATCH_KIND3) {
4632
+ throw new Error(
4633
+ `event ${rootId} has kind ${root.kind}, not a kind:1617 patch \u2014 nothing was published or paid`
4634
+ );
4635
+ }
4636
+ const aTag = `${REPOSITORY_ANNOUNCEMENT_KIND3}:${addr.ownerPubkey}:${addr.repoId}`;
4637
+ if (!root.tags.some((t) => t[0] === "a" && t[1] === aTag)) {
4638
+ throw new Error(
4639
+ `patch ${rootId} belongs to a different repository than ${aTag} \u2014 nothing was published or paid`
4640
+ );
4641
+ }
4642
+ if (isPatchRevision(root)) {
4643
+ throw new Error(
4644
+ `patch ${rootId} is itself a revision \u2014 pass the ROOT patch id to --revises; nothing was published or paid`
4645
+ );
4646
+ }
4647
+ return { pubkey: root.pubkey };
4648
+ }
4649
+ async function warnIfRevisionIgnored(deps, args, rootAuthor) {
4650
+ const { io } = deps;
4651
+ const { identity, addr, relayUrl } = args;
4652
+ const me = identity.pubkey.toLowerCase();
4653
+ if (me === rootAuthor?.toLowerCase()) return;
4654
+ if (me === addr.ownerPubkey.toLowerCase()) return;
4655
+ let authorized;
4656
+ if (relayUrl !== void 0) {
4657
+ try {
4658
+ const remote = await fetchRemoteState({
4659
+ relayUrls: [relayUrl],
4660
+ ownerPubkey: addr.ownerPubkey,
4661
+ repoId: addr.repoId,
4662
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
4663
+ });
4664
+ authorized = authorizedAuthors(
4665
+ addr.ownerPubkey,
4666
+ remote.announceEvent?.tags ?? []
4667
+ );
4668
+ } catch {
4669
+ }
4670
+ }
4671
+ if (authorized?.has(me)) return;
4672
+ io.err(
4673
+ authorized === void 0 ? "warning: could not read the repo announcement to verify your maintainer status \u2014 unless you are the root patch's author or a declared maintainer, readers will IGNORE this revision." : `warning: you (${identity.pubkey.slice(0, 8)}\u2026) are neither the root patch's author nor a maintainer of 30617:${addr.ownerPubkey.slice(0, 8)}\u2026:${addr.repoId} \u2014 readers will IGNORE this revision. Publishing anyway (the relay is permissionless).`
4674
+ );
4675
+ }
3848
4676
  async function runPrCreate(rest, deps) {
3849
4677
  const { io } = deps;
3850
4678
  let flags;
@@ -3854,8 +4682,9 @@ async function runPrCreate(rest, deps) {
3854
4682
  let branch;
3855
4683
  let bodyFlag;
3856
4684
  let bodyFile;
4685
+ let revises;
3857
4686
  try {
3858
- const { values } = parseArgs8({
4687
+ const { values } = parseArgs10({
3859
4688
  args: rest,
3860
4689
  options: {
3861
4690
  ...COMMON_OPTIONS2,
@@ -3864,7 +4693,8 @@ async function runPrCreate(rest, deps) {
3864
4693
  "patch-file": { type: "string" },
3865
4694
  body: { type: "string" },
3866
4695
  "body-file": { type: "string" },
3867
- branch: { type: "string" }
4696
+ branch: { type: "string" },
4697
+ revises: { type: "string" }
3868
4698
  },
3869
4699
  allowPositionals: false
3870
4700
  });
@@ -3888,6 +4718,11 @@ async function runPrCreate(rest, deps) {
3888
4718
  branch = values.branch;
3889
4719
  bodyFlag = values.body;
3890
4720
  bodyFile = values["body-file"];
4721
+ if (values.revises !== void 0) {
4722
+ assertHex64(values.revises, "--revises");
4723
+ revises = values.revises;
4724
+ flags = { ...flags, standalone: true };
4725
+ }
3891
4726
  } catch (err) {
3892
4727
  io.err(err instanceof Error ? err.message : String(err));
3893
4728
  io.err(PR_CREATE_USAGE);
@@ -3912,12 +4747,16 @@ async function runPrCreate(rest, deps) {
3912
4747
  }
3913
4748
  let builtPatchText;
3914
4749
  let builtCommits = [];
4750
+ let rootAuthor;
3915
4751
  return runEvent({
3916
4752
  command: "pr",
3917
4753
  flags,
3918
4754
  deps,
3919
4755
  actionLabel: `patch ${JSON.stringify(title)}`,
3920
- buildEvent: async (addr) => {
4756
+ buildEvent: async (addr, relayUrl) => {
4757
+ if (revises !== void 0) {
4758
+ rootAuthor = (await resolveRevisedRoot(deps, revises, relayUrl, addr)).pubkey;
4759
+ }
3921
4760
  let patchText;
3922
4761
  let commits;
3923
4762
  if (range !== void 0) {
@@ -3952,9 +4791,16 @@ async function runPrCreate(rest, deps) {
3952
4791
  commits,
3953
4792
  branch,
3954
4793
  patchText,
3955
- body
4794
+ body,
4795
+ revises !== void 0 ? {
4796
+ rootEventId: revises,
4797
+ ...rootAuthor !== void 0 ? { rootAuthorPubkey: rootAuthor } : {}
4798
+ } : void 0
3956
4799
  );
3957
4800
  },
4801
+ // A revision from anyone but the root's author or a maintainer is ignored
4802
+ // by readers: warn, but publish (the relay is permissionless).
4803
+ preConfirm: revises === void 0 ? void 0 : (args) => warnIfRevisionIgnored(deps, args, rootAuthor),
3958
4804
  sendDaemon: (client, addr) => {
3959
4805
  if (builtPatchText === void 0) {
3960
4806
  throw new Error("internal: patch text not built before delegation");
@@ -3972,7 +4818,7 @@ async function runPrCreate(rest, deps) {
3972
4818
  }
3973
4819
  var STATUS_KIND_BY_VALUE = {
3974
4820
  open: STATUS_OPEN_KIND2,
3975
- applied: STATUS_APPLIED_KIND2,
4821
+ applied: STATUS_APPLIED_KIND3,
3976
4822
  closed: STATUS_CLOSED_KIND2,
3977
4823
  draft: STATUS_DRAFT_KIND2
3978
4824
  };
@@ -3999,7 +4845,7 @@ async function runStatusCommand(args, deps, spec, fixedStatus) {
3999
4845
  let targetEventId;
4000
4846
  let status;
4001
4847
  try {
4002
- const { values, positionals } = parseArgs8({
4848
+ const { values, positionals } = parseArgs10({
4003
4849
  args,
4004
4850
  options: COMMON_OPTIONS2,
4005
4851
  allowPositionals: true
@@ -4085,8 +4931,8 @@ async function warnIfNotMaintainer(deps, args) {
4085
4931
  // src/cli/entry.ts
4086
4932
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4087
4933
  import { homedir as homedir3 } from "os";
4088
- import { dirname as dirname4, join as join4 } from "path";
4089
- import { parseArgs as parseArgs9 } from "util";
4934
+ import { dirname as dirname4, join as join5 } from "path";
4935
+ import { parseArgs as parseArgs11 } from "util";
4090
4936
  var ENTRY_USAGE = `Usage: rig entry [<connector-url> | clear] [options]
4091
4937
 
4092
4938
  Name the TOON connector rig pays. A connector describes itself on GET /ilp
@@ -4110,8 +4956,8 @@ Options:
4110
4956
  --json machine-readable envelope
4111
4957
  -h, --help show this help`;
4112
4958
  function configPathFor2(env) {
4113
- const dir = env["TOON_CLIENT_HOME"] ?? join4(homedir3(), ".toon-client");
4114
- return join4(dir, "config.json");
4959
+ const dir = env["TOON_CLIENT_HOME"] ?? join5(homedir3(), ".toon-client");
4960
+ return join5(dir, "config.json");
4115
4961
  }
4116
4962
  function readEntryConfig(configPath) {
4117
4963
  try {
@@ -4156,7 +5002,7 @@ async function runEntry(args, deps) {
4156
5002
  let relayFlag;
4157
5003
  let json = false;
4158
5004
  try {
4159
- const parsed = parseArgs9({
5005
+ const parsed = parseArgs11({
4160
5006
  args,
4161
5007
  allowPositionals: true,
4162
5008
  options: {
@@ -4259,278 +5105,52 @@ async function runEntry(args, deps) {
4259
5105
  delete next.connectorUrl;
4260
5106
  delete next.relayUrl;
4261
5107
  } else {
4262
- next.connectorUrl = mutation.connectorUrl;
4263
- if (mutation.relayUrl !== void 0) next.relayUrl = mutation.relayUrl;
4264
- }
4265
- if (env["TOON_CONNECTOR"] || env["TOON_CLIENT_PROXY_URL"]) {
4266
- warnings.push(
4267
- "TOON_CONNECTOR / TOON_CLIENT_PROXY_URL is set in this shell and outranks the config file"
4268
- );
4269
- }
4270
- writeEntryConfig(configPath, next);
4271
- const wrote = {
4272
- connectorUrl: next.connectorUrl ?? null,
4273
- relayUrl: next.relayUrl ?? null
4274
- };
4275
- const state = effectiveEntry(env, next);
4276
- if (json) {
4277
- io.emitJson({
4278
- command: "entry",
4279
- ...state,
4280
- wrote,
4281
- configPath,
4282
- ...warnings.length > 0 ? { warnings } : {}
4283
- });
4284
- return 0;
4285
- }
4286
- if (mutation.kind === "clear") {
4287
- io.out(`Cleared the recorded connector in ${configPath}.`);
4288
- } else {
4289
- io.out(`Recorded connector ${mutation.connectorUrl} in ${configPath}.`);
4290
- if (mutation.relayUrl !== void 0)
4291
- io.out(`Recorded relay ${mutation.relayUrl}.`);
4292
- }
4293
- for (const w of warnings) io.out(`warning: ${w}`);
4294
- io.out(
4295
- `Effective connector: ${state.connectorUrl ?? "(none)"} [${label(state.connectorSource)}]`
4296
- );
4297
- return 0;
4298
- } catch (err) {
4299
- return emitCliError(io, json, "entry", err);
4300
- }
4301
- }
4302
-
4303
- // src/cli/fetch.ts
4304
- import { parseArgs as parseArgs10 } from "util";
4305
- var FETCH_USAGE = `Usage: rig fetch [remote] [options]
4306
-
4307
- Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
4308
- payments, no identity needed). Reads the kind:30618 repository state from the
4309
- remote's relay, downloads only the git objects this repository is missing
4310
- (SHA-1 verified), and updates the remote-tracking refs
4311
- (refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
4312
-
4313
- No merge happens: integrate with the git passthrough, e.g.
4314
- \`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
4315
- \`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
4316
-
4317
- The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
4318
- or \`rig clone\` writes; --repo-id/--owner override.
4319
-
4320
- Options:
4321
- --repo-id <id> repository id / NIP-34 d-tag (default: git config)
4322
- --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
4323
- --relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
4324
- --gateway <url> gateway to read objects through FIRST, on its raw-bytes
4325
- route <url>/raw/<txId>, ahead of the public gateway
4326
- list \u2014 name a private, local or air-gapped gateway to
4327
- fetch objects the public ones do not carry
4328
- (env RIG_ARWEAVE_GATEWAY also sets it)
4329
- --concurrency <n> parallel gateway downloads (default 8)
4330
- --json machine-readable result envelope
4331
- -h, --help show this help`;
4332
- var WS_URL_RE4 = /^wss?:\/\//i;
4333
- function localRefFor(refname, remote) {
4334
- if (refname.startsWith("refs/heads/")) {
4335
- return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
4336
- }
4337
- if (refname.startsWith("refs/tags/")) return refname;
4338
- return null;
4339
- }
4340
- async function resolveLocalRef(repoRoot, refname) {
4341
- try {
4342
- const out = await runGit(repoRoot, [
4343
- "rev-parse",
4344
- "--verify",
4345
- "--quiet",
4346
- refname
4347
- ]);
4348
- const sha = out.trim();
4349
- return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
4350
- } catch {
4351
- return null;
4352
- }
4353
- }
4354
- async function runFetch(args, deps) {
4355
- const { io } = deps;
4356
- let json = false;
4357
- let remote = "origin";
4358
- let relayFlag;
4359
- let repoIdFlag;
4360
- let ownerFlag;
4361
- let gatewayFlag;
4362
- let concurrency;
4363
- try {
4364
- const { values, positionals } = parseArgs10({
4365
- args,
4366
- options: {
4367
- json: { type: "boolean", default: false },
4368
- relay: { type: "string" },
4369
- "repo-id": { type: "string" },
4370
- owner: { type: "string" },
4371
- gateway: { type: "string" },
4372
- concurrency: { type: "string" },
4373
- help: { type: "boolean", short: "h", default: false }
4374
- },
4375
- allowPositionals: true
4376
- });
4377
- if (values.help) {
4378
- io.out(FETCH_USAGE);
4379
- return 0;
4380
- }
4381
- json = values.json === true;
4382
- if (positionals.length > 1) {
4383
- throw new Error(
4384
- `expected at most one [remote] argument, got ${positionals.length}`
4385
- );
4386
- }
4387
- if (positionals[0] !== void 0) remote = positionals[0];
4388
- relayFlag = values.relay;
4389
- repoIdFlag = values["repo-id"];
4390
- ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
4391
- gatewayFlag = values.gateway;
4392
- if (values.concurrency !== void 0) {
4393
- concurrency = Number.parseInt(values.concurrency, 10);
4394
- if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
4395
- throw new Error("--concurrency must be a positive integer");
4396
- }
4397
- }
4398
- } catch (err) {
4399
- io.err(err instanceof Error ? err.message : String(err));
4400
- io.err(FETCH_USAGE);
4401
- return 2;
4402
- }
4403
- try {
4404
- const repoRoot = await resolveRepoRoot(deps.cwd);
4405
- const toonConfig = await readToonConfig(repoRoot);
4406
- const repoId = repoIdFlag ?? toonConfig.repoId;
4407
- if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
4408
- const owner = ownerFlag ?? toonConfig.owner;
4409
- if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
4410
- let relayUrl;
4411
- if (relayFlag !== void 0) {
4412
- relayUrl = relayFlag;
4413
- } else {
4414
- const urls = await getGitRemoteUrls(repoRoot, remote);
4415
- if (urls.length === 0) throw new UnknownRemoteError(remote);
4416
- if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
4417
- relayUrl = urls[0];
4418
- }
4419
- if (!WS_URL_RE4.test(relayUrl)) {
4420
- throw new InvalidRelayUrlError(
4421
- relayUrl,
4422
- `remote ${JSON.stringify(remote)}`
4423
- );
4424
- }
4425
- const remoteState = await fetchRemoteState({
4426
- relayUrls: [relayUrl],
4427
- ownerPubkey: owner,
4428
- repoId,
4429
- ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
4430
- // The GraphQL fallback for SHAs the map does not cover asks the SAME
4431
- // permaweb the objects are read from (#183).
4432
- resolveSha: deps.resolveSha ?? shaResolverFor(gatewayFlag, deps.env)
4433
- });
4434
- const planned = [];
4435
- for (const [refname, sha] of remoteState.refs) {
4436
- if (!isSafeRefname(refname)) {
4437
- io.err(
4438
- `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
4439
- );
4440
- continue;
4441
- }
4442
- const localRef = localRefFor(refname, remote);
4443
- if (localRef === null) {
4444
- io.err(
4445
- `warning: skipping ref outside refs/heads and refs/tags: ${refname}`
4446
- );
4447
- continue;
4448
- }
4449
- planned.push({ refname, localRef, newSha: sha });
4450
- }
4451
- const reader = new GitRepoReader(repoRoot);
4452
- const tips = [...new Set(planned.map((p) => p.newSha))];
4453
- const presentLocally = new Set(await reader.listAllObjectShas());
4454
- const collected = await collectRepoObjects({
4455
- tips,
4456
- shaToTxId: remoteState.shaToTxId,
4457
- resolveMissing: (shas) => remoteState.resolveMissing(shas),
4458
- presentLocally,
4459
- // `--gateway` / RIG_ARWEAVE_GATEWAY first, public list behind it (#176).
4460
- gateways: readGateways(gatewayFlag, deps.env),
4461
- ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
4462
- ...concurrency !== void 0 ? { concurrency } : {}
4463
- });
4464
- if (collected.missing.length > 0) {
4465
- throw new MissingRemoteObjectsError(
4466
- collected.missing,
4467
- `cannot fetch from ${relayUrl}`
4468
- );
4469
- }
4470
- const written = await writeGitObjects(repoRoot, collected.objects.values());
4471
- const updates = [];
4472
- for (const { refname, localRef, newSha } of planned) {
4473
- const oldSha = await resolveLocalRef(repoRoot, localRef);
4474
- if (oldSha === newSha) {
4475
- updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
4476
- continue;
4477
- }
4478
- let kind;
4479
- if (oldSha === null) {
4480
- kind = "new";
4481
- } else {
4482
- kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
4483
- }
4484
- await updateRef(repoRoot, localRef, newSha);
4485
- updates.push({ refname, localRef, oldSha, newSha, kind });
5108
+ next.connectorUrl = mutation.connectorUrl;
5109
+ if (mutation.relayUrl !== void 0) next.relayUrl = mutation.relayUrl;
4486
5110
  }
4487
- const moved = updates.filter((u) => u.kind !== "up-to-date");
5111
+ if (env["TOON_CONNECTOR"] || env["TOON_CLIENT_PROXY_URL"]) {
5112
+ warnings.push(
5113
+ "TOON_CONNECTOR / TOON_CLIENT_PROXY_URL is set in this shell and outranks the config file"
5114
+ );
5115
+ }
5116
+ writeEntryConfig(configPath, next);
5117
+ const wrote = {
5118
+ connectorUrl: next.connectorUrl ?? null,
5119
+ relayUrl: next.relayUrl ?? null
5120
+ };
5121
+ const state = effectiveEntry(env, next);
4488
5122
  if (json) {
4489
5123
  io.emitJson({
4490
- command: "fetch",
4491
- repoAddr: { ownerPubkey: owner, repoId },
4492
- remote,
4493
- relay: relayUrl,
4494
- objectsDownloaded: written,
4495
- updates,
4496
- executed: true
5124
+ command: "entry",
5125
+ ...state,
5126
+ wrote,
5127
+ configPath,
5128
+ ...warnings.length > 0 ? { warnings } : {}
4497
5129
  });
4498
5130
  return 0;
4499
5131
  }
4500
- if (moved.length === 0) {
4501
- io.out("Already up to date.");
4502
- return 0;
4503
- }
4504
- io.out(`From ${relayUrl}`);
4505
- for (const u of moved) {
4506
- const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
4507
- const src = short(u.refname);
4508
- const dst = short(u.localRef);
4509
- if (u.kind === "new") {
4510
- const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
4511
- io.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
4512
- } else if (u.kind === "forced") {
4513
- io.out(
4514
- ` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
4515
- );
4516
- } else {
4517
- io.out(
4518
- ` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
4519
- );
4520
- }
5132
+ if (mutation.kind === "clear") {
5133
+ io.out(`Cleared the recorded connector in ${configPath}.`);
5134
+ } else {
5135
+ io.out(`Recorded connector ${mutation.connectorUrl} in ${configPath}.`);
5136
+ if (mutation.relayUrl !== void 0)
5137
+ io.out(`Recorded relay ${mutation.relayUrl}.`);
4521
5138
  }
4522
- io.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
5139
+ for (const w of warnings) io.out(`warning: ${w}`);
5140
+ io.out(
5141
+ `Effective connector: ${state.connectorUrl ?? "(none)"} [${label(state.connectorSource)}]`
5142
+ );
4523
5143
  return 0;
4524
5144
  } catch (err) {
4525
- return emitCliError(io, json, "fetch", err);
5145
+ return emitCliError(io, json, "entry", err);
4526
5146
  }
4527
5147
  }
4528
5148
 
4529
5149
  // src/cli/fund.ts
4530
5150
  import { readFileSync as readFileSync4 } from "fs";
4531
5151
  import { homedir as homedir4 } from "os";
4532
- import { join as join5 } from "path";
4533
- import { parseArgs as parseArgs11 } from "util";
5152
+ import { join as join6 } from "path";
5153
+ import { parseArgs as parseArgs12 } from "util";
4534
5154
  var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
4535
5155
  var CHAINS = ["evm", "solana"];
4536
5156
  var CHAIN_ALIASES = {
@@ -4668,8 +5288,8 @@ function noFaucetGuidance(network, devnetOrigin) {
4668
5288
  return `${head}. If you meant the shared devnet, set TOON_CLIENT_NETWORK=devnet and re-run \`rig fund\` \u2014 no faucet URL is needed there. If this is a self-hosted network with its own faucet, set TOON_CLIENT_FAUCET_URL (or the faucetUrl config field). ${external}`;
4669
5289
  }
4670
5290
  function readFundConfig(env) {
4671
- const dir = env["TOON_CLIENT_HOME"] ?? join5(homedir4(), ".toon-client");
4672
- const configPath = join5(dir, "config.json");
5291
+ const dir = env["TOON_CLIENT_HOME"] ?? join6(homedir4(), ".toon-client");
5292
+ const configPath = join6(dir, "config.json");
4673
5293
  try {
4674
5294
  return {
4675
5295
  file: JSON.parse(readFileSync4(configPath, "utf8")),
@@ -4690,7 +5310,7 @@ async function runFund(args, deps) {
4690
5310
  let addressFlag;
4691
5311
  let json = false;
4692
5312
  try {
4693
- const { values, positionals } = parseArgs11({
5313
+ const { values, positionals } = parseArgs12({
4694
5314
  args,
4695
5315
  allowPositionals: true,
4696
5316
  options: {
@@ -4916,7 +5536,7 @@ var runGitPassthrough = (argv, options = {}) => {
4916
5536
  import { existsSync, mkdirSync as mkdirSync4 } from "fs";
4917
5537
  import { dirname as dirname5 } from "path";
4918
5538
  import { createInterface } from "readline/promises";
4919
- import { parseArgs as parseArgs12 } from "util";
5539
+ import { parseArgs as parseArgs13 } from "util";
4920
5540
  async function loadKeyOps() {
4921
5541
  const { generateKeystore, importKeystore } = await import("@toon-protocol/client");
4922
5542
  return { generateKeystore, importKeystore, deriveNostrKeyFromMnemonic };
@@ -5088,7 +5708,7 @@ async function runIdentity(args, deps) {
5088
5708
  }
5089
5709
  }
5090
5710
  function parseMintArgs(args, verb) {
5091
- const { values, positionals } = parseArgs12({
5711
+ const { values, positionals } = parseArgs13({
5092
5712
  args,
5093
5713
  options: {
5094
5714
  force: { type: "boolean", default: false },
@@ -5193,7 +5813,7 @@ async function runIdentityShow(args, deps) {
5193
5813
  let json = false;
5194
5814
  let help = false;
5195
5815
  try {
5196
- const parsed = parseArgs12({
5816
+ const parsed = parseArgs13({
5197
5817
  args,
5198
5818
  options: {
5199
5819
  json: { type: "boolean", default: false },
@@ -5300,9 +5920,569 @@ async function defaultReadSecretLine(prompt, isInteractive) {
5300
5920
  }
5301
5921
  }
5302
5922
 
5923
+ // src/cli/inbox.ts
5924
+ import { parseArgs as parseArgs14 } from "util";
5925
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
5926
+ import { join as join7 } from "path";
5927
+ import {
5928
+ ISSUE_KIND as ISSUE_KIND2,
5929
+ PATCH_KIND as PATCH_KIND4,
5930
+ REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND4,
5931
+ STATUS_APPLIED_KIND as STATUS_APPLIED_KIND4,
5932
+ STATUS_CLOSED_KIND as STATUS_CLOSED_KIND3,
5933
+ STATUS_DRAFT_KIND as STATUS_DRAFT_KIND3,
5934
+ STATUS_OPEN_KIND as STATUS_OPEN_KIND3
5935
+ } from "@toon-protocol/core/nip34";
5936
+
5937
+ // src/cli/relay-follow.ts
5938
+ var WS_OPEN = 1;
5939
+ var CONNECT_TIMEOUT_MS = 1e4;
5940
+ function defaultBackoffMs(attempt) {
5941
+ return Math.min(3e4, 1e3 * 2 ** Math.max(0, attempt - 1));
5942
+ }
5943
+ function connectOnce(opts, onSubscribed) {
5944
+ return new Promise((resolve2) => {
5945
+ let ws;
5946
+ let done = false;
5947
+ let subscribed = false;
5948
+ const subIds = [];
5949
+ let connectTimer;
5950
+ const finish = (reason) => {
5951
+ if (done) return;
5952
+ done = true;
5953
+ clearTimeout(connectTimer);
5954
+ opts.signal.removeEventListener("abort", onAbort);
5955
+ try {
5956
+ if (ws.readyState === WS_OPEN) {
5957
+ for (const id of subIds) ws.send(JSON.stringify(["CLOSE", id]));
5958
+ }
5959
+ ws.close();
5960
+ } catch {
5961
+ }
5962
+ resolve2(reason);
5963
+ };
5964
+ const onAbort = () => finish(null);
5965
+ try {
5966
+ ws = opts.webSocketFactory(opts.url);
5967
+ } catch (err) {
5968
+ resolve2(`failed to connect to ${opts.url}: ${String(err)}`);
5969
+ return;
5970
+ }
5971
+ opts.signal.addEventListener("abort", onAbort);
5972
+ connectTimer = setTimeout(
5973
+ () => finish(`timed out connecting to ${opts.url}`),
5974
+ CONNECT_TIMEOUT_MS
5975
+ );
5976
+ ws.addEventListener("open", () => {
5977
+ clearTimeout(connectTimer);
5978
+ opts.filters().forEach((filter, i) => {
5979
+ const subId = `follow-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`;
5980
+ subIds.push(subId);
5981
+ ws.send(JSON.stringify(["REQ", subId, filter]));
5982
+ });
5983
+ });
5984
+ ws.addEventListener("message", (msgEvent) => {
5985
+ try {
5986
+ const msg = JSON.parse(String(msgEvent.data));
5987
+ if (msg[0] === "EVENT" && msg[2] !== void 0) {
5988
+ const event = decodeEventPayload(msg[2]);
5989
+ if (event) opts.onEvent(event, opts.url);
5990
+ } else if (msg[0] === "EOSE" && !subscribed && subIds.includes(msg[1])) {
5991
+ subscribed = true;
5992
+ onSubscribed();
5993
+ }
5994
+ } catch {
5995
+ }
5996
+ });
5997
+ ws.addEventListener("error", (event) => {
5998
+ const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
5999
+ finish(`websocket error on ${opts.url}: ${detail}`);
6000
+ });
6001
+ ws.addEventListener("close", () => {
6002
+ finish(`connection to ${opts.url} closed`);
6003
+ });
6004
+ });
6005
+ }
6006
+ async function followRelay(opts) {
6007
+ let attempt = 0;
6008
+ while (!opts.signal.aborted) {
6009
+ const reason = await connectOnce(opts, () => {
6010
+ attempt = 0;
6011
+ });
6012
+ if (opts.signal.aborted) return;
6013
+ opts.onError(reason ?? `connection to ${opts.url} ended`);
6014
+ attempt += 1;
6015
+ await new Promise((resolve2) => {
6016
+ const done = () => {
6017
+ clearTimeout(timer);
6018
+ opts.signal.removeEventListener("abort", done);
6019
+ resolve2();
6020
+ };
6021
+ const timer = setTimeout(done, opts.backoffMs(attempt));
6022
+ opts.signal.addEventListener("abort", done);
6023
+ });
6024
+ }
6025
+ }
6026
+
6027
+ // src/cli/inbox.ts
6028
+ var INBOX_USAGE = `Usage: rig inbox [options]
6029
+
6030
+ Print everything new since you last checked (free \u2014 relay reads only):
6031
+ patches and revisions, issues, comments, status changes and CI results for
6032
+ repos you own or maintain, plus any event that tags your pubkey.
6033
+
6034
+ Options:
6035
+ --since <time> read from a unix time, an ISO date, or a relative
6036
+ age (30m, 2h, 7d); does not move the cursor
6037
+ --all read everything, ignoring the cursor; does not move it
6038
+ --pubkey <pubkey> act as this pubkey (npub or hex) instead of the
6039
+ resolved identity
6040
+ --remote <name> read via this configured git remote (default: origin)
6041
+ --relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
6042
+ --json machine-readable array, one object per event
6043
+ --follow emit the backlog, then stay connected and stream new
6044
+ events as JSON lines (one object per line) until
6045
+ interrupted; reconnects when a relay drops
6046
+ -h, --help show this help`;
6047
+ var STATUS_KINDS2 = [
6048
+ STATUS_OPEN_KIND3,
6049
+ STATUS_APPLIED_KIND4,
6050
+ STATUS_CLOSED_KIND3,
6051
+ STATUS_DRAFT_KIND3
6052
+ ];
6053
+ var CI_KINDS = [
6054
+ CI_SERVICE_REQUEST_KIND,
6055
+ CI_SERVICE_STOP_KIND,
6056
+ CI_WORKFLOW_RESULT_KIND,
6057
+ CI_WORKFLOW_PROGRESS_KIND,
6058
+ CI_JOB_RESULT_KIND
6059
+ ];
6060
+ var INBOX_KINDS = [
6061
+ PATCH_KIND4,
6062
+ ISSUE_KIND2,
6063
+ COMMENT_KIND,
6064
+ LEGACY_COMMENT_KIND,
6065
+ ...STATUS_KINDS2,
6066
+ ...CI_KINDS
6067
+ ];
6068
+ var HEX64_RE5 = /^[0-9a-f]{64}$/;
6069
+ var INBOX_CURSOR_FILENAME = "inbox-cursors.json";
6070
+ function cursorPath(env) {
6071
+ return join7(clientStateDir(env), INBOX_CURSOR_FILENAME);
6072
+ }
6073
+ function readCursors(env) {
6074
+ try {
6075
+ const parsed = JSON.parse(readFileSync5(cursorPath(env), "utf8"));
6076
+ if (parsed && typeof parsed === "object") {
6077
+ return parsed;
6078
+ }
6079
+ } catch {
6080
+ }
6081
+ return {};
6082
+ }
6083
+ function writeCursor(env, pubkey, cursor) {
6084
+ const cursors = readCursors(env);
6085
+ cursors[pubkey] = cursor;
6086
+ mkdirSync5(clientStateDir(env), { recursive: true });
6087
+ writeFileSync4(cursorPath(env), JSON.stringify(cursors, null, 2) + "\n");
6088
+ }
6089
+ function parseSince(value, nowSec) {
6090
+ const trimmed = value.trim();
6091
+ if (/^\d+$/.test(trimmed)) return Number(trimmed);
6092
+ const rel = /^(\d+)([smhdw])$/.exec(trimmed);
6093
+ if (rel) {
6094
+ const unit = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[rel[2]];
6095
+ return nowSec - Number(rel[1]) * unit;
6096
+ }
6097
+ const ms = Date.parse(trimmed);
6098
+ if (Number.isNaN(ms)) {
6099
+ throw new Error(
6100
+ `--since must be a unix time, an ISO date, or an age like 2h (got ${JSON.stringify(value)})`
6101
+ );
6102
+ }
6103
+ return Math.floor(ms / 1e3);
6104
+ }
6105
+ var tagValue2 = firstTagValue;
6106
+ function hasTopic(event, topic) {
6107
+ return event.tags.some((t) => t[0] === "t" && t[1] === topic);
6108
+ }
6109
+ function typeOf(event) {
6110
+ if (event.kind === PATCH_KIND4) {
6111
+ if (hasTopic(event, "root-revision")) return "revision";
6112
+ if (hasTopic(event, "root") || tagValue2(event.tags, "e") === void 0) {
6113
+ return "patch";
6114
+ }
6115
+ return null;
6116
+ }
6117
+ if (event.kind === ISSUE_KIND2) return "issue";
6118
+ if (event.kind === COMMENT_KIND || event.kind === LEGACY_COMMENT_KIND) {
6119
+ return "comment";
6120
+ }
6121
+ if (STATUS_KINDS2.includes(event.kind)) return "status";
6122
+ if (CI_KINDS.includes(event.kind)) return "ci";
6123
+ return null;
6124
+ }
6125
+ function targetOf(event, type) {
6126
+ const root = tagValue2(event.tags, "E");
6127
+ const parent = tagValue2(event.tags, "e");
6128
+ switch (type) {
6129
+ case "patch":
6130
+ case "issue":
6131
+ return event.id;
6132
+ case "revision":
6133
+ return parent ?? event.id;
6134
+ case "comment":
6135
+ return root ?? parent ?? event.id;
6136
+ case "status":
6137
+ return parent ?? event.id;
6138
+ case "ci":
6139
+ return root ?? tagValue2(event.tags, "c") ?? event.id;
6140
+ }
6141
+ }
6142
+ function toInboxEntry(event) {
6143
+ const type = typeOf(event);
6144
+ if (type === null) return null;
6145
+ const repo = event.tags.find(
6146
+ (t) => t[0] === "a" && t[1]?.startsWith(`${REPOSITORY_ANNOUNCEMENT_KIND4}:`)
6147
+ )?.[1];
6148
+ return {
6149
+ id: event.id,
6150
+ kind: event.kind,
6151
+ type,
6152
+ repo: repo ?? null,
6153
+ target: targetOf(event, type),
6154
+ author: event.pubkey,
6155
+ created_at: event.created_at
6156
+ };
6157
+ }
6158
+ function newest(events) {
6159
+ let winner = null;
6160
+ for (const e of events) {
6161
+ if (winner === null || e.created_at > winner.created_at || e.created_at === winner.created_at && e.id < winner.id) {
6162
+ winner = e;
6163
+ }
6164
+ }
6165
+ return winner;
6166
+ }
6167
+ async function ownerAnnouncement(relays, owner, repoId, ws) {
6168
+ const found = await queryAll(
6169
+ relays,
6170
+ [
6171
+ {
6172
+ kinds: [REPOSITORY_ANNOUNCEMENT_KIND4],
6173
+ authors: [owner],
6174
+ "#d": [repoId]
6175
+ }
6176
+ ],
6177
+ ws
6178
+ ).catch(() => /* @__PURE__ */ new Map());
6179
+ return newest(
6180
+ [...found.values()].filter(
6181
+ (e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND4 && e.pubkey.toLowerCase() === owner && tagValue2(e.tags, "d") === repoId
6182
+ )
6183
+ );
6184
+ }
6185
+ function relaysTag(tags) {
6186
+ return tags.filter((t) => t[0] === "relays").flatMap((t) => t.slice(1)).filter((url) => WS_URL_RE.test(url));
6187
+ }
6188
+ var INBOX_OPTIONS = {
6189
+ json: { type: "boolean", default: false },
6190
+ follow: { type: "boolean", default: false },
6191
+ all: { type: "boolean", default: false },
6192
+ since: { type: "string" },
6193
+ pubkey: { type: "string" },
6194
+ remote: { type: "string" },
6195
+ relay: { type: "string", multiple: true },
6196
+ help: { type: "boolean", short: "h", default: false }
6197
+ };
6198
+ function renderLine(entry) {
6199
+ const when = new Date(entry.created_at * 1e3).toISOString().slice(0, 16).replace("T", " ");
6200
+ const repo = entry.repo ?? "(no repo)";
6201
+ return `${when} ${entry.type.padEnd(8)} ${entry.id.slice(0, 8)} \u2192 ${entry.target.slice(0, 8)} ${repo} (${entry.author.slice(0, 8)})`;
6202
+ }
6203
+ async function runInbox(args, deps) {
6204
+ const { io } = deps;
6205
+ let json = false;
6206
+ let all = false;
6207
+ let follow = false;
6208
+ let sinceArg;
6209
+ let pubkeyArg;
6210
+ let remote;
6211
+ let relayFlags = [];
6212
+ try {
6213
+ const { values, positionals } = parseArgs14({
6214
+ args,
6215
+ options: INBOX_OPTIONS,
6216
+ allowPositionals: true
6217
+ });
6218
+ if (values.help) {
6219
+ io.out(INBOX_USAGE);
6220
+ return 0;
6221
+ }
6222
+ if (positionals.length > 0) {
6223
+ throw new Error("rig inbox takes no positional arguments");
6224
+ }
6225
+ json = values.json === true;
6226
+ all = values.all === true;
6227
+ follow = values.follow === true;
6228
+ sinceArg = values.since;
6229
+ if (values.pubkey !== void 0) pubkeyArg = ownerToHex(values.pubkey);
6230
+ remote = values.remote;
6231
+ relayFlags = values.relay ?? [];
6232
+ if (all && sinceArg !== void 0) {
6233
+ throw new Error("--all and --since are mutually exclusive");
6234
+ }
6235
+ } catch (err) {
6236
+ io.err(err instanceof Error ? err.message : String(err));
6237
+ io.err(INBOX_USAGE);
6238
+ return 2;
6239
+ }
6240
+ try {
6241
+ const nowSec = Math.floor((deps.now ?? Date.now)() / 1e3);
6242
+ const override = sinceArg !== void 0 ? parseSince(sinceArg, nowSec) : null;
6243
+ const me = (pubkeyArg ?? (await (deps.resolveIdentityImpl ?? resolveIdentity)({
6244
+ env: deps.env,
6245
+ cwd: deps.cwd,
6246
+ warn: (line) => io.err(line)
6247
+ })).pubkey).toLowerCase();
6248
+ let repoRoot;
6249
+ let toonRelays = [];
6250
+ try {
6251
+ repoRoot = await resolveRepoRoot(deps.cwd);
6252
+ toonRelays = (await readToonConfig(repoRoot)).relays;
6253
+ } catch {
6254
+ }
6255
+ const resolved = await resolveRelays({
6256
+ relayFlags,
6257
+ remoteName: remote,
6258
+ repoRoot,
6259
+ toonRelays
6260
+ });
6261
+ const configured = resolved.relays.filter((url) => WS_URL_RE.test(url));
6262
+ if (configured.length === 0) {
6263
+ throw new InvalidRelayUrlError(
6264
+ resolved.relays[0] ?? "",
6265
+ "reads need a ws:// or wss:// relay"
6266
+ );
6267
+ }
6268
+ const ws = deps.webSocketFactory ?? defaultWebSocketFactory;
6269
+ const announcements = await queryAll(
6270
+ configured,
6271
+ [
6272
+ { kinds: [REPOSITORY_ANNOUNCEMENT_KIND4], authors: [me] },
6273
+ { kinds: [REPOSITORY_ANNOUNCEMENT_KIND4], "#maintainers": [me] }
6274
+ ],
6275
+ ws
6276
+ );
6277
+ const byRepo = /* @__PURE__ */ new Map();
6278
+ for (const e of announcements.values()) {
6279
+ if (e.kind !== REPOSITORY_ANNOUNCEMENT_KIND4) continue;
6280
+ const d = tagValue2(e.tags, "d");
6281
+ if (!d) continue;
6282
+ const key = `${e.pubkey.toLowerCase()}:${d}`;
6283
+ byRepo.set(key, [...byRepo.get(key) ?? [], e]);
6284
+ }
6285
+ const watched = [];
6286
+ for (const [key, events2] of byRepo) {
6287
+ const owner = events2[0].pubkey.toLowerCase();
6288
+ const repoId = key.slice(owner.length + 1);
6289
+ const latest = owner === me ? newest(events2) : await ownerAnnouncement(configured, owner, repoId, ws);
6290
+ if (latest === null) continue;
6291
+ if (owner !== me && !parseMaintainers(latest.tags).includes(me)) continue;
6292
+ watched.push({
6293
+ address: `${REPOSITORY_ANNOUNCEMENT_KIND4}:${owner}:${repoId}`,
6294
+ ownerPubkey: owner,
6295
+ authorized: authorizedAuthors(owner, latest.tags),
6296
+ relays: relaysTag(latest.tags)
6297
+ });
6298
+ }
6299
+ const cursor = readCursors(deps.env)[me];
6300
+ const since = all ? void 0 : override !== null ? override : cursor?.at;
6301
+ const base = since !== void 0 ? { since } : {};
6302
+ const events = /* @__PURE__ */ new Map();
6303
+ const collect = (found) => {
6304
+ for (const [id, e] of found) if (!events.has(id)) events.set(id, e);
6305
+ };
6306
+ collect(
6307
+ await queryAll(
6308
+ configured,
6309
+ [{ kinds: INBOX_KINDS, "#p": [me], ...base }],
6310
+ ws
6311
+ )
6312
+ );
6313
+ for (const repo of watched) {
6314
+ const relays = [.../* @__PURE__ */ new Set([...configured, ...repo.relays])];
6315
+ collect(
6316
+ await queryAll(
6317
+ relays,
6318
+ [{ kinds: INBOX_KINDS, "#a": [repo.address], ...base }],
6319
+ ws
6320
+ )
6321
+ );
6322
+ }
6323
+ const authCache = /* @__PURE__ */ new Map();
6324
+ for (const repo of watched) authCache.set(repo.address, repo.authorized);
6325
+ const authorizedFor = async (address) => {
6326
+ const hit = authCache.get(address);
6327
+ if (hit) return hit;
6328
+ const m = /^30617:([0-9a-f]{64}):(.+)$/i.exec(address);
6329
+ let set = /* @__PURE__ */ new Set();
6330
+ if (m) {
6331
+ const owner = m[1].toLowerCase();
6332
+ const latest = await ownerAnnouncement(
6333
+ configured,
6334
+ owner,
6335
+ m[2],
6336
+ ws
6337
+ );
6338
+ set = authorizedAuthors(owner, latest?.tags ?? []);
6339
+ }
6340
+ authCache.set(address, set);
6341
+ return set;
6342
+ };
6343
+ const toEntries = async (found) => {
6344
+ const targetAuthors = /* @__PURE__ */ new Map();
6345
+ const statusEvents = [...found.values()].filter(
6346
+ (e) => STATUS_KINDS2.includes(e.kind)
6347
+ );
6348
+ const unknownTargets = /* @__PURE__ */ new Set();
6349
+ for (const e of statusEvents) {
6350
+ const target = tagValue2(e.tags, "e");
6351
+ if (!target || !HEX64_RE5.test(target)) continue;
6352
+ const known = found.get(target);
6353
+ if (known) targetAuthors.set(target, known.pubkey.toLowerCase());
6354
+ else unknownTargets.add(target);
6355
+ }
6356
+ if (unknownTargets.size > 0) {
6357
+ const lookedUp = await queryAll(
6358
+ configured,
6359
+ [{ ids: [...unknownTargets] }],
6360
+ ws
6361
+ ).catch(() => /* @__PURE__ */ new Map());
6362
+ for (const [id, e] of lookedUp)
6363
+ targetAuthors.set(id, e.pubkey.toLowerCase());
6364
+ }
6365
+ const entries = [];
6366
+ for (const event of found.values()) {
6367
+ const entry = toInboxEntry(event);
6368
+ if (entry === null) continue;
6369
+ if (entry.type === "status") {
6370
+ if (entry.repo === null) continue;
6371
+ const repoWide = await authorizedFor(entry.repo);
6372
+ const targetAuthor = targetAuthors.get(entry.target);
6373
+ const authors = targetAuthor === void 0 ? repoWide : withTargetAuthor(repoWide, targetAuthor);
6374
+ if (!authors.has(event.pubkey.toLowerCase())) continue;
6375
+ }
6376
+ entries.push(entry);
6377
+ }
6378
+ return entries;
6379
+ };
6380
+ const entriesOf = await toEntries(events);
6381
+ const isFresh = (e) => all || override !== null || cursor === void 0 || e.created_at > cursor.at || e.created_at === cursor.at && !cursor.seen.includes(e.id);
6382
+ const byAge = (a, b) => a.created_at - b.created_at || (a.id < b.id ? -1 : 1);
6383
+ const fresh = entriesOf.filter(isFresh).sort(byAge);
6384
+ const moveCursor = !all && override === null;
6385
+ let position = cursor;
6386
+ const advance = (emitted) => {
6387
+ if (!moveCursor || emitted.length === 0) return;
6388
+ const at = Math.max(...emitted.map((e) => e.created_at));
6389
+ const seen = emitted.filter((e) => e.created_at === at).map((e) => e.id);
6390
+ if (position?.at === at) seen.push(...position.seen);
6391
+ else if (position !== void 0 && position.at > at) return;
6392
+ position = { at, seen: [...new Set(seen)] };
6393
+ writeCursor(deps.env, me, position);
6394
+ };
6395
+ if (follow) {
6396
+ const line = (entry) => io.emitJsonLine ? io.emitJsonLine(entry) : io.out(JSON.stringify(entry));
6397
+ const emittedIds = /* @__PURE__ */ new Set();
6398
+ let liveSince = position?.at;
6399
+ const emit = (entries) => {
6400
+ for (const entry of entries) {
6401
+ emittedIds.add(entry.id);
6402
+ liveSince = Math.max(liveSince ?? 0, entry.created_at);
6403
+ line(entry);
6404
+ }
6405
+ advance(entries);
6406
+ };
6407
+ emit(fresh);
6408
+ const controller = new AbortController();
6409
+ const stop = () => controller.abort();
6410
+ const external = deps.followSignal;
6411
+ if (external?.aborted) return 0;
6412
+ external?.addEventListener("abort", stop);
6413
+ if (!external) {
6414
+ process.once("SIGINT", stop);
6415
+ process.once("SIGTERM", stop);
6416
+ }
6417
+ let chain = Promise.resolve();
6418
+ const onEvent = (event) => {
6419
+ chain = chain.then(async () => {
6420
+ if (emittedIds.has(event.id)) return;
6421
+ try {
6422
+ const [entry] = await toEntries(/* @__PURE__ */ new Map([[event.id, event]]));
6423
+ if (entry === void 0 || emittedIds.has(entry.id) || !isFresh(entry)) {
6424
+ return;
6425
+ }
6426
+ emit([entry]);
6427
+ } catch (err) {
6428
+ io.err(
6429
+ `rig inbox: could not process live event ${event.id}: ${err instanceof Error ? err.message : String(err)}`
6430
+ );
6431
+ }
6432
+ });
6433
+ };
6434
+ const targets = [];
6435
+ for (const url of configured) {
6436
+ targets.push({ url, filter: { kinds: INBOX_KINDS, "#p": [me] } });
6437
+ }
6438
+ for (const repo of watched) {
6439
+ for (const url of /* @__PURE__ */ new Set([...configured, ...repo.relays])) {
6440
+ targets.push({
6441
+ url,
6442
+ filter: { kinds: INBOX_KINDS, "#a": [repo.address] }
6443
+ });
6444
+ }
6445
+ }
6446
+ const urls = [...new Set(targets.map((t) => t.url))];
6447
+ try {
6448
+ await Promise.all(
6449
+ urls.map(
6450
+ (url) => followRelay({
6451
+ url,
6452
+ filters: () => targets.filter((t) => t.url === url).map((t) => ({ ...t.filter, since: liveSince ?? nowSec })),
6453
+ webSocketFactory: ws,
6454
+ onEvent,
6455
+ onError: (message) => io.err(`rig inbox: ${message}`),
6456
+ signal: controller.signal,
6457
+ backoffMs: deps.followBackoffMs ?? defaultBackoffMs
6458
+ })
6459
+ )
6460
+ );
6461
+ await chain;
6462
+ } finally {
6463
+ external?.removeEventListener("abort", stop);
6464
+ process.off("SIGINT", stop);
6465
+ process.off("SIGTERM", stop);
6466
+ }
6467
+ return 0;
6468
+ }
6469
+ if (json) {
6470
+ io.emitJson(fresh);
6471
+ } else if (fresh.length === 0) {
6472
+ io.out("inbox is empty");
6473
+ } else {
6474
+ for (const entry of fresh) io.out(renderLine(entry));
6475
+ }
6476
+ advance(fresh);
6477
+ return 0;
6478
+ } catch (err) {
6479
+ return emitCliError(io, json, "inbox", err);
6480
+ }
6481
+ }
6482
+
5303
6483
  // src/cli/init.ts
5304
6484
  import { basename as basename2 } from "path";
5305
- import { parseArgs as parseArgs13 } from "util";
6485
+ import { parseArgs as parseArgs15 } from "util";
5306
6486
 
5307
6487
  // src/cli/git-author.ts
5308
6488
  var PROFILE_KIND = 0;
@@ -5420,7 +6600,7 @@ Options:
5420
6600
  --json machine-readable report
5421
6601
  -h, --help show this help`;
5422
6602
  function parseInitArgs(args) {
5423
- const { values, positionals } = parseArgs13({
6603
+ const { values, positionals } = parseArgs15({
5424
6604
  args,
5425
6605
  options: {
5426
6606
  "repo-id": { type: "string" },
@@ -5647,8 +6827,8 @@ async function runInit(args, deps) {
5647
6827
  }
5648
6828
 
5649
6829
  // src/cli/maintainers.ts
5650
- import { parseArgs as parseArgs14 } from "util";
5651
- var HEX64_RE4 = /^[0-9a-f]{64}$/;
6830
+ import { parseArgs as parseArgs16 } from "util";
6831
+ var HEX64_RE6 = /^[0-9a-f]{64}$/;
5652
6832
  var REF_AUTHORITY_NOTICE = "Note: maintainers can move refs (publish honored repo state), not only set status.";
5653
6833
  var MAINTAINERS_USAGE = `Usage: rig maintainers <list|add|remove> [<pubkey>] [options]
5654
6834
 
@@ -5701,7 +6881,7 @@ async function runList2(args, deps) {
5701
6881
  const { io } = deps;
5702
6882
  let flags;
5703
6883
  try {
5704
- const { values, positionals } = parseArgs14({
6884
+ const { values, positionals } = parseArgs16({
5705
6885
  args,
5706
6886
  options: REPO_COMMAND_OPTIONS,
5707
6887
  allowPositionals: true
@@ -5768,7 +6948,7 @@ async function runMutate(op, args, deps) {
5768
6948
  let flags;
5769
6949
  let pubkey;
5770
6950
  try {
5771
- const { values, positionals } = parseArgs14({
6951
+ const { values, positionals } = parseArgs16({
5772
6952
  args,
5773
6953
  options: REPO_COMMAND_OPTIONS,
5774
6954
  allowPositionals: true
@@ -5782,7 +6962,7 @@ async function runMutate(op, args, deps) {
5782
6962
  throw new Error(`expected exactly one <pubkey> to ${op}`);
5783
6963
  }
5784
6964
  pubkey = ownerToHex(positionals[0]).toLowerCase();
5785
- if (!HEX64_RE4.test(pubkey)) {
6965
+ if (!HEX64_RE6.test(pubkey)) {
5786
6966
  throw new Error(
5787
6967
  `<pubkey> must resolve to 64-char hex (got ${JSON.stringify(positionals[0])})`
5788
6968
  );
@@ -5903,7 +7083,7 @@ async function runMutate(op, args, deps) {
5903
7083
  }
5904
7084
 
5905
7085
  // src/cli/refresh.ts
5906
- import { parseArgs as parseArgs15 } from "util";
7086
+ import { parseArgs as parseArgs17 } from "util";
5907
7087
  var REFRESH_USAGE = `Usage: rig refresh [options]
5908
7088
 
5909
7089
  Republish this repo's kind:30617 announcement with no field edit, refreshing
@@ -5939,7 +7119,7 @@ async function runRefresh(args, deps) {
5939
7119
  const { io } = deps;
5940
7120
  let flags;
5941
7121
  try {
5942
- const { values, positionals } = parseArgs15({
7122
+ const { values, positionals } = parseArgs17({
5943
7123
  args,
5944
7124
  options: REPO_COMMAND_OPTIONS,
5945
7125
  allowPositionals: true
@@ -6062,7 +7242,7 @@ async function runRefresh(args, deps) {
6062
7242
 
6063
7243
  // src/cli/name.ts
6064
7244
  import { randomUUID } from "crypto";
6065
- import { parseArgs as parseArgs16 } from "util";
7245
+ import { parseArgs as parseArgs18 } from "util";
6066
7246
  var ARIO_DECIMALS = 6;
6067
7247
  function renderMario(mario) {
6068
7248
  return `${mario.toString()} mARIO (~${formatUnits(mario.toString(), ARIO_DECIMALS)} ARIO)`;
@@ -6571,7 +7751,7 @@ async function resolveNameContext(deps, network, processId, mode) {
6571
7751
  return { identity, solanaAddress, network, processId, sdk, nostrSecretKey };
6572
7752
  }
6573
7753
  function parseNameFlags(args, env) {
6574
- const { values, positionals } = parseArgs16({
7754
+ const { values, positionals } = parseArgs18({
6575
7755
  args,
6576
7756
  allowPositionals: true,
6577
7757
  options: {
@@ -7130,7 +8310,7 @@ async function runName(args, deps) {
7130
8310
  }
7131
8311
 
7132
8312
  // src/cli/payout.ts
7133
- import { parseArgs as parseArgs17 } from "util";
8313
+ import { parseArgs as parseArgs19 } from "util";
7134
8314
  import { getAddress } from "viem";
7135
8315
  var PAYOUT_USAGE = `Usage: rig payout <set|clear|show> [<address>] [options]
7136
8316
 
@@ -7186,7 +8366,7 @@ async function runShow2(args, deps) {
7186
8366
  const { io } = deps;
7187
8367
  let flags;
7188
8368
  try {
7189
- const { values, positionals } = parseArgs17({
8369
+ const { values, positionals } = parseArgs19({
7190
8370
  args,
7191
8371
  options: REPO_COMMAND_OPTIONS,
7192
8372
  allowPositionals: true
@@ -7248,7 +8428,7 @@ async function runMutate2(op, args, deps) {
7248
8428
  let flags;
7249
8429
  let nextPayout = null;
7250
8430
  try {
7251
- const { values, positionals } = parseArgs17({
8431
+ const { values, positionals } = parseArgs19({
7252
8432
  args,
7253
8433
  options: REPO_COMMAND_OPTIONS,
7254
8434
  allowPositionals: true
@@ -7382,7 +8562,7 @@ async function runMutate2(op, args, deps) {
7382
8562
  }
7383
8563
 
7384
8564
  // src/cli/site.ts
7385
- import { parseArgs as parseArgs18 } from "util";
8565
+ import { parseArgs as parseArgs20 } from "util";
7386
8566
 
7387
8567
  // src/arweave-manifest.ts
7388
8568
  var DEFAULT_INDEX = "index.html";
@@ -7401,22 +8581,22 @@ function buildArweaveManifest(entries, indexPath = DEFAULT_INDEX, fallbackTxId)
7401
8581
  }
7402
8582
 
7403
8583
  // src/cli/site-record.ts
7404
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
8584
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
7405
8585
  import { homedir as homedir5 } from "os";
7406
- import { join as join6 } from "path";
8586
+ import { join as join8 } from "path";
7407
8587
  var SITE_MANIFEST_FILENAME = "site-manifests.json";
7408
8588
  function storeDir2(env) {
7409
- return env["TOON_CLIENT_HOME"] ?? join6(homedir5(), ".toon-client");
8589
+ return env["TOON_CLIENT_HOME"] ?? join8(homedir5(), ".toon-client");
7410
8590
  }
7411
8591
  function storePath2(env) {
7412
- return join6(storeDir2(env), SITE_MANIFEST_FILENAME);
8592
+ return join8(storeDir2(env), SITE_MANIFEST_FILENAME);
7413
8593
  }
7414
8594
  function keyOf(repoId, ref) {
7415
8595
  return `${repoId}\0${ref}`;
7416
8596
  }
7417
8597
  function readFile2(env) {
7418
8598
  try {
7419
- const parsed = JSON.parse(readFileSync5(storePath2(env), "utf8"));
8599
+ const parsed = JSON.parse(readFileSync6(storePath2(env), "utf8"));
7420
8600
  if (parsed && typeof parsed === "object") return parsed;
7421
8601
  return {};
7422
8602
  } catch {
@@ -7428,10 +8608,10 @@ function readSiteRecord(env, repoId, ref) {
7428
8608
  }
7429
8609
  function writeSiteRecord(env, record) {
7430
8610
  const dir = storeDir2(env);
7431
- mkdirSync5(dir, { recursive: true });
8611
+ mkdirSync6(dir, { recursive: true });
7432
8612
  const file = readFile2(env);
7433
8613
  file[keyOf(record.repoId, record.ref)] = record;
7434
- writeFileSync4(storePath2(env), `${JSON.stringify(file, null, 2)}
8614
+ writeFileSync5(storePath2(env), `${JSON.stringify(file, null, 2)}
7435
8615
  `, "utf8");
7436
8616
  }
7437
8617
 
@@ -7473,7 +8653,7 @@ Options (publish):
7473
8653
  estimate (nothing uploaded)
7474
8654
  -h, --help show this help`;
7475
8655
  function parseSiteArgs(args) {
7476
- const { values, positionals } = parseArgs18({
8656
+ const { values, positionals } = parseArgs20({
7477
8657
  args,
7478
8658
  options: {
7479
8659
  index: { type: "string" },
@@ -7881,9 +9061,18 @@ Commands rig owns:
7881
9061
  comment <target-event-id> comment (NIP-22 kind:1111) on an issue or patch,
7882
9062
  or reply to an existing kind:1111 comment
7883
9063
  pr create publish a patch (kind:1617) with real
7884
- \`git format-patch\` content
9064
+ \`git format-patch\` content (--revises <patch-id>
9065
+ publishes a revision of an existing patch)
9066
+ pr merge <patch-id> apply the latest revision with git am, push, then
9067
+ publish the applied status (owner/maintainer)
7885
9068
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
7886
9069
  open | applied | closed | draft
9070
+ inbox everything new since you last read it (free):
9071
+ patches, revisions, issues, comments, statuses
9072
+ and CI results for repos you own or maintain,
9073
+ plus events that tag you; --since/--all look
9074
+ back without moving the cursor; --follow keeps
9075
+ streaming new events as JSON lines
7887
9076
  maintainers list show the repo's declared maintainers (free); add/
7888
9077
  maintainers add <pubkey> remove republish the kind:30617 to change who may
7889
9078
  maintainers remove <pubkey> author authoritative issue/PR status (owner is
@@ -8006,6 +9195,8 @@ async function dispatch(argv, deps) {
8006
9195
  return runComment(rest, deps);
8007
9196
  case "pr":
8008
9197
  return runPr(rest, deps);
9198
+ case "inbox":
9199
+ return runInbox(rest, deps);
8009
9200
  case "maintainers":
8010
9201
  return runMaintainers(rest, deps);
8011
9202
  case "payout":
@@ -8062,4 +9253,4 @@ export {
8062
9253
  rigVersion,
8063
9254
  dispatch
8064
9255
  };
8065
- //# sourceMappingURL=chunk-ANVDRW5F.js.map
9256
+ //# sourceMappingURL=chunk-KQ4NQV7T.js.map