@toon-protocol/rig 2.0.1 → 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,22 +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";
34
+ import {
35
+ ChannelMapStore,
36
+ channelStatus,
37
+ defaultDaemonPort,
38
+ resolveChannelPaths
39
+ } from "../chunk-SW7ZHMGS.js";
23
40
  import {
24
41
  MAX_OBJECT_SIZE
25
42
  } from "../chunk-X2CZPPDM.js";
@@ -30,16 +47,157 @@ import { createInterface } from "readline/promises";
30
47
  // src/cli/dispatch.ts
31
48
  import { createRequire } from "module";
32
49
 
33
- // src/cli/events.ts
34
- import { readFile } from "fs/promises";
50
+ // src/cli/balance.ts
35
51
  import { parseArgs as parseArgs3 } from "util";
36
- import {
37
- REPOSITORY_ANNOUNCEMENT_KIND,
38
- STATUS_APPLIED_KIND,
39
- STATUS_CLOSED_KIND,
40
- STATUS_DRAFT_KIND,
41
- STATUS_OPEN_KIND
42
- } from "@toon-protocol/core/nip34";
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
+ }
43
201
 
44
202
  // src/cli/errors.ts
45
203
  var UnconfiguredRepoAddressError = class extends Error {
@@ -202,6 +360,39 @@ function describeError(err, command = "push") {
202
360
  json: { error: "git_error", detail: err.message }
203
361
  };
204
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
+ }
205
396
  const name = err instanceof Error ? err.name : "";
206
397
  const message = err instanceof Error ? err.message : String(err);
207
398
  if (name === "DaemonIdentityConflictError") {
@@ -209,7 +400,7 @@ function describeError(err, command = "push") {
209
400
  code: "daemon_identity_conflict",
210
401
  lines: [
211
402
  message,
212
- "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."
213
404
  ],
214
405
  json: { error: "daemon_identity_conflict", detail: message }
215
406
  };
@@ -221,12 +412,153 @@ function describeError(err, command = "push") {
221
412
  json: { error: "missing_uplink", detail: message }
222
413
  };
223
414
  }
415
+ if (name === "ChannelMapCorruptError") {
416
+ return {
417
+ code: "channel_map_corrupt",
418
+ lines: [message],
419
+ json: { error: "channel_map_corrupt", detail: message }
420
+ };
421
+ }
422
+ if (name === "SettleTooEarlyError") {
423
+ const settleableAt = err.settleableAt;
424
+ return {
425
+ code: "settle_too_early",
426
+ lines: [
427
+ message,
428
+ "The settlement challenge window is still open \u2014 nothing was spent. Re-run `rig channel settle` after the settleable time."
429
+ ],
430
+ json: {
431
+ error: "settle_too_early",
432
+ detail: message,
433
+ ...typeof settleableAt === "string" ? { settleableAt } : {}
434
+ }
435
+ };
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
+ }
224
463
  return {
225
464
  code: "error",
226
465
  lines: [`rig ${command} failed: ${message}`],
227
466
  json: { error: "error", detail: message }
228
467
  };
229
468
  }
469
+ function emitCliError(io2, json, command, err) {
470
+ const described = describeError(err, command);
471
+ if (json) io2.emitJson({ command, ...described.json });
472
+ for (const line of described.lines) io2.err(line);
473
+ return 1;
474
+ }
475
+
476
+ // src/cli/push.ts
477
+ import { parseArgs as parseArgs2 } from "util";
478
+
479
+ // src/cli/render.ts
480
+ function formatNumber(value) {
481
+ return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
482
+ }
483
+ function shortSha(sha) {
484
+ return sha ? sha.slice(0, 7) : "(none)";
485
+ }
486
+ function refLine(update) {
487
+ const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
488
+ return ` ${update.refname} ${arrow} (${update.kind})`;
489
+ }
490
+ function renderIdentityLine(identity) {
491
+ return `Identity: ${identity.pubkey} (from ${identity.sourceLabel})`;
492
+ }
493
+ function renderPlan(plan) {
494
+ const lines = [];
495
+ lines.push(
496
+ `Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
497
+ );
498
+ lines.push("Refs:");
499
+ for (const update of plan.refUpdates) lines.push(refLine(update));
500
+ const est = plan.estimate;
501
+ const skipped = Object.keys(plan.knownShaToTxId).length;
502
+ lines.push(
503
+ `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
504
+ );
505
+ lines.push("Fees (base units):");
506
+ lines.push(
507
+ ` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
508
+ );
509
+ lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
510
+ lines.push(` total ${formatNumber(est.totalFee)}`);
511
+ lines.push("Writes are permanent and non-refundable.");
512
+ return lines;
513
+ }
514
+ function feeLabel(fee) {
515
+ return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
516
+ }
517
+ function renderEventPlan(opts) {
518
+ return [
519
+ `Publish ${opts.action}`,
520
+ `Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
521
+ renderIdentityLine(opts.identity),
522
+ `Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
523
+ ];
524
+ }
525
+ function renderEventReceipt(action, result) {
526
+ const lines = [
527
+ `Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
528
+ ];
529
+ if (result.channelBalanceAfter !== void 0) {
530
+ lines.push(
531
+ `Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
532
+ );
533
+ }
534
+ return lines;
535
+ }
536
+ function renderResult(result) {
537
+ const lines = [];
538
+ lines.push(`Pushed "${result.repoId}":`);
539
+ const paid = result.uploads.filter((u) => !u.skipped);
540
+ const skipped = result.uploads.filter((u) => u.skipped);
541
+ for (const upload of result.uploads) {
542
+ lines.push(
543
+ ` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
544
+ );
545
+ }
546
+ lines.push(
547
+ `Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
548
+ );
549
+ if (result.announceReceipt) {
550
+ lines.push(
551
+ `Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
552
+ );
553
+ }
554
+ lines.push(
555
+ `Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
556
+ );
557
+ lines.push(
558
+ `Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
559
+ );
560
+ return lines;
561
+ }
230
562
 
231
563
  // src/cli/git-config.ts
232
564
  import { execFile } from "child_process";
@@ -320,141 +652,54 @@ async function writeToonConfig(repoPath, config) {
320
652
  }
321
653
  }
322
654
 
