@toon-protocol/rig 2.1.0 → 2.2.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,39 @@ 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-PS5QOT62.js";
13
21
  import {
14
22
  MissingIdentityError,
15
23
  resolveIdentity
16
24
  } from "../chunk-CW4HJNMU.js";
17
25
  import {
26
+ COMMENT_KIND,
18
27
  buildComment,
19
28
  buildIssue,
20
29
  buildPatch,
21
- buildStatus
22
- } from "../chunk-HPSOQP7Q.js";
30
+ buildStatus,
31
+ fetchRemoteState,
32
+ queryRelay
33
+ } from "../chunk-JBB7HBQC.js";
23
34
  import {
24
35
  ChannelMapStore,
25
36
  channelStatus,
37
+ defaultDaemonPort,
26
38
  resolveChannelPaths
27
- } from "../chunk-O6TXHKWG.js";
39
+ } from "../chunk-SW7ZHMGS.js";
28
40
  import {
29
41
  MAX_OBJECT_SIZE
30
42
  } from "../chunk-X2CZPPDM.js";
@@ -38,6 +50,155 @@ import { createRequire } from "module";
38
50
  // src/cli/balance.ts
39
51
  import { parseArgs as parseArgs3 } from "util";
40
52
 
53
+ // src/cli/daemon-session.ts
54
+ function daemonBaseUrl(env) {
55
+ const raw = env["TOON_CLIENT_HTTP_PORT"];
56
+ const parsed = raw ? Number(raw) : NaN;
57
+ const port = Number.isFinite(parsed) && parsed > 0 ? parsed : defaultDaemonPort();
58
+ return `http://127.0.0.1:${port}`;
59
+ }
60
+ async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
61
+ const baseUrl = daemonBaseUrl(env);
62
+ try {
63
+ const res = await fetchImpl(`${baseUrl}/status`, {
64
+ signal: AbortSignal.timeout(timeoutMs)
65
+ });
66
+ if (!res.ok) return { baseUrl, reachable: false };
67
+ const body = await res.json();
68
+ const probe = { baseUrl, reachable: true };
69
+ const pubkey = body?.identity?.nostrPubkey;
70
+ if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
71
+ if (typeof body?.ready === "boolean") probe.ready = body.ready;
72
+ if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
73
+ probe.relayUrl = body.relay.url;
74
+ }
75
+ if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
76
+ probe.feePerEvent = body.feePerEvent;
77
+ }
78
+ return probe;
79
+ } catch {
80
+ return { baseUrl, reachable: false };
81
+ }
82
+ }
83
+ var DaemonRouteError = class extends Error {
84
+ constructor(status, envelope) {
85
+ super(envelope.detail ?? envelope.error);
86
+ this.status = status;
87
+ this.envelope = envelope;
88
+ this.name = "DaemonRouteError";
89
+ }
90
+ status;
91
+ envelope;
92
+ };
93
+ var DaemonUnreachableError = class extends Error {
94
+ constructor(baseUrl, cause) {
95
+ super(
96
+ `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})` : "")
97
+ );
98
+ this.baseUrl = baseUrl;
99
+ this.name = "DaemonUnreachableError";
100
+ }
101
+ baseUrl;
102
+ };
103
+ var DaemonGitClient = class {
104
+ constructor(baseUrl, fetchImpl) {
105
+ this.baseUrl = baseUrl;
106
+ this.fetchImpl = fetchImpl;
107
+ }
108
+ baseUrl;
109
+ fetchImpl;
110
+ gitEstimate(req) {
111
+ return this.post("/git/estimate", req);
112
+ }
113
+ gitPush(req) {
114
+ return this.post("/git/push", req);
115
+ }
116
+ gitIssue(req) {
117
+ return this.post("/git/issue", req);
118
+ }
119
+ gitComment(req) {
120
+ return this.post("/git/comment", req);
121
+ }
122
+ gitPatch(req) {
123
+ return this.post("/git/patch", req);
124
+ }
125
+ gitStatus(req) {
126
+ return this.post("/git/status", req);
127
+ }
128
+ async post(path, body) {
129
+ let res;
130
+ try {
131
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, {
132
+ method: "POST",
133
+ headers: { "content-type": "application/json" },
134
+ body: JSON.stringify(body)
135
+ });
136
+ } catch (err) {
137
+ throw new DaemonUnreachableError(this.baseUrl, err);
138
+ }
139
+ const text = await res.text();
140
+ let parsed;
141
+ try {
142
+ parsed = text === "" ? {} : JSON.parse(text);
143
+ } catch {
144
+ throw new DaemonRouteError(res.status, {
145
+ error: "invalid_response",
146
+ detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
147
+ });
148
+ }
149
+ if (!res.ok) {
150
+ const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
151
+ throw new DaemonRouteError(res.status, envelope);
152
+ }
153
+ return parsed;
154
+ }
155
+ };
156
+ async function resolvePaidSession(options) {
157
+ const fetchImpl = options.fetchImpl ?? fetch;
158
+ const probe = await (options.probeDaemon ?? probeDaemon)(
159
+ options.env,
160
+ fetchImpl
161
+ );
162
+ if (probe.reachable && probe.identity !== void 0) {
163
+ const identity = await resolveIdentity({
164
+ env: options.env,
165
+ cwd: options.cwd,
166
+ warn: options.warn
167
+ });
168
+ if (identity.pubkey === probe.identity) {
169
+ options.warn(
170
+ `rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
171
+ );
172
+ return {
173
+ path: "daemon",
174
+ client: new DaemonGitClient(probe.baseUrl, fetchImpl),
175
+ baseUrl: probe.baseUrl,
176
+ identity: {
177
+ pubkey: identity.pubkey,
178
+ source: identity.source,
179
+ sourceLabel: identity.sourceLabel
180
+ },
181
+ ...probe.feePerEvent !== void 0 ? { feePerEvent: probe.feePerEvent } : {},
182
+ ...probe.relayUrl !== void 0 ? { daemonRelayUrl: probe.relayUrl } : {}
183
+ };
184
+ }
185
+ options.warn(
186
+ `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`
187
+ );
188
+ } else {
189
+ options.warn(
190
+ `rig: paid path: standalone (no toon-clientd at ${probe.baseUrl})`
191
+ );
192
+ }
193
+ const ctx = await options.loadStandalone({
194
+ env: options.env,
195
+ cwd: options.cwd,
196
+ warn: options.warn,
197
+ ...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
198
+ });
199
+ return { path: "standalone", ctx };
200
+ }
201
+
41
202
  // src/cli/errors.ts
42
203
  var UnconfiguredRepoAddressError = class extends Error {
43
204
  constructor(missing) {
@@ -199,6 +360,39 @@ function describeError(err, command = "push") {
199
360
  json: { error: "git_error", detail: err.message }
200
361
  };
201
362
  }
363
+ if (err instanceof DaemonRouteError) {
364
+ const envelope = err.envelope;
365
+ if (envelope.error === "non_fast_forward" && Array.isArray(envelope["refs"])) {
366
+ return {
367
+ code: "non_fast_forward",
368
+ lines: nonFastForwardLines(envelope["refs"]),
369
+ json: { ...envelope }
370
+ };
371
+ }
372
+ if (envelope.error === "oversize_objects" && Array.isArray(envelope["objects"])) {
373
+ return {
374
+ code: "oversize_objects",
375
+ lines: oversizeLines(envelope["objects"]),
376
+ json: { ...envelope }
377
+ };
378
+ }
379
+ const retryHint = envelope.retryable === true ? ["The daemon reports this as retryable \u2014 re-run shortly."] : [];
380
+ return {
381
+ code: envelope.error,
382
+ lines: [
383
+ `daemon rejected the operation (HTTP ${err.status}): ` + (envelope.detail ?? envelope.error),
384
+ ...retryHint
385
+ ],
386
+ json: { ...envelope }
387
+ };
388
+ }
389
+ if (err instanceof DaemonUnreachableError) {
390
+ return {
391
+ code: "daemon_unreachable",
392
+ lines: err.message.split("\n"),
393
+ json: { error: "daemon_unreachable", detail: err.message }
394
+ };
395
+ }
202
396
  const name = err instanceof Error ? err.name : "";
203
397
  const message = err instanceof Error ? err.message : String(err);
204
398
  if (name === "DaemonIdentityConflictError") {
@@ -206,7 +400,7 @@ function describeError(err, command = "push") {
206
400
  code: "daemon_identity_conflict",
207
401
  lines: [
208
402
  message,
209
- "Stop the toon-clientd daemon (or publish through its toon_git_* MCP tools) and re-run."
403
+ "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
404
  ],
211
405
  json: { error: "daemon_identity_conflict", detail: message }
212
406
  };
@@ -240,6 +434,32 @@ function describeError(err, command = "push") {
240
434
  }
241
435
  };
242
436
  }
437
+ if (name === "RepoNotFoundError") {
438
+ return {
439
+ code: "repo_not_found",
440
+ lines: message.split("\n"),
441
+ json: { error: "repo_not_found", detail: message }
442
+ };
443
+ }
444
+ if (name === "MissingRemoteObjectsError") {
445
+ const missing = err.missing;
446
+ return {
447
+ code: "missing_remote_objects",
448
+ lines: message.split("\n"),
449
+ json: {
450
+ error: "missing_remote_objects",
451
+ detail: message,
452
+ ...Array.isArray(missing) ? { missing } : {}
453
+ }
454
+ };
455
+ }
456
+ if (name === "ObjectIntegrityError" || name === "ObjectWriteMismatchError") {
457
+ return {
458
+ code: "object_integrity",
459
+ lines: message.split("\n"),
460
+ json: { error: "object_integrity", detail: message }
461
+ };
462
+ }
243
463
  return {
244
464
  code: "error",
245
465
  lines: [`rig ${command} failed: ${message}`],
@@ -665,8 +885,19 @@ async function runRemote(args, deps) {
665
885
  }
666
886
 
667
887
  // src/cli/push.ts
888
+ function loadPaidSession(deps, relayUrl) {
889
+ return resolvePaidSession({
890
+ env: deps.env,
891
+ cwd: deps.cwd,
892
+ warn: (line) => deps.io.err(line),
893
+ loadStandalone: deps.loadStandalone ?? defaultLoadStandalone,
894
+ ...relayUrl !== void 0 ? { relayUrl } : {},
895
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
896
+ ...deps.probeDaemon ? { probeDaemon: deps.probeDaemon } : {}
897
+ });
898
+ }
668
899
  var defaultLoadStandalone = async (options) => {
669
- const mod = await import("../standalone-mode-FCKTQ33Y.js");
900
+ const mod = await import("../standalone-mode-64CKUVU2.js");
670
901
  return mod.createStandaloneContext(options);
671
902
  };
672
903
  function identityReport(ctx) {
@@ -777,7 +1008,7 @@ function parsePushArgs(args) {
777
1008
  return flags;
778
1009
  }
779
1010
  async function runPush(args, deps) {
780
- const { io: io2, env } = deps;
1011
+ const { io: io2 } = deps;
781
1012
  let flags;
782
1013
  try {
783
1014
  flags = parsePushArgs(args);
@@ -835,40 +1066,74 @@ async function runPush(args, deps) {
835
1066
  io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
836
1067
  return 1;
837
1068
  }
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);
1069
+ const session = await loadPaidSession(deps, relaysUsed[0]);
1070
+ const path = session.path;
1071
+ let identity;
1072
+ if (session.path === "standalone") {
1073
+ standaloneCtx = session.ctx;
1074
+ identity = identityReport(standaloneCtx);
1075
+ } else {
1076
+ identity = session.identity;
1077
+ }
846
1078
  if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
847
1079
  io2.err(
848
1080
  `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
1081
  );
850
1082
  }
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);
1083
+ let plan;
1084
+ let execute;
1085
+ if (session.path === "standalone") {
1086
+ const ctx = session.ctx;
1087
+ const remoteState = await ctx.fetchRemote({
1088
+ ownerPubkey: ctx.ownerPubkey,
1089
+ repoId,
1090
+ relayUrls: relaysUsed
1091
+ });
1092
+ const feeRates = await ctx.publisher.getFeeRates();
1093
+ const pushPlan = await planPush({
1094
+ repoReader: reader,
1095
+ remoteState,
1096
+ feeRates,
1097
+ repoId,
1098
+ refs: refspecs,
1099
+ force: flags.force
1100
+ });
1101
+ plan = serializePushPlan(pushPlan);
1102
+ execute = async () => serializePushResult(
1103
+ pushPlan,
1104
+ await executePush({
1105
+ plan: pushPlan,
1106
+ publisher: ctx.publisher,
1107
+ remoteState,
1108
+ repoReader: reader,
1109
+ relayUrls: relaysUsed
1110
+ })
1111
+ );
1112
+ } else {
1113
+ const client = session.client;
1114
+ plan = await client.gitEstimate({
1115
+ repoPath: repoRoot,
1116
+ repoId,
1117
+ refspecs,
1118
+ force: flags.force,
1119
+ relayUrls: relaysUsed
1120
+ });
1121
+ execute = () => client.gitPush({
1122
+ repoPath: repoRoot,
1123
+ repoId,
1124
+ refspecs,
1125
+ force: flags.force,
1126
+ relayUrls: relaysUsed,
1127
+ confirm: true
1128
+ });
1129
+ }
866
1130
  const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
867
1131
  if (upToDate) {
868
1132
  if (flags.json) {
869
1133
  io2.emitJson({
870
1134
  command: "push",
871
1135
  repoId,
1136
+ path,
872
1137
  identity,
873
1138
  executed: false,
874
1139
  upToDate: true,
@@ -888,6 +1153,7 @@ async function runPush(args, deps) {
888
1153
  io2.emitJson({
889
1154
  command: "push",
890
1155
  repoId,
1156
+ path,
891
1157
  identity,
892
1158
  executed: false,
893
1159
  upToDate: false,
@@ -910,18 +1176,12 @@ async function runPush(args, deps) {
910
1176
  return 1;
911
1177
  }
912
1178
  }
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);
1179
+ const result = await execute();
921
1180
  if (flags.json) {
922
1181
  io2.emitJson({
923
1182
  command: "push",
924
1183
  repoId,
1184
+ path,
925
1185
  identity,
926
1186
  executed: true,
927
1187
  upToDate: false,
@@ -1210,7 +1470,7 @@ async function loadMoneyContext(deps, options) {
1210
1470
  if (!money) {
1211
1471
  await ctx.stop().catch(() => void 0);
1212
1472
  throw new Error(
1213
- "this standalone loader does not expose money operations \u2014 channel open/close/settle need the #263 loader"
1473
+ "this standalone loader does not expose money operations \u2014 channel open/close/settle need a loader with the money lifecycle"
1214
1474
  );
1215
1475
  }
1216
1476
  return { ctx, money };
@@ -1519,84 +1779,794 @@ async function stopQuietly(ctx) {
1519
1779
  }
1520
1780
  }
1521
1781
 
1782
+ // src/cli/clone.ts
1783
+ import { mkdtemp, mkdir, readdir, rename, rm, rmdir } from "fs/promises";
1784
+ import { basename, dirname, join, resolve } from "path";
1785
+ import { parseArgs as parseArgs5 } from "util";
1786
+ var CLONE_USAGE = `Usage: rig clone <relay-url> <owner>/<repo-id> [dir] [options]
1787
+
1788
+ Clone a TOON repository \u2014 FREE (relay reads + Arweave gateway downloads; no
1789
+ payments, no channel, no identity needed). <relay-url> is a ws:// or wss://
1790
+ NIP-01 relay; <owner> is the repo owner's pubkey as npub1\u2026 or 64-char hex;
1791
+ [dir] defaults to <repo-id>.
1792
+
1793
+ Fetches the kind:30617/30618 repository state, downloads every git object
1794
+ the refs need from Arweave gateways (SHA-1 verified \u2014 corrupt content is
1795
+ rejected), and materializes a real git repository: objects via git plumbing,
1796
+ refs, HEAD, checked-out worktree, toon.* config, and the relay preconfigured
1797
+ as the "origin" remote \u2014 so \`rig fetch\`, \`rig push\`, and the issue/pr
1798
+ commands work immediately.
1799
+
1800
+ Note: recently pushed objects can take 10-20 minutes to become fetchable
1801
+ from Arweave gateways. A clone right after a push may report missing
1802
+ objects \u2014 retry after propagation.
1803
+
1804
+ Options:
1805
+ --concurrency <n> parallel gateway downloads (default 8)
1806
+ --json machine-readable result envelope
1807
+ -h, --help show this help`;
1808
+ var RepoNotFoundError = class extends Error {
1809
+ constructor(relay, owner, repoId) {
1810
+ super(
1811
+ `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.`
1812
+ );
1813
+ this.name = "RepoNotFoundError";
1814
+ }
1815
+ };
1816
+ var MissingRemoteObjectsError = class extends Error {
1817
+ constructor(missing, context) {
1818
+ super(missingObjectsMessage(missing, context));
1819
+ this.missing = missing;
1820
+ this.name = "MissingRemoteObjectsError";
1821
+ }
1822
+ missing;
1823
+ };
1824
+ var WS_URL_RE = /^wss?:\/\//i;
1825
+ function parseCloneArgs(args) {
1826
+ const { values, positionals } = parseArgs5({
1827
+ args,
1828
+ options: {
1829
+ json: { type: "boolean", default: false },
1830
+ concurrency: { type: "string" },
1831
+ help: { type: "boolean", short: "h", default: false }
1832
+ },
1833
+ allowPositionals: true
1834
+ });
1835
+ if (values.help) return { help: true };
1836
+ if (positionals.length < 2 || positionals.length > 3) {
1837
+ throw new Error("expected: rig clone <relay-url> <owner>/<repo-id> [dir]");
1838
+ }
1839
+ const [relayUrl, addr, dir] = positionals;
1840
+ if (!WS_URL_RE.test(relayUrl)) {
1841
+ throw new Error(
1842
+ `<relay-url> must be ws:// or wss:// (got ${JSON.stringify(relayUrl)})`
1843
+ );
1844
+ }
1845
+ const slash = addr.indexOf("/");
1846
+ if (slash <= 0 || slash === addr.length - 1) {
1847
+ throw new Error(
1848
+ `expected <owner>/<repo-id> (npub or 64-char hex owner), got ${JSON.stringify(addr)}`
1849
+ );
1850
+ }
1851
+ const owner = ownerToHex(addr.slice(0, slash));
1852
+ const repoId = addr.slice(slash + 1);
1853
+ let concurrency;
1854
+ if (values.concurrency !== void 0) {
1855
+ concurrency = Number.parseInt(values.concurrency, 10);
1856
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
1857
+ throw new Error(`--concurrency must be a positive integer`);
1858
+ }
1859
+ }
1860
+ return {
1861
+ relayUrl,
1862
+ owner,
1863
+ repoId,
1864
+ dir,
1865
+ concurrency,
1866
+ json: values.json === true
1867
+ };
1868
+ }
1869
+ async function isUsableDestination(dir) {
1870
+ try {
1871
+ const entries = await readdir(dir);
1872
+ return entries.length === 0;
1873
+ } catch (err) {
1874
+ return err.code === "ENOENT";
1875
+ }
1876
+ }
1877
+ function initialBranch(headSymref) {
1878
+ if (headSymref?.startsWith("refs/heads/")) {
1879
+ const name = headSymref.slice("refs/heads/".length);
1880
+ if (name && isSafeRefname(headSymref)) return name;
1881
+ }
1882
+ return "main";
1883
+ }
1884
+ async function runClone(args, deps) {
1885
+ const { io: io2 } = deps;
1886
+ let parsed;
1887
+ try {
1888
+ const result = parseCloneArgs(args);
1889
+ if ("help" in result) {
1890
+ io2.out(CLONE_USAGE);
1891
+ return 0;
1892
+ }
1893
+ parsed = result;
1894
+ } catch (err) {
1895
+ io2.err(err instanceof Error ? err.message : String(err));
1896
+ io2.err(CLONE_USAGE);
1897
+ return 2;
1898
+ }
1899
+ const { relayUrl, owner, repoId, json } = parsed;
1900
+ const dest = resolve(deps.cwd, parsed.dir ?? repoId);
1901
+ let tempDir;
1902
+ try {
1903
+ if (!await isUsableDestination(dest)) {
1904
+ throw new Error(
1905
+ `destination path ${JSON.stringify(dest)} already exists and is not an empty directory`
1906
+ );
1907
+ }
1908
+ if (!json) io2.out(`Cloning into '${basename(dest)}'...`);
1909
+ const remoteState = await fetchRemoteState({
1910
+ relayUrls: [relayUrl],
1911
+ ownerPubkey: owner,
1912
+ repoId,
1913
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
1914
+ ...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
1915
+ });
1916
+ if (!remoteState.announced && remoteState.refsEvent === null) {
1917
+ throw new RepoNotFoundError(relayUrl, owner, repoId);
1918
+ }
1919
+ const refs = /* @__PURE__ */ new Map();
1920
+ for (const [refname, sha] of remoteState.refs) {
1921
+ if (!isSafeRefname(refname)) {
1922
+ io2.err(
1923
+ `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
1924
+ );
1925
+ continue;
1926
+ }
1927
+ refs.set(refname, sha);
1928
+ }
1929
+ if (refs.size === 0) {
1930
+ io2.err(
1931
+ "warning: the repository has no refs yet (announced but never pushed) \u2014 cloning an empty repository"
1932
+ );
1933
+ }
1934
+ const tips = [...new Set(refs.values())];
1935
+ const collected = await collectRepoObjects({
1936
+ tips,
1937
+ shaToTxId: remoteState.shaToTxId,
1938
+ resolveMissing: (shas) => remoteState.resolveMissing(shas),
1939
+ ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
1940
+ ...parsed.concurrency !== void 0 ? { concurrency: parsed.concurrency } : {}
1941
+ });
1942
+ if (collected.missing.length > 0) {
1943
+ throw new MissingRemoteObjectsError(
1944
+ collected.missing,
1945
+ `cannot clone ${owner.slice(0, 8)}\u2026/${repoId}`
1946
+ );
1947
+ }
1948
+ for (const { sha, txId } of collected.skippedUnavailable) {
1949
+ io2.err(
1950
+ `warning: unreachable object ${sha} (tx ${txId}) could not be downloaded \u2014 not needed by any ref, continuing`
1951
+ );
1952
+ }
1953
+ const parent = dirname(dest);
1954
+ await mkdir(parent, { recursive: true });
1955
+ tempDir = await mkdtemp(join(parent, `.${basename(dest)}.rig-clone-`));
1956
+ await runGit(tempDir, [
1957
+ "init",
1958
+ "--quiet",
1959
+ `--initial-branch=${initialBranch(remoteState.headSymref)}`
1960
+ ]);
1961
+ const written = await writeGitObjects(tempDir, collected.objects.values());
1962
+ for (const [refname, sha] of refs) {
1963
+ await updateRef(tempDir, refname, sha);
1964
+ if (refname.startsWith("refs/heads/")) {
1965
+ const branch = refname.slice("refs/heads/".length);
1966
+ await updateRef(tempDir, `refs/remotes/origin/${branch}`, sha);
1967
+ }
1968
+ }
1969
+ const head = remoteState.headSymref !== null && isSafeRefname(remoteState.headSymref) && refs.has(remoteState.headSymref) ? remoteState.headSymref : [...refs.keys()].find((r) => r.startsWith("refs/heads/")) ?? null;
1970
+ if (head !== null) {
1971
+ await setHeadSymref(tempDir, head);
1972
+ await runGit(tempDir, ["reset", "--hard", "--quiet"]);
1973
+ }
1974
+ await runGit(tempDir, ["config", "toon.repoid", repoId]);
1975
+ await runGit(tempDir, ["config", "toon.owner", owner]);
1976
+ await runGit(tempDir, ["remote", "add", "origin", relayUrl]);
1977
+ if (head !== null) {
1978
+ const branch = head.slice("refs/heads/".length);
1979
+ await runGit(tempDir, ["config", `branch.${branch}.remote`, "origin"]);
1980
+ await runGit(tempDir, ["config", `branch.${branch}.merge`, head]);
1981
+ }
1982
+ try {
1983
+ await rmdir(dest);
1984
+ } catch {
1985
+ }
1986
+ await rename(tempDir, dest);
1987
+ tempDir = void 0;
1988
+ if (json) {
1989
+ io2.emitJson({
1990
+ command: "clone",
1991
+ repoAddr: { ownerPubkey: owner, repoId },
1992
+ relay: relayUrl,
1993
+ directory: dest,
1994
+ head,
1995
+ refs: Object.fromEntries(refs),
1996
+ name: remoteState.name,
1997
+ objectsDownloaded: written,
1998
+ executed: true
1999
+ });
2000
+ } else {
2001
+ io2.out(`Downloaded ${written} object(s) from Arweave (SHA-1 verified).`);
2002
+ for (const [refname, sha] of refs) {
2003
+ io2.out(` ${sha.slice(0, 7)} ${refname}`);
2004
+ }
2005
+ if (head !== null) io2.out(`HEAD is now at ${head}`);
2006
+ io2.out(
2007
+ `Configured toon.repoid=${repoId}, toon.owner=${owner.slice(0, 8)}\u2026, origin \u2192 ${relayUrl}`
2008
+ );
2009
+ io2.out("Done.");
2010
+ }
2011
+ return 0;
2012
+ } catch (err) {
2013
+ return emitCliError(io2, json, "clone", err);
2014
+ } finally {
2015
+ if (tempDir !== void 0) {
2016
+ await rm(tempDir, { recursive: true, force: true }).catch(
2017
+ () => void 0
2018
+ );
2019
+ }
2020
+ }
2021
+ }
2022
+
1522
2023
  // src/cli/events.ts
1523
2024
  import { readFile } from "fs/promises";
1524
- import { parseArgs as parseArgs5 } from "util";
2025
+ import { parseArgs as parseArgs7 } from "util";
2026
+ import {
2027
+ REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2,
2028
+ STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2,
2029
+ STATUS_CLOSED_KIND as STATUS_CLOSED_KIND2,
2030
+ STATUS_DRAFT_KIND as STATUS_DRAFT_KIND2,
2031
+ STATUS_OPEN_KIND as STATUS_OPEN_KIND2
2032
+ } from "@toon-protocol/core/nip34";
2033
+
2034
+ // src/cli/tracker.ts
2035
+ import { parseArgs as parseArgs6 } from "util";
1525
2036
  import {
2037
+ ISSUE_KIND,
2038
+ PATCH_KIND,
1526
2039
  REPOSITORY_ANNOUNCEMENT_KIND,
1527
2040
  STATUS_APPLIED_KIND,
1528
2041
  STATUS_CLOSED_KIND,
1529
2042
  STATUS_DRAFT_KIND,
1530
2043
  STATUS_OPEN_KIND
1531
2044
  } 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)
2045
+ var READ_COMMON_FLAGS = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config)
2046
+ --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
2047
+ --remote <name> read via this configured git remote (default: origin)
2048
+ --relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
2049
+ --json machine-readable envelope
1550
2050
  -h, --help show this help`;
1551
- var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
1552
-
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.
2051
+ var ISSUE_LIST_USAGE = `Usage: rig issue list [--state open|closed|all] [options]
1556
2052
 
1557
- Body source (exactly one): --body, --body-file, or piped stdin.
2053
+ List the repo's issues (kind:1621) with their derived state \u2014 FREE (relay
2054
+ reads only). State comes from kind:1630-1633 status events: the latest status
2055
+ wins; an issue with no status events is open.
1558
2056
 
1559
2057
  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]
2058
+ --state <state> open | closed | all (default: all)
2059
+ ${READ_COMMON_FLAGS}`;
2060
+ var ISSUE_SHOW_USAGE = `Usage: rig issue show <event-id> [options]
1566
2061
 
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.
2062
+ Show one issue (kind:1621): metadata, derived state, body, and its
2063
+ kind:1622 comments \u2014 FREE (relay reads only). <event-id> is the 64-char hex
2064
+ id \`rig issue create\` printed (also visible in \`rig issue list\`).
1570
2065
 
1571
2066
  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]
2067
+ ${READ_COMMON_FLAGS}`;
2068
+ var PR_LIST_USAGE = `Usage: rig pr list [--state open|applied|closed|draft|all] [options]
1579
2069
 
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.
2070
+ List the repo's patches/PRs (kind:1617) with their derived state \u2014 FREE
2071
+ (relay reads only). State comes from kind:1630-1633 status events: the
2072
+ latest status wins; a patch with no status events is open.
1587
2073
 
1588
2074
  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)
1594
- ${COMMON_FLAGS_USAGE}`;
1595
- var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
2075
+ --state <state> open | applied | closed | draft | all (default: all)
2076
+ ${READ_COMMON_FLAGS}`;
2077
+ var PR_SHOW_USAGE = `Usage: rig pr show <event-id> [options]
1596
2078
 
1597
- Set the status of an issue or patch \u2014 a paid publish; writes are permanent
1598
- and non-refundable. Publishes kind:1630 (open), 1631 (applied), 1632
1599
- (closed), or 1633 (draft) against the 64-char hex id of the target event,
2079
+ Show one patch/PR (kind:1617): metadata, derived state, the FULL patch text
2080
+ (real \`git format-patch\` output \u2014 pipe it to \`git am\` to apply), and its
2081
+ kind:1622 comments \u2014 FREE (relay reads only).
2082
+
2083
+ Options:
2084
+ ${READ_COMMON_FLAGS}`;
2085
+ var STATUS_BY_KIND = {
2086
+ [STATUS_OPEN_KIND]: "open",
2087
+ [STATUS_APPLIED_KIND]: "applied",
2088
+ [STATUS_CLOSED_KIND]: "closed",
2089
+ [STATUS_DRAFT_KIND]: "draft"
2090
+ };
2091
+ var STATUS_KINDS = [
2092
+ STATUS_OPEN_KIND,
2093
+ STATUS_APPLIED_KIND,
2094
+ STATUS_CLOSED_KIND,
2095
+ STATUS_DRAFT_KIND
2096
+ ];
2097
+ function tagValue(tags, name) {
2098
+ return tags.find((t) => t[0] === name)?.[1];
2099
+ }
2100
+ function tagValues(tags, name) {
2101
+ return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
2102
+ }
2103
+ function deriveStatus(targetEventId, statusEvents) {
2104
+ let winner = null;
2105
+ for (const event of statusEvents) {
2106
+ if (STATUS_BY_KIND[event.kind] === void 0) continue;
2107
+ if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
2108
+ continue;
2109
+ if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
2110
+ winner = event;
2111
+ }
2112
+ }
2113
+ return winner === null ? "open" : STATUS_BY_KIND[winner.kind];
2114
+ }
2115
+ var RELAY_TIMEOUT_MS = 1e4;
2116
+ var HEX64_RE = /^[0-9a-f]{64}$/;
2117
+ var WS_URL_RE2 = /^wss?:\/\//i;
2118
+ function defaultWebSocketFactory(url) {
2119
+ const ctor = globalThis.WebSocket;
2120
+ if (!ctor) {
2121
+ throw new Error(
2122
+ "No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
2123
+ );
2124
+ }
2125
+ return new ctor(url);
2126
+ }
2127
+ async function queryAll(relays, filters, webSocketFactory) {
2128
+ const jobs = relays.flatMap(
2129
+ (relay) => filters.map(
2130
+ (filter) => queryRelay(relay, filter, RELAY_TIMEOUT_MS, webSocketFactory)
2131
+ )
2132
+ );
2133
+ const results = await Promise.allSettled(jobs);
2134
+ const byId = /* @__PURE__ */ new Map();
2135
+ let failures = 0;
2136
+ let firstError;
2137
+ for (const result of results) {
2138
+ if (result.status === "rejected") {
2139
+ failures += 1;
2140
+ firstError ??= result.reason;
2141
+ continue;
2142
+ }
2143
+ for (const event of result.value) {
2144
+ if (typeof event.id === "string" && !byId.has(event.id)) {
2145
+ byId.set(event.id, event);
2146
+ }
2147
+ }
2148
+ }
2149
+ if (failures === results.length && results.length > 0) {
2150
+ throw firstError instanceof Error ? firstError : new Error(String(firstError));
2151
+ }
2152
+ return byId;
2153
+ }
2154
+ var TRACKER_OPTIONS = {
2155
+ json: { type: "boolean", default: false },
2156
+ state: { type: "string" },
2157
+ relay: { type: "string", multiple: true },
2158
+ remote: { type: "string" },
2159
+ "repo-id": { type: "string" },
2160
+ owner: { type: "string" },
2161
+ help: { type: "boolean", short: "h", default: false }
2162
+ };
2163
+ function pickTrackerFlags(values) {
2164
+ const flags = {
2165
+ json: values["json"] === true,
2166
+ help: values["help"] === true,
2167
+ relay: Array.isArray(values["relay"]) ? values["relay"] : []
2168
+ };
2169
+ if (typeof values["state"] === "string") flags.state = values["state"];
2170
+ if (typeof values["remote"] === "string") flags.remote = values["remote"];
2171
+ if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
2172
+ if (typeof values["owner"] === "string")
2173
+ flags.owner = ownerToHex(values["owner"]);
2174
+ return flags;
2175
+ }
2176
+ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2177
+ let repoRoot;
2178
+ let toonConfig = {
2179
+ relays: []
2180
+ };
2181
+ try {
2182
+ repoRoot = await resolveRepoRoot(deps.cwd);
2183
+ toonConfig = await readToonConfig(repoRoot);
2184
+ } catch {
2185
+ }
2186
+ let repoAddr = null;
2187
+ if (needRepoAddr) {
2188
+ const repoId = flags.repoId ?? toonConfig.repoId;
2189
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2190
+ const owner = flags.owner ?? toonConfig.owner;
2191
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
2192
+ repoAddr = { ownerPubkey: owner, repoId };
2193
+ }
2194
+ const resolved = await resolveRelays({
2195
+ relayFlags: flags.relay,
2196
+ remoteName: flags.remote,
2197
+ repoRoot,
2198
+ toonRelays: toonConfig.relays
2199
+ });
2200
+ const wsRelays = resolved.relays.filter((url) => WS_URL_RE2.test(url));
2201
+ if (wsRelays.length === 0) {
2202
+ throw new InvalidRelayUrlError(
2203
+ resolved.relays[0] ?? "",
2204
+ "reads need a ws:// or wss:// relay"
2205
+ );
2206
+ }
2207
+ return {
2208
+ relays: wsRelays,
2209
+ repoAddr,
2210
+ webSocketFactory: deps.webSocketFactory ?? defaultWebSocketFactory
2211
+ };
2212
+ }
2213
+ function repoATag(addr) {
2214
+ return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
2215
+ }
2216
+ function parseTrackerItem(event, status) {
2217
+ const item = {
2218
+ eventId: event.id,
2219
+ kind: event.kind,
2220
+ title: tagValue(event.tags, "subject") ?? (event.content.split("\n")[0] ?? "").slice(0, 120),
2221
+ status,
2222
+ authorPubkey: event.pubkey,
2223
+ createdAt: event.created_at,
2224
+ labels: tagValues(event.tags, "t"),
2225
+ content: event.content
2226
+ };
2227
+ if (event.kind === PATCH_KIND) {
2228
+ item.commitShas = tagValues(event.tags, "commit");
2229
+ const branch = tagValue(event.tags, "branch");
2230
+ if (branch !== void 0) item.branch = branch;
2231
+ const description = tagValue(event.tags, "description");
2232
+ if (description !== void 0) item.description = description;
2233
+ }
2234
+ return item;
2235
+ }
2236
+ function isoDate(createdAt) {
2237
+ return new Date(createdAt * 1e3).toISOString().slice(0, 10);
2238
+ }
2239
+ async function fetchItems(ctx, kind) {
2240
+ const aTag = repoATag(
2241
+ ctx.repoAddr
2242
+ );
2243
+ const [itemEvents, aStatusEvents] = await Promise.all([
2244
+ queryAll(
2245
+ ctx.relays,
2246
+ [{ kinds: [kind], "#a": [aTag] }],
2247
+ ctx.webSocketFactory
2248
+ ),
2249
+ queryAll(
2250
+ ctx.relays,
2251
+ [{ kinds: STATUS_KINDS, "#a": [aTag] }],
2252
+ ctx.webSocketFactory
2253
+ )
2254
+ ]);
2255
+ const items = [...itemEvents.values()].filter(
2256
+ (e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag)
2257
+ );
2258
+ const statuses = new Map(aStatusEvents);
2259
+ if (items.length > 0) {
2260
+ const byE = await queryAll(
2261
+ ctx.relays,
2262
+ [{ kinds: STATUS_KINDS, "#e": items.map((i) => i.id) }],
2263
+ ctx.webSocketFactory
2264
+ );
2265
+ for (const [id, event] of byE)
2266
+ if (!statuses.has(id)) statuses.set(id, event);
2267
+ }
2268
+ return items.map(
2269
+ (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values()))
2270
+ ).sort((a, b) => b.createdAt - a.createdAt);
2271
+ }
2272
+ async function fetchItem(ctx, eventId, expectedKind) {
2273
+ const found = await queryAll(
2274
+ ctx.relays,
2275
+ [{ ids: [eventId] }],
2276
+ ctx.webSocketFactory
2277
+ );
2278
+ const event = found.get(eventId);
2279
+ if (!event) {
2280
+ throw new Error(`event ${eventId} not found on ${ctx.relays.join(", ")}`);
2281
+ }
2282
+ if (event.kind !== expectedKind) {
2283
+ 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`)" : "";
2284
+ throw new Error(
2285
+ `event ${eventId} has kind ${event.kind}, expected ${expectedKind}${hint}`
2286
+ );
2287
+ }
2288
+ const [statusEvents, commentEvents] = await Promise.all([
2289
+ queryAll(
2290
+ ctx.relays,
2291
+ [{ kinds: STATUS_KINDS, "#e": [eventId] }],
2292
+ ctx.webSocketFactory
2293
+ ),
2294
+ queryAll(
2295
+ ctx.relays,
2296
+ [{ kinds: [COMMENT_KIND], "#e": [eventId] }],
2297
+ ctx.webSocketFactory
2298
+ )
2299
+ ]);
2300
+ const comments = [...commentEvents.values()].filter(
2301
+ (e) => e.kind === COMMENT_KIND && e.tags.some((t) => t[0] === "e" && t[1] === eventId)
2302
+ ).sort((a, b) => a.created_at - b.created_at).map((e) => ({
2303
+ eventId: e.id,
2304
+ authorPubkey: e.pubkey,
2305
+ createdAt: e.created_at,
2306
+ content: e.content
2307
+ }));
2308
+ return {
2309
+ item: parseTrackerItem(event, deriveStatus(eventId, statusEvents.values())),
2310
+ comments,
2311
+ repoATag: tagValue(event.tags, "a") ?? null
2312
+ };
2313
+ }
2314
+ async function runList(args, deps, spec) {
2315
+ const { io: io2 } = deps;
2316
+ let flags;
2317
+ try {
2318
+ const { values, positionals } = parseArgs6({
2319
+ args,
2320
+ options: TRACKER_OPTIONS,
2321
+ allowPositionals: true
2322
+ });
2323
+ if (positionals.length > 0) {
2324
+ throw new Error(`rig ${spec.command} takes no positional arguments`);
2325
+ }
2326
+ flags = pickTrackerFlags(values);
2327
+ if (flags.state !== void 0 && flags.state !== "all" && !spec.states.includes(flags.state)) {
2328
+ throw new Error(
2329
+ `--state must be one of ${[...spec.states, "all"].join(" | ")} (got ${JSON.stringify(flags.state)})`
2330
+ );
2331
+ }
2332
+ } catch (err) {
2333
+ io2.err(err instanceof Error ? err.message : String(err));
2334
+ io2.err(spec.usage);
2335
+ return 2;
2336
+ }
2337
+ if (flags.help) {
2338
+ io2.out(spec.usage);
2339
+ return 0;
2340
+ }
2341
+ try {
2342
+ const ctx = await resolveTrackerContext(flags, deps, true);
2343
+ const all = await fetchItems(ctx, spec.kind);
2344
+ const state = flags.state ?? "all";
2345
+ const items = state === "all" ? all : all.filter((i) => i.status === state);
2346
+ if (flags.json) {
2347
+ io2.emitJson({
2348
+ command: spec.command,
2349
+ repoAddr: ctx.repoAddr,
2350
+ relays: ctx.relays,
2351
+ state,
2352
+ count: items.length,
2353
+ [spec.jsonKey]: items
2354
+ });
2355
+ return 0;
2356
+ }
2357
+ if (items.length === 0) {
2358
+ io2.out(
2359
+ `no ${state === "all" ? "" : `${state} `}${spec.jsonKey} found for ${repoATag(ctx.repoAddr)}`
2360
+ );
2361
+ return 0;
2362
+ }
2363
+ for (const item of items) {
2364
+ const labels = item.labels.length > 0 ? ` [${item.labels.join(", ")}]` : "";
2365
+ io2.out(
2366
+ `${item.status.padEnd(7)} ${item.eventId.slice(0, 8)} ${item.title} (${item.authorPubkey.slice(0, 8)}, ${isoDate(item.createdAt)})${labels}`
2367
+ );
2368
+ }
2369
+ io2.out(
2370
+ `${items.length} ${spec.jsonKey}${state === "all" ? "" : ` (${state})`}`
2371
+ );
2372
+ return 0;
2373
+ } catch (err) {
2374
+ return emitCliError(io2, flags.json, spec.command, err);
2375
+ }
2376
+ }
2377
+ async function runShow(args, deps, spec) {
2378
+ const { io: io2 } = deps;
2379
+ let flags;
2380
+ let eventId;
2381
+ try {
2382
+ const { values, positionals } = parseArgs6({
2383
+ args,
2384
+ options: TRACKER_OPTIONS,
2385
+ allowPositionals: true
2386
+ });
2387
+ flags = pickTrackerFlags(values);
2388
+ if (flags.help) {
2389
+ io2.out(spec.usage);
2390
+ return 0;
2391
+ }
2392
+ if (positionals.length !== 1) {
2393
+ throw new Error("expected exactly one <event-id>");
2394
+ }
2395
+ eventId = positionals[0];
2396
+ if (!HEX64_RE.test(eventId)) {
2397
+ throw new Error(
2398
+ `<event-id> must be a 64-char lowercase hex id (got ${JSON.stringify(eventId)})`
2399
+ );
2400
+ }
2401
+ } catch (err) {
2402
+ io2.err(err instanceof Error ? err.message : String(err));
2403
+ io2.err(spec.usage);
2404
+ return 2;
2405
+ }
2406
+ try {
2407
+ const ctx = await resolveTrackerContext(flags, deps, false);
2408
+ const shown = await fetchItem(ctx, eventId, spec.kind);
2409
+ if (flags.json) {
2410
+ io2.emitJson({
2411
+ command: spec.command,
2412
+ relays: ctx.relays,
2413
+ repoATag: shown.repoATag,
2414
+ [spec.jsonKey]: shown.item,
2415
+ comments: shown.comments
2416
+ });
2417
+ return 0;
2418
+ }
2419
+ const { item } = shown;
2420
+ io2.out(`${spec.jsonKey} ${item.eventId}`);
2421
+ io2.out(`Title: ${item.title}`);
2422
+ io2.out(`Status: ${item.status}`);
2423
+ io2.out(`Author: ${item.authorPubkey}`);
2424
+ io2.out(`Date: ${isoDate(item.createdAt)}`);
2425
+ if (item.labels.length > 0) io2.out(`Labels: ${item.labels.join(", ")}`);
2426
+ if (item.branch !== void 0) io2.out(`Branch: ${item.branch}`);
2427
+ if (item.commitShas && item.commitShas.length > 0) {
2428
+ io2.out(`Commits: ${item.commitShas.join(", ")}`);
2429
+ }
2430
+ if (shown.repoATag !== null) io2.out(`Repo: ${shown.repoATag}`);
2431
+ if (item.description !== void 0) {
2432
+ io2.out("");
2433
+ io2.out("Body:");
2434
+ for (const line of item.description.split("\n")) io2.out(line);
2435
+ }
2436
+ io2.out("");
2437
+ io2.out(`${spec.bodyLabel}:`);
2438
+ for (const line of item.content.split("\n")) io2.out(line);
2439
+ io2.out("");
2440
+ io2.out(`Comments (${shown.comments.length}):`);
2441
+ for (const comment of shown.comments) {
2442
+ io2.out(
2443
+ `--- ${comment.eventId.slice(0, 8)} by ${comment.authorPubkey.slice(0, 8)} on ${isoDate(comment.createdAt)}`
2444
+ );
2445
+ for (const line of comment.content.split("\n")) io2.out(line);
2446
+ }
2447
+ return 0;
2448
+ } catch (err) {
2449
+ return emitCliError(io2, flags.json, spec.command, err);
2450
+ }
2451
+ }
2452
+ var ISSUE_STATES = ["open", "closed", "applied", "draft"];
2453
+ var PR_STATES = ["open", "applied", "closed", "draft"];
2454
+ function runIssueList(args, deps) {
2455
+ return runList(args, deps, {
2456
+ command: "issue list",
2457
+ kind: ISSUE_KIND,
2458
+ usage: ISSUE_LIST_USAGE,
2459
+ states: ISSUE_STATES,
2460
+ jsonKey: "issues"
2461
+ });
2462
+ }
2463
+ function runIssueShow(args, deps) {
2464
+ return runShow(args, deps, {
2465
+ command: "issue show",
2466
+ kind: ISSUE_KIND,
2467
+ usage: ISSUE_SHOW_USAGE,
2468
+ jsonKey: "issue",
2469
+ bodyLabel: "Body"
2470
+ });
2471
+ }
2472
+ function runPrList(args, deps) {
2473
+ return runList(args, deps, {
2474
+ command: "pr list",
2475
+ kind: PATCH_KIND,
2476
+ usage: PR_LIST_USAGE,
2477
+ states: PR_STATES,
2478
+ jsonKey: "prs"
2479
+ });
2480
+ }
2481
+ function runPrShow(args, deps) {
2482
+ return runShow(args, deps, {
2483
+ command: "pr show",
2484
+ kind: PATCH_KIND,
2485
+ usage: PR_SHOW_USAGE,
2486
+ jsonKey: "pr",
2487
+ bodyLabel: "Patch"
2488
+ });
2489
+ }
2490
+
2491
+ // src/cli/events.ts
2492
+ var defaultReadStdin = async () => {
2493
+ if (process.stdin.isTTY) return "";
2494
+ const chunks = [];
2495
+ for await (const chunk of process.stdin) chunks.push(chunk);
2496
+ return Buffer.concat(chunks).toString("utf-8");
2497
+ };
2498
+ var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
2499
+ toon.repoid \u2014 run \`rig init\` to set it)
2500
+ --owner <pubkey> repository owner pubkey, 64-char hex (default: git config
2501
+ toon.owner, then the active identity)
2502
+ --remote <name> publish via this configured git remote (default: the
2503
+ "origin" remote \u2014 \`rig remote add origin <relay-url>\`)
2504
+ --relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
2505
+ configured remotes. The deprecated v0.1 \`git config
2506
+ toon.relay\` still works as a fallback, with a nudge
2507
+ --yes skip the fee confirmation (required when not a TTY)
2508
+ --json machine-readable receipt; without --yes it is a pure
2509
+ estimate (nothing published, exit 0)
2510
+ -h, --help show this help`;
2511
+ var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
2512
+
2513
+ File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
2514
+ permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
2515
+ from the toon.* git config keys \`rig init\` writes.
2516
+
2517
+ Body source (exactly one): --body, --body-file, or piped stdin.
2518
+
2519
+ Options:
2520
+ --title <title> issue title (subject tag) [required]
2521
+ --body <text> issue body (Markdown)
2522
+ --body-file <path> read the issue body from a file
2523
+ --label <label> label (t tag); repeatable
2524
+ ${COMMON_FLAGS_USAGE}
2525
+
2526
+ Related (FREE reads \u2014 no payment):
2527
+ rig issue list [--state open|closed|all] list the repo's issues
2528
+ rig issue show <event-id> one issue + its comments`;
2529
+ var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
2530
+
2531
+ Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
2532
+ permanent and non-refundable. <root-event-id> is the 64-char hex id of the
2533
+ kind:1621 issue / kind:1617 patch being commented on.
2534
+
2535
+ Options:
2536
+ --body <text> comment body (Markdown) [required]
2537
+ --parent-author <pubkey> pubkey of the TARGET event's author (p threading
2538
+ tag; default: the repo owner)
2539
+ --marker <root|reply> e-tag marker (default: root \u2014 commenting directly
2540
+ on the issue/patch)
2541
+ ${COMMON_FLAGS_USAGE}`;
2542
+ var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
2543
+
2544
+ Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
2545
+ a paid publish; writes are permanent and non-refundable. --range runs
2546
+ \`git format-patch --stdout <range>\` in the local repository and derives the
2547
+ commit/parent-commit tags; --patch-file publishes a pre-generated patch
2548
+ verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
2549
+ full series text \u2014 cover-letter threading (one event per commit) is out of
2550
+ scope in v1.
2551
+
2552
+ The PR body (--body/--body-file) is carried in a dedicated \`description\`
2553
+ tag, not in the event content \u2014 the content stays pure format-patch output so
2554
+ \`rig pr show \u2026 | git am\` keeps working.
2555
+
2556
+ Options:
2557
+ --title <title> patch/PR title (subject tag) [required]
2558
+ --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
2559
+ or <rev>...<rev> (mutually exclusive with --patch-file)
2560
+ --patch-file <path> literal patch text to publish
2561
+ --body <text> PR description (Markdown; description tag)
2562
+ --body-file <path> read the PR description from a file
2563
+ --branch <name> branch name (t tag)
2564
+ ${COMMON_FLAGS_USAGE}`;
2565
+ var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
2566
+
2567
+ Set the status of an issue or patch \u2014 a paid publish; writes are permanent
2568
+ and non-refundable. Publishes kind:1630 (open), 1631 (applied), 1632
2569
+ (closed), or 1633 (draft) against the 64-char hex id of the target event,
1600
2570
  with the repo a-tag attached so readers can scope a status stream to the
1601
2571
  repository. (This command was \`rig status\` before v2 \u2014 bare \`rig status\`
1602
2572
  now passes through to \`git status\`.)
@@ -1605,10 +2575,14 @@ Options:
1605
2575
  ${COMMON_FLAGS_USAGE}`;
1606
2576
  var PR_USAGE = `${PR_CREATE_USAGE}
1607
2577
 
1608
- ${PR_STATUS_USAGE}`;
1609
- var HEX64_RE = /^[0-9a-f]{64}$/;
2578
+ ${PR_STATUS_USAGE}
2579
+
2580
+ ${PR_LIST_USAGE}
2581
+
2582
+ ${PR_SHOW_USAGE}`;
2583
+ var HEX64_RE2 = /^[0-9a-f]{64}$/;
1610
2584
  function assertHex64(value, what) {
1611
- if (!HEX64_RE.test(value)) {
2585
+ if (!HEX64_RE2.test(value)) {
1612
2586
  throw new Error(
1613
2587
  `${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
1614
2588
  );
@@ -1643,7 +2617,7 @@ function pickCommon(values) {
1643
2617
  }
1644
2618
  async function runEvent(opts) {
1645
2619
  const { command, flags, deps, actionLabel } = opts;
1646
- const { io: io2, env } = deps;
2620
+ const { io: io2 } = deps;
1647
2621
  let standaloneCtx;
1648
2622
  try {
1649
2623
  let repoRoot;
@@ -1667,21 +2641,35 @@ async function runEvent(opts) {
1667
2641
  io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1668
2642
  return 1;
1669
2643
  }
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();
2644
+ const session = await loadPaidSession(deps, relaysUsed[0]);
2645
+ const path = session.path;
2646
+ let identity;
2647
+ let fee;
2648
+ if (session.path === "standalone") {
2649
+ standaloneCtx = session.ctx;
2650
+ identity = identityReport(standaloneCtx);
2651
+ fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
2652
+ } else {
2653
+ identity = session.identity;
2654
+ fee = session.feePerEvent;
2655
+ const resolvedRelay = relaysUsed[0];
2656
+ if (resolvedRelay !== void 0 && session.daemonRelayUrl !== void 0 && session.daemonRelayUrl !== resolvedRelay) {
2657
+ io2.err(
2658
+ `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`
2659
+ );
2660
+ }
2661
+ }
1679
2662
  const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
1680
2663
  const addr = { ownerPubkey: owner, repoId };
1681
2664
  const event = await opts.buildEvent(addr);
1682
2665
  const action = `kind:${event.kind} ${actionLabel}`;
1683
2666
  if (!flags.json) {
1684
- for (const line of renderEventPlan({ action, addr, identity, fee })) {
2667
+ for (const line of renderEventPlan({
2668
+ action,
2669
+ addr,
2670
+ identity,
2671
+ ...fee !== void 0 ? { fee } : {}
2672
+ })) {
1685
2673
  io2.out(line);
1686
2674
  }
1687
2675
  }
@@ -1690,10 +2678,11 @@ async function runEvent(opts) {
1690
2678
  io2.emitJson({
1691
2679
  command,
1692
2680
  repoAddr: addr,
2681
+ path,
1693
2682
  identity,
1694
2683
  kind: event.kind,
1695
2684
  executed: false,
1696
- feeEstimate: fee,
2685
+ feeEstimate: fee ?? null,
1697
2686
  hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
1698
2687
  });
1699
2688
  return 0;
@@ -1712,16 +2701,25 @@ async function runEvent(opts) {
1712
2701
  return 1;
1713
2702
  }
1714
2703
  }
1715
- const receipt = await standaloneCtx.publisher.publishEvent(event, relaysUsed);
1716
- const result = serializeEventReceipt(event.kind, receipt);
2704
+ let result;
2705
+ if (session.path === "standalone") {
2706
+ const receipt = await session.ctx.publisher.publishEvent(
2707
+ event,
2708
+ relaysUsed
2709
+ );
2710
+ result = serializeEventReceipt(event.kind, receipt);
2711
+ } else {
2712
+ result = await opts.sendDaemon(session.client, addr);
2713
+ }
1717
2714
  if (flags.json) {
1718
2715
  io2.emitJson({
1719
2716
  command,
1720
2717
  repoAddr: addr,
2718
+ path,
1721
2719
  identity,
1722
2720
  kind: result.kind,
1723
2721
  executed: true,
1724
- feeEstimate: fee,
2722
+ feeEstimate: fee ?? null,
1725
2723
  result
1726
2724
  });
1727
2725
  } else {
@@ -1744,11 +2742,17 @@ async function runIssue(args, deps) {
1744
2742
  const [sub, ...rest] = args;
1745
2743
  if (sub === "--help" || sub === "-h" || sub === "help") {
1746
2744
  io2.out(ISSUE_USAGE);
2745
+ io2.out("");
2746
+ io2.out(ISSUE_LIST_USAGE);
2747
+ io2.out("");
2748
+ io2.out(ISSUE_SHOW_USAGE);
1747
2749
  return 0;
1748
2750
  }
2751
+ if (sub === "list") return runIssueList(rest, deps);
2752
+ if (sub === "show") return runIssueShow(rest, deps);
1749
2753
  if (sub !== "create") {
1750
2754
  io2.err(
1751
- sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
2755
+ sub === void 0 ? "missing subcommand: rig issue <create|list|show>" : `unknown rig issue subcommand: ${sub}`
1752
2756
  );
1753
2757
  io2.err(ISSUE_USAGE);
1754
2758
  return 2;
@@ -1759,7 +2763,7 @@ async function runIssue(args, deps) {
1759
2763
  let bodyFile;
1760
2764
  let labels;
1761
2765
  try {
1762
- const { values } = parseArgs5({
2766
+ const { values } = parseArgs7({
1763
2767
  args: rest,
1764
2768
  options: {
1765
2769
  ...COMMON_OPTIONS,
@@ -1818,7 +2822,13 @@ async function runIssue(args, deps) {
1818
2822
  flags,
1819
2823
  deps,
1820
2824
  actionLabel: `issue ${JSON.stringify(title)}`,
1821
- buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels)
2825
+ buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
2826
+ sendDaemon: (client, addr) => client.gitIssue({
2827
+ repoAddr: addr,
2828
+ title,
2829
+ body,
2830
+ ...labels.length > 0 ? { labels } : {}
2831
+ })
1822
2832
  });
1823
2833
  }
1824
2834
  async function runComment(args, deps) {
@@ -1829,7 +2839,7 @@ async function runComment(args, deps) {
1829
2839
  let parentAuthor;
1830
2840
  let marker;
1831
2841
  try {
1832
- const { values, positionals } = parseArgs5({
2842
+ const { values, positionals } = parseArgs7({
1833
2843
  args,
1834
2844
  options: {
1835
2845
  ...COMMON_OPTIONS,
@@ -1879,7 +2889,14 @@ async function runComment(args, deps) {
1879
2889
  parentAuthor ?? addr.ownerPubkey,
1880
2890
  body,
1881
2891
  marker
1882
- )
2892
+ ),
2893
+ sendDaemon: (client, addr) => client.gitComment({
2894
+ repoAddr: addr,
2895
+ rootEventId,
2896
+ body,
2897
+ ...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {},
2898
+ marker
2899
+ })
1883
2900
  });
1884
2901
  }
1885
2902
  var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
@@ -1894,6 +2911,11 @@ async function runPr(args, deps) {
1894
2911
  return runPrCreate(rest, deps);
1895
2912
  case "status":
1896
2913
  return runPrStatus(rest, deps);
2914
+ // FREE read subcommands (#278): relay queries only, no payment path.
2915
+ case "list":
2916
+ return runPrList(rest, deps);
2917
+ case "show":
2918
+ return runPrShow(rest, deps);
1897
2919
  case "--help":
1898
2920
  case "-h":
1899
2921
  case "help":
@@ -1901,7 +2923,7 @@ async function runPr(args, deps) {
1901
2923
  return 0;
1902
2924
  default:
1903
2925
  io2.err(
1904
- sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
2926
+ sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
1905
2927
  );
1906
2928
  io2.err(PR_USAGE);
1907
2929
  return 2;
@@ -1914,14 +2936,18 @@ async function runPrCreate(rest, deps) {
1914
2936
  let range;
1915
2937
  let patchFile;
1916
2938
  let branch;
2939
+ let bodyFlag;
2940
+ let bodyFile;
1917
2941
  try {
1918
- const { values } = parseArgs5({
2942
+ const { values } = parseArgs7({
1919
2943
  args: rest,
1920
2944
  options: {
1921
2945
  ...COMMON_OPTIONS,
1922
2946
  title: { type: "string" },
1923
2947
  range: { type: "string" },
1924
2948
  "patch-file": { type: "string" },
2949
+ body: { type: "string" },
2950
+ "body-file": { type: "string" },
1925
2951
  branch: { type: "string" }
1926
2952
  },
1927
2953
  allowPositionals: false
@@ -1937,15 +2963,39 @@ async function runPrCreate(rest, deps) {
1937
2963
  if (values.range === void 0 === (values["patch-file"] === void 0)) {
1938
2964
  throw new Error("exactly one of --range or --patch-file is required");
1939
2965
  }
2966
+ if (values.body !== void 0 && values["body-file"] !== void 0) {
2967
+ throw new Error("--body and --body-file are mutually exclusive");
2968
+ }
1940
2969
  title = values.title;
1941
2970
  range = values.range;
1942
2971
  patchFile = values["patch-file"];
1943
2972
  branch = values.branch;
2973
+ bodyFlag = values.body;
2974
+ bodyFile = values["body-file"];
1944
2975
  } catch (err) {
1945
2976
  io2.err(err instanceof Error ? err.message : String(err));
1946
2977
  io2.err(PR_CREATE_USAGE);
1947
2978
  return 2;
1948
2979
  }
2980
+ let body;
2981
+ if (bodyFlag !== void 0) {
2982
+ body = bodyFlag;
2983
+ } else if (bodyFile !== void 0) {
2984
+ try {
2985
+ body = await readFile(bodyFile, "utf-8");
2986
+ } catch (err) {
2987
+ io2.err(
2988
+ `cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
2989
+ );
2990
+ return 2;
2991
+ }
2992
+ }
2993
+ if (body !== void 0 && body.trim() === "") {
2994
+ io2.err("the PR body is empty \u2014 drop --body/--body-file or pass text");
2995
+ return 2;
2996
+ }
2997
+ let builtPatchText;
2998
+ let builtCommits = [];
1949
2999
  return runEvent({
1950
3000
  command: "pr",
1951
3001
  flags,
@@ -1975,22 +3025,38 @@ async function runPrCreate(rest, deps) {
1975
3025
  }
1976
3026
  commits = [];
1977
3027
  }
3028
+ builtPatchText = patchText;
3029
+ builtCommits = commits;
1978
3030
  return buildPatch(
1979
3031
  addr.ownerPubkey,
1980
3032
  addr.repoId,
1981
3033
  title,
1982
3034
  commits,
1983
3035
  branch,
1984
- patchText
3036
+ patchText,
3037
+ body
1985
3038
  );
3039
+ },
3040
+ sendDaemon: (client, addr) => {
3041
+ if (builtPatchText === void 0) {
3042
+ throw new Error("internal: patch text not built before delegation");
3043
+ }
3044
+ return client.gitPatch({
3045
+ repoAddr: addr,
3046
+ title,
3047
+ patchText: builtPatchText,
3048
+ ...body !== void 0 ? { description: body } : {},
3049
+ ...builtCommits.length > 0 ? { commits: builtCommits } : {},
3050
+ ...branch !== void 0 ? { branch } : {}
3051
+ });
1986
3052
  }
1987
3053
  });
1988
3054
  }
1989
3055
  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
3056
+ open: STATUS_OPEN_KIND2,
3057
+ applied: STATUS_APPLIED_KIND2,
3058
+ closed: STATUS_CLOSED_KIND2,
3059
+ draft: STATUS_DRAFT_KIND2
1994
3060
  };
1995
3061
  function isStatusValue(value) {
1996
3062
  return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
@@ -2001,7 +3067,7 @@ async function runPrStatus(args, deps) {
2001
3067
  let targetEventId;
2002
3068
  let status;
2003
3069
  try {
2004
- const { values, positionals } = parseArgs5({
3070
+ const { values, positionals } = parseArgs7({
2005
3071
  args,
2006
3072
  options: COMMON_OPTIONS,
2007
3073
  allowPositionals: true
@@ -2039,18 +3105,238 @@ async function runPrStatus(args, deps) {
2039
3105
  const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
2040
3106
  event.tags.push([
2041
3107
  "a",
2042
- `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
3108
+ `${REPOSITORY_ANNOUNCEMENT_KIND2}:${addr.ownerPubkey}:${addr.repoId}`
2043
3109
  ]);
2044
3110
  return event;
2045
- }
3111
+ },
3112
+ sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
2046
3113
  });
2047
3114
  }
2048
3115
 
3116
+ // src/cli/fetch.ts
3117
+ import { parseArgs as parseArgs8 } from "util";
3118
+ var FETCH_USAGE = `Usage: rig fetch [remote] [options]
3119
+
3120
+ Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
3121
+ payments, no identity needed). Reads the kind:30618 repository state from the
3122
+ remote's relay, downloads only the git objects this repository is missing
3123
+ (SHA-1 verified), and updates the remote-tracking refs
3124
+ (refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
3125
+
3126
+ No merge happens: integrate with the git passthrough, e.g.
3127
+ \`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
3128
+ \`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
3129
+
3130
+ The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
3131
+ or \`rig clone\` writes; --repo-id/--owner override.
3132
+
3133
+ Options:
3134
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
3135
+ --owner <pubkey> repository owner (npub or 64-char hex; default: git config)
3136
+ --relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
3137
+ --concurrency <n> parallel gateway downloads (default 8)
3138
+ --json machine-readable result envelope
3139
+ -h, --help show this help`;
3140
+ var WS_URL_RE3 = /^wss?:\/\//i;
3141
+ function localRefFor(refname, remote) {
3142
+ if (refname.startsWith("refs/heads/")) {
3143
+ return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
3144
+ }
3145
+ if (refname.startsWith("refs/tags/")) return refname;
3146
+ return null;
3147
+ }
3148
+ async function resolveLocalRef(repoRoot, refname) {
3149
+ try {
3150
+ const out = await runGit(repoRoot, [
3151
+ "rev-parse",
3152
+ "--verify",
3153
+ "--quiet",
3154
+ refname
3155
+ ]);
3156
+ const sha = out.trim();
3157
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
3158
+ } catch {
3159
+ return null;
3160
+ }
3161
+ }
3162
+ async function runFetch(args, deps) {
3163
+ const { io: io2 } = deps;
3164
+ let json = false;
3165
+ let remote = "origin";
3166
+ let relayFlag;
3167
+ let repoIdFlag;
3168
+ let ownerFlag;
3169
+ let concurrency;
3170
+ try {
3171
+ const { values, positionals } = parseArgs8({
3172
+ args,
3173
+ options: {
3174
+ json: { type: "boolean", default: false },
3175
+ relay: { type: "string" },
3176
+ "repo-id": { type: "string" },
3177
+ owner: { type: "string" },
3178
+ concurrency: { type: "string" },
3179
+ help: { type: "boolean", short: "h", default: false }
3180
+ },
3181
+ allowPositionals: true
3182
+ });
3183
+ if (values.help) {
3184
+ io2.out(FETCH_USAGE);
3185
+ return 0;
3186
+ }
3187
+ json = values.json === true;
3188
+ if (positionals.length > 1) {
3189
+ throw new Error(
3190
+ `expected at most one [remote] argument, got ${positionals.length}`
3191
+ );
3192
+ }
3193
+ if (positionals[0] !== void 0) remote = positionals[0];
3194
+ relayFlag = values.relay;
3195
+ repoIdFlag = values["repo-id"];
3196
+ ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
3197
+ if (values.concurrency !== void 0) {
3198
+ concurrency = Number.parseInt(values.concurrency, 10);
3199
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
3200
+ throw new Error("--concurrency must be a positive integer");
3201
+ }
3202
+ }
3203
+ } catch (err) {
3204
+ io2.err(err instanceof Error ? err.message : String(err));
3205
+ io2.err(FETCH_USAGE);
3206
+ return 2;
3207
+ }
3208
+ try {
3209
+ const repoRoot = await resolveRepoRoot(deps.cwd);
3210
+ const toonConfig = await readToonConfig(repoRoot);
3211
+ const repoId = repoIdFlag ?? toonConfig.repoId;
3212
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
3213
+ const owner = ownerFlag ?? toonConfig.owner;
3214
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
3215
+ let relayUrl;
3216
+ if (relayFlag !== void 0) {
3217
+ relayUrl = relayFlag;
3218
+ } else {
3219
+ const urls = await getGitRemoteUrls(repoRoot, remote);
3220
+ if (urls.length === 0) throw new UnknownRemoteError(remote);
3221
+ if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
3222
+ relayUrl = urls[0];
3223
+ }
3224
+ if (!WS_URL_RE3.test(relayUrl)) {
3225
+ throw new InvalidRelayUrlError(
3226
+ relayUrl,
3227
+ `remote ${JSON.stringify(remote)}`
3228
+ );
3229
+ }
3230
+ const remoteState = await fetchRemoteState({
3231
+ relayUrls: [relayUrl],
3232
+ ownerPubkey: owner,
3233
+ repoId,
3234
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
3235
+ ...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
3236
+ });
3237
+ const planned = [];
3238
+ for (const [refname, sha] of remoteState.refs) {
3239
+ if (!isSafeRefname(refname)) {
3240
+ io2.err(
3241
+ `warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
3242
+ );
3243
+ continue;
3244
+ }
3245
+ const localRef = localRefFor(refname, remote);
3246
+ if (localRef === null) {
3247
+ io2.err(
3248
+ `warning: skipping ref outside refs/heads and refs/tags: ${refname}`
3249
+ );
3250
+ continue;
3251
+ }
3252
+ planned.push({ refname, localRef, newSha: sha });
3253
+ }
3254
+ const reader = new GitRepoReader(repoRoot);
3255
+ const tips = [...new Set(planned.map((p) => p.newSha))];
3256
+ const candidates = [.../* @__PURE__ */ new Set([...remoteState.shaToTxId.keys(), ...tips])];
3257
+ const { missing: absent } = await reader.statObjects(candidates);
3258
+ const absentSet = new Set(absent);
3259
+ const presentLocally = new Set(
3260
+ candidates.filter((sha) => !absentSet.has(sha))
3261
+ );
3262
+ const collected = await collectRepoObjects({
3263
+ tips,
3264
+ shaToTxId: remoteState.shaToTxId,
3265
+ resolveMissing: (shas) => remoteState.resolveMissing(shas),
3266
+ presentLocally,
3267
+ ...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
3268
+ ...concurrency !== void 0 ? { concurrency } : {}
3269
+ });
3270
+ if (collected.missing.length > 0) {
3271
+ throw new MissingRemoteObjectsError(
3272
+ collected.missing,
3273
+ `cannot fetch from ${relayUrl}`
3274
+ );
3275
+ }
3276
+ const written = await writeGitObjects(repoRoot, collected.objects.values());
3277
+ const updates = [];
3278
+ for (const { refname, localRef, newSha } of planned) {
3279
+ const oldSha = await resolveLocalRef(repoRoot, localRef);
3280
+ if (oldSha === newSha) {
3281
+ updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
3282
+ continue;
3283
+ }
3284
+ let kind;
3285
+ if (oldSha === null) {
3286
+ kind = "new";
3287
+ } else {
3288
+ kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
3289
+ }
3290
+ await updateRef(repoRoot, localRef, newSha);
3291
+ updates.push({ refname, localRef, oldSha, newSha, kind });
3292
+ }
3293
+ const moved = updates.filter((u) => u.kind !== "up-to-date");
3294
+ if (json) {
3295
+ io2.emitJson({
3296
+ command: "fetch",
3297
+ repoAddr: { ownerPubkey: owner, repoId },
3298
+ remote,
3299
+ relay: relayUrl,
3300
+ objectsDownloaded: written,
3301
+ updates,
3302
+ executed: true
3303
+ });
3304
+ return 0;
3305
+ }
3306
+ if (moved.length === 0) {
3307
+ io2.out("Already up to date.");
3308
+ return 0;
3309
+ }
3310
+ io2.out(`From ${relayUrl}`);
3311
+ for (const u of moved) {
3312
+ const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
3313
+ const src = short(u.refname);
3314
+ const dst = short(u.localRef);
3315
+ if (u.kind === "new") {
3316
+ const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
3317
+ io2.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
3318
+ } else if (u.kind === "forced") {
3319
+ io2.out(
3320
+ ` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
3321
+ );
3322
+ } else {
3323
+ io2.out(
3324
+ ` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
3325
+ );
3326
+ }
3327
+ }
3328
+ io2.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
3329
+ return 0;
3330
+ } catch (err) {
3331
+ return emitCliError(io2, json, "fetch", err);
3332
+ }
3333
+ }
3334
+
2049
3335
  // src/cli/fund.ts
2050
3336
  import { readFileSync } from "fs";
2051
3337
  import { homedir } from "os";
2052
- import { join } from "path";
2053
- import { parseArgs as parseArgs6 } from "util";
3338
+ import { join as join2 } from "path";
3339
+ import { parseArgs as parseArgs9 } from "util";
2054
3340
  var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
2055
3341
  var CHAINS = ["evm", "solana", "mina"];
2056
3342
  var FUND_USAGE = `Usage: rig fund [options]
@@ -2069,9 +3355,36 @@ Options:
2069
3355
  --address <address> fund this address instead of the identity's own
2070
3356
  --json machine-readable envelope
2071
3357
  -h, --help show this help`;
3358
+ var SHARED_DEVNET_SUFFIX = ".devnet.toonprotocol.dev";
3359
+ function sharedDevnetOrigin(env, file) {
3360
+ const candidates = [
3361
+ env["TOON_CLIENT_RELAY_URL"] ?? file.relayUrl,
3362
+ env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl,
3363
+ env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl
3364
+ ];
3365
+ for (const url of candidates) {
3366
+ if (!url) continue;
3367
+ try {
3368
+ const { hostname } = new URL(url);
3369
+ if (hostname.endsWith(SHARED_DEVNET_SUFFIX) || hostname === SHARED_DEVNET_SUFFIX.slice(1)) {
3370
+ return url;
3371
+ }
3372
+ } catch {
3373
+ }
3374
+ }
3375
+ return void 0;
3376
+ }
3377
+ function noFaucetGuidance(network, devnetOrigin) {
3378
+ const head = `no faucet is configured for network ${JSON.stringify(network ?? "custom")}`;
3379
+ 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.";
3380
+ if (devnetOrigin !== void 0) {
3381
+ 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}`;
3382
+ }
3383
+ 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}`;
3384
+ }
2072
3385
  function readFundConfig(env) {
2073
- const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
2074
- const configPath = join(dir, "config.json");
3386
+ const dir = env["TOON_CLIENT_HOME"] ?? join2(homedir(), ".toon-client");
3387
+ const configPath = join2(dir, "config.json");
2075
3388
  try {
2076
3389
  return {
2077
3390
  file: JSON.parse(readFileSync(configPath, "utf8")),
@@ -2092,7 +3405,7 @@ async function runFund(args, deps) {
2092
3405
  let addressFlag;
2093
3406
  let json = false;
2094
3407
  try {
2095
- const { values } = parseArgs6({
3408
+ const { values } = parseArgs9({
2096
3409
  args,
2097
3410
  options: {
2098
3411
  chain: { type: "string" },
@@ -2149,7 +3462,7 @@ async function runFund(args, deps) {
2149
3462
  mina: derived.mina.publicKey || null
2150
3463
  };
2151
3464
  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.`;
3465
+ const guidance = noFaucetGuidance(network, sharedDevnetOrigin(env, file));
2153
3466
  if (json) {
2154
3467
  io2.emitJson({
2155
3468
  command: "fund",
@@ -2221,7 +3534,7 @@ function signalExitCode(signal) {
2221
3534
  var runGitPassthrough = (argv2, options = {}) => {
2222
3535
  const err = options.err ?? ((line) => process.stderr.write(`${line}
2223
3536
  `));
2224
- return new Promise((resolve) => {
3537
+ return new Promise((resolve2) => {
2225
3538
  const child = spawn("git", argv2, {
2226
3539
  stdio: "inherit",
2227
3540
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
@@ -2244,22 +3557,22 @@ var runGitPassthrough = (argv2, options = {}) => {
2244
3557
  err(
2245
3558
  `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
3559
  );
2247
- resolve(GIT_NOT_FOUND_EXIT);
3560
+ resolve2(GIT_NOT_FOUND_EXIT);
2248
3561
  return;
2249
3562
  }
2250
3563
  err(`rig: failed to run git: ${spawnErr.message}`);
2251
- resolve(1);
3564
+ resolve2(1);
2252
3565
  });
2253
3566
  child.on("close", (code, signal) => {
2254
3567
  restoreSignals();
2255
- resolve(signal !== null ? signalExitCode(signal) : code ?? 1);
3568
+ resolve2(signal !== null ? signalExitCode(signal) : code ?? 1);
2256
3569
  });
2257
3570
  });
2258
3571
  };
2259
3572
 
2260
3573
  // src/cli/init.ts
2261
- import { basename } from "path";
2262
- import { parseArgs as parseArgs7 } from "util";
3574
+ import { basename as basename2 } from "path";
3575
+ import { parseArgs as parseArgs10 } from "util";
2263
3576
  var INIT_USAGE = `Usage: rig init [options]
2264
3577
 
2265
3578
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -2279,7 +3592,7 @@ Options:
2279
3592
  --json machine-readable report
2280
3593
  -h, --help show this help`;
2281
3594
  function parseInitArgs(args) {
2282
- const { values, positionals } = parseArgs7({
3595
+ const { values, positionals } = parseArgs10({
2283
3596
  args,
2284
3597
  options: {
2285
3598
  "repo-id": { type: "string" },
@@ -2329,7 +3642,7 @@ async function runInit(args, deps) {
2329
3642
  warn: (line) => io2.err(line)
2330
3643
  });
2331
3644
  const existing = await readToonConfig(repoRoot);
2332
- const repoId = flags.repoId ?? existing.repoId ?? basename(repoRoot);
3645
+ const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
2333
3646
  const changed = {
2334
3647
  repoId: existing.repoId !== repoId,
2335
3648
  owner: existing.owner !== identity.pubkey
@@ -2415,12 +3728,22 @@ Commands rig owns:
2415
3728
  remote add <name> <url> add a relay as a REAL git remote ("origin" is
2416
3729
  remote remove <name> the default publish target); remove/list manage
2417
3730
  remote list them \u2014 \`git remote -v\` shows the same data
3731
+ clone <relay-url> <owner>/<repo-id> [dir]
3732
+ clone a TOON repo (free): relay state + Arweave
3733
+ objects (SHA-1 verified) \u2192 a real git repository,
3734
+ push/pull-capable out of the box
3735
+ fetch [remote] fetch remote refs + missing objects (free) and
3736
+ update refs/remotes/<remote>/*; no merge. Shadows
3737
+ git fetch (plain \`git fetch\` stays available)
2418
3738
  push [remote] [refspecs...] plan, price, confirm, and execute a paid push
2419
3739
  to TOON (defaults to the "origin" remote). rig
2420
3740
  push is the TOON transport and shadows git push;
2421
3741
  plain-git pushes remain available by running
2422
3742
  \`git push\` directly
2423
3743
  issue create file an issue (kind:1621) against a repo
3744
+ issue list | show <id> read the repo's issues + comments (free)
3745
+ pr list | show <id> read the repo's patches; show prints the full
3746
+ patch text (free)
2424
3747
  comment <root-event-id> comment (kind:1622) on an issue or patch
2425
3748
  pr create publish a patch (kind:1617) with real
2426
3749
  \`git format-patch\` content
@@ -2443,8 +3766,10 @@ Any other command is passed through to git verbatim: \`rig status\` runs
2443
3766
  \`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
2444
3767
 
2445
3768
  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
3769
+ \`rig remote\`, \`rig clone\`, \`rig fetch\`, \`rig issue list/show\`,
3770
+ \`rig pr list/show\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\`
3771
+ are free; push/issue create/comment/pr create/pr status are paid writes \u2014
3772
+ permanent and non-refundable \u2014
2448
3773
  and channel open/close/settle are on-chain wallet transactions; each states
2449
3774
  what it will spend and asks for confirmation before doing so (--yes skips,
2450
3775
  --json emits machine output).
@@ -2474,6 +3799,11 @@ async function dispatch(argv2, deps) {
2474
3799
  return runInit(rest, deps);
2475
3800
  case "remote":
2476
3801
  return runRemote(rest, deps);
3802
+ // The #278 read path — FREE (relay + Arweave gateway reads, no payment).
3803
+ case "clone":
3804
+ return runClone(rest, deps);
3805
+ case "fetch":
3806
+ return runFetch(rest, deps);
2477
3807
  case "push":
2478
3808
  return runPush(rest, deps);
2479
3809
  case "issue":
@@ -2553,6 +3883,8 @@ function makeCliIo(options) {
2553
3883
  var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
2554
3884
  "init",
2555
3885
  "remote",
3886
+ "clone",
3887
+ "fetch",
2556
3888
  "push",
2557
3889
  "issue",
2558
3890
  "comment",
@@ -2570,6 +3902,29 @@ function isJsonInvocation(argv2) {
2570
3902
  }
2571
3903
  return false;
2572
3904
  }
3905
+ var ANNOUNCE_FAILED_RE = /\[Bootstrap\] Announce failed/;
3906
+ var ANNOUNCE_402_RE = /402|payment required/i;
3907
+ 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";
3908
+ function calmBootstrapNoise() {
3909
+ const original = process.stderr.write;
3910
+ let reframed = false;
3911
+ const patched = ((chunk, encodingOrCb, cb) => {
3912
+ let text = chunk;
3913
+ if (typeof chunk === "string" && ANNOUNCE_FAILED_RE.test(chunk)) {
3914
+ if (ANNOUNCE_402_RE.test(chunk)) {
3915
+ text = reframed ? "" : ANNOUNCE_402_INFO;
3916
+ reframed = true;
3917
+ }
3918
+ }
3919
+ return typeof encodingOrCb === "function" ? original.call(process.stderr, text, encodingOrCb) : original.call(process.stderr, text, encodingOrCb, cb);
3920
+ });
3921
+ process.stderr.write = patched;
3922
+ return {
3923
+ restore: () => {
3924
+ if (process.stderr.write === patched) process.stderr.write = original;
3925
+ }
3926
+ };
3927
+ }
2573
3928
  function redirectStdoutToStderr(realWrite) {
2574
3929
  const original = process.stdout.write;
2575
3930
  const real = realWrite ?? original.bind(process.stdout);
@@ -2591,6 +3946,7 @@ function redirectStdoutToStderr(realWrite) {
2591
3946
 
2592
3947
  // src/cli/rig.ts
2593
3948
  function makeIo(jsonMode) {
3949
+ calmBootstrapNoise();
2594
3950
  const guard = jsonMode ? redirectStdoutToStderr() : void 0;
2595
3951
  return makeCliIo({
2596
3952
  jsonMode,
@@ -2615,6 +3971,16 @@ function makeIo(jsonMode) {
2615
3971
  }
2616
3972
  });
2617
3973
  }
3974
+ function exitWhenFlushed(code) {
3975
+ process.exitCode = code;
3976
+ let pending = 2;
3977
+ const done = () => {
3978
+ pending -= 1;
3979
+ if (pending === 0) process.exit(code);
3980
+ };
3981
+ process.stdout.write("", done);
3982
+ process.stderr.write("", done);
3983
+ }
2618
3984
  var argv = process.argv.slice(2);
2619
3985
  var io = makeIo(isJsonInvocation(argv));
2620
3986
  dispatch(argv, {
@@ -2624,7 +3990,7 @@ dispatch(argv, {
2624
3990
  }).then(
2625
3991
  (code) => {
2626
3992
  io.ensureSingleJsonDoc(code);
2627
- process.exitCode = code;
3993
+ exitWhenFlushed(code);
2628
3994
  },
2629
3995
  (err) => {
2630
3996
  process.stderr.write(
@@ -2632,7 +3998,7 @@ dispatch(argv, {
2632
3998
  `
2633
3999
  );
2634
4000
  io.ensureSingleJsonDoc(1);
2635
- process.exitCode = 1;
4001
+ exitWhenFlushed(1);
2636
4002
  }
2637
4003
  );
2638
4004
  //# sourceMappingURL=rig.js.map