@toon-protocol/rig 2.1.0 → 2.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.
package/dist/cli/rig.js CHANGED
@@ -4,27 +4,42 @@ import {
4
4
  GitRepoReader,
5
5
  NonFastForwardError,
6
6
  OversizeObjectsError,
7
+ collectRepoObjects,
7
8
  executePush,
9
+ isSafeRefname,
10
+ missingObjectsMessage,
11
+ ownerToHex,
8
12
  planPush,
13
+ runGit,
9
14
  serializeEventReceipt,
10
15
  serializePushPlan,
11
- serializePushResult
12
- } from "../chunk-LFGDLD6J.js";
16
+ serializePushResult,
17
+ setHeadSymref,
18
+ updateRef,
19
+ writeGitObjects
20
+ } from "../chunk-W2SDL2PE.js";
13
21
  import {
14
22
  MissingIdentityError,
15
23
  resolveIdentity
16
24
  } from "../chunk-CW4HJNMU.js";
17
25
  import {
26
+ COMMENT_KIND,
27
+ authorizedStatusAuthors,
18
28
  buildComment,
19
29
  buildIssue,
20
30
  buildPatch,
21
- buildStatus
22
- } from "../chunk-HPSOQP7Q.js";
31
+ buildRepoAnnouncement,
32
+ buildStatus,
33
+ fetchRemoteState,
34
+ parseMaintainers,
35
+ queryRelay
36
+ } from "../chunk-3HRFDH7H.js";
23
37
  import {
24
38
  ChannelMapStore,
25
39
  channelStatus,
40
+ defaultDaemonPort,
26
41
  resolveChannelPaths
27
- } from "../chunk-O6TXHKWG.js";
42
+ } from "../chunk-SW7ZHMGS.js";
28
43
  import {
29
44
  MAX_OBJECT_SIZE
30
45
  } from "../chunk-X2CZPPDM.js";
@@ -38,6 +53,155 @@ import { createRequire } from "module";
38
53
  // src/cli/balance.ts
39
54
  import { parseArgs as parseArgs3 } from "util";
40
55
 