323
- // src/cli/push.ts
324
- import { parseArgs as parseArgs2 } from "util";
325
-
326
- // src/cli/render.ts
327
- function formatNumber(value) {
328
- return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
655
+ // src/cli/remote.ts
656
+ import { parseArgs } from "util";
657
+ var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
658
+ function isRelayUrl(url) {
659
+ try {
660
+ return RELAY_PROTOCOLS.has(new URL(url).protocol);
661
+ } catch {
662
+ return false;
663
+ }
329
664
  }
330
- function shortSha(sha) {
331
- return sha ? sha.slice(0, 7) : "(none)";
665
+ function assertRelayUrl(url, context) {
666
+ if (!isRelayUrl(url)) throw new InvalidRelayUrlError(url, context);
332
667
  }
333
- function refLine(update) {
334
- const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
335
- return ` ${update.refname} ${arrow} (${update.kind})`;
668
+ async function resolveRelays(opts) {
669
+ if (opts.relayFlags.length > 0) {
670
+ return { relays: opts.relayFlags, source: "relay-flag" };
671
+ }
672
+ if (opts.remoteName !== void 0) {
673
+ const urls = opts.repoRoot !== void 0 ? await getGitRemoteUrls(opts.repoRoot, opts.remoteName) : [];
674
+ if (urls.length === 0) throw new UnknownRemoteError(opts.remoteName);
675
+ if (urls.length > 1) throw new MultiUrlRemoteError(opts.remoteName, urls);
676
+ const url = urls[0];
677
+ assertRelayUrl(url, `remote ${JSON.stringify(opts.remoteName)}`);
678
+ return { relays: urls, source: "remote", remoteName: opts.remoteName };
679
+ }
680
+ let nonRelayOriginUrl;
681
+ if (opts.repoRoot !== void 0) {
682
+ const urls = await getGitRemoteUrls(opts.repoRoot, "origin");
683
+ if (urls.length === 1 && isRelayUrl(urls[0])) {
684
+ return { relays: urls, source: "remote", remoteName: "origin" };
685
+ }
686
+ if (urls.length > 1 && urls.some(isRelayUrl)) {
687
+ throw new MultiUrlRemoteError("origin", urls);
688
+ }
689
+ if (urls.length > 0) nonRelayOriginUrl = urls[0];
690
+ }
691
+ if (opts.toonRelays.length > 0) {
692
+ return {
693
+ relays: opts.toonRelays,
694
+ source: "toon.relay",
695
+ nudge: `note: git config toon.relay is deprecated (removed in v0.3) \u2014 migrate: rig remote add origin ${opts.toonRelays[0]}`
696
+ };
697
+ }
698
+ throw new NoOriginConfiguredError(nonRelayOriginUrl);
336
699
  }
337
- function renderIdentityLine(identity) {
338
- return `Identity: ${identity.pubkey} (from ${identity.sourceLabel})`;
339
- }
340
- function renderPlan(plan) {
341
- const lines = [];
342
- lines.push(
343
- `Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
344
- );
345
- lines.push("Refs:");
346
- for (const update of plan.refUpdates) lines.push(refLine(update));
347
- const est = plan.estimate;
348
- const skipped = Object.keys(plan.knownShaToTxId).length;
349
- lines.push(
350
- `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
351
- );
352
- lines.push("Fees (base units):");
353
- lines.push(
354
- ` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
355
- );
356
- lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
357
- lines.push(` total ${formatNumber(est.totalFee)}`);
358
- lines.push("Writes are permanent and non-refundable.");
359
- return lines;
360
- }
361
- function feeLabel(fee) {
362
- return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
363
- }
364
- function renderEventPlan(opts) {
365
- return [
366
- `Publish ${opts.action}`,
367
- `Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
368
- renderIdentityLine(opts.identity),
369
- `Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
370
- ];
371
- }
372
- function renderEventReceipt(action, result) {
373
- const lines = [
374
- `Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
375
- ];
376
- if (result.channelBalanceAfter !== void 0) {
377
- lines.push(
378
- `Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
379
- );
380
- }
381
- return lines;
382
- }
383
- function renderResult(result) {
384
- const lines = [];
385
- lines.push(`Pushed "${result.repoId}":`);
386
- const paid = result.uploads.filter((u) => !u.skipped);
387
- const skipped = result.uploads.filter((u) => u.skipped);
388
- for (const upload of result.uploads) {
389
- lines.push(
390
- ` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
391
- );
392
- }
393
- lines.push(
394
- `Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
395
- );
396
- if (result.announceReceipt) {
397
- lines.push(
398
- `Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
399
- );
400
- }
401
- lines.push(
402
- `Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
403
- );
404
- lines.push(
405
- `Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
406
- );
407
- return lines;
408
- }
409
-
410
- // src/cli/remote.ts
411
- import { parseArgs } from "util";
412
- var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
413
- function isRelayUrl(url) {
414
- try {
415
- return RELAY_PROTOCOLS.has(new URL(url).protocol);
416
- } catch {
417
- return false;
418
- }
419
- }
420
- function assertRelayUrl(url, context) {
421
- if (!isRelayUrl(url)) throw new InvalidRelayUrlError(url, context);
422
- }
423
- async function resolveRelays(opts) {
424
- if (opts.relayFlags.length > 0) {
425
- return { relays: opts.relayFlags, source: "relay-flag" };
426
- }
427
- if (opts.remoteName !== void 0) {
428
- const urls = opts.repoRoot !== void 0 ? await getGitRemoteUrls(opts.repoRoot, opts.remoteName) : [];
429
- if (urls.length === 0) throw new UnknownRemoteError(opts.remoteName);
430
- if (urls.length > 1) throw new MultiUrlRemoteError(opts.remoteName, urls);
431
- const url = urls[0];
432
- assertRelayUrl(url, `remote ${JSON.stringify(opts.remoteName)}`);
433
- return { relays: urls, source: "remote", remoteName: opts.remoteName };
434
- }
435
- let nonRelayOriginUrl;
436
- if (opts.repoRoot !== void 0) {
437
- const urls = await getGitRemoteUrls(opts.repoRoot, "origin");
438
- if (urls.length === 1 && isRelayUrl(urls[0])) {
439
- return { relays: urls, source: "remote", remoteName: "origin" };
440
- }
441
- if (urls.length > 1 && urls.some(isRelayUrl)) {
442
- throw new MultiUrlRemoteError("origin", urls);
443
- }
444
- if (urls.length > 0) nonRelayOriginUrl = urls[0];
445
- }
446
- if (opts.toonRelays.length > 0) {
447
- return {
448
- relays: opts.toonRelays,
449
- source: "toon.relay",
450
- nudge: `note: git config toon.relay is deprecated (removed in v0.3) \u2014 migrate: rig remote add origin ${opts.toonRelays[0]}`
451
- };
452
- }
453
- throw new NoOriginConfiguredError(nonRelayOriginUrl);
454
- }
455
- function singleRelayRefusal(resolved, nothingHappened) {
456
- const fix = resolved.source === "relay-flag" ? "pass exactly one --relay <url>" : "trim git config toon.relay to one URL (better: migrate \u2014 `rig remote add origin <relay-url>`)";
457
- return `rig publishes to a single relay, but ${resolved.relays.length} are configured (${resolved.relays.join(", ")}) \u2014 ${fix}. ${nothingHappened}`;
700
+ function singleRelayRefusal(resolved, nothingHappened) {
701
+ const fix = resolved.source === "relay-flag" ? "pass exactly one --relay <url>" : "trim git config toon.relay to one URL (better: migrate \u2014 `rig remote add origin <relay-url>`)";
702
+ return `rig publishes to a single relay, but ${resolved.relays.length} are configured (${resolved.relays.join(", ")}) \u2014 ${fix}. ${nothingHappened}`;
458
703
  }
459
704
  var REMOTE_USAGE = `Usage: rig remote <add|remove|list> [args] [options]
460
705
 
@@ -471,10 +716,10 @@ Commands:
471
716
  list list remote names + URLs (the default subcommand)
472
717
 
473
718
  Options:
474
- --json machine-readable output (list)
719
+ --json machine-readable envelope (all subcommands)
475
720
  -h, --help show this help`;
476
721
  async function runRemote(args, deps) {
477
- const { io } = deps;
722
+ const { io: io2 } = deps;
478
723
  let sub;
479
724
  let rest;
480
725
  let json;
@@ -488,34 +733,34 @@ async function runRemote(args, deps) {
488
733
  allowPositionals: true
489
734
  });
490
735
  if (values.help) {
491
- io.out(REMOTE_USAGE);
736
+ io2.out(REMOTE_USAGE);
492
737
  return 0;
493
738
  }
494
739
  json = values.json ?? false;
495
740
  [sub, ...rest] = positionals;
496
741
  } catch (err) {
497
- io.err(err instanceof Error ? err.message : String(err));
498
- io.err(REMOTE_USAGE);
742
+ io2.err(err instanceof Error ? err.message : String(err));
743
+ io2.err(REMOTE_USAGE);
499
744
  return 2;
500
745
  }
501
746
  switch (sub) {
502
747
  case void 0:
503
748
  case "list":
504
749
  if (rest.length > 0) {
505
- io.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
506
- io.err(REMOTE_USAGE);
750
+ io2.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
751
+ io2.err(REMOTE_USAGE);
507
752
  return 2;
508
753
  }
509
754
  break;
510
755
  case "add": {
511
756
  if (rest.length !== 2) {
512
- io.err("usage: rig remote add <name> <relay-url>");
513
- io.err(REMOTE_USAGE);
757
+ io2.err("usage: rig remote add <name> <relay-url>");
758
+ io2.err(REMOTE_USAGE);
514
759
  return 2;
515
760
  }
516
761
  const url = rest[1];
517
762
  if (!isRelayUrl(url)) {
518
- io.err(
763
+ io2.err(
519
764
  `cannot add remote: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
520
765
  );
521
766
  return 2;
@@ -524,14 +769,14 @@ async function runRemote(args, deps) {
524
769
  }
525
770
  case "remove":
526
771
  if (rest.length !== 1) {
527
- io.err("usage: rig remote remove <name>");
528
- io.err(REMOTE_USAGE);
772
+ io2.err("usage: rig remote remove <name>");
773
+ io2.err(REMOTE_USAGE);
529
774
  return 2;
530
775
  }
531
776
  break;
532
777
  default:
533
- io.err(`unknown rig remote subcommand: ${sub}`);
534
- io.err(REMOTE_USAGE);
778
+ io2.err(`unknown rig remote subcommand: ${sub}`);
779
+ io2.err(REMOTE_USAGE);
535
780
  return 2;
536
781
  }
537
782
  try {
@@ -546,23 +791,23 @@ async function runRemote(args, deps) {
546
791
  case "list": {
547
792
  const remotes = await listGitRemotes(repoRoot);
548
793
  if (json) {
549
- io.out(JSON.stringify({ command: "remote", remotes }, null, 2));
794
+ io2.emitJson({ command: "remote", remotes });
550
795
  return 0;
551
796
  }
552
797
  if (remotes.length === 0) {
553
- io.out(
798
+ io2.out(
554
799
  "no remotes configured \u2014 add one: rig remote add origin <relay-url>"
555
800
  );
556
801
  return 0;
557
802
  }
558
803
  for (const remote of remotes) {
559
804
  for (const url of remote.urls) {
560
- io.out(
805
+ io2.out(
561
806
  `${remote.name} ${url}` + (isRelayUrl(url) ? "" : " (not a relay URL \u2014 ignored by rig)")
562
807
  );
563
808
  }
564
809
  if (remote.urls.length > 1) {
565
- io.err(
810
+ io2.err(
566
811
  `warning: remote "${remote.name}" has ${remote.urls.length} URLs \u2014 rig supports one relay URL per remote (fix with \`git remote set-url ${remote.name} <relay-url>\`)`
567
812
  );
568
813
  }
@@ -573,37 +818,61 @@ async function runRemote(args, deps) {
573
818
  const [name, url] = rest;
574
819
  const existing = await getGitRemoteUrls(repoRoot, name);
575
820
  if (existing.length > 0) {
576
- io.err(
577
- `remote ${JSON.stringify(name)} already exists (${existing.join(", ")}) \u2014 nothing changed. Point it somewhere else with \`git remote set-url ${name} <relay-url>\`, or \`rig remote remove ${name}\` first.`
578
- );
821
+ const detail = `remote ${JSON.stringify(name)} already exists (${existing.join(", ")}) \u2014 nothing changed. Point it somewhere else with \`git remote set-url ${name} <relay-url>\`, or \`rig remote remove ${name}\` first.`;
822
+ if (json) {
823
+ io2.emitJson({
824
+ command: "remote",
825
+ error: "remote_exists",
826
+ detail,
827
+ remote: name,
828
+ urls: existing
829
+ });
830
+ }
831
+ io2.err(detail);
579
832
  return 1;
580
833
  }
581
834
  await addGitRemote(repoRoot, name, url);
582
- io.out(`Added remote ${name} \u2192 ${url}`);
835
+ if (!json) io2.out(`Added remote ${name} \u2192 ${url}`);
583
836
  if (name === "origin") {
584
- io.out(
585
- "`rig push` and the event commands now publish here by default."
586
- );
837
+ if (!json) {
838
+ io2.out(
839
+ "`rig push` and the event commands now publish here by default."
840
+ );
841
+ }
587
842
  const toonConfig = await readToonConfig(repoRoot);
588
843
  if (toonConfig.relays.length > 0) {
589
- io.err(
844
+ io2.err(
590
845
  `note: git config toon.relay (${toonConfig.relays.join(", ")}) is deprecated and now shadowed by the origin remote \u2014 drop it with \`git config --unset-all toon.relay\` (the key is removed in v0.3).`
591
846
  );
592
847
  }
593
848
  }
849
+ if (json) {
850
+ io2.emitJson({ command: "remote", action: "add", name, url });
851
+ }
594
852
  return 0;
595
853
  }
596
854
  case "remove": {
597
855
  const name = rest[0];
598
856
  const existing = await getGitRemoteUrls(repoRoot, name);
599
857
  if (existing.length === 0) {
600
- io.err(
601
- `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`
602
- );
858
+ const detail = `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`;
859
+ if (json) {
860
+ io2.emitJson({
861
+ command: "remote",
862
+ error: "unknown_remote",
863
+ detail,
864
+ remote: name
865
+ });
866
+ }
867
+ io2.err(detail);
603
868
  return 1;
604
869
  }
605
870
  await removeGitRemote(repoRoot, name);
606
- io.out(`Removed remote ${name}`);
871
+ if (json) {
872
+ io2.emitJson({ command: "remote", action: "remove", name });
873
+ } else {
874
+ io2.out(`Removed remote ${name}`);
875
+ }
607
876
  return 0;
608
877
  }
609
878
  /* v8 ignore next 2 -- unreachable: validated above */
@@ -611,19 +880,24 @@ async function runRemote(args, deps) {
611
880
  return 2;
612
881
  }
613
882
  } catch (err) {
614
- const described = describeError(err, "remote");
615
- if (json) {
616
- io.out(JSON.stringify({ command: "remote", ...described.json }, null, 2));
617
- } else {
618
- for (const line of described.lines) io.err(line);
619
- }
620
- return 1;
883
+ return emitCliError(io2, json, "remote", err);
621
884
  }
622
885
  }
623
886
 
624
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
+ }
625
899
  var defaultLoadStandalone = async (options) => {
626
- const mod = await import("../standalone-mode-Q3FFZUXE.js");
900
+ const mod = await import("../standalone-mode-64CKUVU2.js");
627
901
  return mod.createStandaloneContext(options);
628
902
  };
629
903
  function identityReport(ctx) {
@@ -734,17 +1008,17 @@ function parsePushArgs(args) {
734
1008
  return flags;
735
1009
  }
736
1010
  async function runPush(args, deps) {
737
- const { io, env } = deps;
1011
+ const { io: io2 } = deps;
738
1012
  let flags;
739
1013
  try {
740
1014
  flags = parsePushArgs(args);
741
1015
  } catch (err) {
742
- io.err(err instanceof Error ? err.message : String(err));
743
- io.err(PUSH_USAGE);
1016
+ io2.err(err instanceof Error ? err.message : String(err));
1017
+ io2.err(PUSH_USAGE);
744
1018
  return 2;
745
1019
  }
746
1020
  if (flags.help) {
747
- io.out(PUSH_USAGE);
1021
+ io2.out(PUSH_USAGE);
748
1022
  return 0;
749
1023
  }
750
1024
  let standaloneCtx;
@@ -786,106 +1060,140 @@ async function runPush(args, deps) {
786
1060
  repoRoot,
787
1061
  toonRelays: toonConfig.relays
788
1062
  });
789
- if (resolved.nudge !== void 0) io.err(resolved.nudge);
1063
+ if (resolved.nudge !== void 0) io2.err(resolved.nudge);
790
1064
  const relaysUsed = resolved.relays;
791
1065
  if (relaysUsed.length > 1) {
792
- io.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
1066
+ io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
793
1067
  return 1;
794
1068
  }
795
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
796
- env,
797
- cwd: deps.cwd,
798
- warn: (line) => io.err(line)
799
- });
800
- 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
+ }
801
1078
  if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
802
- io.err(
1079
+ io2.err(
803
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.`
804
1081
  );
805
1082
  }
806
- const remoteState = await standaloneCtx.fetchRemote({
807
- ownerPubkey: standaloneCtx.ownerPubkey,
808
- repoId,
809
- relayUrls: relaysUsed
810
- });
811
- const feeRates = await standaloneCtx.publisher.getFeeRates();
812
- const pushPlan = await planPush({
813
- repoReader: reader,
814
- remoteState,
815
- feeRates,
816
- repoId,
817
- refs: refspecs,
818
- force: flags.force
819
- });
820
- 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
+ }
821
1130
  const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
822
1131
  if (upToDate) {
823
1132
  if (flags.json) {
824
- io.out(
825
- jsonOut({ command: "push", repoId, identity, executed: false, upToDate: true, plan })
826
- );
1133
+ io2.emitJson({
1134
+ command: "push",
1135
+ repoId,
1136
+ path,
1137
+ identity,
1138
+ executed: false,
1139
+ upToDate: true,
1140
+ plan
1141
+ });
827
1142
  } else {
828
- io.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
1143
+ io2.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
829
1144
  }
830
1145
  return 0;
831
1146
  }
832
1147
  if (!flags.json) {
833
- for (const line of renderPlan(plan)) io.out(line);
834
- io.out(renderIdentityLine(identity));
1148
+ for (const line of renderPlan(plan)) io2.out(line);
1149
+ io2.out(renderIdentityLine(identity));
835
1150
  }
836
1151
  if (!flags.yes) {
837
1152
  if (flags.json) {
838
- io.out(
839
- jsonOut({
840
- command: "push",
841
- repoId,
842
- identity,
843
- executed: false,
844
- upToDate: false,
845
- plan,
846
- hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
847
- })
848
- );
1153
+ io2.emitJson({
1154
+ command: "push",
1155
+ repoId,
1156
+ path,
1157
+ identity,
1158
+ executed: false,
1159
+ upToDate: false,
1160
+ plan,
1161
+ hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
1162
+ });
849
1163
  return 0;
850
1164
  }
851
- if (!io.isInteractive) {
852
- io.err(
1165
+ if (!io2.isInteractive) {
1166
+ io2.err(
853
1167
  "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
854
1168
  );
855
1169
  return 1;
856
1170
  }
857
- const proceed = await io.confirm(
1171
+ const proceed = await io2.confirm(
858
1172
  `Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
859
1173
  );
860
1174
  if (!proceed) {
861
- io.err("aborted \u2014 nothing was uploaded or published.");
1175
+ io2.err("aborted \u2014 nothing was uploaded or published.");
862
1176
  return 1;
863
1177
  }
864
1178
  }
865
- const pushResult = await executePush({
866
- plan: pushPlan,
867
- publisher: standaloneCtx.publisher,
868
- remoteState,
869
- repoReader: reader,
870
- relayUrls: relaysUsed
871
- });
872
- const result = serializePushResult(pushPlan, pushResult);
1179
+ const result = await execute();
873
1180
  if (flags.json) {
874
- io.out(
875
- jsonOut({ command: "push", repoId, identity, executed: true, upToDate: false, plan, result })
876
- );
1181
+ io2.emitJson({
1182
+ command: "push",
1183
+ repoId,
1184
+ path,
1185
+ identity,
1186
+ executed: true,
1187
+ upToDate: false,
1188
+ plan,
1189
+ result
1190
+ });
877
1191
  } else {
878
- for (const line of renderResult(result)) io.out(line);
1192
+ for (const line of renderResult(result)) io2.out(line);
879
1193
  }
880
1194
  return 0;
881
1195
  } catch (err) {
882
- const described = describeError(err);
883
- if (flags.json) {
884
- io.out(JSON.stringify({ command: "push", ...described.json }, null, 2));
885
- } else {
886
- for (const line of described.lines) io.err(line);
887
- }
888
- return 1;
1196
+ return emitCliError(io2, flags.json, "push", err);
889
1197
  } finally {
890
1198
  if (standaloneCtx) {
891
1199
  try {
@@ -895,8 +1203,1289 @@ async function runPush(args, deps) {
895
1203
  }
896
1204
  }
897
1205
  }
898
- function jsonOut(output) {
899
- return JSON.stringify(output, null, 2);
1206
+
1207
+ // src/cli/balance.ts
1208
+ var BALANCE_USAGE = `Usage: rig balance [--json]
1209
+
1210
+ Show the active identity's money: on-chain wallet balances (per configured
1211
+ chain, read from the settlement chain the payment channels actually use) and
1212
+ recorded payment-channel holdings (deposited / claimed / available). Free \u2014
1213
+ reads chain RPCs and local state only; nothing is signed or paid.
1214
+
1215
+ Options:
1216
+ --json machine-readable envelope (base units as strings)
1217
+ -h, --help show this help`;
1218
+ async function runBalance(args, deps) {
1219
+ const { io: io2, env } = deps;
1220
+ let json = false;
1221
+ try {
1222
+ const { values } = parseArgs3({
1223
+ args,
1224
+ options: {
1225
+ json: { type: "boolean", default: false },
1226
+ help: { type: "boolean", short: "h", default: false }
1227
+ }
1228
+ });
1229
+ if (values.help) {
1230
+ io2.out(BALANCE_USAGE);
1231
+ return 0;
1232
+ }
1233
+ json = values.json ?? false;
1234
+ } catch (err) {
1235
+ io2.err(err instanceof Error ? err.message : String(err));
1236
+ io2.err(BALANCE_USAGE);
1237
+ return 2;
1238
+ }
1239
+ let ctx;
1240
+ try {
1241
+ ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1242
+ env,
1243
+ cwd: deps.cwd,
1244
+ warn: (line) => io2.err(line),
1245
+ // Free read: works without a proxy/BTP write uplink.
1246
+ requireUplink: false
1247
+ });
1248
+ const identity = identityReport(ctx);
1249
+ const wallet = ctx.money ? await ctx.money.walletBalances() : [];
1250
+ const store = new ChannelMapStore(resolveChannelPaths(env));
1251
+ const channels = store.list().filter((record) => record.identity === identity.pubkey).map((record) => {
1252
+ const watermark = store.readWatermark(record.channelId);
1253
+ const deposited = record.depositTotal;
1254
+ const claimed = watermark?.cumulativeAmount;
1255
+ let available = null;
1256
+ if (deposited !== void 0 && claimed !== void 0) {
1257
+ const remaining = BigInt(deposited) - BigInt(claimed);
1258
+ available = (remaining > 0n ? remaining : 0n).toString();
1259
+ }
1260
+ return {
1261
+ channelId: record.channelId,
1262
+ destination: record.destination,
1263
+ peerId: record.peerId,
1264
+ chain: record.chain,
1265
+ status: channelStatus(watermark),
1266
+ depositTotal: deposited ?? null,
1267
+ cumulativeClaimed: claimed ?? null,
1268
+ nonce: watermark?.nonce ?? null,
1269
+ available
1270
+ };
1271
+ });
1272
+ if (json) {
1273
+ io2.emitJson({
1274
+ command: "balance",
1275
+ identity,
1276
+ wallet,
1277
+ channels
1278
+ });
1279
+ return 0;
1280
+ }
1281
+ io2.out(renderIdentityLine(identity));
1282
+ io2.out("");
1283
+ io2.out("Wallet (on-chain):");
1284
+ if (wallet.length === 0) {
1285
+ io2.out(
1286
+ " (no balance readable \u2014 no chain configured, the RPC is unreachable, or the chain's keys derive only during a client start)"
1287
+ );
1288
+ }
1289
+ for (const balance of wallet) {
1290
+ io2.out(
1291
+ ` ${balance.chain.padEnd(7)} ${balance.address} ${balance.amount}` + (balance.asset ? ` ${balance.asset}` : " base units") + (balance.assetScale !== void 0 ? ` (scale ${balance.assetScale})` : "")
1292
+ );
1293
+ }
1294
+ io2.out("");
1295
+ io2.out("Channels (recorded):");
1296
+ if (channels.length === 0) {
1297
+ io2.out(
1298
+ " none \u2014 paid commands record their channel on first use; `rig channel open` records one explicitly."
1299
+ );
1300
+ }
1301
+ for (const c of channels) {
1302
+ io2.out(
1303
+ ` ${c.channelId} [${c.status}] deposited ${c.depositTotal ?? "?"} claimed ${c.cumulativeClaimed ?? "?"} available ${c.available ?? "?"} (${c.chain} \u2192 ${c.destination})`
1304
+ );
1305
+ }
1306
+ return 0;
1307
+ } catch (err) {
1308
+ return emitCliError(io2, json, "balance", err);
1309
+ } finally {
1310
+ if (ctx) {
1311
+ try {
1312
+ await ctx.stop();
1313
+ } catch {
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+
1319
+ // src/cli/channel.ts
1320
+ import { parseArgs as parseArgs4 } from "util";
1321
+ var CHANNEL_USAGE = `Usage: rig channel <subcommand>
1322
+
1323
+ Manage the payment channels paid rig commands hold with relay/store peers.
1324
+ Paid commands open a channel lazily on first use and record it under
1325
+ TOON_CLIENT_HOME (default ~/.toon-client, rig-channels.json), so later
1326
+ invocations resume the same channel instead of opening a new one per run.
1327
+
1328
+ Subcommands:
1329
+ list [--json] show recorded channels \u2014 peer, chain, channel id, deposit,
1330
+ cumulative claimed, status. Free: reads local state only.
1331
+
1332
+ open [--peer <ilp-destination>] [--deposit <base-units>]
1333
+ explicitly open the payment channel for a peer \u2014 the SAME
1334
+ path paid commands use lazily: resumes the recorded live
1335
+ channel if one exists (no on-chain spend), else opens and
1336
+ records a fresh one (locks the peer-negotiated initial
1337
+ deposit on-chain). --peer is the ILP destination to anchor
1338
+ to (default: the configured destination, e.g. the devnet
1339
+ apex); --deposit adds that much extra collateral after the
1340
+ open/resume. On-chain: asks for confirmation (--yes skips).
1341
+
1342
+ close <channelId>
1343
+ close a recorded channel \u2014 an on-chain tx that starts the
1344
+ settlement challenge window (the peer's settlementTimeout).
1345
+ The channel stops paying immediately; the remaining
1346
+ collateral stays locked until \`rig channel settle\` after
1347
+ the window elapses. Asks for confirmation (--yes skips).
1348
+
1349
+ settle <channelId>
1350
+ settle a closed channel once its challenge window elapsed \u2014
1351
+ an on-chain tx that releases the remaining collateral back
1352
+ to your wallet. Refused (without spending gas) while the
1353
+ window is still open. Asks for confirmation (--yes skips).
1354
+
1355
+ Common options: --json (machine-readable envelopes; without --yes lifecycle
1356
+ commands emit a pure plan and execute nothing), --yes, -h/--help.`;
1357
+ async function runChannel(args, deps) {
1358
+ const { io: io2 } = deps;
1359
+ const [sub, ...rest] = args;
1360
+ switch (sub) {
1361
+ case "list":
1362
+ return runChannelList(rest, deps);
1363
+ case "open":
1364
+ return runChannelOpen(rest, deps);
1365
+ case "close":
1366
+ return runChannelWithdrawStep(rest, deps, "close");
1367
+ case "settle":
1368
+ return runChannelWithdrawStep(rest, deps, "settle");
1369
+ case "help":
1370
+ case "--help":
1371
+ case "-h":
1372
+ io2.out(CHANNEL_USAGE);
1373
+ return 0;
1374
+ case void 0:
1375
+ io2.err(CHANNEL_USAGE);
1376
+ return 2;
1377
+ default:
1378
+ io2.err(`rig channel: unknown subcommand ${JSON.stringify(sub)}`);
1379
+ io2.err(CHANNEL_USAGE);
1380
+ return 2;
1381
+ }
1382
+ }
1383
+ function runChannelList(args, deps) {
1384
+ const { io: io2, env } = deps;
1385
+ let json = false;
1386
+ try {
1387
+ const { values } = parseArgs4({
1388
+ args,
1389
+ options: {
1390
+ json: { type: "boolean", default: false },
1391
+ help: { type: "boolean", short: "h", default: false }
1392
+ }
1393
+ });
1394
+ if (values.help) {
1395
+ io2.out(CHANNEL_USAGE);
1396
+ return 0;
1397
+ }
1398
+ json = values.json ?? false;
1399
+ } catch (err) {
1400
+ io2.err(err instanceof Error ? err.message : String(err));
1401
+ io2.err(CHANNEL_USAGE);
1402
+ return 2;
1403
+ }
1404
+ try {
1405
+ const paths = resolveChannelPaths(env);
1406
+ const store = new ChannelMapStore(paths);
1407
+ const records = store.list();
1408
+ const rows = records.map((record) => describeChannel(record, store));
1409
+ if (json) {
1410
+ io2.emitJson({ command: "channel list", channels: rows });
1411
+ return 0;
1412
+ }
1413
+ if (rows.length === 0) {
1414
+ io2.out(
1415
+ "No payment channels recorded \u2014 paid rig commands (push, issue, comment, pr) record the channel they open on first use, and `rig channel open` records an explicit one."
1416
+ );
1417
+ return 0;
1418
+ }
1419
+ io2.out(
1420
+ `${rows.length} payment channel${rows.length === 1 ? "" : "s"} recorded in ${paths.mapPath}:`
1421
+ );
1422
+ for (const row of rows) {
1423
+ io2.out("");
1424
+ for (const line of renderChannel(row)) io2.out(line);
1425
+ }
1426
+ return 0;
1427
+ } catch (err) {
1428
+ return emitCliError(io2, json, "channel list", err);
1429
+ }
1430
+ }
1431
+ function describeChannel(record, store) {
1432
+ const watermark = store.readWatermark(
1433
+ record.channelId
1434
+ );
1435
+ return {
1436
+ channelId: record.channelId,
1437
+ peerId: record.peerId,
1438
+ identity: record.identity,
1439
+ destination: record.destination,
1440
+ chain: record.chain,
1441
+ tokenNetwork: record.tokenNetwork,
1442
+ depositTotal: record.depositTotal ?? null,
1443
+ cumulativeClaimed: watermark?.cumulativeAmount ?? null,
1444
+ nonce: watermark?.nonce ?? null,
1445
+ status: channelStatus(watermark),
1446
+ openedAt: record.openedAt,
1447
+ lastUsedAt: record.lastUsedAt
1448
+ };
1449
+ }
1450
+ function renderChannel(row) {
1451
+ const claimed = row.cumulativeClaimed === null ? "unknown (no local claim state)" : `${row.cumulativeClaimed} base units (nonce ${row.nonce})`;
1452
+ return [
1453
+ `channel ${row.channelId} [${row.status}]`,
1454
+ ` peer ${row.destination} (${row.peerId})`,
1455
+ ` identity ${row.identity.slice(0, 8)}\u2026`,
1456
+ ` chain ${row.chain}` + (row.tokenNetwork ? ` token-network ${row.tokenNetwork}` : ""),
1457
+ ` deposited ${row.depositTotal ?? "unrecorded"}` + (row.depositTotal ? " base units" : ""),
1458
+ ` claimed ${claimed}`,
1459
+ ` opened ${row.openedAt} (last used ${row.lastUsedAt})`
1460
+ ];
1461
+ }
1462
+ async function loadMoneyContext(deps, options) {
1463
+ const ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1464
+ env: deps.env,
1465
+ cwd: deps.cwd,
1466
+ warn: (line) => deps.io.err(line),
1467
+ ...options?.channelDestination ? { channelDestination: options.channelDestination } : {}
1468
+ });
1469
+ const money = ctx.money;
1470
+ if (!money) {
1471
+ await ctx.stop().catch(() => void 0);
1472
+ throw new Error(
1473
+ "this standalone loader does not expose money operations \u2014 channel open/close/settle need a loader with the money lifecycle"
1474
+ );
1475
+ }
1476
+ return { ctx, money };
1477
+ }
1478
+ async function confirmOnChain(io2, flags, question) {
1479
+ if (flags.yes) return "proceed";
1480
+ if (flags.json) return "json-plan";
1481
+ if (!io2.isInteractive) {
1482
+ io2.err(
1483
+ "refusing to move on-chain funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for a plan)"
1484
+ );
1485
+ return 1;
1486
+ }
1487
+ const proceed = await io2.confirm(question);
1488
+ if (proceed) return "proceed";
1489
+ io2.err("aborted \u2014 no on-chain transaction was sent.");
1490
+ return 1;
1491
+ }
1492
+ function formatUnixSeconds(value) {
1493
+ const ms = Number(value) * 1e3;
1494
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : `t=${value}s`;
1495
+ }
1496
+ function secondsUntil(value, nowSec) {
1497
+ return Number(value) - nowSec;
1498
+ }
1499
+ async function runChannelOpen(args, deps) {
1500
+ const { io: io2 } = deps;
1501
+ let peer;
1502
+ let deposit;
1503
+ let yes = false;
1504
+ let json = false;
1505
+ try {
1506
+ const { values } = parseArgs4({
1507
+ args,
1508
+ options: {
1509
+ peer: { type: "string" },
1510
+ deposit: { type: "string" },
1511
+ yes: { type: "boolean", default: false },
1512
+ json: { type: "boolean", default: false },
1513
+ help: { type: "boolean", short: "h", default: false }
1514
+ }
1515
+ });
1516
+ if (values.help) {
1517
+ io2.out(CHANNEL_USAGE);
1518
+ return 0;
1519
+ }
1520
+ peer = values.peer;
1521
+ yes = values.yes ?? false;
1522
+ json = values.json ?? false;
1523
+ if (values.deposit !== void 0) {
1524
+ if (!/^\d+$/.test(values.deposit) || BigInt(values.deposit) <= 0n) {
1525
+ throw new Error(
1526
+ `--deposit must be a positive base-unit integer, got ${JSON.stringify(values.deposit)}`
1527
+ );
1528
+ }
1529
+ deposit = BigInt(values.deposit);
1530
+ }
1531
+ } catch (err) {
1532
+ io2.err(err instanceof Error ? err.message : String(err));
1533
+ io2.err(CHANNEL_USAGE);
1534
+ return 2;
1535
+ }
1536
+ let ctx;
1537
+ try {
1538
+ const loaded = await loadMoneyContext(deps, {
1539
+ ...peer ? { channelDestination: peer } : {}
1540
+ });
1541
+ ctx = loaded.ctx;
1542
+ const identity = identityReport(ctx);
1543
+ const plan = {
1544
+ destination: peer ?? null,
1545
+ deposit: deposit?.toString() ?? null
1546
+ };
1547
+ if (!json) {
1548
+ io2.out("Channel open plan:");
1549
+ io2.out(` peer (ILP) ${peer ?? "(configured default destination)"}`);
1550
+ io2.out(
1551
+ ` deposit ${deposit !== void 0 ? `+${deposit} base units of extra collateral after the open/resume` : "none beyond the peer-negotiated initial deposit"}`
1552
+ );
1553
+ io2.out(renderIdentityLine(identity));
1554
+ io2.out(
1555
+ "If a live channel is already recorded for this peer it is RESUMED (no on-chain spend); otherwise an on-chain channel open locks the peer-negotiated initial deposit from your wallet (plus gas)."
1556
+ );
1557
+ }
1558
+ const gate = await confirmOnChain(
1559
+ io2,
1560
+ { yes, json },
1561
+ "Proceed with on-chain channel open? [y/N] "
1562
+ );
1563
+ if (gate === "json-plan") {
1564
+ io2.emitJson({
1565
+ command: "channel open",
1566
+ identity,
1567
+ executed: false,
1568
+ plan,
1569
+ hint: "plan only \u2014 re-run with --yes to open (on-chain: locks collateral and spends gas)"
1570
+ });
1571
+ return 0;
1572
+ }
1573
+ if (gate !== "proceed") return gate;
1574
+ const outcome = await loaded.money.openChannel(
1575
+ deposit !== void 0 ? { deposit } : void 0
1576
+ );
1577
+ if (json) {
1578
+ io2.emitJson({
1579
+ command: "channel open",
1580
+ identity,
1581
+ executed: true,
1582
+ plan,
1583
+ result: {
1584
+ channelId: outcome.channelId,
1585
+ resumed: outcome.resumed,
1586
+ destination: outcome.destination,
1587
+ chain: outcome.chain ?? null,
1588
+ peerId: outcome.peerId ?? null,
1589
+ depositTotal: outcome.depositTotal ?? null,
1590
+ depositAdded: outcome.depositAdded ?? null,
1591
+ depositTxHash: outcome.depositTxHash ?? null
1592
+ }
1593
+ });
1594
+ return 0;
1595
+ }
1596
+ io2.out(
1597
+ outcome.resumed ? `Resumed recorded channel ${outcome.channelId} \u2014 no on-chain open was needed.` : `Opened channel ${outcome.channelId}${outcome.chain ? ` on ${outcome.chain}` : ""} and recorded it for reuse.`
1598
+ );
1599
+ io2.out(` peer ${outcome.destination}${outcome.peerId ? ` (${outcome.peerId})` : ""}`);
1600
+ if (outcome.depositAdded) {
1601
+ io2.out(
1602
+ ` deposited +${outcome.depositAdded} base units` + (outcome.depositTxHash ? ` (tx ${outcome.depositTxHash})` : "")
1603
+ );
1604
+ }
1605
+ if (outcome.depositTotal) {
1606
+ io2.out(` collateral ${outcome.depositTotal} base units total on-chain`);
1607
+ }
1608
+ return 0;
1609
+ } catch (err) {
1610
+ return emitCliError(io2, json, "channel open", err);
1611
+ } finally {
1612
+ await stopQuietly(ctx);
1613
+ }
1614
+ }
1615
+ function withdrawPrecheck(step, channelId, watermark, nowSec) {
1616
+ const status = channelStatus(watermark, nowSec);
1617
+ if (status === "settled") {
1618
+ return `channel ${channelId} is already settled \u2014 nothing left to ${step}.`;
1619
+ }
1620
+ if (step === "close") {
1621
+ if (status === "closing" || status === "settleable") {
1622
+ const at = watermark?.settleableAt;
1623
+ return `channel ${channelId} is already closing \u2014 ` + (status === "settleable" ? "its challenge window has elapsed; run `rig channel settle` to release the collateral." : `settleable at ${at ? formatUnixSeconds(at) : "the end of its challenge window"} (run \`rig channel settle\` then).`);
1624
+ }
1625
+ return void 0;
1626
+ }
1627
+ if (status === "open") {
1628
+ return `channel ${channelId} is not closed \u2014 run \`rig channel close ${channelId}\` first (settle only releases collateral after the challenge window of a closed channel).`;
1629
+ }
1630
+ if (status === "closing" && watermark?.settleableAt !== void 0) {
1631
+ const remain = secondsUntil(watermark.settleableAt, nowSec);
1632
+ return `channel ${channelId} is not settleable yet \u2014 the challenge window is still open (${remain}s remain, settleable at ${formatUnixSeconds(watermark.settleableAt)}). Nothing was spent; re-run after that time.`;
1633
+ }
1634
+ return void 0;
1635
+ }
1636
+ async function runChannelWithdrawStep(args, deps, step) {
1637
+ const { io: io2, env } = deps;
1638
+ const command = `channel ${step}`;
1639
+ let channelId;
1640
+ let yes = false;
1641
+ let json = false;
1642
+ try {
1643
+ const { values, positionals } = parseArgs4({
1644
+ args,
1645
+ options: {
1646
+ yes: { type: "boolean", default: false },
1647
+ json: { type: "boolean", default: false },
1648
+ help: { type: "boolean", short: "h", default: false }
1649
+ },
1650
+ allowPositionals: true
1651
+ });
1652
+ if (values.help) {
1653
+ io2.out(CHANNEL_USAGE);
1654
+ return 0;
1655
+ }
1656
+ yes = values.yes ?? false;
1657
+ json = values.json ?? false;
1658
+ if (positionals.length !== 1) {
1659
+ throw new Error(
1660
+ `rig channel ${step} takes exactly one <channelId> (got ${positionals.length}) \u2014 \`rig channel list\` shows recorded channels`
1661
+ );
1662
+ }
1663
+ channelId = positionals[0];
1664
+ } catch (err) {
1665
+ io2.err(err instanceof Error ? err.message : String(err));
1666
+ io2.err(CHANNEL_USAGE);
1667
+ return 2;
1668
+ }
1669
+ let ctx;
1670
+ try {
1671
+ const store = new ChannelMapStore(resolveChannelPaths(env));
1672
+ const record = store.list().find((r) => r.channelId === channelId);
1673
+ if (!record) {
1674
+ throw new Error(
1675
+ `no recorded channel ${JSON.stringify(channelId)} \u2014 \`rig channel list\` shows the channels this identity holds`
1676
+ );
1677
+ }
1678
+ const watermark = store.readWatermark(channelId);
1679
+ const nowSec = Math.floor(Date.now() / 1e3);
1680
+ const refusal = withdrawPrecheck(step, channelId, watermark, nowSec);
1681
+ if (refusal) throw new Error(refusal);
1682
+ const loaded = await loadMoneyContext(deps);
1683
+ ctx = loaded.ctx;
1684
+ const identity = identityReport(ctx);
1685
+ if (record.identity !== identity.pubkey) {
1686
+ throw new Error(
1687
+ `channel ${channelId} was opened by identity ${record.identity.slice(0, 8)}\u2026 but the active identity is ${identity.pubkey.slice(0, 8)}\u2026 (from ${identity.sourceLabel}) \u2014 on-chain ${step} must be signed by the opener's wallet key. Switch RIG_MNEMONIC (or the keystore) to that identity.`
1688
+ );
1689
+ }
1690
+ if (!json) {
1691
+ const claimed = watermark?.cumulativeAmount;
1692
+ io2.out(`Channel ${step} plan:`);
1693
+ io2.out(` channel ${channelId} [${channelStatus(watermark, nowSec)}]`);
1694
+ io2.out(` peer ${record.destination} (${record.peerId})`);
1695
+ io2.out(` chain ${record.chain}`);
1696
+ io2.out(
1697
+ ` deposited ${record.depositTotal ?? "unrecorded"}` + (claimed !== void 0 ? ` claimed ${claimed} base units` : "")
1698
+ );
1699
+ io2.out(renderIdentityLine(identity));
1700
+ if (step === "close") {
1701
+ io2.out(
1702
+ "Closing is an ON-CHAIN transaction (gas) that starts the settlement challenge window: the channel stops paying immediately, and the remaining collateral stays locked until `rig channel settle` succeeds after the window elapses."
1703
+ );
1704
+ } else {
1705
+ io2.out(
1706
+ "Settling is an ON-CHAIN transaction (gas) that releases the remaining collateral back to your wallet. The chain's challenge window is authoritative \u2014 a too-early settle is refused before any gas is spent."
1707
+ );
1708
+ }
1709
+ }
1710
+ const gate = await confirmOnChain(
1711
+ io2,
1712
+ { yes, json },
1713
+ `Proceed with on-chain channel ${step}? [y/N] `
1714
+ );
1715
+ if (gate === "json-plan") {
1716
+ io2.emitJson({
1717
+ command,
1718
+ identity,
1719
+ executed: false,
1720
+ channelId,
1721
+ hint: `plan only \u2014 re-run with --yes to ${step} (on-chain transaction)`
1722
+ });
1723
+ return 0;
1724
+ }
1725
+ if (gate !== "proceed") return gate;
1726
+ if (step === "close") {
1727
+ const result2 = await loaded.money.closeChannel(record);
1728
+ if (json) {
1729
+ io2.emitJson({
1730
+ command,
1731
+ identity,
1732
+ executed: true,
1733
+ channelId,
1734
+ result: {
1735
+ txHash: result2.txHash ?? null,
1736
+ closedAt: result2.closedAt,
1737
+ settleableAt: result2.settleableAt
1738
+ }
1739
+ });
1740
+ return 0;
1741
+ }
1742
+ io2.out(
1743
+ `Channel ${channelId} is closing` + (result2.txHash ? ` (tx ${result2.txHash})` : "") + "."
1744
+ );
1745
+ io2.out(
1746
+ ` challenge window: settleable at ${formatUnixSeconds(result2.settleableAt)} (~${Math.max(0, secondsUntil(result2.settleableAt, nowSec))}s from now)`
1747
+ );
1748
+ io2.out(
1749
+ ` run \`rig channel settle ${channelId}\` after that time to release the collateral.`
1750
+ );
1751
+ return 0;
1752
+ }
1753
+ const result = await loaded.money.settleChannel(record);
1754
+ if (json) {
1755
+ io2.emitJson({
1756
+ command,
1757
+ identity,
1758
+ executed: true,
1759
+ channelId,
1760
+ result: { txHash: result.txHash ?? null }
1761
+ });
1762
+ return 0;
1763
+ }
1764
+ io2.out(
1765
+ `Channel ${channelId} settled` + (result.txHash ? ` (tx ${result.txHash})` : "") + " \u2014 the remaining collateral was released to your wallet."
1766
+ );
1767
+ return 0;
1768
+ } catch (err) {
1769
+ return emitCliError(io2, json, command, err);
1770
+ } finally {
1771
+ await stopQuietly(ctx);
1772
+ }
1773
+ }
1774
+ async function stopQuietly(ctx) {
1775
+ if (!ctx) return;
1776
+ try {
1777
+ await ctx.stop();
1778
+ } catch {
1779
+ }
1780
+ }
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
+
2023
+ // src/cli/events.ts
2024
+ import { readFile } from "fs/promises";
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";
2036
+ import {
2037
+ ISSUE_KIND,
2038
+ PATCH_KIND,
2039
+ REPOSITORY_ANNOUNCEMENT_KIND,
2040
+ STATUS_APPLIED_KIND,
2041
+ STATUS_CLOSED_KIND,
2042
+ STATUS_DRAFT_KIND,
2043
+ STATUS_OPEN_KIND
2044
+ } from "@toon-protocol/core/nip34";
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
2050
+ -h, --help show this help`;
2051
+ var ISSUE_LIST_USAGE = `Usage: rig issue list [--state open|closed|all] [options]
2052
+
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.
2056
+
2057
+ 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]
2061
+
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\`).
2065
+
2066
+ Options:
2067
+ ${READ_COMMON_FLAGS}`;
2068
+ var PR_LIST_USAGE = `Usage: rig pr list [--state open|applied|closed|draft|all] [options]
2069
+
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.
2073
+
2074
+ 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]
2078
+
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
+ });
900
2489
  }
901
2490
 
902
2491
  // src/cli/events.ts
@@ -932,7 +2521,11 @@ Options:
932
2521
  --body <text> issue body (Markdown)
933
2522
  --body-file <path> read the issue body from a file
934
2523
  --label <label> label (t tag); repeatable
935
- ${COMMON_FLAGS_USAGE}`;
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`;
936
2529
  var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
937
2530
 
938
2531
  Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
@@ -956,11 +2549,17 @@ verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
956
2549
  full series text \u2014 cover-letter threading (one event per commit) is out of
957
2550
  scope in v1.
958
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
+
959
2556
  Options:
960
2557
  --title <title> patch/PR title (subject tag) [required]
961
2558
  --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
962
2559
  or <rev>...<rev> (mutually exclusive with --patch-file)
963
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
964
2563
  --branch <name> branch name (t tag)
965
2564
  ${COMMON_FLAGS_USAGE}`;
966
2565
  var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
@@ -976,10 +2575,14 @@ Options:
976
2575
  ${COMMON_FLAGS_USAGE}`;
977
2576
  var PR_USAGE = `${PR_CREATE_USAGE}
978
2577
 
979
- ${PR_STATUS_USAGE}`;
980
- 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}$/;
981
2584
  function assertHex64(value, what) {
982
- if (!HEX64_RE.test(value)) {
2585
+ if (!HEX64_RE2.test(value)) {
983
2586
  throw new Error(
984
2587
  `${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
985
2588
  );
@@ -1014,7 +2617,7 @@ function pickCommon(values) {
1014
2617
  }
1015
2618
  async function runEvent(opts) {
1016
2619
  const { command, flags, deps, actionLabel } = opts;
1017
- const { io, env } = deps;
2620
+ const { io: io2 } = deps;
1018
2621
  let standaloneCtx;
1019
2622
  try {
1020
2623
  let repoRoot;
@@ -1032,83 +2635,99 @@ async function runEvent(opts) {
1032
2635
  repoRoot,
1033
2636
  toonRelays: toonConfig.relays
1034
2637
  });
1035
- if (resolved.nudge !== void 0) io.err(resolved.nudge);
2638
+ if (resolved.nudge !== void 0) io2.err(resolved.nudge);
1036
2639
  const relaysUsed = resolved.relays;
1037
2640
  if (relaysUsed.length > 1) {
1038
- io.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
2641
+ io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1039
2642
  return 1;
1040
2643
  }
1041
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1042
- env,
1043
- cwd: deps.cwd,
1044
- warn: (line) => io.err(line)
1045
- });
1046
- const identity = identityReport(standaloneCtx);
1047
- 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
+ }
1048
2662
  const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
1049
2663
  const addr = { ownerPubkey: owner, repoId };
1050
2664
  const event = await opts.buildEvent(addr);
1051
2665
  const action = `kind:${event.kind} ${actionLabel}`;
1052
2666
  if (!flags.json) {
1053
- for (const line of renderEventPlan({ action, addr, identity, fee })) {
1054
- io.out(line);
2667
+ for (const line of renderEventPlan({
2668
+ action,
2669
+ addr,
2670
+ identity,
2671
+ ...fee !== void 0 ? { fee } : {}
2672
+ })) {
2673
+ io2.out(line);
1055
2674
  }
1056
2675
  }
1057
2676
  if (!flags.yes) {
1058
2677
  if (flags.json) {
1059
- io.out(
1060
- jsonOut2({
1061
- command,
1062
- repoAddr: addr,
1063
- identity,
1064
- kind: event.kind,
1065
- executed: false,
1066
- feeEstimate: fee,
1067
- hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
1068
- })
1069
- );
2678
+ io2.emitJson({
2679
+ command,
2680
+ repoAddr: addr,
2681
+ path,
2682
+ identity,
2683
+ kind: event.kind,
2684
+ executed: false,
2685
+ feeEstimate: fee ?? null,
2686
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
2687
+ });
1070
2688
  return 0;
1071
2689
  }
1072
- if (!io.isInteractive) {
1073
- io.err(
2690
+ if (!io2.isInteractive) {
2691
+ io2.err(
1074
2692
  "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
1075
2693
  );
1076
2694
  return 1;
1077
2695
  }
1078
- const proceed = await io.confirm(
2696
+ const proceed = await io2.confirm(
1079
2697
  `Proceed with paid publish (${feeLabel(fee)})? [y/N] `
1080
2698
  );
1081
2699
  if (!proceed) {
1082
- io.err("aborted \u2014 nothing was published.");
2700
+ io2.err("aborted \u2014 nothing was published.");
1083
2701
  return 1;
1084
2702
  }
1085
2703
  }
1086
- const receipt = await standaloneCtx.publisher.publishEvent(event, relaysUsed);
1087
- const result = serializeEventReceipt(event.kind, receipt);
1088
- if (flags.json) {
1089
- io.out(
1090
- jsonOut2({
1091
- command,
1092
- repoAddr: addr,
1093
- identity,
1094
- kind: result.kind,
1095
- executed: true,
1096
- feeEstimate: fee,
1097
- result
1098
- })
2704
+ let result;
2705
+ if (session.path === "standalone") {
2706
+ const receipt = await session.ctx.publisher.publishEvent(
2707
+ event,
2708
+ relaysUsed
1099
2709
  );
2710
+ result = serializeEventReceipt(event.kind, receipt);
1100
2711
  } else {
1101
- for (const line of renderEventReceipt(action, result)) io.out(line);
2712
+ result = await opts.sendDaemon(session.client, addr);
1102
2713
  }
1103
- return 0;
1104
- } catch (err) {
1105
- const described = describeError(err, command);
1106
2714
  if (flags.json) {
1107
- io.out(JSON.stringify({ command, ...described.json }, null, 2));
2715
+ io2.emitJson({
2716
+ command,
2717
+ repoAddr: addr,
2718
+ path,
2719
+ identity,
2720
+ kind: result.kind,
2721
+ executed: true,
2722
+ feeEstimate: fee ?? null,
2723
+ result
2724
+ });
1108
2725
  } else {
1109
- for (const line of described.lines) io.err(line);
2726
+ for (const line of renderEventReceipt(action, result)) io2.out(line);
1110
2727
  }
1111
- return 1;
2728
+ return 0;
2729
+ } catch (err) {
2730
+ return emitCliError(io2, flags.json, command, err);
1112
2731
  } finally {
1113
2732
  if (standaloneCtx) {
1114
2733
  try {
@@ -1118,21 +2737,24 @@ async function runEvent(opts) {
1118
2737
  }
1119
2738
  }
1120
2739
  }
1121
- function jsonOut2(output) {
1122
- return JSON.stringify(output, null, 2);
1123
- }
1124
2740
  async function runIssue(args, deps) {
1125
- const { io } = deps;
2741
+ const { io: io2 } = deps;
1126
2742
  const [sub, ...rest] = args;
1127
2743
  if (sub === "--help" || sub === "-h" || sub === "help") {
1128
- io.out(ISSUE_USAGE);
2744
+ io2.out(ISSUE_USAGE);
2745
+ io2.out("");
2746
+ io2.out(ISSUE_LIST_USAGE);
2747
+ io2.out("");
2748
+ io2.out(ISSUE_SHOW_USAGE);
1129
2749
  return 0;
1130
2750
  }
2751
+ if (sub === "list") return runIssueList(rest, deps);
2752
+ if (sub === "show") return runIssueShow(rest, deps);
1131
2753
  if (sub !== "create") {
1132
- io.err(
1133
- sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
2754
+ io2.err(
2755
+ sub === void 0 ? "missing subcommand: rig issue <create|list|show>" : `unknown rig issue subcommand: ${sub}`
1134
2756
  );
1135
- io.err(ISSUE_USAGE);
2757
+ io2.err(ISSUE_USAGE);
1136
2758
  return 2;
1137
2759
  }
1138
2760
  let flags;
@@ -1141,7 +2763,7 @@ async function runIssue(args, deps) {
1141
2763
  let bodyFile;
1142
2764
  let labels;
1143
2765
  try {
1144
- const { values } = parseArgs3({
2766
+ const { values } = parseArgs7({
1145
2767
  args: rest,
1146
2768
  options: {
1147
2769
  ...COMMON_OPTIONS,
@@ -1164,12 +2786,12 @@ async function runIssue(args, deps) {
1164
2786
  bodyFile = values["body-file"];
1165
2787
  labels = values.label ?? [];
1166
2788
  } catch (err) {
1167
- io.err(err instanceof Error ? err.message : String(err));
1168
- io.err(ISSUE_USAGE);
2789
+ io2.err(err instanceof Error ? err.message : String(err));
2790
+ io2.err(ISSUE_USAGE);
1169
2791
  return 2;
1170
2792
  }
1171
2793
  if (flags.help) {
1172
- io.out(ISSUE_USAGE);
2794
+ io2.out(ISSUE_USAGE);
1173
2795
  return 0;
1174
2796
  }
1175
2797
  let body;
@@ -1179,20 +2801,20 @@ async function runIssue(args, deps) {
1179
2801
  try {
1180
2802
  body = await readFile(bodyFile, "utf-8");
1181
2803
  } catch (err) {
1182
- io.err(
2804
+ io2.err(
1183
2805
  `cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
1184
2806
  );
1185
2807
  return 2;
1186
2808
  }
1187
- } else if (!io.isInteractive) {
2809
+ } else if (!io2.isInteractive) {
1188
2810
  body = await (deps.readStdin ?? defaultReadStdin)();
1189
2811
  } else {
1190
- io.err("an issue body is required: pass --body/--body-file or pipe stdin");
1191
- io.err(ISSUE_USAGE);
2812
+ io2.err("an issue body is required: pass --body/--body-file or pipe stdin");
2813
+ io2.err(ISSUE_USAGE);
1192
2814
  return 2;
1193
2815
  }
1194
2816
  if (body.trim() === "") {
1195
- io.err("the issue body is empty \u2014 nothing to publish");
2817
+ io2.err("the issue body is empty \u2014 nothing to publish");
1196
2818
  return 2;
1197
2819
  }
1198
2820
  return runEvent({
@@ -1200,18 +2822,24 @@ async function runIssue(args, deps) {
1200
2822
  flags,
1201
2823
  deps,
1202
2824
  actionLabel: `issue ${JSON.stringify(title)}`,
1203
- 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
+ })
1204
2832
  });
1205
2833
  }
1206
2834
  async function runComment(args, deps) {
1207
- const { io } = deps;
2835
+ const { io: io2 } = deps;
1208
2836
  let flags;
1209
2837
  let rootEventId;
1210
2838
  let body;
1211
2839
  let parentAuthor;
1212
2840
  let marker;
1213
2841
  try {
1214
- const { values, positionals } = parseArgs3({
2842
+ const { values, positionals } = parseArgs7({
1215
2843
  args,
1216
2844
  options: {
1217
2845
  ...COMMON_OPTIONS,
@@ -1223,7 +2851,7 @@ async function runComment(args, deps) {
1223
2851
  });
1224
2852
  flags = pickCommon(values);
1225
2853
  if (flags.help) {
1226
- io.out(COMMENT_USAGE);
2854
+ io2.out(COMMENT_USAGE);
1227
2855
  return 0;
1228
2856
  }
1229
2857
  if (positionals.length !== 1) {
@@ -1245,8 +2873,8 @@ async function runComment(args, deps) {
1245
2873
  }
1246
2874
  marker = rawMarker;
1247
2875
  } catch (err) {
1248
- io.err(err instanceof Error ? err.message : String(err));
1249
- io.err(COMMENT_USAGE);
2876
+ io2.err(err instanceof Error ? err.message : String(err));
2877
+ io2.err(COMMENT_USAGE);
1250
2878
  return 2;
1251
2879
  }
1252
2880
  return runEvent({
@@ -1261,7 +2889,14 @@ async function runComment(args, deps) {
1261
2889
  parentAuthor ?? addr.ownerPubkey,
1262
2890
  body,
1263
2891
  marker
1264
- )
2892
+ ),
2893
+ sendDaemon: (client, addr) => client.gitComment({
2894
+ repoAddr: addr,
2895
+ rootEventId,
2896
+ body,
2897
+ ...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {},
2898
+ marker
2899
+ })
1265
2900
  });
1266
2901
  }
1267
2902
  var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
@@ -1269,48 +2904,57 @@ function extractPatchShas(patchText) {
1269
2904
  return [...patchText.matchAll(PATCH_FROM_RE)].map((m) => m[1]);
1270
2905
  }
1271
2906
  async function runPr(args, deps) {
1272
- const { io } = deps;
2907
+ const { io: io2 } = deps;
1273
2908
  const [sub, ...rest] = args;
1274
2909
  switch (sub) {
1275
2910
  case "create":
1276
2911
  return runPrCreate(rest, deps);
1277
2912
  case "status":
1278
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);
1279
2919
  case "--help":
1280
2920
  case "-h":
1281
2921
  case "help":
1282
- io.out(PR_USAGE);
2922
+ io2.out(PR_USAGE);
1283
2923
  return 0;
1284
2924
  default:
1285
- io.err(
1286
- sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
2925
+ io2.err(
2926
+ sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
1287
2927
  );
1288
- io.err(PR_USAGE);
2928
+ io2.err(PR_USAGE);
1289
2929
  return 2;
1290
2930
  }
1291
2931
  }
1292
2932
  async function runPrCreate(rest, deps) {
1293
- const { io } = deps;
2933
+ const { io: io2 } = deps;
1294
2934
  let flags;
1295
2935
  let title;
1296
2936
  let range;
1297
2937
  let patchFile;
1298
2938
  let branch;
2939
+ let bodyFlag;
2940
+ let bodyFile;
1299
2941
  try {
1300
- const { values } = parseArgs3({
2942
+ const { values } = parseArgs7({
1301
2943
  args: rest,
1302
2944
  options: {
1303
2945
  ...COMMON_OPTIONS,
1304
2946
  title: { type: "string" },
1305
2947
  range: { type: "string" },
1306
2948
  "patch-file": { type: "string" },
2949
+ body: { type: "string" },
2950
+ "body-file": { type: "string" },
1307
2951
  branch: { type: "string" }
1308
2952
  },
1309
2953
  allowPositionals: false
1310
2954
  });
1311
2955
  flags = pickCommon(values);
1312
2956
  if (flags.help) {
1313
- io.out(PR_CREATE_USAGE);
2957
+ io2.out(PR_CREATE_USAGE);
1314
2958
  return 0;
1315
2959
  }
1316
2960
  if (values.title === void 0 || values.title === "") {
@@ -1319,15 +2963,39 @@ async function runPrCreate(rest, deps) {
1319
2963
  if (values.range === void 0 === (values["patch-file"] === void 0)) {
1320
2964
  throw new Error("exactly one of --range or --patch-file is required");
1321
2965
  }
2966
+ if (values.body !== void 0 && values["body-file"] !== void 0) {
2967
+ throw new Error("--body and --body-file are mutually exclusive");
2968
+ }
1322
2969
  title = values.title;
1323
2970
  range = values.range;
1324
2971
  patchFile = values["patch-file"];
1325
2972
  branch = values.branch;
2973
+ bodyFlag = values.body;
2974
+ bodyFile = values["body-file"];
1326
2975
  } catch (err) {
1327
- io.err(err instanceof Error ? err.message : String(err));
1328
- io.err(PR_CREATE_USAGE);
2976
+ io2.err(err instanceof Error ? err.message : String(err));
2977
+ io2.err(PR_CREATE_USAGE);
2978
+ return 2;
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");
1329
2995
  return 2;
1330
2996
  }
2997
+ let builtPatchText;
2998
+ let builtCommits = [];
1331
2999
  return runEvent({
1332
3000
  command: "pr",
1333
3001
  flags,
@@ -1357,40 +3025,56 @@ async function runPrCreate(rest, deps) {
1357
3025
  }
1358
3026
  commits = [];
1359
3027
  }
3028
+ builtPatchText = patchText;
3029
+ builtCommits = commits;
1360
3030
  return buildPatch(
1361
3031
  addr.ownerPubkey,
1362
3032
  addr.repoId,
1363
3033
  title,
1364
3034
  commits,
1365
3035
  branch,
1366
- patchText
3036
+ patchText,
3037
+ body
1367
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
+ });
1368
3052
  }
1369
3053
  });
1370
3054
  }
1371
3055
  var STATUS_KIND_BY_VALUE = {
1372
- open: STATUS_OPEN_KIND,
1373
- applied: STATUS_APPLIED_KIND,
1374
- closed: STATUS_CLOSED_KIND,
1375
- draft: STATUS_DRAFT_KIND
3056
+ open: STATUS_OPEN_KIND2,
3057
+ applied: STATUS_APPLIED_KIND2,
3058
+ closed: STATUS_CLOSED_KIND2,
3059
+ draft: STATUS_DRAFT_KIND2
1376
3060
  };
1377
3061
  function isStatusValue(value) {
1378
3062
  return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
1379
3063
  }
1380
3064
  async function runPrStatus(args, deps) {
1381
- const { io } = deps;
3065
+ const { io: io2 } = deps;
1382
3066
  let flags;
1383
3067
  let targetEventId;
1384
3068
  let status;
1385
3069
  try {
1386
- const { values, positionals } = parseArgs3({
3070
+ const { values, positionals } = parseArgs7({
1387
3071
  args,
1388
3072
  options: COMMON_OPTIONS,
1389
3073
  allowPositionals: true
1390
3074
  });
1391
3075
  flags = pickCommon(values);
1392
3076
  if (flags.help) {
1393
- io.out(PR_STATUS_USAGE);
3077
+ io2.out(PR_STATUS_USAGE);
1394
3078
  return 0;
1395
3079
  }
1396
3080
  if (positionals.length !== 2) {
@@ -1408,8 +3092,8 @@ async function runPrStatus(args, deps) {
1408
3092
  }
1409
3093
  status = rawStatus;
1410
3094
  } catch (err) {
1411
- io.err(err instanceof Error ? err.message : String(err));
1412
- io.err(PR_STATUS_USAGE);
3095
+ io2.err(err instanceof Error ? err.message : String(err));
3096
+ io2.err(PR_STATUS_USAGE);
1413
3097
  return 2;
1414
3098
  }
1415
3099
  return runEvent({
@@ -1421,13 +3105,423 @@ async function runPrStatus(args, deps) {
1421
3105
  const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
1422
3106
  event.tags.push([
1423
3107
  "a",
1424
- `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
3108
+ `${REPOSITORY_ANNOUNCEMENT_KIND2}:${addr.ownerPubkey}:${addr.repoId}`
1425
3109
  ]);
1426
3110
  return event;
1427
- }
3111
+ },
3112
+ sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
1428
3113
  });
1429
3114
  }
1430
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
+
3335
+ // src/cli/fund.ts
3336
+ import { readFileSync } from "fs";
3337
+ import { homedir } from "os";
3338
+ import { join as join2 } from "path";
3339
+ import { parseArgs as parseArgs9 } from "util";
3340
+ var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
3341
+ var CHAINS = ["evm", "solana", "mina"];
3342
+ var FUND_USAGE = `Usage: rig fund [options]
3343
+
3344
+ Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
3345
+ pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
3346
+ ~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
3347
+ faucetUrl config field, or the deployed devnet faucet when the configured
3348
+ network is devnet. The faucet drips a FIXED amount per chain (there is no
3349
+ --amount). On a network without a faucet, prints the wallet address(es) to
3350
+ fund externally instead.
3351
+
3352
+ Options:
3353
+ --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
3354
+ \`chain\` config field, else evm)
3355
+ --address <address> fund this address instead of the identity's own
3356
+ --json machine-readable envelope
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
+ }
3385
+ function readFundConfig(env) {
3386
+ const dir = env["TOON_CLIENT_HOME"] ?? join2(homedir(), ".toon-client");
3387
+ const configPath = join2(dir, "config.json");
3388
+ try {
3389
+ return {
3390
+ file: JSON.parse(readFileSync(configPath, "utf8")),
3391
+ configPath
3392
+ };
3393
+ } catch (err) {
3394
+ if (err.code === "ENOENT") {
3395
+ return { file: {}, configPath };
3396
+ }
3397
+ throw new Error(
3398
+ `failed to read client config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
3399
+ );
3400
+ }
3401
+ }
3402
+ async function runFund(args, deps) {
3403
+ const { io: io2, env } = deps;
3404
+ let chainFlag;
3405
+ let addressFlag;
3406
+ let json = false;
3407
+ try {
3408
+ const { values } = parseArgs9({
3409
+ args,
3410
+ options: {
3411
+ chain: { type: "string" },
3412
+ address: { type: "string" },
3413
+ json: { type: "boolean", default: false },
3414
+ help: { type: "boolean", short: "h", default: false }
3415
+ }
3416
+ });
3417
+ if (values.help) {
3418
+ io2.out(FUND_USAGE);
3419
+ return 0;
3420
+ }
3421
+ chainFlag = values.chain;
3422
+ addressFlag = values.address;
3423
+ json = values.json ?? false;
3424
+ if (chainFlag !== void 0 && !CHAINS.includes(chainFlag)) {
3425
+ throw new Error(
3426
+ `--chain must be one of ${CHAINS.join(" | ")}, got ${JSON.stringify(chainFlag)}`
3427
+ );
3428
+ }
3429
+ } catch (err) {
3430
+ io2.err(err instanceof Error ? err.message : String(err));
3431
+ io2.err(FUND_USAGE);
3432
+ return 2;
3433
+ }
3434
+ try {
3435
+ const { file } = readFundConfig(env);
3436
+ const chain = chainFlag ?? env["TOON_CLIENT_CHAIN"] ?? file.chain ?? "evm";
3437
+ if (!CHAINS.includes(chain)) {
3438
+ throw new Error(
3439
+ `configured settlement chain ${JSON.stringify(chain)} has no faucet \u2014 pass --chain ${CHAINS.join(" | ")}`
3440
+ );
3441
+ }
3442
+ const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
3443
+ const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (network === "devnet" ? DEVNET_FAUCET_URL : void 0);
3444
+ const resolved = await resolveIdentity({
3445
+ env,
3446
+ cwd: deps.cwd,
3447
+ warn: (line) => io2.err(line)
3448
+ });
3449
+ const identity = {
3450
+ pubkey: resolved.pubkey,
3451
+ source: resolved.source,
3452
+ sourceLabel: resolved.sourceLabel
3453
+ };
3454
+ const client = await import("@toon-protocol/client");
3455
+ const derived = await client.deriveFullIdentity(
3456
+ resolved.mnemonic,
3457
+ resolved.accountIndex
3458
+ );
3459
+ const addresses = {
3460
+ evm: derived.evm.address || null,
3461
+ solana: derived.solana.publicKey || null,
3462
+ mina: derived.mina.publicKey || null
3463
+ };
3464
+ if (!faucetUrl) {
3465
+ const guidance = noFaucetGuidance(network, sharedDevnetOrigin(env, file));
3466
+ if (json) {
3467
+ io2.emitJson({
3468
+ command: "fund",
3469
+ identity,
3470
+ funded: false,
3471
+ network: network ?? null,
3472
+ chain,
3473
+ addresses,
3474
+ guidance
3475
+ });
3476
+ return 0;
3477
+ }
3478
+ io2.out(renderIdentityLine(identity));
3479
+ io2.out(guidance);
3480
+ io2.out("Wallet addresses:");
3481
+ io2.out(` evm ${addresses.evm ?? "(no key derived)"}`);
3482
+ io2.out(` solana ${addresses.solana ?? "(no key derived)"}`);
3483
+ io2.out(` mina ${addresses.mina ?? "(no key derived \u2014 optional mina-signer dependency missing)"}`);
3484
+ return 0;
3485
+ }
3486
+ const address = addressFlag ?? addresses[chain];
3487
+ if (!address) {
3488
+ throw new Error(
3489
+ `no ${chain} address could be derived for this identity \u2014 pass an explicit --address (for mina, install the optional mina-signer dependency)`
3490
+ );
3491
+ }
3492
+ const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
3493
+ const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
3494
+ if (!json) {
3495
+ io2.out(
3496
+ `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
3497
+ );
3498
+ }
3499
+ const { response } = await client.fundWallet(faucetUrl, address, chain, {
3500
+ timeout,
3501
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
3502
+ });
3503
+ if (json) {
3504
+ io2.emitJson({
3505
+ command: "fund",
3506
+ identity,
3507
+ funded: true,
3508
+ network: network ?? null,
3509
+ chain,
3510
+ address,
3511
+ faucetUrl,
3512
+ response
3513
+ });
3514
+ return 0;
3515
+ }
3516
+ io2.out(`Faucet drip succeeded: ${chain} \u2192 ${address}`);
3517
+ io2.out(renderIdentityLine(identity));
3518
+ io2.out("Re-check with `rig balance` (a drip can take a few blocks to land).");
3519
+ return 0;
3520
+ } catch (err) {
3521
+ return emitCliError(io2, json, "fund", err);
3522
+ }
3523
+ }
3524
+
1431
3525
  // src/cli/git-passthrough.ts
1432
3526
  import { spawn } from "child_process";
1433
3527
  import { constants as osConstants } from "os";
@@ -1437,11 +3531,11 @@ function signalExitCode(signal) {
1437
3531
  const num = osConstants.signals[signal];
1438
3532
  return num === void 0 ? 1 : 128 + num;
1439
3533
  }
1440
- var runGitPassthrough = (argv, options = {}) => {
3534
+ var runGitPassthrough = (argv2, options = {}) => {
1441
3535
  const err = options.err ?? ((line) => process.stderr.write(`${line}
1442
3536
  `));
1443
- return new Promise((resolve) => {
1444
- const child = spawn("git", argv, {
3537
+ return new Promise((resolve2) => {
3538
+ const child = spawn("git", argv2, {
1445
3539
  stdio: "inherit",
1446
3540
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1447
3541
  env: options.env ?? process.env
@@ -1461,24 +3555,24 @@ var runGitPassthrough = (argv, options = {}) => {
1461
3555
  restoreSignals();
1462
3556
  if (spawnErr.code === "ENOENT") {
1463
3557
  err(
1464
- `rig: git not found \u2014 \`rig ${argv[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.`
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.`
1465
3559
  );
1466
- resolve(GIT_NOT_FOUND_EXIT);
3560
+ resolve2(GIT_NOT_FOUND_EXIT);
1467
3561
  return;
1468
3562
  }
1469
3563
  err(`rig: failed to run git: ${spawnErr.message}`);
1470
- resolve(1);
3564
+ resolve2(1);
1471
3565
  });
1472
3566
  child.on("close", (code, signal) => {
1473
3567
  restoreSignals();
1474
- resolve(signal !== null ? signalExitCode(signal) : code ?? 1);
3568
+ resolve2(signal !== null ? signalExitCode(signal) : code ?? 1);
1475
3569
  });
1476
3570
  });
1477
3571
  };
1478
3572
 
1479
3573
  // src/cli/init.ts
1480
- import { basename } from "path";
1481
- import { parseArgs as parseArgs4 } from "util";
3574
+ import { basename as basename2 } from "path";
3575
+ import { parseArgs as parseArgs10 } from "util";
1482
3576
  var INIT_USAGE = `Usage: rig init [options]
1483
3577
 
1484
3578
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -1498,7 +3592,7 @@ Options:
1498
3592
  --json machine-readable report
1499
3593
  -h, --help show this help`;
1500
3594
  function parseInitArgs(args) {
1501
- const { values, positionals } = parseArgs4({
3595
+ const { values, positionals } = parseArgs10({
1502
3596
  args,
1503
3597
  options: {
1504
3598
  "repo-id": { type: "string" },
@@ -1522,17 +3616,17 @@ function parseInitArgs(args) {
1522
3616
  return flags;
1523
3617
  }
1524
3618
  async function runInit(args, deps) {
1525
- const { io, env } = deps;
3619
+ const { io: io2, env } = deps;
1526
3620
  let flags;
1527
3621
  try {
1528
3622
  flags = parseInitArgs(args);
1529
3623
  } catch (err) {
1530
- io.err(err instanceof Error ? err.message : String(err));
1531
- io.err(INIT_USAGE);
3624
+ io2.err(err instanceof Error ? err.message : String(err));
3625
+ io2.err(INIT_USAGE);
1532
3626
  return 2;
1533
3627
  }
1534
3628
  if (flags.help) {
1535
- io.out(INIT_USAGE);
3629
+ io2.out(INIT_USAGE);
1536
3630
  return 0;
1537
3631
  }
1538
3632
  try {
@@ -1545,10 +3639,10 @@ async function runInit(args, deps) {
1545
3639
  const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
1546
3640
  env,
1547
3641
  cwd: deps.cwd,
1548
- warn: (line) => io.err(line)
3642
+ warn: (line) => io2.err(line)
1549
3643
  });
1550
3644
  const existing = await readToonConfig(repoRoot);
1551
- const repoId = flags.repoId ?? existing.repoId ?? basename(repoRoot);
3645
+ const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
1552
3646
  const changed = {
1553
3647
  repoId: existing.repoId !== repoId,
1554
3648
  owner: existing.owner !== identity.pubkey
@@ -1582,50 +3676,44 @@ async function runInit(args, deps) {
1582
3676
  migratedToonRelay,
1583
3677
  changed
1584
3678
  };
1585
- io.out(JSON.stringify(output, null, 2));
3679
+ io2.emitJson(output);
1586
3680
  return 0;
1587
3681
  }
1588
- io.out(`Initialized rig for ${repoRoot}`);
1589
- io.out(renderIdentityLine(identity));
1590
- io.out(
3682
+ io2.out(`Initialized rig for ${repoRoot}`);
3683
+ io2.out(renderIdentityLine(identity));
3684
+ io2.out(
1591
3685
  ` toon.repoid = ${repoId}` + (changed.repoId ? "" : " (unchanged)") + (existing.repoId && changed.repoId ? ` (was ${existing.repoId})` : "")
1592
3686
  );
1593
- io.out(
3687
+ io2.out(
1594
3688
  ` toon.owner = ${identity.pubkey}` + (changed.owner ? "" : " (unchanged)") + (existing.owner && changed.owner ? ` (was ${existing.owner})` : "")
1595
3689
  );
1596
3690
  if (originRelay !== void 0) {
1597
- io.out(
3691
+ io2.out(
1598
3692
  ` origin = ${originRelay}` + (migratedToonRelay ? " (migrated from git config toon.relay)" : "")
1599
3693
  );
1600
3694
  if (migratedToonRelay) {
1601
- io.out(
3695
+ io2.out(
1602
3696
  "note: toon.relay is deprecated \u2014 it stays readable as a fallback and is removed in v0.3; drop it now with `git config --unset-all toon.relay`."
1603
3697
  );
1604
3698
  }
1605
- io.out('Ready: `rig push` publishes this repo via remote "origin".');
3699
+ io2.out('Ready: `rig push` publishes this repo via remote "origin".');
1606
3700
  } else if (existing.relays.length > 0) {
1607
- io.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
1608
- io.out(
3701
+ io2.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
3702
+ io2.out(
1609
3703
  origin !== void 0 ? `The "origin" remote (${origin.urls.join(", ")}) is not a single relay URL, so toon.relay stays the fallback \u2014 add the relay under another name (\`rig remote add toon <relay-url>\`) and push with \`rig push toon\`.` : existing.relays.length > 1 ? `toon.relay has ${existing.relays.length} values and rig publishes to one relay \u2014 migrate the right one: \`rig remote add origin <relay-url>\`.` : "This value is not a relay URL (ws://, wss://, http://, or https://) \u2014 set a real one: `rig remote add origin <relay-url>`."
1610
3704
  );
1611
3705
  } else if (origin !== void 0) {
1612
- io.out(
3706
+ io2.out(
1613
3707
  `No relay configured yet \u2014 "origin" (${origin.urls.join(", ")}) is not a relay URL, so add the relay under another name: \`rig remote add toon <relay-url>\`, then \`rig push toon\`.`
1614
3708
  );
1615
3709
  } else {
1616
- io.out(
3710
+ io2.out(
1617
3711
  "No relay configured yet \u2014 add one as the follow-up step: `rig remote add origin <relay-url>` (a real git remote; `git remote -v` shows it). One-off pushes can pass --relay <url>."
1618
3712
  );
1619
3713
  }
1620
3714
  return 0;
1621
3715
  } catch (err) {
1622
- const described = describeError(err, "init");
1623
- if (flags.json) {
1624
- io.out(JSON.stringify({ command: "init", ...described.json }, null, 2));
1625
- } else {
1626
- for (const line of described.lines) io.err(line);
1627
- }
1628
- return 1;
3716
+ return emitCliError(io2, flags.json, "init", err);
1629
3717
  }
1630
3718
  }
1631
3719
 
@@ -1640,26 +3728,58 @@ Commands rig owns:
1640
3728
  remote add <name> <url> add a relay as a REAL git remote ("origin" is
1641
3729
  remote remove <name> the default publish target); remove/list manage
1642
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)
1643
3738
  push [remote] [refspecs...] plan, price, confirm, and execute a paid push
1644
3739
  to TOON (defaults to the "origin" remote). rig
1645
3740
  push is the TOON transport and shadows git push;
1646
3741
  plain-git pushes remain available by running
1647
3742
  \`git push\` directly
1648
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)
1649
3747
  comment <root-event-id> comment (kind:1622) on an issue or patch
1650
3748
  pr create publish a patch (kind:1617) with real
1651
3749
  \`git format-patch\` content
1652
3750
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
1653
3751
  open | applied | closed | draft
3752
+ fund drip devnet faucet funds to this identity's
3753
+ wallet (free); on other networks prints the
3754
+ address(es) to fund externally
3755
+ balance wallet balances + payment-channel holdings
3756
+ (free \u2014 chain reads and local state only)
3757
+ channel list show the payment channels paid commands hold
3758
+ (free \u2014 reads local state)
3759
+ channel open explicitly open (or resume) the channel for a
3760
+ peer; --deposit adds collateral (on-chain)
3761
+ channel close <channelId> start the settlement challenge window (on-chain)
3762
+ channel settle <channelId> release collateral after the window (on-chain)
1654
3763
 
1655
3764
  Any other command is passed through to git verbatim: \`rig status\` runs
1656
3765
  \`git status\`, and \`rig add -p\`, \`rig commit\`, \`rig log --oneline\`,
1657
3766
  \`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
1658
3767
 
1659
- Run \`rig <command> --help\` for a rig command's flags. \`rig init\` and
1660
- \`rig remote\` are free; the other rig commands are paid writes \u2014 permanent
1661
- and non-refundable; each quotes its fee and asks for confirmation before
1662
- spending (--yes skips, --json emits machine output).`;
3768
+ Run \`rig <command> --help\` for a rig command's flags. \`rig init\`,
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
3773
+ and channel open/close/settle are on-chain wallet transactions; each states
3774
+ what it will spend and asks for confirmation before doing so (--yes skips,
3775
+ --json emits machine output).
3776
+
3777
+ With --json, stdout carries exactly ONE JSON document (everything human-facing
3778
+ goes to stderr), so \`rig <command> --json | jq\` always parses. --json is a
3779
+ per-subcommand flag on the commands rig owns, NOT a global rig flag: it does
3780
+ not apply to the git passthrough (\`rig status --json\` runs
3781
+ \`git status --json\`), and flags placed before the subcommand
3782
+ (\`rig --json status\`) pass through to git untouched.`;
1663
3783
  function rigVersion() {
1664
3784
  const require2 = createRequire(import.meta.url);
1665
3785
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
@@ -1671,14 +3791,19 @@ function rigVersion() {
1671
3791
  }
1672
3792
  return "unknown";
1673
3793
  }
1674
- async function dispatch(argv, deps) {
1675
- const [command, ...rest] = argv;
1676
- const { io } = deps;
3794
+ async function dispatch(argv2, deps) {
3795
+ const [command, ...rest] = argv2;
3796
+ const { io: io2 } = deps;
1677
3797
  switch (command) {
1678
3798
  case "init":
1679
3799
  return runInit(rest, deps);
1680
3800
  case "remote":
1681
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);
1682
3807
  case "push":
1683
3808
  return runPush(rest, deps);
1684
3809
  case "issue":
@@ -1687,35 +3812,150 @@ async function dispatch(argv, deps) {
1687
3812
  return runComment(rest, deps);
1688
3813
  case "pr":
1689
3814
  return runPr(rest, deps);
3815
+ case "channel":
3816
+ return runChannel(rest, deps);
3817
+ case "fund":
3818
+ return runFund(rest, deps);
3819
+ case "balance":
3820
+ return runBalance(rest, deps);
1690
3821
  case "help":
1691
3822
  case "--help":
1692
3823
  case "-h":
1693
- io.out(USAGE);
1694
- io.out("");
1695
- io.out(PUSH_USAGE);
3824
+ io2.out(USAGE);
3825
+ io2.out("");
3826
+ io2.out(PUSH_USAGE);
1696
3827
  return 0;
1697
3828
  case "--version":
1698
- io.out(`rig ${rigVersion()}`);
3829
+ io2.out(`rig ${rigVersion()}`);
1699
3830
  return 0;
1700
3831
  case void 0:
1701
- io.err(USAGE);
3832
+ io2.err(USAGE);
1702
3833
  return 2;
1703
3834
  default:
1704
- return (deps.runGit ?? runGitPassthrough)(argv, {
3835
+ return (deps.runGit ?? runGitPassthrough)(argv2, {
1705
3836
  cwd: deps.cwd,
1706
3837
  env: deps.env,
1707
- err: (line) => io.err(line)
3838
+ err: (line) => io2.err(line)
1708
3839
  });
1709
3840
  }
1710
3841
  }
1711
3842
 
1712
- // src/cli/rig.ts
1713
- function makeIo() {
3843
+ // src/cli/output.ts
3844
+ function makeCliIo(options) {
3845
+ const { jsonMode, writeStdout, writeStderr } = options;
3846
+ let emittedJson = false;
3847
+ const humanLines = [];
3848
+ const toStderr = (line) => {
3849
+ if (jsonMode) humanLines.push(line);
3850
+ writeStderr(`${line}
3851
+ `);
3852
+ };
3853
+ return {
3854
+ jsonMode,
3855
+ get emittedJson() {
3856
+ return emittedJson;
3857
+ },
3858
+ out: (line) => {
3859
+ if (jsonMode) {
3860
+ toStderr(line);
3861
+ } else {
3862
+ writeStdout(`${line}
3863
+ `);
3864
+ }
3865
+ },
3866
+ err: toStderr,
3867
+ emitJson: (payload) => {
3868
+ emittedJson = true;
3869
+ writeStdout(`${JSON.stringify(payload, null, 2)}
3870
+ `);
3871
+ },
3872
+ isInteractive: options.isInteractive,
3873
+ confirm: options.confirm,
3874
+ ensureSingleJsonDoc(exitCode) {
3875
+ if (!jsonMode || emittedJson) return;
3876
+ const detail = humanLines.join("\n") || (exitCode === 0 ? "the command produced no machine output" : "the command failed before emitting machine output \u2014 see stderr");
3877
+ this.emitJson(
3878
+ exitCode === 0 ? { error: null, exitCode, detail } : { error: "error", exitCode, detail }
3879
+ );
3880
+ }
3881
+ };
3882
+ }
3883
+ var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
3884
+ "init",
3885
+ "remote",
3886
+ "clone",
3887
+ "fetch",
3888
+ "push",
3889
+ "issue",
3890
+ "comment",
3891
+ "pr",
3892
+ "channel",
3893
+ "fund",
3894
+ "balance"
3895
+ ]);
3896
+ function isJsonInvocation(argv2) {
3897
+ const [verb, ...rest] = argv2;
3898
+ if (verb === void 0 || !RIG_OWNED_VERBS.has(verb)) return false;
3899
+ for (const arg of rest) {
3900
+ if (arg === "--") return false;
3901
+ if (arg === "--json") return true;
3902
+ }
3903
+ return false;
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
+ }
3928
+ function redirectStdoutToStderr(realWrite) {
3929
+ const original = process.stdout.write;
3930
+ const real = realWrite ?? original.bind(process.stdout);
3931
+ const patched = ((chunk, encodingOrCb, cb) => typeof encodingOrCb === "function" ? process.stderr.write(chunk, encodingOrCb) : process.stderr.write(
3932
+ chunk,
3933
+ encodingOrCb,
3934
+ cb
3935
+ ));
3936
+ process.stdout.write = patched;
1714
3937
  return {
1715
- out: (line) => process.stdout.write(`${line}
1716
- `),
1717
- err: (line) => process.stderr.write(`${line}
1718
- `),
3938
+ write: (text) => {
3939
+ real(text);
3940
+ },
3941
+ restore: () => {
3942
+ if (process.stdout.write === patched) process.stdout.write = original;
3943
+ }
3944
+ };
3945
+ }
3946
+
3947
+ // src/cli/rig.ts
3948
+ function makeIo(jsonMode) {
3949
+ calmBootstrapNoise();
3950
+ const guard = jsonMode ? redirectStdoutToStderr() : void 0;
3951
+ return makeCliIo({
3952
+ jsonMode,
3953
+ writeStdout: guard ? guard.write : (text) => {
3954
+ process.stdout.write(text);
3955
+ },
3956
+ writeStderr: (text) => {
3957
+ process.stderr.write(text);
3958
+ },
1719
3959
  isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1720
3960
  confirm: async (question) => {
1721
3961
  const rl = createInterface({
@@ -1729,22 +3969,36 @@ function makeIo() {
1729
3969
  rl.close();
1730
3970
  }
1731
3971
  }
3972
+ });
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);
1732
3980
  };
3981
+ process.stdout.write("", done);
3982
+ process.stderr.write("", done);
1733
3983
  }
1734
- dispatch(process.argv.slice(2), {
1735
- io: makeIo(),
3984
+ var argv = process.argv.slice(2);
3985
+ var io = makeIo(isJsonInvocation(argv));
3986
+ dispatch(argv, {
3987
+ io,
1736
3988
  env: process.env,
1737
3989
  cwd: process.cwd()
1738
3990
  }).then(
1739
3991
  (code) => {
1740
- process.exitCode = code;
3992
+ io.ensureSingleJsonDoc(code);
3993
+ exitWhenFlushed(code);
1741
3994
  },
1742
3995
  (err) => {
1743
3996
  process.stderr.write(
1744
3997
  `rig: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
1745
3998
  `
1746
3999
  );
1747
- process.exitCode = 1;
4000
+ io.ensureSingleJsonDoc(1);
4001
+ exitWhenFlushed(1);
1748
4002
  }
1749
4003
  );
1750
4004
  //# sourceMappingURL=rig.js.map