56
+ // src/cli/daemon-session.ts
57
+ function daemonBaseUrl(env) {
58
+ const raw = env["TOON_CLIENT_HTTP_PORT"];
59
+ const parsed = raw ? Number(raw) : NaN;
60
+ const port = Number.isFinite(parsed) && parsed > 0 ? parsed : defaultDaemonPort();
61
+ return `http://127.0.0.1:${port}`;
62
+ }
63
+ async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
64
+ const baseUrl = daemonBaseUrl(env);
65
+ try {
66
+ const res = await fetchImpl(`${baseUrl}/status`, {
67
+ signal: AbortSignal.timeout(timeoutMs)
68
+ });
69
+ if (!res.ok) return { baseUrl, reachable: false };
70
+ const body = await res.json();
71
+ const probe = { baseUrl, reachable: true };
72
+ const pubkey = body?.identity?.nostrPubkey;
73
+ if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
74
+ if (typeof body?.ready === "boolean") probe.ready = body.ready;
75
+ if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
76
+ probe.relayUrl = body.relay.url;
77
+ }
78
+ if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
79
+ probe.feePerEvent = body.feePerEvent;
80
+ }
81
+ return probe;
82
+ } catch {
83
+ return { baseUrl, reachable: false };
84
+ }
85
+ }
86
+ var DaemonRouteError = class extends Error {
87
+ constructor(status, envelope) {
88
+ super(envelope.detail ?? envelope.error);
89
+ this.status = status;
90
+ this.envelope = envelope;
91
+ this.name = "DaemonRouteError";
92
+ }
93
+ status;
94
+ envelope;
95
+ };
96
+ var DaemonUnreachableError = class extends Error {
97
+ constructor(baseUrl, cause) {
98
+ super(
99
+ `toon-clientd stopped answering at ${baseUrl} after the identity probe \u2014 nothing was paid. Re-run: rig falls back to standalone automatically when no daemon responds` + (cause instanceof Error ? ` (${cause.message})` : "")
100
+ );
101
+ this.baseUrl = baseUrl;
102
+ this.name = "DaemonUnreachableError";
103
+ }
104
+ baseUrl;
105
+ };
106
+ var DaemonGitClient = class {
107
+ constructor(baseUrl, fetchImpl) {
108
+ this.baseUrl = baseUrl;
109
+ this.fetchImpl = fetchImpl;
110
+ }
111
+ baseUrl;
112
+ fetchImpl;
113
+ gitEstimate(req) {
114
+ return this.post("/git/estimate", req);
115
+ }
116
+ gitPush(req) {
117
+ return this.post("/git/push", req);
118
+ }
119
+ gitIssue(req) {
120
+ return this.post("/git/issue", req);
121
+ }
122
+ gitComment(req) {
123
+ return this.post("/git/comment", req);
124
+ }
125
+ gitPatch(req) {
126
+ return this.post("/git/patch", req);
127
+ }
128
+ gitStatus(req) {
129
+ return this.post("/git/status", req);
130
+ }
131
+ async post(path, body) {
132
+ let res;
133
+ try {
134
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, {
135
+ method: "POST",
136
+ headers: { "content-type": "application/json" },
137
+ body: JSON.stringify(body)
138
+ });
139
+ } catch (err) {
140
+ throw new DaemonUnreachableError(this.baseUrl, err);
141
+ }
142
+ const text = await res.text();
143
+ let parsed;
144
+ try {
145
+ parsed = text === "" ? {} : JSON.parse(text);
146
+ } catch {
147
+ throw new DaemonRouteError(res.status, {
148
+ error: "invalid_response",
149
+ detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
150
+ });
151
+ }
152
+ if (!res.ok) {
153
+ const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
154
+ throw new DaemonRouteError(res.status, envelope);
155
+ }
156
+ return parsed;
157
+ }
158
+ };
159
+ async function resolvePaidSession(options) {
160
+ const fetchImpl = options.fetchImpl ?? fetch;
161
+ const probe = await (options.probeDaemon ?? probeDaemon)(
162
+ options.env,
163
+ fetchImpl
164
+ );
165
+ if (probe.reachable && probe.identity !== void 0) {
166
+ const identity = await resolveIdentity({
167
+ env: options.env,
168
+ cwd: options.cwd,
169
+ warn: options.warn
170
+ });
171
+ if (identity.pubkey === probe.identity) {
172
+ options.warn(
173
+ `rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
174
+ );
175
+ return {
176
+ path: "daemon",
177
+ client: new DaemonGitClient(probe.baseUrl, fetchImpl),
178
+ baseUrl: probe.baseUrl,
179
+ identity: {
180
+ pubkey: identity.pubkey,
181
+ source: identity.source,
182
+ sourceLabel: identity.sourceLabel
183
+ },
184
+ ...probe.feePerEvent !== void 0 ? { feePerEvent: probe.feePerEvent } : {},
185
+ ...probe.relayUrl !== void 0 ? { daemonRelayUrl: probe.relayUrl } : {}
186
+ };
187
+ }
188
+ options.warn(
189
+ `rig: paid path: standalone \u2014 the toon-clientd at ${probe.baseUrl} runs a different identity (${probe.identity.slice(0, 8)}\u2026), no shared channel state`
190
+ );
191
+ } else {
192
+ options.warn(
193
+ `rig: paid path: standalone (no toon-clientd at ${probe.baseUrl})`
194
+ );
195
+ }
196
+ const ctx = await options.loadStandalone({
197
+ env: options.env,
198
+ cwd: options.cwd,
199
+ warn: options.warn,
200
+ ...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
201
+ });
202
+ return { path: "standalone", ctx };
203
+ }
204
+
41
205
  // src/cli/errors.ts
42
206
  var UnconfiguredRepoAddressError = class extends Error {
43
207
  constructor(missing) {
@@ -199,6 +363,39 @@ function describeError(err, command = "push") {
199
363
  json: { error: "git_error", detail: err.message }
200
364
  };
201
365
  }
366
+ if (err instanceof DaemonRouteError) {
367
+ const envelope = err.envelope;
368
+ if (envelope.error === "non_fast_forward" && Array.isArray(envelope["refs"])) {
369
+ return {
370
+ code: "non_fast_forward",
371
+ lines: nonFastForwardLines(envelope["refs"]),
372
+ json: { ...envelope }
373
+ };
374
+ }
375
+ if (envelope.error === "oversize_objects" && Array.isArray(envelope["objects"])) {
376
+ return {
377
+ code: "oversize_objects",
378
+ lines: oversizeLines(envelope["objects"]),
379
+ json: { ...envelope }
380
+ };
381
+ }
382
+ const retryHint = envelope.retryable === true ? ["The daemon reports this as retryable \u2014 re-run shortly."] : [];
383
+ return {
384
+ code: envelope.error,
385
+ lines: [
386
+ `daemon rejected the operation (HTTP ${err.status}): ` + (envelope.detail ?? envelope.error),
387
+ ...retryHint
388
+ ],
389
+ json: { ...envelope }
390
+ };
391
+ }
392
+ if (err instanceof DaemonUnreachableError) {
393
+ return {
394
+ code: "daemon_unreachable",
395
+ lines: err.message.split("\n"),
396
+ json: { error: "daemon_unreachable", detail: err.message }
397
+ };
398
+ }
202
399
  const name = err instanceof Error ? err.name : "";
203
400
  const message = err instanceof Error ? err.message : String(err);
204
401
  if (name === "DaemonIdentityConflictError") {
@@ -206,7 +403,7 @@ function describeError(err, command = "push") {
206
403
  code: "daemon_identity_conflict",
207
404
  lines: [
208
405
  message,
209
- "Stop the toon-clientd daemon (or publish through its toon_git_* MCP tools) and re-run."
406
+ "Paid writes delegate to a same-identity daemon automatically; seeing this means the daemon appeared mid-run or this operation has no daemon route \u2014 stop the daemon and re-run."
210
407
  ],
211
408
  json: { error: "daemon_identity_conflict", detail: message }
212
409
  };
@@ -240,6 +437,32 @@ function describeError(err, command = "push") {
240
437
  }
241
438
  };
242
439
  }
440
+ if (name === "RepoNotFoundError") {
441
+ return {
442
+ code: "repo_not_found",
443
+ lines: message.split("\n"),
444
+ json: { error: "repo_not_found", detail: message }
445
+ };
446
+ }
447
+ if (name === "MissingRemoteObjectsError") {
448
+ const missing = err.missing;
449
+ return {
450
+ code: "missing_remote_objects",
451
+ lines: message.split("\n"),
452
+ json: {
453
+ error: "missing_remote_objects",
454
+ detail: message,
455
+ ...Array.isArray(missing) ? { missing } : {}
456
+ }
457
+ };
458
+ }
459
+ if (name === "ObjectIntegrityError" || name === "ObjectWriteMismatchError") {
460
+ return {
461
+ code: "object_integrity",
462
+ lines: message.split("\n"),
463
+ json: { error: "object_integrity", detail: message }
464
+ };
465
+ }
243
466
  return {
244
467
  code: "error",
245
468
  lines: [`rig ${command} failed: ${message}`],
@@ -665,8 +888,19 @@ async function runRemote(args, deps) {
665
888
  }
666
889
 
667
890
  // src/cli/push.ts
891
+ function loadPaidSession(deps, relayUrl) {
892
+ return resolvePaidSession({
893
+ env: deps.env,
894
+ cwd: deps.cwd,
895
+ warn: (line) => deps.io.err(line),
896
+ loadStandalone: deps.loadStandalone ?? defaultLoadStandalone,
897
+ ...relayUrl !== void 0 ? { relayUrl } : {},
898
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
899
+ ...deps.probeDaemon ? { probeDaemon: deps.probeDaemon } : {}
900
+ });
901
+ }
668
902
  var defaultLoadStandalone = async (options) => {
669
- const mod = await import("../standalone-mode-FCKTQ33Y.js");
903
+ const mod = await import("../standalone-mode-JLGAUMRF.js");
670
904
  return mod.createStandaloneContext(options);
671
905
  };
672
906
  function identityReport(ctx) {
@@ -777,7 +1011,7 @@ function parsePushArgs(args) {
777
1011
  return flags;
778
1012
  }
779
1013
  async function runPush(args, deps) {
780
- const { io: io2, env } = deps;
1014
+ const { io: io2 } = deps;
781
1015
  let flags;
782
1016
  try {
783
1017
  flags = parsePushArgs(args);
@@ -835,40 +1069,74 @@ async function runPush(args, deps) {
835
1069
  io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
836
1070
  return 1;
837
1071
  }
838
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
839
- env,
840
- cwd: deps.cwd,
841
- warn: (line) => io2.err(line),
842
- // Relay-origin for #264 network bootstrap (announce discovery).
843
- ...relaysUsed[0] !== void 0 ? { relayUrl: relaysUsed[0] } : {}
844
- });
845
- const identity = identityReport(standaloneCtx);
1072
+ const session = await loadPaidSession(deps, relaysUsed[0]);
1073
+ const path = session.path;
1074
+ let identity;
1075
+ if (session.path === "standalone") {
1076
+ standaloneCtx = session.ctx;
1077
+ identity = identityReport(standaloneCtx);
1078
+ } else {
1079
+ identity = session.identity;
1080
+ }
846
1081
  if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
847
1082
  io2.err(
848
1083
  `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.`
849
1084
  );
850
1085
  }
851
- const remoteState = await standaloneCtx.fetchRemote({
852
- ownerPubkey: standaloneCtx.ownerPubkey,
853
- repoId,
854
- relayUrls: relaysUsed
855
- });
856
- const feeRates = await standaloneCtx.publisher.getFeeRates();
857
- const pushPlan = await planPush({
858
- repoReader: reader,
859
- remoteState,
860
- feeRates,
861
- repoId,
862
- refs: refspecs,
863
- force: flags.force
864
- });
865
- const plan = serializePushPlan(pushPlan);
1086
+ let plan;
1087
+ let execute;
1088
+ if (session.path === "standalone") {
1089
+ const ctx = session.ctx;
1090
+ const remoteState = await ctx.fetchRemote({
1091
+ ownerPubkey: ctx.ownerPubkey,
1092
+ repoId,
1093
+ relayUrls: relaysUsed
1094
+ });
1095
+ const feeRates = await ctx.publisher.getFeeRates();
1096
+ const pushPlan = await planPush({
1097
+ repoReader: reader,
1098
+ remoteState,
1099
+ feeRates,
1100
+ repoId,
1101
+ refs: refspecs,
1102
+ force: flags.force
1103
+ });
1104
+ plan = serializePushPlan(pushPlan);
1105
+ execute = async () => serializePushResult(
1106
+ pushPlan,
1107
+ await executePush({
1108
+ plan: pushPlan,
1109
+ publisher: ctx.publisher,
1110
+ remoteState,
1111
+ repoReader: reader,
1112
+ relayUrls: relaysUsed
1113
+ })
1114
+ );
1115
+ } else {
1116
+ const client = session.client;
1117
+ plan = await client.gitEstimate({
1118
+ repoPath: repoRoot,
1119
+ repoId,
1120
+ refspecs,
1121
+ force: flags.force,
1122
+ relayUrls: relaysUsed
1123
+ });
1124
+ execute = () => client.gitPush({
1125
+ repoPath: repoRoot,
1126
+ repoId,
1127
+ refspecs,
1128
+ force: flags.force,
1129
+ relayUrls: relaysUsed,
1130
+ confirm: true
1131
+ });
1132
+ }
866
1133
  const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
867
1134
  if (upToDate) {
868
1135
  if (flags.json) {
869
1136
  io2.emitJson({
870
1137
  command: "push",
871
1138
  repoId,
1139
+ path,
872
1140
  identity,
873
1141
  executed: false,
874
1142
  upToDate: true,
@@ -888,6 +1156,7 @@ async function runPush(args, deps) {
888
1156
  io2.emitJson({
889
1157
  command: "push",
890
1158
  repoId,
1159
+ path,
891
1160
  identity,
892
1161
  executed: false,
893
1162
  upToDate: false,
@@ -910,18 +1179,12 @@ async function runPush(args, deps) {
910
1179
  return 1;
911
1180
  }
912
1181
  }
913
- const pushResult = await executePush({
914
- plan: pushPlan,
915
- publisher: standaloneCtx.publisher,
916
- remoteState,
917
- repoReader: reader,
918
- relayUrls: relaysUsed
919
- });
920
- const result = serializePushResult(pushPlan, pushResult);
1182
+ const result = await execute();
921
1183
  if (flags.json) {
922
1184
  io2.emitJson({
923
1185
  command: "push",
924
1186
  repoId,
1187
+ path,
925
1188
  identity,
926
1189
  executed: true,
927
1190
  upToDate: false,
@@ -1210,7 +1473,7 @@ async function loadMoneyContext(deps, options) {
1210
1473
  if (!money) {
1211
1474
  await ctx.stop().catch(() => void 0);
1212
1475
  throw new Error(
1213
- "this standalone loader does not expose money operations \u2014 channel open/close/settle need the #263 loader"
1476
+ "this standalone loader does not expose money operations \u2014 channel open/close/settle need a loader with the money lifecycle"
1214
1477
  );
1215
1478
  }
1216
1479
  return { ctx, money };
@@ -1519,78 +1782,838 @@ async function stopQuietly(ctx) {
1519
1782
  }
1520
1783
  }
1521
1784
 
1785
+ // src/cli/clone.ts
1786
+ import { mkdtemp, mkdir, readdir, rename, rm, rmdir } from "fs/promises";
1787
+ import { basename, dirname, join, resolve } from "path";
1788
+ import { parseArgs as parseArgs5 } from "util";
1789
+ var CLONE_USAGE = `Usage: rig clone <relay-url> <owner>/<repo-id> [dir] [options]
1790
+
1791
+ Clone a TOON repository \u2014 FREE (relay reads + Arweave gateway downloads; no
1792
+ payments, no channel, no identity needed). <relay-url> is a ws:// or wss://
1793
+ NIP-01 relay; <owner> is the repo owner's pubkey as npub1\u2026 or 64-char hex;
1794
+ [dir] defaults to <repo-id>.
1795
+
1796
+ Fetches the kind:30617/30618 repository state, downloads every git object
1797
+ the refs need from Arweave gateways (SHA-1 verified \u2014 corrupt content is
1798
+ rejected), and materializes a real git repository: objects via git plumbing,
1799
+ refs, HEAD, checked-out worktree, toon.* config, and the relay preconfigured
1800
+ as the "origin" remote \u2014 so \`rig fetch\`, \`rig push\`, and the issue/pr
1801
+ commands work immediately.
1802
+
1803
+ Note: recently pushed objects can take 10-20 minutes to become fetchable
1804
+ from Arweave gateways. A clone right after a push may report missing
1805
+ objects \u2014 retry after propagation.
1806
+
1807
+ Options:
1808
+ --concurrency <n> parallel gateway downloads (default 8)
1809
+ --json machine-readable result envelope
1810
+ -h, --help show this help`;
1811
+ var RepoNotFoundError = class extends Error {
1812
+ constructor(relay, owner, repoId) {
1813
+ super(
1814
+ `repository 30617:${owner}:${repoId} not found on ${relay} \u2014 no kind:30617 announcement or kind:30618 state event exists there. Check the relay URL, the owner pubkey (npub or hex), and the repo id.`
1815
+ );
1816
+ this.name = "RepoNotFoundError";
1817
+ }
1818
+ };
1819
+ var MissingRemoteObjectsError = class extends Error {
1820
+ constructor(missing, context) {
1821
+ super(missingObjectsMessage(missing, context));
1822
+ this.missing = missing;
1823
+ this.name = "MissingRemoteObjectsError";
1824
+ }
1825
+ missing;
1826
+ };
1827
+ var WS_URL_RE = /^wss?:\/\//i;
1828
+ function parseCloneArgs(args) {
1829
+ const { values, positionals } = parseArgs5({
1830
+ args,
1831
+ options: {
1832
+ json: { type: "boolean", default: false },
1833
+ concurrency: { type: "string" },
1834
+ help: { type: "boolean", short: "h", default: false }
1835
+ },
1836
+ allowPositionals: true
1837
+ });
1838
+ if (values.help) return { help: true };
1839
+ if (positionals.length < 2 || positionals.length > 3) {
1840
+ throw new Error("expected: rig clone <relay-url> <owner>/<repo-id> [dir]");
1841
+ }
1842
+ const [relayUrl, addr, dir] = positionals;
1843
+ if (!WS_URL_RE.test(relayUrl)) {
1844
+ throw new Error(
1845
+ `<relay-url> must be ws:// or wss:// (got ${JSON.stringify(relayUrl)})`
1846
+ );
1847
+ }
1848
+ const slash = addr.indexOf("/");
1849
+ if (slash <= 0 || slash === addr.length - 1) {
1850
+ throw new Error(
1851
+ `expected <owner>/<repo-id> (npub or 64-char hex owner), got ${JSON.stringify(addr)}`
1852
+ );
1853
+ }
1854
+ const owner = ownerToHex(addr.slice(0, slash));
1855
+ const repoId = addr.slice(slash + 1);
1856
+ let concurrency;
1857
+ if (values.concurrency !== void 0) {
1858
+ concurrency = Number.parseInt(values.concurrency, 10);
1859
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
1860
+ throw new Error(`--concurrency must be a positive integer`);
1861
+ }
1862
+ }
1863
+ return {
1864
+ relayUrl,
1865
+ owner,
1866
+ repoId,
1867
+ dir,
1868
+ concurrency,
1869
+ json: values.json === true
1870
+ };
1871
+ }
1872
+ async function isUsableDestination(dir) {
1873
+ try {
1874
+ const entries = await readdir(dir);
1875
+ return entries.length === 0;
1876
+ } catch (err) {
1877
+ return err.code === "ENOENT";
1878
+ }
1879
+ }
1880
+ function initialBranch(headSymref) {
1881
+ if (headSymref?.startsWith("refs/heads/")) {
1882
+ const name = headSymref.slice("refs/heads/".length);
1883
+ if (name && isSafeRefname(headSymref)) return name;
1884
+ }
1885
+ return "main";
1886
+ }
1887
+ async function runClone(args, deps) {
1888
+ const { io: io2 } = deps;
1889
+ let parsed;
1890
+ try {
1891
+ const result = parseCloneArgs(args);
1892
+ if ("help" in result) {
1893
+ io2.out(CLONE_USAGE);
1894
+ return 0;
1895
+ }
1896
+ parsed = result;
1897
+ } catch (err) {
1898
+ io2.err(err instanceof Error ? err.message : String(err));
1899
+ io2.err(CLONE_USAGE);
1900
+ return 2;
1901
+ }
1902
+ const { relayUrl, owner, repoId, json } = parsed;
1903
+ const dest = resolve(deps.cwd, parsed.dir ?? repoId);
1904
+ let tempDir;
1905
+ try {
1906
+ if (!await isUsableDestination(dest)) {
1907
+ throw new Error(
1908
+ `destination path ${JSON.stringify(dest)} already exists and is not an empty directory`
1909
+ );
1910
+ }
1911
+ if (!json) io2.out(`Cloning into '${basename(dest)}'...`);
1912
+ const remoteState = await fetchRemoteState({
1913
+ relayUrls: [relayUrl],
1914
+ ownerPubkey: owner,
1915
+ repoId,
1916
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
1917
+ ...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
1918
+ });
1919
+ if (!remoteState.announced && remoteState.refsEvent === null) {
1920
+ throw new RepoNotFoundError(relayUrl, owner, repoId);
1921
+ }
1922
+ const refs = /* @__PURE__ */ new Map();
1923
+ for (const [refname, sha] of remoteState.refs) {
1924
+ if (!isSafeRefname(refname)) {
1925
+ io2.err(
1926
+ `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
1927
+ );
1928
+ continue;
1929
+ }
1930
+ refs.set(refname, sha);
1931
+ }
1932
+ if (refs.size === 0) {
1933
+ io2.err(
1934
+ "warning: the repository has no refs yet (announced but never pushed) \u2014 cloning an empty repository"
1935
+ );
1936
+ }
1937
+ const tips = [...new Set(refs.values())];
1938
+ const collected = await collectRepoObjects({
1939
+ tips,
1940
+ shaToTxId: remoteState.shaToTxId,
1941
+ resolveMissing: (shas) => remoteState.resolveMissing(shas),
1942
+ ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
1943
+ ...parsed.concurrency !== void 0 ? { concurrency: parsed.concurrency } : {}
1944
+ });
1945
+ if (collected.missing.length > 0) {
1946
+ throw new MissingRemoteObjectsError(
1947
+ collected.missing,
1948
+ `cannot clone ${owner.slice(0, 8)}\u2026/${repoId}`
1949
+ );
1950
+ }
1951
+ for (const { sha, txId } of collected.skippedUnavailable) {
1952
+ io2.err(
1953
+ `warning: unreachable object ${sha} (tx ${txId}) could not be downloaded \u2014 not needed by any ref, continuing`
1954
+ );
1955
+ }
1956
+ const parent = dirname(dest);
1957
+ await mkdir(parent, { recursive: true });
1958
+ tempDir = await mkdtemp(join(parent, `.${basename(dest)}.rig-clone-`));
1959
+ await runGit(tempDir, [
1960
+ "init",
1961
+ "--quiet",
1962
+ `--initial-branch=${initialBranch(remoteState.headSymref)}`
1963
+ ]);
1964
+ const written = await writeGitObjects(tempDir, collected.objects.values());
1965
+ for (const [refname, sha] of refs) {
1966
+ await updateRef(tempDir, refname, sha);
1967
+ if (refname.startsWith("refs/heads/")) {
1968
+ const branch = refname.slice("refs/heads/".length);
1969
+ await updateRef(tempDir, `refs/remotes/origin/${branch}`, sha);
1970
+ }
1971
+ }
1972
+ const head = remoteState.headSymref !== null && isSafeRefname(remoteState.headSymref) && refs.has(remoteState.headSymref) ? remoteState.headSymref : [...refs.keys()].find((r) => r.startsWith("refs/heads/")) ?? null;
1973
+ if (head !== null) {
1974
+ await setHeadSymref(tempDir, head);
1975
+ await runGit(tempDir, ["reset", "--hard", "--quiet"]);
1976
+ }
1977
+ await runGit(tempDir, ["config", "toon.repoid", repoId]);
1978
+ await runGit(tempDir, ["config", "toon.owner", owner]);
1979
+ await runGit(tempDir, ["remote", "add", "origin", relayUrl]);
1980
+ if (head !== null) {
1981
+ const branch = head.slice("refs/heads/".length);
1982
+ await runGit(tempDir, ["config", `branch.${branch}.remote`, "origin"]);
1983
+ await runGit(tempDir, ["config", `branch.${branch}.merge`, head]);
1984
+ }
1985
+ try {
1986
+ await rmdir(dest);
1987
+ } catch {
1988
+ }
1989
+ await rename(tempDir, dest);
1990
+ tempDir = void 0;
1991
+ if (json) {
1992
+ io2.emitJson({
1993
+ command: "clone",
1994
+ repoAddr: { ownerPubkey: owner, repoId },
1995
+ relay: relayUrl,
1996
+ directory: dest,
1997
+ head,
1998
+ refs: Object.fromEntries(refs),
1999
+ name: remoteState.name,
2000
+ objectsDownloaded: written,
2001
+ executed: true
2002
+ });
2003
+ } else {
2004
+ io2.out(`Downloaded ${written} object(s) from Arweave (SHA-1 verified).`);
2005
+ for (const [refname, sha] of refs) {
2006
+ io2.out(` ${sha.slice(0, 7)} ${refname}`);
2007
+ }
2008
+ if (head !== null) io2.out(`HEAD is now at ${head}`);
2009
+ io2.out(
2010
+ `Configured toon.repoid=${repoId}, toon.owner=${owner.slice(0, 8)}\u2026, origin \u2192 ${relayUrl}`
2011
+ );
2012
+ io2.out("Done.");
2013
+ }
2014
+ return 0;
2015
+ } catch (err) {
2016
+ return emitCliError(io2, json, "clone", err);
2017
+ } finally {
2018
+ if (tempDir !== void 0) {
2019
+ await rm(tempDir, { recursive: true, force: true }).catch(
2020
+ () => void 0
2021
+ );
2022
+ }
2023
+ }
2024
+ }
2025
+
1522
2026
  // src/cli/events.ts
1523
2027
  import { readFile } from "fs/promises";
1524
- import { parseArgs as parseArgs5 } from "util";
2028
+ import { parseArgs as parseArgs7 } from "util";
2029
+ import {
2030
+ REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2,
2031
+ STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2,
2032
+ STATUS_CLOSED_KIND as STATUS_CLOSED_KIND2,
2033
+ STATUS_DRAFT_KIND as STATUS_DRAFT_KIND2,
2034
+ STATUS_OPEN_KIND as STATUS_OPEN_KIND2
2035
+ } from "@toon-protocol/core/nip34";
2036
+
2037
+ // src/cli/tracker.ts
2038
+ import { parseArgs as parseArgs6 } from "util";
1525
2039
  import {
2040
+ ISSUE_KIND,
2041
+ PATCH_KIND,
1526
2042
  REPOSITORY_ANNOUNCEMENT_KIND,
1527
2043
  STATUS_APPLIED_KIND,
1528
2044
  STATUS_CLOSED_KIND,
1529
2045
  STATUS_DRAFT_KIND,
1530
2046
  STATUS_OPEN_KIND
1531
2047
  } from "@toon-protocol/core/nip34";
1532
- var defaultReadStdin = async () => {
1533
- if (process.stdin.isTTY) return "";
1534
- const chunks = [];
1535
- for await (const chunk of process.stdin) chunks.push(chunk);
1536
- return Buffer.concat(chunks).toString("utf-8");
1537
- };
1538
- var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
1539
- toon.repoid \u2014 run \`rig init\` to set it)
1540
- --owner <pubkey> repository owner pubkey, 64-char hex (default: git config
1541
- toon.owner, then the active identity)
1542
- --remote <name> publish via this configured git remote (default: the
1543
- "origin" remote \u2014 \`rig remote add origin <relay-url>\`)
1544
- --relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
1545
- configured remotes. The deprecated v0.1 \`git config
1546
- toon.relay\` still works as a fallback, with a nudge
1547
- --yes skip the fee confirmation (required when not a TTY)
1548
- --json machine-readable receipt; without --yes it is a pure
1549
- estimate (nothing published, exit 0)
2048
+ var READ_COMMON_FLAGS = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config)
2049
+ --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
2050
+ --remote <name> read via this configured git remote (default: origin)
2051
+ --relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
2052
+ --json machine-readable envelope
1550
2053
  -h, --help show this help`;
1551
- var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
2054
+ var ISSUE_LIST_USAGE = `Usage: rig issue list [--state open|closed|all] [options]
1552
2055
 
1553
- File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
1554
- permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
1555
- from the toon.* git config keys \`rig init\` writes.
2056
+ List the repo's issues (kind:1621) with their derived state \u2014 FREE (relay
2057
+ reads only). State comes from kind:1630-1633 status events: the latest status
2058
+ wins; an issue with no status events is open.
1556
2059
 
1557
- Body source (exactly one): --body, --body-file, or piped stdin.
2060
+ Options:
2061
+ --state <state> open | closed | all (default: all)
2062
+ ${READ_COMMON_FLAGS}`;
2063
+ var ISSUE_SHOW_USAGE = `Usage: rig issue show <event-id> [options]
2064
+
2065
+ Show one issue (kind:1621): metadata, derived state, body, and its
2066
+ kind:1622 comments \u2014 FREE (relay reads only). <event-id> is the 64-char hex
2067
+ id \`rig issue create\` printed (also visible in \`rig issue list\`).
1558
2068
 
1559
2069
  Options:
1560
- --title <title> issue title (subject tag) [required]
1561
- --body <text> issue body (Markdown)
1562
- --body-file <path> read the issue body from a file
1563
- --label <label> label (t tag); repeatable
1564
- ${COMMON_FLAGS_USAGE}`;
1565
- var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
2070
+ ${READ_COMMON_FLAGS}`;
2071
+ var PR_LIST_USAGE = `Usage: rig pr list [--state open|applied|closed|draft|all] [options]
1566
2072
 
1567
- Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
1568
- permanent and non-refundable. <root-event-id> is the 64-char hex id of the
1569
- kind:1621 issue / kind:1617 patch being commented on.
2073
+ List the repo's patches/PRs (kind:1617) with their derived state \u2014 FREE
2074
+ (relay reads only). State comes from kind:1630-1633 status events: the
2075
+ latest status wins; a patch with no status events is open.
1570
2076
 
1571
2077
  Options:
1572
- --body <text> comment body (Markdown) [required]
1573
- --parent-author <pubkey> pubkey of the TARGET event's author (p threading
1574
- tag; default: the repo owner)
1575
- --marker <root|reply> e-tag marker (default: root \u2014 commenting directly
1576
- on the issue/patch)
1577
- ${COMMON_FLAGS_USAGE}`;
1578
- var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
2078
+ --state <state> open | applied | closed | draft | all (default: all)
2079
+ ${READ_COMMON_FLAGS}`;
2080
+ var PR_SHOW_USAGE = `Usage: rig pr show <event-id> [options]
1579
2081
 
1580
- Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
1581
- a paid publish; writes are permanent and non-refundable. --range runs
1582
- \`git format-patch --stdout <range>\` in the local repository and derives the
1583
- commit/parent-commit tags; --patch-file publishes a pre-generated patch
1584
- verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
1585
- full series text \u2014 cover-letter threading (one event per commit) is out of
1586
- scope in v1.
2082
+ Show one patch/PR (kind:1617): metadata, derived state, the FULL patch text
2083
+ (real \`git format-patch\` output \u2014 pipe it to \`git am\` to apply), and its
2084
+ kind:1622 comments \u2014 FREE (relay reads only).
1587
2085
 
1588
2086
  Options:
1589
- --title <title> patch/PR title (subject tag) [required]
1590
- --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
1591
- or <rev>...<rev> (mutually exclusive with --patch-file)
1592
- --patch-file <path> literal patch text to publish
1593
- --branch <name> branch name (t tag)
2087
+ ${READ_COMMON_FLAGS}`;
2088
+ var STATUS_BY_KIND = {
2089
+ [STATUS_OPEN_KIND]: "open",
2090
+ [STATUS_APPLIED_KIND]: "applied",
2091
+ [STATUS_CLOSED_KIND]: "closed",
2092
+ [STATUS_DRAFT_KIND]: "draft"
2093
+ };
2094
+ var STATUS_KINDS = [
2095
+ STATUS_OPEN_KIND,
2096
+ STATUS_APPLIED_KIND,
2097
+ STATUS_CLOSED_KIND,
2098
+ STATUS_DRAFT_KIND
2099
+ ];
2100
+ function tagValue(tags, name) {
2101
+ return tags.find((t) => t[0] === name)?.[1];
2102
+ }
2103
+ function tagValues(tags, name) {
2104
+ return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
2105
+ }
2106
+ function deriveStatus(targetEventId, statusEvents, authorized) {
2107
+ let winner = null;
2108
+ for (const event of statusEvents) {
2109
+ if (STATUS_BY_KIND[event.kind] === void 0) continue;
2110
+ if (!authorized.has(event.pubkey.toLowerCase())) continue;
2111
+ if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
2112
+ continue;
2113
+ if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
2114
+ winner = event;
2115
+ }
2116
+ }
2117
+ return winner === null ? "open" : STATUS_BY_KIND[winner.kind];
2118
+ }
2119
+ var RELAY_TIMEOUT_MS = 1e4;
2120
+ var HEX64_RE = /^[0-9a-f]{64}$/;
2121
+ var WS_URL_RE2 = /^wss?:\/\//i;
2122
+ function defaultWebSocketFactory(url) {
2123
+ const ctor = globalThis.WebSocket;
2124
+ if (!ctor) {
2125
+ throw new Error(
2126
+ "No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
2127
+ );
2128
+ }
2129
+ return new ctor(url);
2130
+ }
2131
+ async function queryAll(relays, filters, webSocketFactory) {
2132
+ const jobs = relays.flatMap(
2133
+ (relay) => filters.map(
2134
+ (filter) => queryRelay(relay, filter, RELAY_TIMEOUT_MS, webSocketFactory)
2135
+ )
2136
+ );
2137
+ const results = await Promise.allSettled(jobs);
2138
+ const byId = /* @__PURE__ */ new Map();
2139
+ let failures = 0;
2140
+ let firstError;
2141
+ for (const result of results) {
2142
+ if (result.status === "rejected") {
2143
+ failures += 1;
2144
+ firstError ??= result.reason;
2145
+ continue;
2146
+ }
2147
+ for (const event of result.value) {
2148
+ if (typeof event.id === "string" && !byId.has(event.id)) {
2149
+ byId.set(event.id, event);
2150
+ }
2151
+ }
2152
+ }
2153
+ if (failures === results.length && results.length > 0) {
2154
+ throw firstError instanceof Error ? firstError : new Error(String(firstError));
2155
+ }
2156
+ return byId;
2157
+ }
2158
+ var TRACKER_OPTIONS = {
2159
+ json: { type: "boolean", default: false },
2160
+ state: { type: "string" },
2161
+ relay: { type: "string", multiple: true },
2162
+ remote: { type: "string" },
2163
+ "repo-id": { type: "string" },
2164
+ owner: { type: "string" },
2165
+ help: { type: "boolean", short: "h", default: false }
2166
+ };
2167
+ function pickTrackerFlags(values) {
2168
+ const flags = {
2169
+ json: values["json"] === true,
2170
+ help: values["help"] === true,
2171
+ relay: Array.isArray(values["relay"]) ? values["relay"] : []
2172
+ };
2173
+ if (typeof values["state"] === "string") flags.state = values["state"];
2174
+ if (typeof values["remote"] === "string") flags.remote = values["remote"];
2175
+ if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
2176
+ if (typeof values["owner"] === "string")
2177
+ flags.owner = ownerToHex(values["owner"]);
2178
+ return flags;
2179
+ }
2180
+ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2181
+ let repoRoot;
2182
+ let toonConfig = {
2183
+ relays: []
2184
+ };
2185
+ try {
2186
+ repoRoot = await resolveRepoRoot(deps.cwd);
2187
+ toonConfig = await readToonConfig(repoRoot);
2188
+ } catch {
2189
+ }
2190
+ let repoAddr = null;
2191
+ const repoId = flags.repoId ?? toonConfig.repoId;
2192
+ const owner = flags.owner ?? toonConfig.owner;
2193
+ if (repoId && owner) {
2194
+ repoAddr = { ownerPubkey: owner, repoId };
2195
+ } else if (needRepoAddr) {
2196
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2197
+ throw new UnconfiguredRepoAddressError("repository owner");
2198
+ }
2199
+ const resolved = await resolveRelays({
2200
+ relayFlags: flags.relay,
2201
+ remoteName: flags.remote,
2202
+ repoRoot,
2203
+ toonRelays: toonConfig.relays
2204
+ });
2205
+ const wsRelays = resolved.relays.filter((url) => WS_URL_RE2.test(url));
2206
+ if (wsRelays.length === 0) {
2207
+ throw new InvalidRelayUrlError(
2208
+ resolved.relays[0] ?? "",
2209
+ "reads need a ws:// or wss:// relay"
2210
+ );
2211
+ }
2212
+ return {
2213
+ relays: wsRelays,
2214
+ repoAddr,
2215
+ webSocketFactory: deps.webSocketFactory ?? defaultWebSocketFactory
2216
+ };
2217
+ }
2218
+ function repoATag(addr) {
2219
+ return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
2220
+ }
2221
+ async function fetchAuthorizedAuthors(ctx, ownerPubkey, repoId) {
2222
+ let announceTags = [];
2223
+ try {
2224
+ const events = await queryAll(
2225
+ ctx.relays,
2226
+ [
2227
+ {
2228
+ kinds: [REPOSITORY_ANNOUNCEMENT_KIND],
2229
+ authors: [ownerPubkey],
2230
+ "#d": [repoId]
2231
+ }
2232
+ ],
2233
+ ctx.webSocketFactory
2234
+ );
2235
+ let latest = null;
2236
+ for (const event of events.values()) {
2237
+ if (event.kind !== REPOSITORY_ANNOUNCEMENT_KIND) continue;
2238
+ if (event.pubkey !== ownerPubkey) continue;
2239
+ if (!event.tags.some((t) => t[0] === "d" && t[1] === repoId)) continue;
2240
+ if (latest === null || event.created_at > latest.created_at || event.created_at === latest.created_at && event.id < latest.id) {
2241
+ latest = event;
2242
+ }
2243
+ }
2244
+ if (latest) announceTags = latest.tags;
2245
+ } catch {
2246
+ }
2247
+ return authorizedStatusAuthors(ownerPubkey, announceTags);
2248
+ }
2249
+ function parseTrackerItem(event, status) {
2250
+ const item = {
2251
+ eventId: event.id,
2252
+ kind: event.kind,
2253
+ title: tagValue(event.tags, "subject") ?? (event.content.split("\n")[0] ?? "").slice(0, 120),
2254
+ status,
2255
+ authorPubkey: event.pubkey,
2256
+ createdAt: event.created_at,
2257
+ labels: tagValues(event.tags, "t"),
2258
+ content: event.content
2259
+ };
2260
+ if (event.kind === PATCH_KIND) {
2261
+ item.commitShas = tagValues(event.tags, "commit");
2262
+ const branch = tagValue(event.tags, "branch");
2263
+ if (branch !== void 0) item.branch = branch;
2264
+ const description = tagValue(event.tags, "description");
2265
+ if (description !== void 0) item.description = description;
2266
+ }
2267
+ return item;
2268
+ }
2269
+ function isoDate(createdAt) {
2270
+ return new Date(createdAt * 1e3).toISOString().slice(0, 10);
2271
+ }
2272
+ async function fetchItems(ctx, kind) {
2273
+ const addr = ctx.repoAddr;
2274
+ const aTag = repoATag(addr);
2275
+ const authorizedPromise = fetchAuthorizedAuthors(
2276
+ ctx,
2277
+ addr.ownerPubkey,
2278
+ addr.repoId
2279
+ );
2280
+ const [itemEvents, aStatusEvents] = await Promise.all([
2281
+ queryAll(
2282
+ ctx.relays,
2283
+ [{ kinds: [kind], "#a": [aTag] }],
2284
+ ctx.webSocketFactory
2285
+ ),
2286
+ queryAll(
2287
+ ctx.relays,
2288
+ [{ kinds: STATUS_KINDS, "#a": [aTag] }],
2289
+ ctx.webSocketFactory
2290
+ )
2291
+ ]);
2292
+ const items = [...itemEvents.values()].filter(
2293
+ (e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag)
2294
+ );
2295
+ const statuses = new Map(aStatusEvents);
2296
+ if (items.length > 0) {
2297
+ const byE = await queryAll(
2298
+ ctx.relays,
2299
+ [{ kinds: STATUS_KINDS, "#e": items.map((i) => i.id) }],
2300
+ ctx.webSocketFactory
2301
+ );
2302
+ for (const [id, event] of byE)
2303
+ if (!statuses.has(id)) statuses.set(id, event);
2304
+ }
2305
+ const authorized = await authorizedPromise;
2306
+ return items.map(
2307
+ (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values(), authorized))
2308
+ ).sort((a, b) => b.createdAt - a.createdAt);
2309
+ }
2310
+ async function fetchItem(ctx, eventId, expectedKind) {
2311
+ const found = await queryAll(
2312
+ ctx.relays,
2313
+ [{ ids: [eventId] }],
2314
+ ctx.webSocketFactory
2315
+ );
2316
+ const event = found.get(eventId);
2317
+ if (!event) {
2318
+ throw new Error(`event ${eventId} not found on ${ctx.relays.join(", ")}`);
2319
+ }
2320
+ if (event.kind !== expectedKind) {
2321
+ 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`)" : "";
2322
+ throw new Error(
2323
+ `event ${eventId} has kind ${event.kind}, expected ${expectedKind}${hint}`
2324
+ );
2325
+ }
2326
+ const [statusEvents, commentEvents] = await Promise.all([
2327
+ queryAll(
2328
+ ctx.relays,
2329
+ [{ kinds: STATUS_KINDS, "#e": [eventId] }],
2330
+ ctx.webSocketFactory
2331
+ ),
2332
+ queryAll(
2333
+ ctx.relays,
2334
+ [{ kinds: [COMMENT_KIND], "#e": [eventId] }],
2335
+ ctx.webSocketFactory
2336
+ )
2337
+ ]);
2338
+ const comments = [...commentEvents.values()].filter(
2339
+ (e) => e.kind === COMMENT_KIND && e.tags.some((t) => t[0] === "e" && t[1] === eventId)
2340
+ ).sort((a, b) => a.created_at - b.created_at).map((e) => ({
2341
+ eventId: e.id,
2342
+ authorPubkey: e.pubkey,
2343
+ createdAt: e.created_at,
2344
+ content: e.content
2345
+ }));
2346
+ const repoATag2 = tagValue(event.tags, "a") ?? null;
2347
+ const parsedAddr = parseRepoATag(repoATag2) ?? ctx.repoAddr;
2348
+ const authorized = parsedAddr ? await fetchAuthorizedAuthors(ctx, parsedAddr.ownerPubkey, parsedAddr.repoId) : /* @__PURE__ */ new Set();
2349
+ return {
2350
+ item: parseTrackerItem(
2351
+ event,
2352
+ deriveStatus(eventId, statusEvents.values(), authorized)
2353
+ ),
2354
+ comments,
2355
+ repoATag: repoATag2
2356
+ };
2357
+ }
2358
+ function parseRepoATag(aTag) {
2359
+ if (!aTag) return null;
2360
+ const [kind, ownerPubkey, ...repoIdParts] = aTag.split(":");
2361
+ const repoId = repoIdParts.join(":");
2362
+ if (kind !== String(REPOSITORY_ANNOUNCEMENT_KIND) || !ownerPubkey || !repoId) {
2363
+ return null;
2364
+ }
2365
+ return { ownerPubkey, repoId };
2366
+ }
2367
+ async function runList(args, deps, spec) {
2368
+ const { io: io2 } = deps;
2369
+ let flags;
2370
+ try {
2371
+ const { values, positionals } = parseArgs6({
2372
+ args,
2373
+ options: TRACKER_OPTIONS,
2374
+ allowPositionals: true
2375
+ });
2376
+ if (positionals.length > 0) {
2377
+ throw new Error(`rig ${spec.command} takes no positional arguments`);
2378
+ }
2379
+ flags = pickTrackerFlags(values);
2380
+ if (flags.state !== void 0 && flags.state !== "all" && !spec.states.includes(flags.state)) {
2381
+ throw new Error(
2382
+ `--state must be one of ${[...spec.states, "all"].join(" | ")} (got ${JSON.stringify(flags.state)})`
2383
+ );
2384
+ }
2385
+ } catch (err) {
2386
+ io2.err(err instanceof Error ? err.message : String(err));
2387
+ io2.err(spec.usage);
2388
+ return 2;
2389
+ }
2390
+ if (flags.help) {
2391
+ io2.out(spec.usage);
2392
+ return 0;
2393
+ }
2394
+ try {
2395
+ const ctx = await resolveTrackerContext(flags, deps, true);
2396
+ const all = await fetchItems(ctx, spec.kind);
2397
+ const state = flags.state ?? "all";
2398
+ const items = state === "all" ? all : all.filter((i) => i.status === state);
2399
+ if (flags.json) {
2400
+ io2.emitJson({
2401
+ command: spec.command,
2402
+ repoAddr: ctx.repoAddr,
2403
+ relays: ctx.relays,
2404
+ state,
2405
+ count: items.length,
2406
+ [spec.jsonKey]: items
2407
+ });
2408
+ return 0;
2409
+ }
2410
+ if (items.length === 0) {
2411
+ io2.out(
2412
+ `no ${state === "all" ? "" : `${state} `}${spec.jsonKey} found for ${repoATag(ctx.repoAddr)}`
2413
+ );
2414
+ return 0;
2415
+ }
2416
+ for (const item of items) {
2417
+ const labels = item.labels.length > 0 ? ` [${item.labels.join(", ")}]` : "";
2418
+ io2.out(
2419
+ `${item.status.padEnd(7)} ${item.eventId.slice(0, 8)} ${item.title} (${item.authorPubkey.slice(0, 8)}, ${isoDate(item.createdAt)})${labels}`
2420
+ );
2421
+ }
2422
+ io2.out(
2423
+ `${items.length} ${spec.jsonKey}${state === "all" ? "" : ` (${state})`}`
2424
+ );
2425
+ return 0;
2426
+ } catch (err) {
2427
+ return emitCliError(io2, flags.json, spec.command, err);
2428
+ }
2429
+ }
2430
+ async function runShow(args, deps, spec) {
2431
+ const { io: io2 } = deps;
2432
+ let flags;
2433
+ let eventId;
2434
+ try {
2435
+ const { values, positionals } = parseArgs6({
2436
+ args,
2437
+ options: TRACKER_OPTIONS,
2438
+ allowPositionals: true
2439
+ });
2440
+ flags = pickTrackerFlags(values);
2441
+ if (flags.help) {
2442
+ io2.out(spec.usage);
2443
+ return 0;
2444
+ }
2445
+ if (positionals.length !== 1) {
2446
+ throw new Error("expected exactly one <event-id>");
2447
+ }
2448
+ eventId = positionals[0];
2449
+ if (!HEX64_RE.test(eventId)) {
2450
+ throw new Error(
2451
+ `<event-id> must be a 64-char lowercase hex id (got ${JSON.stringify(eventId)})`
2452
+ );
2453
+ }
2454
+ } catch (err) {
2455
+ io2.err(err instanceof Error ? err.message : String(err));
2456
+ io2.err(spec.usage);
2457
+ return 2;
2458
+ }
2459
+ try {
2460
+ const ctx = await resolveTrackerContext(flags, deps, false);
2461
+ const shown = await fetchItem(ctx, eventId, spec.kind);
2462
+ if (flags.json) {
2463
+ io2.emitJson({
2464
+ command: spec.command,
2465
+ relays: ctx.relays,
2466
+ repoATag: shown.repoATag,
2467
+ [spec.jsonKey]: shown.item,
2468
+ comments: shown.comments
2469
+ });
2470
+ return 0;
2471
+ }
2472
+ const { item } = shown;
2473
+ io2.out(`${spec.jsonKey} ${item.eventId}`);
2474
+ io2.out(`Title: ${item.title}`);
2475
+ io2.out(`Status: ${item.status}`);
2476
+ io2.out(`Author: ${item.authorPubkey}`);
2477
+ io2.out(`Date: ${isoDate(item.createdAt)}`);
2478
+ if (item.labels.length > 0) io2.out(`Labels: ${item.labels.join(", ")}`);
2479
+ if (item.branch !== void 0) io2.out(`Branch: ${item.branch}`);
2480
+ if (item.commitShas && item.commitShas.length > 0) {
2481
+ io2.out(`Commits: ${item.commitShas.join(", ")}`);
2482
+ }
2483
+ if (shown.repoATag !== null) io2.out(`Repo: ${shown.repoATag}`);
2484
+ if (item.description !== void 0) {
2485
+ io2.out("");
2486
+ io2.out("Body:");
2487
+ for (const line of item.description.split("\n")) io2.out(line);
2488
+ }
2489
+ io2.out("");
2490
+ io2.out(`${spec.bodyLabel}:`);
2491
+ for (const line of item.content.split("\n")) io2.out(line);
2492
+ io2.out("");
2493
+ io2.out(`Comments (${shown.comments.length}):`);
2494
+ for (const comment of shown.comments) {
2495
+ io2.out(
2496
+ `--- ${comment.eventId.slice(0, 8)} by ${comment.authorPubkey.slice(0, 8)} on ${isoDate(comment.createdAt)}`
2497
+ );
2498
+ for (const line of comment.content.split("\n")) io2.out(line);
2499
+ }
2500
+ return 0;
2501
+ } catch (err) {
2502
+ return emitCliError(io2, flags.json, spec.command, err);
2503
+ }
2504
+ }
2505
+ var ISSUE_STATES = ["open", "closed", "applied", "draft"];
2506
+ var PR_STATES = ["open", "applied", "closed", "draft"];
2507
+ function runIssueList(args, deps) {
2508
+ return runList(args, deps, {
2509
+ command: "issue list",
2510
+ kind: ISSUE_KIND,
2511
+ usage: ISSUE_LIST_USAGE,
2512
+ states: ISSUE_STATES,
2513
+ jsonKey: "issues"
2514
+ });
2515
+ }
2516
+ function runIssueShow(args, deps) {
2517
+ return runShow(args, deps, {
2518
+ command: "issue show",
2519
+ kind: ISSUE_KIND,
2520
+ usage: ISSUE_SHOW_USAGE,
2521
+ jsonKey: "issue",
2522
+ bodyLabel: "Body"
2523
+ });
2524
+ }
2525
+ function runPrList(args, deps) {
2526
+ return runList(args, deps, {
2527
+ command: "pr list",
2528
+ kind: PATCH_KIND,
2529
+ usage: PR_LIST_USAGE,
2530
+ states: PR_STATES,
2531
+ jsonKey: "prs"
2532
+ });
2533
+ }
2534
+ function runPrShow(args, deps) {
2535
+ return runShow(args, deps, {
2536
+ command: "pr show",
2537
+ kind: PATCH_KIND,
2538
+ usage: PR_SHOW_USAGE,
2539
+ jsonKey: "pr",
2540
+ bodyLabel: "Patch"
2541
+ });
2542
+ }
2543
+
2544
+ // src/cli/events.ts
2545
+ var defaultReadStdin = async () => {
2546
+ if (process.stdin.isTTY) return "";
2547
+ const chunks = [];
2548
+ for await (const chunk of process.stdin) chunks.push(chunk);
2549
+ return Buffer.concat(chunks).toString("utf-8");
2550
+ };
2551
+ var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
2552
+ toon.repoid \u2014 run \`rig init\` to set it)
2553
+ --owner <pubkey> repository owner pubkey, 64-char hex (default: git config
2554
+ toon.owner, then the active identity)
2555
+ --remote <name> publish via this configured git remote (default: the
2556
+ "origin" remote \u2014 \`rig remote add origin <relay-url>\`)
2557
+ --relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
2558
+ configured remotes. The deprecated v0.1 \`git config
2559
+ toon.relay\` still works as a fallback, with a nudge
2560
+ --yes skip the fee confirmation (required when not a TTY)
2561
+ --json machine-readable receipt; without --yes it is a pure
2562
+ estimate (nothing published, exit 0)
2563
+ -h, --help show this help`;
2564
+ var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
2565
+
2566
+ File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
2567
+ permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
2568
+ from the toon.* git config keys \`rig init\` writes.
2569
+
2570
+ Body source (exactly one): --body, --body-file, or piped stdin.
2571
+
2572
+ Options:
2573
+ --title <title> issue title (subject tag) [required]
2574
+ --body <text> issue body (Markdown)
2575
+ --body-file <path> read the issue body from a file
2576
+ --label <label> label (t tag); repeatable
2577
+ ${COMMON_FLAGS_USAGE}
2578
+
2579
+ Related (FREE reads \u2014 no payment):
2580
+ rig issue list [--state open|closed|all] list the repo's issues
2581
+ rig issue show <event-id> one issue + its comments`;
2582
+ var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
2583
+
2584
+ Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
2585
+ permanent and non-refundable. <root-event-id> is the 64-char hex id of the
2586
+ kind:1621 issue / kind:1617 patch being commented on.
2587
+
2588
+ Options:
2589
+ --body <text> comment body (Markdown) [required]
2590
+ --parent-author <pubkey> pubkey of the TARGET event's author (p threading
2591
+ tag; default: the repo owner)
2592
+ --marker <root|reply> e-tag marker (default: root \u2014 commenting directly
2593
+ on the issue/patch)
2594
+ ${COMMON_FLAGS_USAGE}`;
2595
+ var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
2596
+
2597
+ Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
2598
+ a paid publish; writes are permanent and non-refundable. --range runs
2599
+ \`git format-patch --stdout <range>\` in the local repository and derives the
2600
+ commit/parent-commit tags; --patch-file publishes a pre-generated patch
2601
+ verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
2602
+ full series text \u2014 cover-letter threading (one event per commit) is out of
2603
+ scope in v1.
2604
+
2605
+ The PR body (--body/--body-file) is carried in a dedicated \`description\`
2606
+ tag, not in the event content \u2014 the content stays pure format-patch output so
2607
+ \`rig pr show \u2026 | git am\` keeps working.
2608
+
2609
+ Options:
2610
+ --title <title> patch/PR title (subject tag) [required]
2611
+ --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
2612
+ or <rev>...<rev> (mutually exclusive with --patch-file)
2613
+ --patch-file <path> literal patch text to publish
2614
+ --body <text> PR description (Markdown; description tag)
2615
+ --body-file <path> read the PR description from a file
2616
+ --branch <name> branch name (t tag)
1594
2617
  ${COMMON_FLAGS_USAGE}`;
1595
2618
  var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
1596
2619
 
@@ -1605,10 +2628,14 @@ Options:
1605
2628
  ${COMMON_FLAGS_USAGE}`;
1606
2629
  var PR_USAGE = `${PR_CREATE_USAGE}
1607
2630
 
1608
- ${PR_STATUS_USAGE}`;
1609
- var HEX64_RE = /^[0-9a-f]{64}$/;
2631
+ ${PR_STATUS_USAGE}
2632
+
2633
+ ${PR_LIST_USAGE}
2634
+
2635
+ ${PR_SHOW_USAGE}`;
2636
+ var HEX64_RE2 = /^[0-9a-f]{64}$/;
1610
2637
  function assertHex64(value, what) {
1611
- if (!HEX64_RE.test(value)) {
2638
+ if (!HEX64_RE2.test(value)) {
1612
2639
  throw new Error(
1613
2640
  `${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
1614
2641
  );
@@ -1643,7 +2670,7 @@ function pickCommon(values) {
1643
2670
  }
1644
2671
  async function runEvent(opts) {
1645
2672
  const { command, flags, deps, actionLabel } = opts;
1646
- const { io: io2, env } = deps;
2673
+ const { io: io2 } = deps;
1647
2674
  let standaloneCtx;
1648
2675
  try {
1649
2676
  let repoRoot;
@@ -1667,21 +2694,38 @@ async function runEvent(opts) {
1667
2694
  io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1668
2695
  return 1;
1669
2696
  }
1670
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1671
- env,
1672
- cwd: deps.cwd,
1673
- warn: (line) => io2.err(line),
1674
- // Relay-origin for #264 network bootstrap (announce discovery).
1675
- ...relaysUsed[0] !== void 0 ? { relayUrl: relaysUsed[0] } : {}
1676
- });
1677
- const identity = identityReport(standaloneCtx);
1678
- const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
2697
+ const session = await loadPaidSession(deps, relaysUsed[0]);
2698
+ const path = session.path;
2699
+ let identity;
2700
+ let fee;
2701
+ if (session.path === "standalone") {
2702
+ standaloneCtx = session.ctx;
2703
+ identity = identityReport(standaloneCtx);
2704
+ fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
2705
+ } else {
2706
+ identity = session.identity;
2707
+ fee = session.feePerEvent;
2708
+ const resolvedRelay = relaysUsed[0];
2709
+ if (resolvedRelay !== void 0 && session.daemonRelayUrl !== void 0 && session.daemonRelayUrl !== resolvedRelay) {
2710
+ io2.err(
2711
+ `rig: the daemon publishes via its configured relay (${session.daemonRelayUrl}), not the resolved relay (${resolvedRelay}) \u2014 stop the daemon to force the standalone path if that matters`
2712
+ );
2713
+ }
2714
+ }
1679
2715
  const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
1680
2716
  const addr = { ownerPubkey: owner, repoId };
1681
2717
  const event = await opts.buildEvent(addr);
1682
2718
  const action = `kind:${event.kind} ${actionLabel}`;
2719
+ if (opts.preConfirm) {
2720
+ await opts.preConfirm({ identity, addr, relayUrl: relaysUsed[0] });
2721
+ }
1683
2722
  if (!flags.json) {
1684
- for (const line of renderEventPlan({ action, addr, identity, fee })) {
2723
+ for (const line of renderEventPlan({
2724
+ action,
2725
+ addr,
2726
+ identity,
2727
+ ...fee !== void 0 ? { fee } : {}
2728
+ })) {
1685
2729
  io2.out(line);
1686
2730
  }
1687
2731
  }
@@ -1690,10 +2734,11 @@ async function runEvent(opts) {
1690
2734
  io2.emitJson({
1691
2735
  command,
1692
2736
  repoAddr: addr,
2737
+ path,
1693
2738
  identity,
1694
2739
  kind: event.kind,
1695
2740
  executed: false,
1696
- feeEstimate: fee,
2741
+ feeEstimate: fee ?? null,
1697
2742
  hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
1698
2743
  });
1699
2744
  return 0;
@@ -1712,16 +2757,25 @@ async function runEvent(opts) {
1712
2757
  return 1;
1713
2758
  }
1714
2759
  }
1715
- const receipt = await standaloneCtx.publisher.publishEvent(event, relaysUsed);
1716
- const result = serializeEventReceipt(event.kind, receipt);
2760
+ let result;
2761
+ if (session.path === "standalone") {
2762
+ const receipt = await session.ctx.publisher.publishEvent(
2763
+ event,
2764
+ relaysUsed
2765
+ );
2766
+ result = serializeEventReceipt(event.kind, receipt);
2767
+ } else {
2768
+ result = await opts.sendDaemon(session.client, addr);
2769
+ }
1717
2770
  if (flags.json) {
1718
2771
  io2.emitJson({
1719
2772
  command,
1720
2773
  repoAddr: addr,
2774
+ path,
1721
2775
  identity,
1722
2776
  kind: result.kind,
1723
2777
  executed: true,
1724
- feeEstimate: fee,
2778
+ feeEstimate: fee ?? null,
1725
2779
  result
1726
2780
  });
1727
2781
  } else {
@@ -1744,11 +2798,17 @@ async function runIssue(args, deps) {
1744
2798
  const [sub, ...rest] = args;
1745
2799
  if (sub === "--help" || sub === "-h" || sub === "help") {
1746
2800
  io2.out(ISSUE_USAGE);
2801
+ io2.out("");
2802
+ io2.out(ISSUE_LIST_USAGE);
2803
+ io2.out("");
2804
+ io2.out(ISSUE_SHOW_USAGE);
1747
2805
  return 0;
1748
2806
  }
2807
+ if (sub === "list") return runIssueList(rest, deps);
2808
+ if (sub === "show") return runIssueShow(rest, deps);
1749
2809
  if (sub !== "create") {
1750
2810
  io2.err(
1751
- sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
2811
+ sub === void 0 ? "missing subcommand: rig issue <create|list|show>" : `unknown rig issue subcommand: ${sub}`
1752
2812
  );
1753
2813
  io2.err(ISSUE_USAGE);
1754
2814
  return 2;
@@ -1759,7 +2819,7 @@ async function runIssue(args, deps) {
1759
2819
  let bodyFile;
1760
2820
  let labels;
1761
2821
  try {
1762
- const { values } = parseArgs5({
2822
+ const { values } = parseArgs7({
1763
2823
  args: rest,
1764
2824
  options: {
1765
2825
  ...COMMON_OPTIONS,
@@ -1818,7 +2878,13 @@ async function runIssue(args, deps) {
1818
2878
  flags,
1819
2879
  deps,
1820
2880
  actionLabel: `issue ${JSON.stringify(title)}`,
1821
- buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels)
2881
+ buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
2882
+ sendDaemon: (client, addr) => client.gitIssue({
2883
+ repoAddr: addr,
2884
+ title,
2885
+ body,
2886
+ ...labels.length > 0 ? { labels } : {}
2887
+ })
1822
2888
  });
1823
2889
  }
1824
2890
  async function runComment(args, deps) {
@@ -1829,7 +2895,7 @@ async function runComment(args, deps) {
1829
2895
  let parentAuthor;
1830
2896
  let marker;
1831
2897
  try {
1832
- const { values, positionals } = parseArgs5({
2898
+ const { values, positionals } = parseArgs7({
1833
2899
  args,
1834
2900
  options: {
1835
2901
  ...COMMON_OPTIONS,
@@ -1879,7 +2945,14 @@ async function runComment(args, deps) {
1879
2945
  parentAuthor ?? addr.ownerPubkey,
1880
2946
  body,
1881
2947
  marker
1882
- )
2948
+ ),
2949
+ sendDaemon: (client, addr) => client.gitComment({
2950
+ repoAddr: addr,
2951
+ rootEventId,
2952
+ body,
2953
+ ...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {},
2954
+ marker
2955
+ })
1883
2956
  });
1884
2957
  }
1885
2958
  var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
@@ -1894,6 +2967,11 @@ async function runPr(args, deps) {
1894
2967
  return runPrCreate(rest, deps);
1895
2968
  case "status":
1896
2969
  return runPrStatus(rest, deps);
2970
+ // FREE read subcommands (#278): relay queries only, no payment path.
2971
+ case "list":
2972
+ return runPrList(rest, deps);
2973
+ case "show":
2974
+ return runPrShow(rest, deps);
1897
2975
  case "--help":
1898
2976
  case "-h":
1899
2977
  case "help":
@@ -1901,7 +2979,7 @@ async function runPr(args, deps) {
1901
2979
  return 0;
1902
2980
  default:
1903
2981
  io2.err(
1904
- sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
2982
+ sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
1905
2983
  );
1906
2984
  io2.err(PR_USAGE);
1907
2985
  return 2;
@@ -1914,14 +2992,18 @@ async function runPrCreate(rest, deps) {
1914
2992
  let range;
1915
2993
  let patchFile;
1916
2994
  let branch;
2995
+ let bodyFlag;
2996
+ let bodyFile;
1917
2997
  try {
1918
- const { values } = parseArgs5({
2998
+ const { values } = parseArgs7({
1919
2999
  args: rest,
1920
3000
  options: {
1921
3001
  ...COMMON_OPTIONS,
1922
3002
  title: { type: "string" },
1923
3003
  range: { type: "string" },
1924
3004
  "patch-file": { type: "string" },
3005
+ body: { type: "string" },
3006
+ "body-file": { type: "string" },
1925
3007
  branch: { type: "string" }
1926
3008
  },
1927
3009
  allowPositionals: false
@@ -1937,15 +3019,39 @@ async function runPrCreate(rest, deps) {
1937
3019
  if (values.range === void 0 === (values["patch-file"] === void 0)) {
1938
3020
  throw new Error("exactly one of --range or --patch-file is required");
1939
3021
  }
3022
+ if (values.body !== void 0 && values["body-file"] !== void 0) {
3023
+ throw new Error("--body and --body-file are mutually exclusive");
3024
+ }
1940
3025
  title = values.title;
1941
3026
  range = values.range;
1942
3027
  patchFile = values["patch-file"];
1943
3028
  branch = values.branch;
3029
+ bodyFlag = values.body;
3030
+ bodyFile = values["body-file"];
1944
3031
  } catch (err) {
1945
3032
  io2.err(err instanceof Error ? err.message : String(err));
1946
3033
  io2.err(PR_CREATE_USAGE);
1947
3034
  return 2;
1948
3035
  }
3036
+ let body;
3037
+ if (bodyFlag !== void 0) {
3038
+ body = bodyFlag;
3039
+ } else if (bodyFile !== void 0) {
3040
+ try {
3041
+ body = await readFile(bodyFile, "utf-8");
3042
+ } catch (err) {
3043
+ io2.err(
3044
+ `cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
3045
+ );
3046
+ return 2;
3047
+ }
3048
+ }
3049
+ if (body !== void 0 && body.trim() === "") {
3050
+ io2.err("the PR body is empty \u2014 drop --body/--body-file or pass text");
3051
+ return 2;
3052
+ }
3053
+ let builtPatchText;
3054
+ let builtCommits = [];
1949
3055
  return runEvent({
1950
3056
  command: "pr",
1951
3057
  flags,
@@ -1975,22 +3081,38 @@ async function runPrCreate(rest, deps) {
1975
3081
  }
1976
3082
  commits = [];
1977
3083
  }
3084
+ builtPatchText = patchText;
3085
+ builtCommits = commits;
1978
3086
  return buildPatch(
1979
3087
  addr.ownerPubkey,
1980
3088
  addr.repoId,
1981
3089
  title,
1982
3090
  commits,
1983
3091
  branch,
1984
- patchText
3092
+ patchText,
3093
+ body
1985
3094
  );
3095
+ },
3096
+ sendDaemon: (client, addr) => {
3097
+ if (builtPatchText === void 0) {
3098
+ throw new Error("internal: patch text not built before delegation");
3099
+ }
3100
+ return client.gitPatch({
3101
+ repoAddr: addr,
3102
+ title,
3103
+ patchText: builtPatchText,
3104
+ ...body !== void 0 ? { description: body } : {},
3105
+ ...builtCommits.length > 0 ? { commits: builtCommits } : {},
3106
+ ...branch !== void 0 ? { branch } : {}
3107
+ });
1986
3108
  }
1987
3109
  });
1988
3110
  }
1989
3111
  var STATUS_KIND_BY_VALUE = {
1990
- open: STATUS_OPEN_KIND,
1991
- applied: STATUS_APPLIED_KIND,
1992
- closed: STATUS_CLOSED_KIND,
1993
- draft: STATUS_DRAFT_KIND
3112
+ open: STATUS_OPEN_KIND2,
3113
+ applied: STATUS_APPLIED_KIND2,
3114
+ closed: STATUS_CLOSED_KIND2,
3115
+ draft: STATUS_DRAFT_KIND2
1994
3116
  };
1995
3117
  function isStatusValue(value) {
1996
3118
  return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
@@ -2001,7 +3123,7 @@ async function runPrStatus(args, deps) {
2001
3123
  let targetEventId;
2002
3124
  let status;
2003
3125
  try {
2004
- const { values, positionals } = parseArgs5({
3126
+ const { values, positionals } = parseArgs7({
2005
3127
  args,
2006
3128
  options: COMMON_OPTIONS,
2007
3129
  allowPositionals: true
@@ -2024,33 +3146,285 @@ async function runPrStatus(args, deps) {
2024
3146
  `status must be one of open | applied | closed | draft (got ${JSON.stringify(rawStatus)})`
2025
3147
  );
2026
3148
  }
2027
- status = rawStatus;
3149
+ status = rawStatus;
3150
+ } catch (err) {
3151
+ io2.err(err instanceof Error ? err.message : String(err));
3152
+ io2.err(PR_STATUS_USAGE);
3153
+ return 2;
3154
+ }
3155
+ return runEvent({
3156
+ command: "pr status",
3157
+ flags,
3158
+ deps,
3159
+ actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
3160
+ buildEvent: async (addr) => {
3161
+ const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
3162
+ event.tags.push([
3163
+ "a",
3164
+ `${REPOSITORY_ANNOUNCEMENT_KIND2}:${addr.ownerPubkey}:${addr.repoId}`
3165
+ ]);
3166
+ return event;
3167
+ },
3168
+ // Authority warning (#287): a status event only moves an issue/PR's
3169
+ // resolved state for repo-authority-honoring clients if its author is the
3170
+ // owner or a declared maintainer. Warn clearly when the active identity is
3171
+ // neither — the publish still happens (permissionless relay; the caller
3172
+ // may be acting on their own repo/fork), but the futility is made obvious.
3173
+ preConfirm: (args2) => warnIfNotMaintainer(deps, args2),
3174
+ sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
3175
+ });
3176
+ }
3177
+ async function warnIfNotMaintainer(deps, args) {
3178
+ const { io: io2 } = deps;
3179
+ const { identity, addr, relayUrl } = args;
3180
+ if (identity.pubkey.toLowerCase() === addr.ownerPubkey.toLowerCase()) return;
3181
+ if (relayUrl === void 0) return;
3182
+ try {
3183
+ const remote = await fetchRemoteState({
3184
+ relayUrls: [relayUrl],
3185
+ ownerPubkey: addr.ownerPubkey,
3186
+ repoId: addr.repoId,
3187
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
3188
+ });
3189
+ const authorized = authorizedStatusAuthors(
3190
+ addr.ownerPubkey,
3191
+ remote.announceEvent?.tags ?? []
3192
+ );
3193
+ if (authorized.has(identity.pubkey.toLowerCase())) return;
3194
+ io2.err(
3195
+ `warning: you (${identity.pubkey.slice(0, 8)}\u2026) are not a maintainer of 30617:${addr.ownerPubkey.slice(0, 8)}\u2026:${addr.repoId} \u2014 clients honoring repo authority (rig, rig-web) will IGNORE this status when resolving the issue/PR's state. Publishing anyway (the relay is permissionless); ask the owner to \`rig maintainers add\` you if this should stick.`
3196
+ );
3197
+ } catch {
3198
+ io2.err(
3199
+ "warning: could not read the repo announcement to verify your maintainer status \u2014 if you are not the owner or a declared maintainer, authority-honoring clients will ignore this status."
3200
+ );
3201
+ }
3202
+ }
3203
+
3204
+ // src/cli/fetch.ts
3205
+ import { parseArgs as parseArgs8 } from "util";
3206
+ var FETCH_USAGE = `Usage: rig fetch [remote] [options]
3207
+
3208
+ Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
3209
+ payments, no identity needed). Reads the kind:30618 repository state from the
3210
+ remote's relay, downloads only the git objects this repository is missing
3211
+ (SHA-1 verified), and updates the remote-tracking refs
3212
+ (refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
3213
+
3214
+ No merge happens: integrate with the git passthrough, e.g.
3215
+ \`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
3216
+ \`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
3217
+
3218
+ The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
3219
+ or \`rig clone\` writes; --repo-id/--owner override.
3220
+
3221
+ Options:
3222
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
3223
+ --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
3224
+ --relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
3225
+ --concurrency <n> parallel gateway downloads (default 8)
3226
+ --json machine-readable result envelope
3227
+ -h, --help show this help`;
3228
+ var WS_URL_RE3 = /^wss?:\/\//i;
3229
+ function localRefFor(refname, remote) {
3230
+ if (refname.startsWith("refs/heads/")) {
3231
+ return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
3232
+ }
3233
+ if (refname.startsWith("refs/tags/")) return refname;
3234
+ return null;
3235
+ }
3236
+ async function resolveLocalRef(repoRoot, refname) {
3237
+ try {
3238
+ const out = await runGit(repoRoot, [
3239
+ "rev-parse",
3240
+ "--verify",
3241
+ "--quiet",
3242
+ refname
3243
+ ]);
3244
+ const sha = out.trim();
3245
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
3246
+ } catch {
3247
+ return null;
3248
+ }
3249
+ }
3250
+ async function runFetch(args, deps) {
3251
+ const { io: io2 } = deps;
3252
+ let json = false;
3253
+ let remote = "origin";
3254
+ let relayFlag;
3255
+ let repoIdFlag;
3256
+ let ownerFlag;
3257
+ let concurrency;
3258
+ try {
3259
+ const { values, positionals } = parseArgs8({
3260
+ args,
3261
+ options: {
3262
+ json: { type: "boolean", default: false },
3263
+ relay: { type: "string" },
3264
+ "repo-id": { type: "string" },
3265
+ owner: { type: "string" },
3266
+ concurrency: { type: "string" },
3267
+ help: { type: "boolean", short: "h", default: false }
3268
+ },
3269
+ allowPositionals: true
3270
+ });
3271
+ if (values.help) {
3272
+ io2.out(FETCH_USAGE);
3273
+ return 0;
3274
+ }
3275
+ json = values.json === true;
3276
+ if (positionals.length > 1) {
3277
+ throw new Error(
3278
+ `expected at most one [remote] argument, got ${positionals.length}`
3279
+ );
3280
+ }
3281
+ if (positionals[0] !== void 0) remote = positionals[0];
3282
+ relayFlag = values.relay;
3283
+ repoIdFlag = values["repo-id"];
3284
+ ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
3285
+ if (values.concurrency !== void 0) {
3286
+ concurrency = Number.parseInt(values.concurrency, 10);
3287
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
3288
+ throw new Error("--concurrency must be a positive integer");
3289
+ }
3290
+ }
2028
3291
  } catch (err) {
2029
3292
  io2.err(err instanceof Error ? err.message : String(err));
2030
- io2.err(PR_STATUS_USAGE);
3293
+ io2.err(FETCH_USAGE);
2031
3294
  return 2;
2032
3295
  }
2033
- return runEvent({
2034
- command: "pr status",
2035
- flags,
2036
- deps,
2037
- actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
2038
- buildEvent: async (addr) => {
2039
- const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
2040
- event.tags.push([
2041
- "a",
2042
- `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
2043
- ]);
2044
- return event;
3296
+ try {
3297
+ const repoRoot = await resolveRepoRoot(deps.cwd);
3298
+ const toonConfig = await readToonConfig(repoRoot);
3299
+ const repoId = repoIdFlag ?? toonConfig.repoId;
3300
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
3301
+ const owner = ownerFlag ?? toonConfig.owner;
3302
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
3303
+ let relayUrl;
3304
+ if (relayFlag !== void 0) {
3305
+ relayUrl = relayFlag;
3306
+ } else {
3307
+ const urls = await getGitRemoteUrls(repoRoot, remote);
3308
+ if (urls.length === 0) throw new UnknownRemoteError(remote);
3309
+ if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
3310
+ relayUrl = urls[0];
2045
3311
  }
2046
- });
3312
+ if (!WS_URL_RE3.test(relayUrl)) {
3313
+ throw new InvalidRelayUrlError(
3314
+ relayUrl,
3315
+ `remote ${JSON.stringify(remote)}`
3316
+ );
3317
+ }
3318
+ const remoteState = await fetchRemoteState({
3319
+ relayUrls: [relayUrl],
3320
+ ownerPubkey: owner,
3321
+ repoId,
3322
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
3323
+ ...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
3324
+ });
3325
+ const planned = [];
3326
+ for (const [refname, sha] of remoteState.refs) {
3327
+ if (!isSafeRefname(refname)) {
3328
+ io2.err(
3329
+ `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
3330
+ );
3331
+ continue;
3332
+ }
3333
+ const localRef = localRefFor(refname, remote);
3334
+ if (localRef === null) {
3335
+ io2.err(
3336
+ `warning: skipping ref outside refs/heads and refs/tags: ${refname}`
3337
+ );
3338
+ continue;
3339
+ }
3340
+ planned.push({ refname, localRef, newSha: sha });
3341
+ }
3342
+ const reader = new GitRepoReader(repoRoot);
3343
+ const tips = [...new Set(planned.map((p) => p.newSha))];
3344
+ const candidates = [.../* @__PURE__ */ new Set([...remoteState.shaToTxId.keys(), ...tips])];
3345
+ const { missing: absent } = await reader.statObjects(candidates);
3346
+ const absentSet = new Set(absent);
3347
+ const presentLocally = new Set(
3348
+ candidates.filter((sha) => !absentSet.has(sha))
3349
+ );
3350
+ const collected = await collectRepoObjects({
3351
+ tips,
3352
+ shaToTxId: remoteState.shaToTxId,
3353
+ resolveMissing: (shas) => remoteState.resolveMissing(shas),
3354
+ presentLocally,
3355
+ ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
3356
+ ...concurrency !== void 0 ? { concurrency } : {}
3357
+ });
3358
+ if (collected.missing.length > 0) {
3359
+ throw new MissingRemoteObjectsError(
3360
+ collected.missing,
3361
+ `cannot fetch from ${relayUrl}`
3362
+ );
3363
+ }
3364
+ const written = await writeGitObjects(repoRoot, collected.objects.values());
3365
+ const updates = [];
3366
+ for (const { refname, localRef, newSha } of planned) {
3367
+ const oldSha = await resolveLocalRef(repoRoot, localRef);
3368
+ if (oldSha === newSha) {
3369
+ updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
3370
+ continue;
3371
+ }
3372
+ let kind;
3373
+ if (oldSha === null) {
3374
+ kind = "new";
3375
+ } else {
3376
+ kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
3377
+ }
3378
+ await updateRef(repoRoot, localRef, newSha);
3379
+ updates.push({ refname, localRef, oldSha, newSha, kind });
3380
+ }
3381
+ const moved = updates.filter((u) => u.kind !== "up-to-date");
3382
+ if (json) {
3383
+ io2.emitJson({
3384
+ command: "fetch",
3385
+ repoAddr: { ownerPubkey: owner, repoId },
3386
+ remote,
3387
+ relay: relayUrl,
3388
+ objectsDownloaded: written,
3389
+ updates,
3390
+ executed: true
3391
+ });
3392
+ return 0;
3393
+ }
3394
+ if (moved.length === 0) {
3395
+ io2.out("Already up to date.");
3396
+ return 0;
3397
+ }
3398
+ io2.out(`From ${relayUrl}`);
3399
+ for (const u of moved) {
3400
+ const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
3401
+ const src = short(u.refname);
3402
+ const dst = short(u.localRef);
3403
+ if (u.kind === "new") {
3404
+ const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
3405
+ io2.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
3406
+ } else if (u.kind === "forced") {
3407
+ io2.out(
3408
+ ` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
3409
+ );
3410
+ } else {
3411
+ io2.out(
3412
+ ` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
3413
+ );
3414
+ }
3415
+ }
3416
+ io2.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
3417
+ return 0;
3418
+ } catch (err) {
3419
+ return emitCliError(io2, json, "fetch", err);
3420
+ }
2047
3421
  }
2048
3422
 
2049
3423
  // src/cli/fund.ts
2050
3424
  import { readFileSync } from "fs";
2051
3425
  import { homedir } from "os";
2052
- import { join } from "path";
2053
- import { parseArgs as parseArgs6 } from "util";
3426
+ import { join as join2 } from "path";
3427
+ import { parseArgs as parseArgs9 } from "util";
2054
3428
  var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
2055
3429
  var CHAINS = ["evm", "solana", "mina"];
2056
3430
  var FUND_USAGE = `Usage: rig fund [options]
@@ -2058,10 +3432,11 @@ var FUND_USAGE = `Usage: rig fund [options]
2058
3432
  Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
2059
3433
  pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
2060
3434
  ~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
2061
- faucetUrl config field, or the deployed devnet faucet when the configured
2062
- network is devnet. The faucet drips a FIXED amount per chain (there is no
2063
- --amount). On a network without a faucet, prints the wallet address(es) to
2064
- fund externally instead.
3435
+ faucetUrl config field, or the deployed devnet faucet when the network is
3436
+ devnet \u2014 including when a configured *.devnet.toonprotocol.dev origin infers
3437
+ it. The faucet drips a FIXED amount per chain (there is no --amount). On a
3438
+ network without a faucet, prints the wallet address(es) to fund externally
3439
+ instead.
2065
3440
 
2066
3441
  Options:
2067
3442
  --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
@@ -2069,9 +3444,36 @@ Options:
2069
3444
  --address <address> fund this address instead of the identity's own
2070
3445
  --json machine-readable envelope
2071
3446
  -h, --help show this help`;
3447
+ var SHARED_DEVNET_SUFFIX = ".devnet.toonprotocol.dev";
3448
+ function sharedDevnetOrigin(env, file) {
3449
+ const candidates = [
3450
+ env["TOON_CLIENT_RELAY_URL"] ?? file.relayUrl,
3451
+ env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl,
3452
+ env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl
3453
+ ];
3454
+ for (const url of candidates) {
3455
+ if (!url) continue;
3456
+ try {
3457
+ const { hostname } = new URL(url);
3458
+ if (hostname.endsWith(SHARED_DEVNET_SUFFIX) || hostname === SHARED_DEVNET_SUFFIX.slice(1)) {
3459
+ return url;
3460
+ }
3461
+ } catch {
3462
+ }
3463
+ }
3464
+ return void 0;
3465
+ }
3466
+ function noFaucetGuidance(network, devnetOrigin) {
3467
+ const head = `no faucet is configured for network ${JSON.stringify(network ?? "custom")}`;
3468
+ const external = "To fund the wallet externally instead, send the settlement token plus native gas to the address below for the chain your channels settle on.";
3469
+ if (devnetOrigin !== void 0) {
3470
+ return `${head} \u2014 but your configured origin (${devnetOrigin}) looks like the shared devnet. Set TOON_CLIENT_NETWORK=devnet (or "network": "devnet" in the client config) and re-run \`rig fund\` \u2014 no faucet URL is needed there. ${external}`;
3471
+ }
3472
+ 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}`;
3473
+ }
2072
3474
  function readFundConfig(env) {
2073
- const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
2074
- const configPath = join(dir, "config.json");
3475
+ const dir = env["TOON_CLIENT_HOME"] ?? join2(homedir(), ".toon-client");
3476
+ const configPath = join2(dir, "config.json");
2075
3477
  try {
2076
3478
  return {
2077
3479
  file: JSON.parse(readFileSync(configPath, "utf8")),
@@ -2092,7 +3494,7 @@ async function runFund(args, deps) {
2092
3494
  let addressFlag;
2093
3495
  let json = false;
2094
3496
  try {
2095
- const { values } = parseArgs6({
3497
+ const { values } = parseArgs9({
2096
3498
  args,
2097
3499
  options: {
2098
3500
  chain: { type: "string" },
@@ -2127,7 +3529,10 @@ async function runFund(args, deps) {
2127
3529
  );
2128
3530
  }
2129
3531
  const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
2130
- const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (network === "devnet" ? DEVNET_FAUCET_URL : void 0);
3532
+ const devnetOrigin = sharedDevnetOrigin(env, file);
3533
+ const inferredDevnet = devnetOrigin !== void 0 && (network === void 0 || network === "custom");
3534
+ const effectiveNetwork = inferredDevnet ? "devnet" : network;
3535
+ const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (effectiveNetwork === "devnet" ? DEVNET_FAUCET_URL : void 0);
2131
3536
  const resolved = await resolveIdentity({
2132
3537
  env,
2133
3538
  cwd: deps.cwd,
@@ -2149,7 +3554,7 @@ async function runFund(args, deps) {
2149
3554
  mina: derived.mina.publicKey || null
2150
3555
  };
2151
3556
  if (!faucetUrl) {
2152
- const guidance = `no faucet on network ${JSON.stringify(network ?? "custom")} \u2014 fund the wallet externally (send the settlement token + native gas to the address for the chain your channels settle on), or set TOON_CLIENT_FAUCET_URL / the faucetUrl config field if this network has one.`;
3557
+ const guidance = noFaucetGuidance(network, devnetOrigin);
2153
3558
  if (json) {
2154
3559
  io2.emitJson({
2155
3560
  command: "fund",
@@ -2178,6 +3583,11 @@ async function runFund(args, deps) {
2178
3583
  }
2179
3584
  const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
2180
3585
  const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
3586
+ if (!json && inferredDevnet) {
3587
+ io2.out(
3588
+ `Inferred network 'devnet' from the configured origin ${devnetOrigin} (network was ${JSON.stringify(network ?? "custom")}). Set TOON_CLIENT_NETWORK explicitly to override.`
3589
+ );
3590
+ }
2181
3591
  if (!json) {
2182
3592
  io2.out(
2183
3593
  `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
@@ -2192,11 +3602,12 @@ async function runFund(args, deps) {
2192
3602
  command: "fund",
2193
3603
  identity,
2194
3604
  funded: true,
2195
- network: network ?? null,
3605
+ network: effectiveNetwork ?? null,
2196
3606
  chain,
2197
3607
  address,
2198
3608
  faucetUrl,
2199
- response
3609
+ response,
3610
+ ...inferredDevnet ? { inferredDevnetFrom: devnetOrigin } : {}
2200
3611
  });
2201
3612
  return 0;
2202
3613
  }
@@ -2221,7 +3632,7 @@ function signalExitCode(signal) {
2221
3632
  var runGitPassthrough = (argv2, options = {}) => {
2222
3633
  const err = options.err ?? ((line) => process.stderr.write(`${line}
2223
3634
  `));
2224
- return new Promise((resolve) => {
3635
+ return new Promise((resolve2) => {
2225
3636
  const child = spawn("git", argv2, {
2226
3637
  stdio: "inherit",
2227
3638
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
@@ -2244,22 +3655,22 @@ var runGitPassthrough = (argv2, options = {}) => {
2244
3655
  err(
2245
3656
  `rig: git not found \u2014 \`rig ${argv2[0] ?? ""}\` is not a rig command, so it is passed through to the system \`git\`, which is not on your PATH. Install git (https://git-scm.com) or fix your PATH.`
2246
3657
  );
2247
- resolve(GIT_NOT_FOUND_EXIT);
3658
+ resolve2(GIT_NOT_FOUND_EXIT);
2248
3659
  return;
2249
3660
  }
2250
3661
  err(`rig: failed to run git: ${spawnErr.message}`);
2251
- resolve(1);
3662
+ resolve2(1);
2252
3663
  });
2253
3664
  child.on("close", (code, signal) => {
2254
3665
  restoreSignals();
2255
- resolve(signal !== null ? signalExitCode(signal) : code ?? 1);
3666
+ resolve2(signal !== null ? signalExitCode(signal) : code ?? 1);
2256
3667
  });
2257
3668
  });
2258
3669
  };
2259
3670
 
2260
3671
  // src/cli/init.ts
2261
- import { basename } from "path";
2262
- import { parseArgs as parseArgs7 } from "util";
3672
+ import { basename as basename2 } from "path";
3673
+ import { parseArgs as parseArgs10 } from "util";
2263
3674
  var INIT_USAGE = `Usage: rig init [options]
2264
3675
 
2265
3676
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -2279,7 +3690,7 @@ Options:
2279
3690
  --json machine-readable report
2280
3691
  -h, --help show this help`;
2281
3692
  function parseInitArgs(args) {
2282
- const { values, positionals } = parseArgs7({
3693
+ const { values, positionals } = parseArgs10({
2283
3694
  args,
2284
3695
  options: {
2285
3696
  "repo-id": { type: "string" },
@@ -2329,7 +3740,7 @@ async function runInit(args, deps) {
2329
3740
  warn: (line) => io2.err(line)
2330
3741
  });
2331
3742
  const existing = await readToonConfig(repoRoot);
2332
- const repoId = flags.repoId ?? existing.repoId ?? basename(repoRoot);
3743
+ const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
2333
3744
  const changed = {
2334
3745
  repoId: existing.repoId !== repoId,
2335
3746
  owner: existing.owner !== identity.pubkey
@@ -2404,6 +3815,326 @@ async function runInit(args, deps) {
2404
3815
  }
2405
3816
  }
2406
3817
 
3818
+ // src/cli/maintainers.ts
3819
+ import { parseArgs as parseArgs11 } from "util";
3820
+ var HEX64_RE3 = /^[0-9a-f]{64}$/;
3821
+ var WS_URL_RE4 = /^wss?:\/\//i;
3822
+ var MAINTAINERS_USAGE = `Usage: rig maintainers <list|add|remove> [<pubkey>] [options]
3823
+
3824
+ Manage a repo's declared maintainers (#287). Status authority is consumer-side:
3825
+ rig and rig-web honor an issue/PR status (kind:1630-1633) ONLY when its author
3826
+ is the repo owner or a declared maintainer. The owner is always an implicit
3827
+ maintainer; this command edits the explicit set on the kind:30617 announcement.
3828
+
3829
+ Subcommands:
3830
+ list show the owner + declared maintainers \u2014 FREE (relay read)
3831
+ add <pubkey> add a maintainer (npub or 64-char hex) \u2014 PAID: republishes
3832
+ the kind:30617 (permanent, non-refundable)
3833
+ remove <pubkey> remove a maintainer \u2014 PAID: republishes the kind:30617
3834
+
3835
+ add/remove must run under the repo OWNER's identity (only the owner's
3836
+ announcement is authoritative).
3837
+
3838
+ Options:
3839
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
3840
+ --owner <pubkey> repository owner (npub or hex; default: git config)
3841
+ --remote <name> publish/read via this configured git remote (default: origin)
3842
+ --relay <url> ad-hoc relay override (exactly one for add/remove)
3843
+ --yes skip the fee confirmation (required when not a TTY)
3844
+ --json machine-readable envelope
3845
+ -h, --help show this help`;
3846
+ var MAINT_OPTIONS = {
3847
+ json: { type: "boolean", default: false },
3848
+ yes: { type: "boolean", default: false },
3849
+ relay: { type: "string", multiple: true },
3850
+ remote: { type: "string" },
3851
+ "repo-id": { type: "string" },
3852
+ owner: { type: "string" },
3853
+ help: { type: "boolean", short: "h", default: false }
3854
+ };
3855
+ function pickFlags(values) {
3856
+ const flags = {
3857
+ json: values["json"] === true,
3858
+ yes: values["yes"] === true,
3859
+ help: values["help"] === true,
3860
+ relay: Array.isArray(values["relay"]) ? values["relay"] : []
3861
+ };
3862
+ if (typeof values["remote"] === "string") flags.remote = values["remote"];
3863
+ if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
3864
+ if (typeof values["owner"] === "string")
3865
+ flags.owner = ownerToHex(values["owner"]);
3866
+ return flags;
3867
+ }
3868
+ async function resolveContext(flags, deps) {
3869
+ let repoRoot;
3870
+ let toonConfig = {
3871
+ relays: []
3872
+ };
3873
+ try {
3874
+ repoRoot = await resolveRepoRoot(deps.cwd);
3875
+ toonConfig = await readToonConfig(repoRoot);
3876
+ } catch {
3877
+ }
3878
+ const repoId = flags.repoId ?? toonConfig.repoId;
3879
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
3880
+ const owner = flags.owner ?? toonConfig.owner;
3881
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
3882
+ const resolved = await resolveRelays({
3883
+ relayFlags: flags.relay,
3884
+ remoteName: flags.remote,
3885
+ repoRoot,
3886
+ toonRelays: toonConfig.relays
3887
+ });
3888
+ if (resolved.nudge !== void 0) deps.io.err(resolved.nudge);
3889
+ return {
3890
+ repoId,
3891
+ owner,
3892
+ relays: resolved.relays,
3893
+ resolved,
3894
+ ...repoRoot !== void 0 ? { repoRoot } : {}
3895
+ };
3896
+ }
3897
+ async function runMaintainers(args, deps) {
3898
+ const { io: io2 } = deps;
3899
+ const [sub, ...rest] = args;
3900
+ switch (sub) {
3901
+ case "list":
3902
+ return runList2(rest, deps);
3903
+ case "add":
3904
+ return runMutate("add", rest, deps);
3905
+ case "remove":
3906
+ return runMutate("remove", rest, deps);
3907
+ case "--help":
3908
+ case "-h":
3909
+ case "help":
3910
+ io2.out(MAINTAINERS_USAGE);
3911
+ return 0;
3912
+ default:
3913
+ io2.err(
3914
+ sub === void 0 ? "missing subcommand: rig maintainers <list|add|remove>" : `unknown rig maintainers subcommand: ${sub}`
3915
+ );
3916
+ io2.err(MAINTAINERS_USAGE);
3917
+ return 2;
3918
+ }
3919
+ }
3920
+ async function runList2(args, deps) {
3921
+ const { io: io2 } = deps;
3922
+ let flags;
3923
+ try {
3924
+ const { values, positionals } = parseArgs11({
3925
+ args,
3926
+ options: MAINT_OPTIONS,
3927
+ allowPositionals: true
3928
+ });
3929
+ if (positionals.length > 0) {
3930
+ throw new Error("rig maintainers list takes no positional arguments");
3931
+ }
3932
+ flags = pickFlags(values);
3933
+ } catch (err) {
3934
+ io2.err(err instanceof Error ? err.message : String(err));
3935
+ io2.err(MAINTAINERS_USAGE);
3936
+ return 2;
3937
+ }
3938
+ if (flags.help) {
3939
+ io2.out(MAINTAINERS_USAGE);
3940
+ return 0;
3941
+ }
3942
+ try {
3943
+ const ctx = await resolveContext(flags, deps);
3944
+ const wsRelays = ctx.relays.filter((url) => WS_URL_RE4.test(url));
3945
+ if (wsRelays.length === 0) {
3946
+ throw new InvalidRelayUrlError(
3947
+ ctx.relays[0] ?? "",
3948
+ "reads need a ws:// or wss:// relay"
3949
+ );
3950
+ }
3951
+ const remote = await fetchRemoteState({
3952
+ relayUrls: wsRelays,
3953
+ ownerPubkey: ctx.owner,
3954
+ repoId: ctx.repoId,
3955
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
3956
+ });
3957
+ const maintainers = remote.maintainers;
3958
+ if (flags.json) {
3959
+ io2.emitJson({
3960
+ command: "maintainers list",
3961
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
3962
+ announced: remote.announced,
3963
+ owner: ctx.owner,
3964
+ maintainers
3965
+ });
3966
+ return 0;
3967
+ }
3968
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
3969
+ io2.out(`Owner: ${ctx.owner} (implicit maintainer)`);
3970
+ if (!remote.announced) {
3971
+ io2.out("No kind:30617 announcement found \u2014 owner-only authority.");
3972
+ return 0;
3973
+ }
3974
+ if (maintainers.length === 0) {
3975
+ io2.out("Maintainers: (none declared \u2014 owner-only authority)");
3976
+ } else {
3977
+ io2.out(`Maintainers (${maintainers.length}):`);
3978
+ for (const m of maintainers) io2.out(` ${m}`);
3979
+ }
3980
+ return 0;
3981
+ } catch (err) {
3982
+ return emitCliError(io2, flags.json, "maintainers list", err);
3983
+ }
3984
+ }
3985
+ async function runMutate(op, args, deps) {
3986
+ const { io: io2 } = deps;
3987
+ const command = `maintainers ${op}`;
3988
+ let flags;
3989
+ let pubkey;
3990
+ try {
3991
+ const { values, positionals } = parseArgs11({
3992
+ args,
3993
+ options: MAINT_OPTIONS,
3994
+ allowPositionals: true
3995
+ });
3996
+ flags = pickFlags(values);
3997
+ if (flags.help) {
3998
+ io2.out(MAINTAINERS_USAGE);
3999
+ return 0;
4000
+ }
4001
+ if (positionals.length !== 1) {
4002
+ throw new Error(`expected exactly one <pubkey> to ${op}`);
4003
+ }
4004
+ pubkey = ownerToHex(positionals[0]).toLowerCase();
4005
+ if (!HEX64_RE3.test(pubkey)) {
4006
+ throw new Error(`<pubkey> must resolve to 64-char hex (got ${JSON.stringify(positionals[0])})`);
4007
+ }
4008
+ } catch (err) {
4009
+ io2.err(err instanceof Error ? err.message : String(err));
4010
+ io2.err(MAINTAINERS_USAGE);
4011
+ return 2;
4012
+ }
4013
+ let standaloneCtx;
4014
+ try {
4015
+ const ctx = await resolveContext(flags, deps);
4016
+ if (ctx.relays.length > 1) {
4017
+ io2.err(singleRelayRefusal(ctx.resolved, "Nothing was published or paid."));
4018
+ return 1;
4019
+ }
4020
+ const relayUrl = ctx.relays[0];
4021
+ if (relayUrl === void 0 || !WS_URL_RE4.test(relayUrl)) {
4022
+ throw new InvalidRelayUrlError(
4023
+ relayUrl ?? "",
4024
+ "a paid publish needs a ws:// or wss:// relay"
4025
+ );
4026
+ }
4027
+ const load = deps.loadStandalone ?? defaultLoadStandalone;
4028
+ standaloneCtx = await load({
4029
+ env: deps.env,
4030
+ cwd: deps.cwd,
4031
+ warn: (line) => io2.err(line),
4032
+ relayUrl
4033
+ });
4034
+ const identity = identityReport(standaloneCtx);
4035
+ if (identity.pubkey.toLowerCase() !== ctx.owner.toLowerCase()) {
4036
+ io2.err(
4037
+ `rig: only the repo owner (${ctx.owner.slice(0, 8)}\u2026) can change the maintainer set \u2014 the active identity is ${identity.pubkey.slice(0, 8)}\u2026. A non-owner republish would write your own (ignored) announcement. Nothing was published or paid.`
4038
+ );
4039
+ return 1;
4040
+ }
4041
+ const remote = await fetchRemoteState({
4042
+ relayUrls: [relayUrl],
4043
+ ownerPubkey: ctx.owner,
4044
+ repoId: ctx.repoId,
4045
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
4046
+ });
4047
+ if (!remote.announced) {
4048
+ io2.err(
4049
+ `rig: 30617:${ctx.owner.slice(0, 8)}\u2026:${ctx.repoId} has no announcement yet \u2014 run \`rig push\` to publish the repo (with its real name/description) before managing maintainers. Nothing was published or paid.`
4050
+ );
4051
+ return 1;
4052
+ }
4053
+ const current = remote.announceEvent ? parseMaintainers(remote.announceEvent.tags) : [];
4054
+ const currentSet = new Set(current);
4055
+ if (op === "add" && currentSet.has(pubkey)) {
4056
+ io2.err(
4057
+ `rig: ${pubkey.slice(0, 8)}\u2026 is already a maintainer \u2014 nothing to do (not published).`
4058
+ );
4059
+ return 0;
4060
+ }
4061
+ if (op === "remove" && !currentSet.has(pubkey)) {
4062
+ io2.err(
4063
+ `rig: ${pubkey.slice(0, 8)}\u2026 is not a declared maintainer \u2014 nothing to do (not published).`
4064
+ );
4065
+ return 0;
4066
+ }
4067
+ const next = op === "add" ? [...current, pubkey] : current.filter((m) => m !== pubkey);
4068
+ const name = remote.name ?? ctx.repoId;
4069
+ const description = remote.description ?? "";
4070
+ const event = buildRepoAnnouncement(ctx.repoId, name, description, next);
4071
+ const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
4072
+ const action = `kind:30617 maintainers ${op} ${pubkey.slice(0, 8)}\u2026`;
4073
+ if (!flags.json) {
4074
+ io2.out(`Republish ${action}`);
4075
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
4076
+ io2.out(`Maintainers after: ${next.length === 0 ? "(none)" : next.join(", ")}`);
4077
+ io2.out(`Fee: ${feeLabel(fee)}. Writes are permanent and non-refundable.`);
4078
+ }
4079
+ if (!flags.yes) {
4080
+ if (flags.json) {
4081
+ io2.emitJson({
4082
+ command,
4083
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4084
+ identity,
4085
+ executed: false,
4086
+ feeEstimate: fee,
4087
+ maintainers: next,
4088
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
4089
+ });
4090
+ return 0;
4091
+ }
4092
+ if (!io2.isInteractive) {
4093
+ io2.err(
4094
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
4095
+ );
4096
+ return 1;
4097
+ }
4098
+ const proceed = await io2.confirm(
4099
+ `Proceed with paid republish (${feeLabel(fee)})? [y/N] `
4100
+ );
4101
+ if (!proceed) {
4102
+ io2.err("aborted \u2014 nothing was published.");
4103
+ return 1;
4104
+ }
4105
+ }
4106
+ const receipt = await standaloneCtx.publisher.publishEvent(event, [
4107
+ relayUrl
4108
+ ]);
4109
+ const result = serializeEventReceipt(event.kind, receipt);
4110
+ if (flags.json) {
4111
+ io2.emitJson({
4112
+ command,
4113
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4114
+ identity,
4115
+ executed: true,
4116
+ feeEstimate: fee,
4117
+ maintainers: next,
4118
+ result
4119
+ });
4120
+ } else {
4121
+ io2.out(
4122
+ `Published ${action}: ${result.eventId} paid ${result.feePaid} base units`
4123
+ );
4124
+ }
4125
+ return 0;
4126
+ } catch (err) {
4127
+ return emitCliError(io2, flags.json, command, err);
4128
+ } finally {
4129
+ if (standaloneCtx) {
4130
+ try {
4131
+ await standaloneCtx.stop();
4132
+ } catch {
4133
+ }
4134
+ }
4135
+ }
4136
+ }
4137
+
2407
4138
  // src/cli/dispatch.ts
2408
4139
  var USAGE = `rig \u2014 git with a TOON remote (pay-to-write Nostr + Arweave)
2409
4140
 
@@ -2415,17 +4146,31 @@ Commands rig owns:
2415
4146
  remote add <name> <url> add a relay as a REAL git remote ("origin" is
2416
4147
  remote remove <name> the default publish target); remove/list manage
2417
4148
  remote list them \u2014 \`git remote -v\` shows the same data
4149
+ clone <relay-url> <owner>/<repo-id> [dir]
4150
+ clone a TOON repo (free): relay state + Arweave
4151
+ objects (SHA-1 verified) \u2192 a real git repository,
4152
+ push/pull-capable out of the box
4153
+ fetch [remote] fetch remote refs + missing objects (free) and
4154
+ update refs/remotes/<remote>/*; no merge. Shadows
4155
+ git fetch (plain \`git fetch\` stays available)
2418
4156
  push [remote] [refspecs...] plan, price, confirm, and execute a paid push
2419
4157
  to TOON (defaults to the "origin" remote). rig
2420
4158
  push is the TOON transport and shadows git push;
2421
4159
  plain-git pushes remain available by running
2422
4160
  \`git push\` directly
2423
4161
  issue create file an issue (kind:1621) against a repo
4162
+ issue list | show <id> read the repo's issues + comments (free)
4163
+ pr list | show <id> read the repo's patches; show prints the full
4164
+ patch text (free)
2424
4165
  comment <root-event-id> comment (kind:1622) on an issue or patch
2425
4166
  pr create publish a patch (kind:1617) with real
2426
4167
  \`git format-patch\` content
2427
4168
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
2428
4169
  open | applied | closed | draft
4170
+ maintainers list show the repo's declared maintainers (free); add/
4171
+ maintainers add <pubkey> remove republish the kind:30617 to change who may
4172
+ maintainers remove <pubkey> author authoritative issue/PR status (owner is
4173
+ always an implicit maintainer)
2429
4174
  fund drip devnet faucet funds to this identity's
2430
4175
  wallet (free); on other networks prints the
2431
4176
  address(es) to fund externally
@@ -2443,8 +4188,10 @@ Any other command is passed through to git verbatim: \`rig status\` runs
2443
4188
  \`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
2444
4189
 
2445
4190
  Run \`rig <command> --help\` for a rig command's flags. \`rig init\`,
2446
- \`rig remote\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\` are
2447
- free; push/issue/comment/pr are paid writes \u2014 permanent and non-refundable \u2014
4191
+ \`rig remote\`, \`rig clone\`, \`rig fetch\`, \`rig issue list/show\`,
4192
+ \`rig pr list/show\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\`
4193
+ are free; push/issue create/comment/pr create/pr status are paid writes \u2014
4194
+ permanent and non-refundable \u2014
2448
4195
  and channel open/close/settle are on-chain wallet transactions; each states
2449
4196
  what it will spend and asks for confirmation before doing so (--yes skips,
2450
4197
  --json emits machine output).
@@ -2474,6 +4221,11 @@ async function dispatch(argv2, deps) {
2474
4221
  return runInit(rest, deps);
2475
4222
  case "remote":
2476
4223
  return runRemote(rest, deps);
4224
+ // The #278 read path — FREE (relay + Arweave gateway reads, no payment).
4225
+ case "clone":
4226
+ return runClone(rest, deps);
4227
+ case "fetch":
4228
+ return runFetch(rest, deps);
2477
4229
  case "push":
2478
4230
  return runPush(rest, deps);
2479
4231
  case "issue":
@@ -2482,6 +4234,8 @@ async function dispatch(argv2, deps) {
2482
4234
  return runComment(rest, deps);
2483
4235
  case "pr":
2484
4236
  return runPr(rest, deps);
4237
+ case "maintainers":
4238
+ return runMaintainers(rest, deps);
2485
4239
  case "channel":
2486
4240
  return runChannel(rest, deps);
2487
4241
  case "fund":
@@ -2553,10 +4307,13 @@ function makeCliIo(options) {
2553
4307
  var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
2554
4308
  "init",
2555
4309
  "remote",
4310
+ "clone",
4311
+ "fetch",
2556
4312
  "push",
2557
4313
  "issue",
2558
4314
  "comment",
2559
4315
  "pr",
4316
+ "maintainers",
2560
4317
  "channel",
2561
4318
  "fund",
2562
4319
  "balance"
@@ -2570,6 +4327,29 @@ function isJsonInvocation(argv2) {
2570
4327
  }
2571
4328
  return false;
2572
4329
  }
4330
+ var ANNOUNCE_FAILED_RE = /\[Bootstrap\] Announce failed/;
4331
+ var ANNOUNCE_402_RE = /402|payment required/i;
4332
+ var ANNOUNCE_402_INFO = "rig: skipped the optional identity self-announce \u2014 the payment peer charges for announces and this identity has not paid for one (expected on a fresh or unfunded identity); harmless, the command continues.\n";
4333
+ function calmBootstrapNoise() {
4334
+ const original = process.stderr.write;
4335
+ let reframed = false;
4336
+ const patched = ((chunk, encodingOrCb, cb) => {
4337
+ let text = chunk;
4338
+ if (typeof chunk === "string" && ANNOUNCE_FAILED_RE.test(chunk)) {
4339
+ if (ANNOUNCE_402_RE.test(chunk)) {
4340
+ text = reframed ? "" : ANNOUNCE_402_INFO;
4341
+ reframed = true;
4342
+ }
4343
+ }
4344
+ return typeof encodingOrCb === "function" ? original.call(process.stderr, text, encodingOrCb) : original.call(process.stderr, text, encodingOrCb, cb);
4345
+ });
4346
+ process.stderr.write = patched;
4347
+ return {
4348
+ restore: () => {
4349
+ if (process.stderr.write === patched) process.stderr.write = original;
4350
+ }
4351
+ };
4352
+ }
2573
4353
  function redirectStdoutToStderr(realWrite) {
2574
4354
  const original = process.stdout.write;
2575
4355
  const real = realWrite ?? original.bind(process.stdout);
@@ -2591,6 +4371,7 @@ function redirectStdoutToStderr(realWrite) {
2591
4371
 
2592
4372
  // src/cli/rig.ts
2593
4373
  function makeIo(jsonMode) {
4374
+ calmBootstrapNoise();
2594
4375
  const guard = jsonMode ? redirectStdoutToStderr() : void 0;
2595
4376
  return makeCliIo({
2596
4377
  jsonMode,
@@ -2615,6 +4396,16 @@ function makeIo(jsonMode) {
2615
4396
  }
2616
4397
  });
2617
4398
  }
4399
+ function exitWhenFlushed(code) {
4400
+ process.exitCode = code;
4401
+ let pending = 2;
4402
+ const done = () => {
4403
+ pending -= 1;
4404
+ if (pending === 0) process.exit(code);
4405
+ };
4406
+ process.stdout.write("", done);
4407
+ process.stderr.write("", done);
4408
+ }
2618
4409
  var argv = process.argv.slice(2);
2619
4410
  var io = makeIo(isJsonInvocation(argv));
2620
4411
  dispatch(argv, {
@@ -2624,7 +4415,7 @@ dispatch(argv, {
2624
4415
  }).then(
2625
4416
  (code) => {
2626
4417
  io.ensureSingleJsonDoc(code);
2627
- process.exitCode = code;
4418
+ exitWhenFlushed(code);
2628
4419
  },
2629
4420
  (err) => {
2630
4421
  process.stderr.write(
@@ -2632,7 +4423,7 @@ dispatch(argv, {
2632
4423
  `
2633
4424
  );
2634
4425
  io.ensureSingleJsonDoc(1);
2635
- process.exitCode = 1;
4426
+ exitWhenFlushed(1);
2636
4427
  }
2637
4428
  );
2638
4429
  //# sourceMappingURL=rig.js.map