@toon-protocol/rig 2.0.1 → 2.1.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
@@ -20,6 +20,11 @@ import {
20
20
  buildPatch,
21
21
  buildStatus
22
22
  } from "../chunk-HPSOQP7Q.js";
23
+ import {
24
+ ChannelMapStore,
25
+ channelStatus,
26
+ resolveChannelPaths
27
+ } from "../chunk-O6TXHKWG.js";
23
28
  import {
24
29
  MAX_OBJECT_SIZE
25
30
  } from "../chunk-X2CZPPDM.js";
@@ -30,16 +35,8 @@ import { createInterface } from "readline/promises";
30
35
  // src/cli/dispatch.ts
31
36
  import { createRequire } from "module";
32
37
 
33
- // src/cli/events.ts
34
- import { readFile } from "fs/promises";
38
+ // src/cli/balance.ts
35
39
  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";
43
40
 
44
41
  // src/cli/errors.ts
45
42
  var UnconfiguredRepoAddressError = class extends Error {
@@ -221,12 +218,127 @@ function describeError(err, command = "push") {
221
218
  json: { error: "missing_uplink", detail: message }
222
219
  };
223
220
  }
221
+ if (name === "ChannelMapCorruptError") {
222
+ return {
223
+ code: "channel_map_corrupt",
224
+ lines: [message],
225
+ json: { error: "channel_map_corrupt", detail: message }
226
+ };
227
+ }
228
+ if (name === "SettleTooEarlyError") {
229
+ const settleableAt = err.settleableAt;
230
+ return {
231
+ code: "settle_too_early",
232
+ lines: [
233
+ message,
234
+ "The settlement challenge window is still open \u2014 nothing was spent. Re-run `rig channel settle` after the settleable time."
235
+ ],
236
+ json: {
237
+ error: "settle_too_early",
238
+ detail: message,
239
+ ...typeof settleableAt === "string" ? { settleableAt } : {}
240
+ }
241
+ };
242
+ }
224
243
  return {
225
244
  code: "error",
226
245
  lines: [`rig ${command} failed: ${message}`],
227
246
  json: { error: "error", detail: message }
228
247
  };
229
248
  }
249
+ function emitCliError(io2, json, command, err) {
250
+ const described = describeError(err, command);
251
+ if (json) io2.emitJson({ command, ...described.json });
252
+ for (const line of described.lines) io2.err(line);
253
+ return 1;
254
+ }
255
+
256
+ // src/cli/push.ts
257
+ import { parseArgs as parseArgs2 } from "util";
258
+
259
+ // src/cli/render.ts
260
+ function formatNumber(value) {
261
+ return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
262
+ }
263
+ function shortSha(sha) {
264
+ return sha ? sha.slice(0, 7) : "(none)";
265
+ }
266
+ function refLine(update) {
267
+ const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
268
+ return ` ${update.refname} ${arrow} (${update.kind})`;
269
+ }
270
+ function renderIdentityLine(identity) {
271
+ return `Identity: ${identity.pubkey} (from ${identity.sourceLabel})`;
272
+ }
273
+ function renderPlan(plan) {
274
+ const lines = [];
275
+ lines.push(
276
+ `Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
277
+ );
278
+ lines.push("Refs:");
279
+ for (const update of plan.refUpdates) lines.push(refLine(update));
280
+ const est = plan.estimate;
281
+ const skipped = Object.keys(plan.knownShaToTxId).length;
282
+ lines.push(
283
+ `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
284
+ );
285
+ lines.push("Fees (base units):");
286
+ lines.push(
287
+ ` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
288
+ );
289
+ lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
290
+ lines.push(` total ${formatNumber(est.totalFee)}`);
291
+ lines.push("Writes are permanent and non-refundable.");
292
+ return lines;
293
+ }
294
+ function feeLabel(fee) {
295
+ return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
296
+ }
297
+ function renderEventPlan(opts) {
298
+ return [
299
+ `Publish ${opts.action}`,
300
+ `Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
301
+ renderIdentityLine(opts.identity),
302
+ `Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
303
+ ];
304
+ }
305
+ function renderEventReceipt(action, result) {
306
+ const lines = [
307
+ `Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
308
+ ];
309
+ if (result.channelBalanceAfter !== void 0) {
310
+ lines.push(
311
+ `Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
312
+ );
313
+ }
314
+ return lines;
315
+ }
316
+ function renderResult(result) {
317
+ const lines = [];
318
+ lines.push(`Pushed "${result.repoId}":`);
319
+ const paid = result.uploads.filter((u) => !u.skipped);
320
+ const skipped = result.uploads.filter((u) => u.skipped);
321
+ for (const upload of result.uploads) {
322
+ lines.push(
323
+ ` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
324
+ );
325
+ }
326
+ lines.push(
327
+ `Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
328
+ );
329
+ if (result.announceReceipt) {
330
+ lines.push(
331
+ `Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
332
+ );
333
+ }
334
+ lines.push(
335
+ `Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
336
+ );
337
+ lines.push(
338
+ `Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
339
+ );
340
+ return lines;
341
+ }
230
342
 
231
343
  // src/cli/git-config.ts
232
344
  import { execFile } from "child_process";
@@ -320,93 +432,6 @@ async function writeToonConfig(repoPath, config) {
320
432
  }
321
433
  }
322
434
 
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, ",");
329
- }
330
- function shortSha(sha) {
331
- return sha ? sha.slice(0, 7) : "(none)";
332
- }
333
- function refLine(update) {
334
- const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
335
- return ` ${update.refname} ${arrow} (${update.kind})`;
336
- }
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
435
  // src/cli/remote.ts
411
436
  import { parseArgs } from "util";
412
437
  var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
@@ -471,10 +496,10 @@ Commands:
471
496
  list list remote names + URLs (the default subcommand)
472
497
 
473
498
  Options:
474
- --json machine-readable output (list)
499
+ --json machine-readable envelope (all subcommands)
475
500
  -h, --help show this help`;
476
501
  async function runRemote(args, deps) {
477
- const { io } = deps;
502
+ const { io: io2 } = deps;
478
503
  let sub;
479
504
  let rest;
480
505
  let json;
@@ -488,34 +513,34 @@ async function runRemote(args, deps) {
488
513
  allowPositionals: true
489
514
  });
490
515
  if (values.help) {
491
- io.out(REMOTE_USAGE);
516
+ io2.out(REMOTE_USAGE);
492
517
  return 0;
493
518
  }
494
519
  json = values.json ?? false;
495
520
  [sub, ...rest] = positionals;
496
521
  } catch (err) {
497
- io.err(err instanceof Error ? err.message : String(err));
498
- io.err(REMOTE_USAGE);
522
+ io2.err(err instanceof Error ? err.message : String(err));
523
+ io2.err(REMOTE_USAGE);
499
524
  return 2;
500
525
  }
501
526
  switch (sub) {
502
527
  case void 0:
503
528
  case "list":
504
529
  if (rest.length > 0) {
505
- io.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
506
- io.err(REMOTE_USAGE);
530
+ io2.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
531
+ io2.err(REMOTE_USAGE);
507
532
  return 2;
508
533
  }
509
534
  break;
510
535
  case "add": {
511
536
  if (rest.length !== 2) {
512
- io.err("usage: rig remote add <name> <relay-url>");
513
- io.err(REMOTE_USAGE);
537
+ io2.err("usage: rig remote add <name> <relay-url>");
538
+ io2.err(REMOTE_USAGE);
514
539
  return 2;
515
540
  }
516
541
  const url = rest[1];
517
542
  if (!isRelayUrl(url)) {
518
- io.err(
543
+ io2.err(
519
544
  `cannot add remote: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
520
545
  );
521
546
  return 2;
@@ -524,14 +549,14 @@ async function runRemote(args, deps) {
524
549
  }
525
550
  case "remove":
526
551
  if (rest.length !== 1) {
527
- io.err("usage: rig remote remove <name>");
528
- io.err(REMOTE_USAGE);
552
+ io2.err("usage: rig remote remove <name>");
553
+ io2.err(REMOTE_USAGE);
529
554
  return 2;
530
555
  }
531
556
  break;
532
557
  default:
533
- io.err(`unknown rig remote subcommand: ${sub}`);
534
- io.err(REMOTE_USAGE);
558
+ io2.err(`unknown rig remote subcommand: ${sub}`);
559
+ io2.err(REMOTE_USAGE);
535
560
  return 2;
536
561
  }
537
562
  try {
@@ -546,23 +571,23 @@ async function runRemote(args, deps) {
546
571
  case "list": {
547
572
  const remotes = await listGitRemotes(repoRoot);
548
573
  if (json) {
549
- io.out(JSON.stringify({ command: "remote", remotes }, null, 2));
574
+ io2.emitJson({ command: "remote", remotes });
550
575
  return 0;
551
576
  }
552
577
  if (remotes.length === 0) {
553
- io.out(
578
+ io2.out(
554
579
  "no remotes configured \u2014 add one: rig remote add origin <relay-url>"
555
580
  );
556
581
  return 0;
557
582
  }
558
583
  for (const remote of remotes) {
559
584
  for (const url of remote.urls) {
560
- io.out(
585
+ io2.out(
561
586
  `${remote.name} ${url}` + (isRelayUrl(url) ? "" : " (not a relay URL \u2014 ignored by rig)")
562
587
  );
563
588
  }
564
589
  if (remote.urls.length > 1) {
565
- io.err(
590
+ io2.err(
566
591
  `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
592
  );
568
593
  }
@@ -573,37 +598,61 @@ async function runRemote(args, deps) {
573
598
  const [name, url] = rest;
574
599
  const existing = await getGitRemoteUrls(repoRoot, name);
575
600
  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
- );
601
+ 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.`;
602
+ if (json) {
603
+ io2.emitJson({
604
+ command: "remote",
605
+ error: "remote_exists",
606
+ detail,
607
+ remote: name,
608
+ urls: existing
609
+ });
610
+ }
611
+ io2.err(detail);
579
612
  return 1;
580
613
  }
581
614
  await addGitRemote(repoRoot, name, url);
582
- io.out(`Added remote ${name} \u2192 ${url}`);
615
+ if (!json) io2.out(`Added remote ${name} \u2192 ${url}`);
583
616
  if (name === "origin") {
584
- io.out(
585
- "`rig push` and the event commands now publish here by default."
586
- );
617
+ if (!json) {
618
+ io2.out(
619
+ "`rig push` and the event commands now publish here by default."
620
+ );
621
+ }
587
622
  const toonConfig = await readToonConfig(repoRoot);
588
623
  if (toonConfig.relays.length > 0) {
589
- io.err(
624
+ io2.err(
590
625
  `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
626
  );
592
627
  }
593
628
  }
629
+ if (json) {
630
+ io2.emitJson({ command: "remote", action: "add", name, url });
631
+ }
594
632
  return 0;
595
633
  }
596
634
  case "remove": {
597
635
  const name = rest[0];
598
636
  const existing = await getGitRemoteUrls(repoRoot, name);
599
637
  if (existing.length === 0) {
600
- io.err(
601
- `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`
602
- );
638
+ const detail = `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`;
639
+ if (json) {
640
+ io2.emitJson({
641
+ command: "remote",
642
+ error: "unknown_remote",
643
+ detail,
644
+ remote: name
645
+ });
646
+ }
647
+ io2.err(detail);
603
648
  return 1;
604
649
  }
605
650
  await removeGitRemote(repoRoot, name);
606
- io.out(`Removed remote ${name}`);
651
+ if (json) {
652
+ io2.emitJson({ command: "remote", action: "remove", name });
653
+ } else {
654
+ io2.out(`Removed remote ${name}`);
655
+ }
607
656
  return 0;
608
657
  }
609
658
  /* v8 ignore next 2 -- unreachable: validated above */
@@ -611,19 +660,13 @@ async function runRemote(args, deps) {
611
660
  return 2;
612
661
  }
613
662
  } 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;
663
+ return emitCliError(io2, json, "remote", err);
621
664
  }
622
665
  }
623
666
 
624
667
  // src/cli/push.ts
625
668
  var defaultLoadStandalone = async (options) => {
626
- const mod = await import("../standalone-mode-Q3FFZUXE.js");
669
+ const mod = await import("../standalone-mode-FCKTQ33Y.js");
627
670
  return mod.createStandaloneContext(options);
628
671
  };
629
672
  function identityReport(ctx) {
@@ -734,172 +777,758 @@ function parsePushArgs(args) {
734
777
  return flags;
735
778
  }
736
779
  async function runPush(args, deps) {
737
- const { io, env } = deps;
780
+ const { io: io2, env } = deps;
738
781
  let flags;
739
782
  try {
740
783
  flags = parsePushArgs(args);
741
784
  } catch (err) {
742
- io.err(err instanceof Error ? err.message : String(err));
743
- io.err(PUSH_USAGE);
785
+ io2.err(err instanceof Error ? err.message : String(err));
786
+ io2.err(PUSH_USAGE);
787
+ return 2;
788
+ }
789
+ if (flags.help) {
790
+ io2.out(PUSH_USAGE);
791
+ return 0;
792
+ }
793
+ let standaloneCtx;
794
+ try {
795
+ const repoRoot = await resolveRepoRoot(deps.cwd);
796
+ const toonConfig = await readToonConfig(repoRoot);
797
+ const repoId = flags.repoId ?? toonConfig.repoId;
798
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
799
+ const reader = new GitRepoReader(repoRoot);
800
+ let remoteName;
801
+ let refspecArgs = flags.positionals;
802
+ if (flags.relay.length === 0 && refspecArgs.length > 0) {
803
+ const first = refspecArgs[0];
804
+ const remoteNames = new Set(
805
+ (await listGitRemotes(repoRoot)).map((r) => r.name)
806
+ );
807
+ if (remoteNames.has(first)) {
808
+ remoteName = first;
809
+ refspecArgs = refspecArgs.slice(1);
810
+ } else {
811
+ const { refs } = await reader.listRefs();
812
+ const known = new Set(refs.map((r) => r.refname));
813
+ if (!known.has(first) && !known.has(`refs/heads/${first}`) && !known.has(`refs/tags/${first}`)) {
814
+ throw new Error(
815
+ `${JSON.stringify(first)} is neither a configured remote nor a local branch/tag \u2014 add the relay remote (\`rig remote add ${first} <relay-url>\`; \`rig remote list\` shows configured remotes) or fix the refspec`
816
+ );
817
+ }
818
+ }
819
+ }
820
+ const refspecs = await selectRefspecs(
821
+ reader,
822
+ refspecArgs,
823
+ flags.all,
824
+ flags.tags
825
+ );
826
+ const resolved = await resolveRelays({
827
+ relayFlags: flags.relay,
828
+ remoteName,
829
+ repoRoot,
830
+ toonRelays: toonConfig.relays
831
+ });
832
+ if (resolved.nudge !== void 0) io2.err(resolved.nudge);
833
+ const relaysUsed = resolved.relays;
834
+ if (relaysUsed.length > 1) {
835
+ io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
836
+ return 1;
837
+ }
838
+ standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
839
+ env,
840
+ cwd: deps.cwd,
841
+ warn: (line) => io2.err(line),
842
+ // Relay-origin for #264 network bootstrap (announce discovery).
843
+ ...relaysUsed[0] !== void 0 ? { relayUrl: relaysUsed[0] } : {}
844
+ });
845
+ const identity = identityReport(standaloneCtx);
846
+ if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
847
+ io2.err(
848
+ `warning: git config toon.owner (${toonConfig.owner.slice(0, 8)}\u2026) differs from the active identity (${identity.pubkey.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's. Re-run \`rig init\` to adopt the active identity.`
849
+ );
850
+ }
851
+ const remoteState = await standaloneCtx.fetchRemote({
852
+ ownerPubkey: standaloneCtx.ownerPubkey,
853
+ repoId,
854
+ relayUrls: relaysUsed
855
+ });
856
+ const feeRates = await standaloneCtx.publisher.getFeeRates();
857
+ const pushPlan = await planPush({
858
+ repoReader: reader,
859
+ remoteState,
860
+ feeRates,
861
+ repoId,
862
+ refs: refspecs,
863
+ force: flags.force
864
+ });
865
+ const plan = serializePushPlan(pushPlan);
866
+ const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
867
+ if (upToDate) {
868
+ if (flags.json) {
869
+ io2.emitJson({
870
+ command: "push",
871
+ repoId,
872
+ identity,
873
+ executed: false,
874
+ upToDate: true,
875
+ plan
876
+ });
877
+ } else {
878
+ io2.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
879
+ }
880
+ return 0;
881
+ }
882
+ if (!flags.json) {
883
+ for (const line of renderPlan(plan)) io2.out(line);
884
+ io2.out(renderIdentityLine(identity));
885
+ }
886
+ if (!flags.yes) {
887
+ if (flags.json) {
888
+ io2.emitJson({
889
+ command: "push",
890
+ repoId,
891
+ identity,
892
+ executed: false,
893
+ upToDate: false,
894
+ plan,
895
+ hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
896
+ });
897
+ return 0;
898
+ }
899
+ if (!io2.isInteractive) {
900
+ io2.err(
901
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
902
+ );
903
+ return 1;
904
+ }
905
+ const proceed = await io2.confirm(
906
+ `Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
907
+ );
908
+ if (!proceed) {
909
+ io2.err("aborted \u2014 nothing was uploaded or published.");
910
+ return 1;
911
+ }
912
+ }
913
+ const pushResult = await executePush({
914
+ plan: pushPlan,
915
+ publisher: standaloneCtx.publisher,
916
+ remoteState,
917
+ repoReader: reader,
918
+ relayUrls: relaysUsed
919
+ });
920
+ const result = serializePushResult(pushPlan, pushResult);
921
+ if (flags.json) {
922
+ io2.emitJson({
923
+ command: "push",
924
+ repoId,
925
+ identity,
926
+ executed: true,
927
+ upToDate: false,
928
+ plan,
929
+ result
930
+ });
931
+ } else {
932
+ for (const line of renderResult(result)) io2.out(line);
933
+ }
934
+ return 0;
935
+ } catch (err) {
936
+ return emitCliError(io2, flags.json, "push", err);
937
+ } finally {
938
+ if (standaloneCtx) {
939
+ try {
940
+ await standaloneCtx.stop();
941
+ } catch {
942
+ }
943
+ }
944
+ }
945
+ }
946
+
947
+ // src/cli/balance.ts
948
+ var BALANCE_USAGE = `Usage: rig balance [--json]
949
+
950
+ Show the active identity's money: on-chain wallet balances (per configured
951
+ chain, read from the settlement chain the payment channels actually use) and
952
+ recorded payment-channel holdings (deposited / claimed / available). Free \u2014
953
+ reads chain RPCs and local state only; nothing is signed or paid.
954
+
955
+ Options:
956
+ --json machine-readable envelope (base units as strings)
957
+ -h, --help show this help`;
958
+ async function runBalance(args, deps) {
959
+ const { io: io2, env } = deps;
960
+ let json = false;
961
+ try {
962
+ const { values } = parseArgs3({
963
+ args,
964
+ options: {
965
+ json: { type: "boolean", default: false },
966
+ help: { type: "boolean", short: "h", default: false }
967
+ }
968
+ });
969
+ if (values.help) {
970
+ io2.out(BALANCE_USAGE);
971
+ return 0;
972
+ }
973
+ json = values.json ?? false;
974
+ } catch (err) {
975
+ io2.err(err instanceof Error ? err.message : String(err));
976
+ io2.err(BALANCE_USAGE);
977
+ return 2;
978
+ }
979
+ let ctx;
980
+ try {
981
+ ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
982
+ env,
983
+ cwd: deps.cwd,
984
+ warn: (line) => io2.err(line),
985
+ // Free read: works without a proxy/BTP write uplink.
986
+ requireUplink: false
987
+ });
988
+ const identity = identityReport(ctx);
989
+ const wallet = ctx.money ? await ctx.money.walletBalances() : [];
990
+ const store = new ChannelMapStore(resolveChannelPaths(env));
991
+ const channels = store.list().filter((record) => record.identity === identity.pubkey).map((record) => {
992
+ const watermark = store.readWatermark(record.channelId);
993
+ const deposited = record.depositTotal;
994
+ const claimed = watermark?.cumulativeAmount;
995
+ let available = null;
996
+ if (deposited !== void 0 && claimed !== void 0) {
997
+ const remaining = BigInt(deposited) - BigInt(claimed);
998
+ available = (remaining > 0n ? remaining : 0n).toString();
999
+ }
1000
+ return {
1001
+ channelId: record.channelId,
1002
+ destination: record.destination,
1003
+ peerId: record.peerId,
1004
+ chain: record.chain,
1005
+ status: channelStatus(watermark),
1006
+ depositTotal: deposited ?? null,
1007
+ cumulativeClaimed: claimed ?? null,
1008
+ nonce: watermark?.nonce ?? null,
1009
+ available
1010
+ };
1011
+ });
1012
+ if (json) {
1013
+ io2.emitJson({
1014
+ command: "balance",
1015
+ identity,
1016
+ wallet,
1017
+ channels
1018
+ });
1019
+ return 0;
1020
+ }
1021
+ io2.out(renderIdentityLine(identity));
1022
+ io2.out("");
1023
+ io2.out("Wallet (on-chain):");
1024
+ if (wallet.length === 0) {
1025
+ io2.out(
1026
+ " (no balance readable \u2014 no chain configured, the RPC is unreachable, or the chain's keys derive only during a client start)"
1027
+ );
1028
+ }
1029
+ for (const balance of wallet) {
1030
+ io2.out(
1031
+ ` ${balance.chain.padEnd(7)} ${balance.address} ${balance.amount}` + (balance.asset ? ` ${balance.asset}` : " base units") + (balance.assetScale !== void 0 ? ` (scale ${balance.assetScale})` : "")
1032
+ );
1033
+ }
1034
+ io2.out("");
1035
+ io2.out("Channels (recorded):");
1036
+ if (channels.length === 0) {
1037
+ io2.out(
1038
+ " none \u2014 paid commands record their channel on first use; `rig channel open` records one explicitly."
1039
+ );
1040
+ }
1041
+ for (const c of channels) {
1042
+ io2.out(
1043
+ ` ${c.channelId} [${c.status}] deposited ${c.depositTotal ?? "?"} claimed ${c.cumulativeClaimed ?? "?"} available ${c.available ?? "?"} (${c.chain} \u2192 ${c.destination})`
1044
+ );
1045
+ }
1046
+ return 0;
1047
+ } catch (err) {
1048
+ return emitCliError(io2, json, "balance", err);
1049
+ } finally {
1050
+ if (ctx) {
1051
+ try {
1052
+ await ctx.stop();
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+
1059
+ // src/cli/channel.ts
1060
+ import { parseArgs as parseArgs4 } from "util";
1061
+ var CHANNEL_USAGE = `Usage: rig channel <subcommand>
1062
+
1063
+ Manage the payment channels paid rig commands hold with relay/store peers.
1064
+ Paid commands open a channel lazily on first use and record it under
1065
+ TOON_CLIENT_HOME (default ~/.toon-client, rig-channels.json), so later
1066
+ invocations resume the same channel instead of opening a new one per run.
1067
+
1068
+ Subcommands:
1069
+ list [--json] show recorded channels \u2014 peer, chain, channel id, deposit,
1070
+ cumulative claimed, status. Free: reads local state only.
1071
+
1072
+ open [--peer <ilp-destination>] [--deposit <base-units>]
1073
+ explicitly open the payment channel for a peer \u2014 the SAME
1074
+ path paid commands use lazily: resumes the recorded live
1075
+ channel if one exists (no on-chain spend), else opens and
1076
+ records a fresh one (locks the peer-negotiated initial
1077
+ deposit on-chain). --peer is the ILP destination to anchor
1078
+ to (default: the configured destination, e.g. the devnet
1079
+ apex); --deposit adds that much extra collateral after the
1080
+ open/resume. On-chain: asks for confirmation (--yes skips).
1081
+
1082
+ close <channelId>
1083
+ close a recorded channel \u2014 an on-chain tx that starts the
1084
+ settlement challenge window (the peer's settlementTimeout).
1085
+ The channel stops paying immediately; the remaining
1086
+ collateral stays locked until \`rig channel settle\` after
1087
+ the window elapses. Asks for confirmation (--yes skips).
1088
+
1089
+ settle <channelId>
1090
+ settle a closed channel once its challenge window elapsed \u2014
1091
+ an on-chain tx that releases the remaining collateral back
1092
+ to your wallet. Refused (without spending gas) while the
1093
+ window is still open. Asks for confirmation (--yes skips).
1094
+
1095
+ Common options: --json (machine-readable envelopes; without --yes lifecycle
1096
+ commands emit a pure plan and execute nothing), --yes, -h/--help.`;
1097
+ async function runChannel(args, deps) {
1098
+ const { io: io2 } = deps;
1099
+ const [sub, ...rest] = args;
1100
+ switch (sub) {
1101
+ case "list":
1102
+ return runChannelList(rest, deps);
1103
+ case "open":
1104
+ return runChannelOpen(rest, deps);
1105
+ case "close":
1106
+ return runChannelWithdrawStep(rest, deps, "close");
1107
+ case "settle":
1108
+ return runChannelWithdrawStep(rest, deps, "settle");
1109
+ case "help":
1110
+ case "--help":
1111
+ case "-h":
1112
+ io2.out(CHANNEL_USAGE);
1113
+ return 0;
1114
+ case void 0:
1115
+ io2.err(CHANNEL_USAGE);
1116
+ return 2;
1117
+ default:
1118
+ io2.err(`rig channel: unknown subcommand ${JSON.stringify(sub)}`);
1119
+ io2.err(CHANNEL_USAGE);
1120
+ return 2;
1121
+ }
1122
+ }
1123
+ function runChannelList(args, deps) {
1124
+ const { io: io2, env } = deps;
1125
+ let json = false;
1126
+ try {
1127
+ const { values } = parseArgs4({
1128
+ args,
1129
+ options: {
1130
+ json: { type: "boolean", default: false },
1131
+ help: { type: "boolean", short: "h", default: false }
1132
+ }
1133
+ });
1134
+ if (values.help) {
1135
+ io2.out(CHANNEL_USAGE);
1136
+ return 0;
1137
+ }
1138
+ json = values.json ?? false;
1139
+ } catch (err) {
1140
+ io2.err(err instanceof Error ? err.message : String(err));
1141
+ io2.err(CHANNEL_USAGE);
1142
+ return 2;
1143
+ }
1144
+ try {
1145
+ const paths = resolveChannelPaths(env);
1146
+ const store = new ChannelMapStore(paths);
1147
+ const records = store.list();
1148
+ const rows = records.map((record) => describeChannel(record, store));
1149
+ if (json) {
1150
+ io2.emitJson({ command: "channel list", channels: rows });
1151
+ return 0;
1152
+ }
1153
+ if (rows.length === 0) {
1154
+ io2.out(
1155
+ "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."
1156
+ );
1157
+ return 0;
1158
+ }
1159
+ io2.out(
1160
+ `${rows.length} payment channel${rows.length === 1 ? "" : "s"} recorded in ${paths.mapPath}:`
1161
+ );
1162
+ for (const row of rows) {
1163
+ io2.out("");
1164
+ for (const line of renderChannel(row)) io2.out(line);
1165
+ }
1166
+ return 0;
1167
+ } catch (err) {
1168
+ return emitCliError(io2, json, "channel list", err);
1169
+ }
1170
+ }
1171
+ function describeChannel(record, store) {
1172
+ const watermark = store.readWatermark(
1173
+ record.channelId
1174
+ );
1175
+ return {
1176
+ channelId: record.channelId,
1177
+ peerId: record.peerId,
1178
+ identity: record.identity,
1179
+ destination: record.destination,
1180
+ chain: record.chain,
1181
+ tokenNetwork: record.tokenNetwork,
1182
+ depositTotal: record.depositTotal ?? null,
1183
+ cumulativeClaimed: watermark?.cumulativeAmount ?? null,
1184
+ nonce: watermark?.nonce ?? null,
1185
+ status: channelStatus(watermark),
1186
+ openedAt: record.openedAt,
1187
+ lastUsedAt: record.lastUsedAt
1188
+ };
1189
+ }
1190
+ function renderChannel(row) {
1191
+ const claimed = row.cumulativeClaimed === null ? "unknown (no local claim state)" : `${row.cumulativeClaimed} base units (nonce ${row.nonce})`;
1192
+ return [
1193
+ `channel ${row.channelId} [${row.status}]`,
1194
+ ` peer ${row.destination} (${row.peerId})`,
1195
+ ` identity ${row.identity.slice(0, 8)}\u2026`,
1196
+ ` chain ${row.chain}` + (row.tokenNetwork ? ` token-network ${row.tokenNetwork}` : ""),
1197
+ ` deposited ${row.depositTotal ?? "unrecorded"}` + (row.depositTotal ? " base units" : ""),
1198
+ ` claimed ${claimed}`,
1199
+ ` opened ${row.openedAt} (last used ${row.lastUsedAt})`
1200
+ ];
1201
+ }
1202
+ async function loadMoneyContext(deps, options) {
1203
+ const ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1204
+ env: deps.env,
1205
+ cwd: deps.cwd,
1206
+ warn: (line) => deps.io.err(line),
1207
+ ...options?.channelDestination ? { channelDestination: options.channelDestination } : {}
1208
+ });
1209
+ const money = ctx.money;
1210
+ if (!money) {
1211
+ await ctx.stop().catch(() => void 0);
1212
+ throw new Error(
1213
+ "this standalone loader does not expose money operations \u2014 channel open/close/settle need the #263 loader"
1214
+ );
1215
+ }
1216
+ return { ctx, money };
1217
+ }
1218
+ async function confirmOnChain(io2, flags, question) {
1219
+ if (flags.yes) return "proceed";
1220
+ if (flags.json) return "json-plan";
1221
+ if (!io2.isInteractive) {
1222
+ io2.err(
1223
+ "refusing to move on-chain funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for a plan)"
1224
+ );
1225
+ return 1;
1226
+ }
1227
+ const proceed = await io2.confirm(question);
1228
+ if (proceed) return "proceed";
1229
+ io2.err("aborted \u2014 no on-chain transaction was sent.");
1230
+ return 1;
1231
+ }
1232
+ function formatUnixSeconds(value) {
1233
+ const ms = Number(value) * 1e3;
1234
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : `t=${value}s`;
1235
+ }
1236
+ function secondsUntil(value, nowSec) {
1237
+ return Number(value) - nowSec;
1238
+ }
1239
+ async function runChannelOpen(args, deps) {
1240
+ const { io: io2 } = deps;
1241
+ let peer;
1242
+ let deposit;
1243
+ let yes = false;
1244
+ let json = false;
1245
+ try {
1246
+ const { values } = parseArgs4({
1247
+ args,
1248
+ options: {
1249
+ peer: { type: "string" },
1250
+ deposit: { type: "string" },
1251
+ yes: { type: "boolean", default: false },
1252
+ json: { type: "boolean", default: false },
1253
+ help: { type: "boolean", short: "h", default: false }
1254
+ }
1255
+ });
1256
+ if (values.help) {
1257
+ io2.out(CHANNEL_USAGE);
1258
+ return 0;
1259
+ }
1260
+ peer = values.peer;
1261
+ yes = values.yes ?? false;
1262
+ json = values.json ?? false;
1263
+ if (values.deposit !== void 0) {
1264
+ if (!/^\d+$/.test(values.deposit) || BigInt(values.deposit) <= 0n) {
1265
+ throw new Error(
1266
+ `--deposit must be a positive base-unit integer, got ${JSON.stringify(values.deposit)}`
1267
+ );
1268
+ }
1269
+ deposit = BigInt(values.deposit);
1270
+ }
1271
+ } catch (err) {
1272
+ io2.err(err instanceof Error ? err.message : String(err));
1273
+ io2.err(CHANNEL_USAGE);
1274
+ return 2;
1275
+ }
1276
+ let ctx;
1277
+ try {
1278
+ const loaded = await loadMoneyContext(deps, {
1279
+ ...peer ? { channelDestination: peer } : {}
1280
+ });
1281
+ ctx = loaded.ctx;
1282
+ const identity = identityReport(ctx);
1283
+ const plan = {
1284
+ destination: peer ?? null,
1285
+ deposit: deposit?.toString() ?? null
1286
+ };
1287
+ if (!json) {
1288
+ io2.out("Channel open plan:");
1289
+ io2.out(` peer (ILP) ${peer ?? "(configured default destination)"}`);
1290
+ io2.out(
1291
+ ` deposit ${deposit !== void 0 ? `+${deposit} base units of extra collateral after the open/resume` : "none beyond the peer-negotiated initial deposit"}`
1292
+ );
1293
+ io2.out(renderIdentityLine(identity));
1294
+ io2.out(
1295
+ "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)."
1296
+ );
1297
+ }
1298
+ const gate = await confirmOnChain(
1299
+ io2,
1300
+ { yes, json },
1301
+ "Proceed with on-chain channel open? [y/N] "
1302
+ );
1303
+ if (gate === "json-plan") {
1304
+ io2.emitJson({
1305
+ command: "channel open",
1306
+ identity,
1307
+ executed: false,
1308
+ plan,
1309
+ hint: "plan only \u2014 re-run with --yes to open (on-chain: locks collateral and spends gas)"
1310
+ });
1311
+ return 0;
1312
+ }
1313
+ if (gate !== "proceed") return gate;
1314
+ const outcome = await loaded.money.openChannel(
1315
+ deposit !== void 0 ? { deposit } : void 0
1316
+ );
1317
+ if (json) {
1318
+ io2.emitJson({
1319
+ command: "channel open",
1320
+ identity,
1321
+ executed: true,
1322
+ plan,
1323
+ result: {
1324
+ channelId: outcome.channelId,
1325
+ resumed: outcome.resumed,
1326
+ destination: outcome.destination,
1327
+ chain: outcome.chain ?? null,
1328
+ peerId: outcome.peerId ?? null,
1329
+ depositTotal: outcome.depositTotal ?? null,
1330
+ depositAdded: outcome.depositAdded ?? null,
1331
+ depositTxHash: outcome.depositTxHash ?? null
1332
+ }
1333
+ });
1334
+ return 0;
1335
+ }
1336
+ io2.out(
1337
+ 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.`
1338
+ );
1339
+ io2.out(` peer ${outcome.destination}${outcome.peerId ? ` (${outcome.peerId})` : ""}`);
1340
+ if (outcome.depositAdded) {
1341
+ io2.out(
1342
+ ` deposited +${outcome.depositAdded} base units` + (outcome.depositTxHash ? ` (tx ${outcome.depositTxHash})` : "")
1343
+ );
1344
+ }
1345
+ if (outcome.depositTotal) {
1346
+ io2.out(` collateral ${outcome.depositTotal} base units total on-chain`);
1347
+ }
1348
+ return 0;
1349
+ } catch (err) {
1350
+ return emitCliError(io2, json, "channel open", err);
1351
+ } finally {
1352
+ await stopQuietly(ctx);
1353
+ }
1354
+ }
1355
+ function withdrawPrecheck(step, channelId, watermark, nowSec) {
1356
+ const status = channelStatus(watermark, nowSec);
1357
+ if (status === "settled") {
1358
+ return `channel ${channelId} is already settled \u2014 nothing left to ${step}.`;
1359
+ }
1360
+ if (step === "close") {
1361
+ if (status === "closing" || status === "settleable") {
1362
+ const at = watermark?.settleableAt;
1363
+ 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).`);
1364
+ }
1365
+ return void 0;
1366
+ }
1367
+ if (status === "open") {
1368
+ 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).`;
1369
+ }
1370
+ if (status === "closing" && watermark?.settleableAt !== void 0) {
1371
+ const remain = secondsUntil(watermark.settleableAt, nowSec);
1372
+ 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.`;
1373
+ }
1374
+ return void 0;
1375
+ }
1376
+ async function runChannelWithdrawStep(args, deps, step) {
1377
+ const { io: io2, env } = deps;
1378
+ const command = `channel ${step}`;
1379
+ let channelId;
1380
+ let yes = false;
1381
+ let json = false;
1382
+ try {
1383
+ const { values, positionals } = parseArgs4({
1384
+ args,
1385
+ options: {
1386
+ yes: { type: "boolean", default: false },
1387
+ json: { type: "boolean", default: false },
1388
+ help: { type: "boolean", short: "h", default: false }
1389
+ },
1390
+ allowPositionals: true
1391
+ });
1392
+ if (values.help) {
1393
+ io2.out(CHANNEL_USAGE);
1394
+ return 0;
1395
+ }
1396
+ yes = values.yes ?? false;
1397
+ json = values.json ?? false;
1398
+ if (positionals.length !== 1) {
1399
+ throw new Error(
1400
+ `rig channel ${step} takes exactly one <channelId> (got ${positionals.length}) \u2014 \`rig channel list\` shows recorded channels`
1401
+ );
1402
+ }
1403
+ channelId = positionals[0];
1404
+ } catch (err) {
1405
+ io2.err(err instanceof Error ? err.message : String(err));
1406
+ io2.err(CHANNEL_USAGE);
744
1407
  return 2;
745
1408
  }
746
- if (flags.help) {
747
- io.out(PUSH_USAGE);
748
- return 0;
749
- }
750
- let standaloneCtx;
1409
+ let ctx;
751
1410
  try {
752
- const repoRoot = await resolveRepoRoot(deps.cwd);
753
- const toonConfig = await readToonConfig(repoRoot);
754
- const repoId = flags.repoId ?? toonConfig.repoId;
755
- if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
756
- const reader = new GitRepoReader(repoRoot);
757
- let remoteName;
758
- let refspecArgs = flags.positionals;
759
- if (flags.relay.length === 0 && refspecArgs.length > 0) {
760
- const first = refspecArgs[0];
761
- const remoteNames = new Set(
762
- (await listGitRemotes(repoRoot)).map((r) => r.name)
1411
+ const store = new ChannelMapStore(resolveChannelPaths(env));
1412
+ const record = store.list().find((r) => r.channelId === channelId);
1413
+ if (!record) {
1414
+ throw new Error(
1415
+ `no recorded channel ${JSON.stringify(channelId)} \u2014 \`rig channel list\` shows the channels this identity holds`
763
1416
  );
764
- if (remoteNames.has(first)) {
765
- remoteName = first;
766
- refspecArgs = refspecArgs.slice(1);
767
- } else {
768
- const { refs } = await reader.listRefs();
769
- const known = new Set(refs.map((r) => r.refname));
770
- if (!known.has(first) && !known.has(`refs/heads/${first}`) && !known.has(`refs/tags/${first}`)) {
771
- throw new Error(
772
- `${JSON.stringify(first)} is neither a configured remote nor a local branch/tag \u2014 add the relay remote (\`rig remote add ${first} <relay-url>\`; \`rig remote list\` shows configured remotes) or fix the refspec`
773
- );
774
- }
775
- }
776
- }
777
- const refspecs = await selectRefspecs(
778
- reader,
779
- refspecArgs,
780
- flags.all,
781
- flags.tags
782
- );
783
- const resolved = await resolveRelays({
784
- relayFlags: flags.relay,
785
- remoteName,
786
- repoRoot,
787
- toonRelays: toonConfig.relays
788
- });
789
- if (resolved.nudge !== void 0) io.err(resolved.nudge);
790
- const relaysUsed = resolved.relays;
791
- if (relaysUsed.length > 1) {
792
- io.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
793
- return 1;
794
1417
  }
795
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
796
- env,
797
- cwd: deps.cwd,
798
- warn: (line) => io.err(line)
799
- });
800
- const identity = identityReport(standaloneCtx);
801
- if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
802
- io.err(
803
- `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.`
1418
+ const watermark = store.readWatermark(channelId);
1419
+ const nowSec = Math.floor(Date.now() / 1e3);
1420
+ const refusal = withdrawPrecheck(step, channelId, watermark, nowSec);
1421
+ if (refusal) throw new Error(refusal);
1422
+ const loaded = await loadMoneyContext(deps);
1423
+ ctx = loaded.ctx;
1424
+ const identity = identityReport(ctx);
1425
+ if (record.identity !== identity.pubkey) {
1426
+ throw new Error(
1427
+ `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.`
804
1428
  );
805
1429
  }
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);
821
- const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
822
- if (upToDate) {
823
- if (flags.json) {
824
- io.out(
825
- jsonOut({ command: "push", repoId, identity, executed: false, upToDate: true, plan })
1430
+ if (!json) {
1431
+ const claimed = watermark?.cumulativeAmount;
1432
+ io2.out(`Channel ${step} plan:`);
1433
+ io2.out(` channel ${channelId} [${channelStatus(watermark, nowSec)}]`);
1434
+ io2.out(` peer ${record.destination} (${record.peerId})`);
1435
+ io2.out(` chain ${record.chain}`);
1436
+ io2.out(
1437
+ ` deposited ${record.depositTotal ?? "unrecorded"}` + (claimed !== void 0 ? ` claimed ${claimed} base units` : "")
1438
+ );
1439
+ io2.out(renderIdentityLine(identity));
1440
+ if (step === "close") {
1441
+ io2.out(
1442
+ "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."
826
1443
  );
827
1444
  } else {
828
- io.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
1445
+ io2.out(
1446
+ "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."
1447
+ );
829
1448
  }
830
- return 0;
831
1449
  }
832
- if (!flags.json) {
833
- for (const line of renderPlan(plan)) io.out(line);
834
- io.out(renderIdentityLine(identity));
1450
+ const gate = await confirmOnChain(
1451
+ io2,
1452
+ { yes, json },
1453
+ `Proceed with on-chain channel ${step}? [y/N] `
1454
+ );
1455
+ if (gate === "json-plan") {
1456
+ io2.emitJson({
1457
+ command,
1458
+ identity,
1459
+ executed: false,
1460
+ channelId,
1461
+ hint: `plan only \u2014 re-run with --yes to ${step} (on-chain transaction)`
1462
+ });
1463
+ return 0;
835
1464
  }
836
- if (!flags.yes) {
837
- 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
- );
1465
+ if (gate !== "proceed") return gate;
1466
+ if (step === "close") {
1467
+ const result2 = await loaded.money.closeChannel(record);
1468
+ if (json) {
1469
+ io2.emitJson({
1470
+ command,
1471
+ identity,
1472
+ executed: true,
1473
+ channelId,
1474
+ result: {
1475
+ txHash: result2.txHash ?? null,
1476
+ closedAt: result2.closedAt,
1477
+ settleableAt: result2.settleableAt
1478
+ }
1479
+ });
849
1480
  return 0;
850
1481
  }
851
- if (!io.isInteractive) {
852
- io.err(
853
- "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
854
- );
855
- return 1;
856
- }
857
- const proceed = await io.confirm(
858
- `Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
1482
+ io2.out(
1483
+ `Channel ${channelId} is closing` + (result2.txHash ? ` (tx ${result2.txHash})` : "") + "."
859
1484
  );
860
- if (!proceed) {
861
- io.err("aborted \u2014 nothing was uploaded or published.");
862
- return 1;
863
- }
864
- }
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);
873
- if (flags.json) {
874
- io.out(
875
- jsonOut({ command: "push", repoId, identity, executed: true, upToDate: false, plan, result })
1485
+ io2.out(
1486
+ ` challenge window: settleable at ${formatUnixSeconds(result2.settleableAt)} (~${Math.max(0, secondsUntil(result2.settleableAt, nowSec))}s from now)`
876
1487
  );
877
- } else {
878
- for (const line of renderResult(result)) io.out(line);
1488
+ io2.out(
1489
+ ` run \`rig channel settle ${channelId}\` after that time to release the collateral.`
1490
+ );
1491
+ return 0;
1492
+ }
1493
+ const result = await loaded.money.settleChannel(record);
1494
+ if (json) {
1495
+ io2.emitJson({
1496
+ command,
1497
+ identity,
1498
+ executed: true,
1499
+ channelId,
1500
+ result: { txHash: result.txHash ?? null }
1501
+ });
1502
+ return 0;
879
1503
  }
1504
+ io2.out(
1505
+ `Channel ${channelId} settled` + (result.txHash ? ` (tx ${result.txHash})` : "") + " \u2014 the remaining collateral was released to your wallet."
1506
+ );
880
1507
  return 0;
881
1508
  } 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;
1509
+ return emitCliError(io2, json, command, err);
889
1510
  } finally {
890
- if (standaloneCtx) {
891
- try {
892
- await standaloneCtx.stop();
893
- } catch {
894
- }
895
- }
1511
+ await stopQuietly(ctx);
896
1512
  }
897
1513
  }
898
- function jsonOut(output) {
899
- return JSON.stringify(output, null, 2);
1514
+ async function stopQuietly(ctx) {
1515
+ if (!ctx) return;
1516
+ try {
1517
+ await ctx.stop();
1518
+ } catch {
1519
+ }
900
1520
  }
901
1521
 
902
1522
  // src/cli/events.ts
1523
+ import { readFile } from "fs/promises";
1524
+ import { parseArgs as parseArgs5 } from "util";
1525
+ import {
1526
+ REPOSITORY_ANNOUNCEMENT_KIND,
1527
+ STATUS_APPLIED_KIND,
1528
+ STATUS_CLOSED_KIND,
1529
+ STATUS_DRAFT_KIND,
1530
+ STATUS_OPEN_KIND
1531
+ } from "@toon-protocol/core/nip34";
903
1532
  var defaultReadStdin = async () => {
904
1533
  if (process.stdin.isTTY) return "";
905
1534
  const chunks = [];
@@ -1014,7 +1643,7 @@ function pickCommon(values) {
1014
1643
  }
1015
1644
  async function runEvent(opts) {
1016
1645
  const { command, flags, deps, actionLabel } = opts;
1017
- const { io, env } = deps;
1646
+ const { io: io2, env } = deps;
1018
1647
  let standaloneCtx;
1019
1648
  try {
1020
1649
  let repoRoot;
@@ -1032,16 +1661,18 @@ async function runEvent(opts) {
1032
1661
  repoRoot,
1033
1662
  toonRelays: toonConfig.relays
1034
1663
  });
1035
- if (resolved.nudge !== void 0) io.err(resolved.nudge);
1664
+ if (resolved.nudge !== void 0) io2.err(resolved.nudge);
1036
1665
  const relaysUsed = resolved.relays;
1037
1666
  if (relaysUsed.length > 1) {
1038
- io.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1667
+ io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1039
1668
  return 1;
1040
1669
  }
1041
1670
  standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1042
1671
  env,
1043
1672
  cwd: deps.cwd,
1044
- warn: (line) => io.err(line)
1673
+ warn: (line) => io2.err(line),
1674
+ // Relay-origin for #264 network bootstrap (announce discovery).
1675
+ ...relaysUsed[0] !== void 0 ? { relayUrl: relaysUsed[0] } : {}
1045
1676
  });
1046
1677
  const identity = identityReport(standaloneCtx);
1047
1678
  const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
@@ -1051,64 +1682,54 @@ async function runEvent(opts) {
1051
1682
  const action = `kind:${event.kind} ${actionLabel}`;
1052
1683
  if (!flags.json) {
1053
1684
  for (const line of renderEventPlan({ action, addr, identity, fee })) {
1054
- io.out(line);
1685
+ io2.out(line);
1055
1686
  }
1056
1687
  }
1057
1688
  if (!flags.yes) {
1058
1689
  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
- );
1690
+ io2.emitJson({
1691
+ command,
1692
+ repoAddr: addr,
1693
+ identity,
1694
+ kind: event.kind,
1695
+ executed: false,
1696
+ feeEstimate: fee,
1697
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
1698
+ });
1070
1699
  return 0;
1071
1700
  }
1072
- if (!io.isInteractive) {
1073
- io.err(
1701
+ if (!io2.isInteractive) {
1702
+ io2.err(
1074
1703
  "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
1075
1704
  );
1076
1705
  return 1;
1077
1706
  }
1078
- const proceed = await io.confirm(
1707
+ const proceed = await io2.confirm(
1079
1708
  `Proceed with paid publish (${feeLabel(fee)})? [y/N] `
1080
1709
  );
1081
1710
  if (!proceed) {
1082
- io.err("aborted \u2014 nothing was published.");
1711
+ io2.err("aborted \u2014 nothing was published.");
1083
1712
  return 1;
1084
1713
  }
1085
1714
  }
1086
1715
  const receipt = await standaloneCtx.publisher.publishEvent(event, relaysUsed);
1087
1716
  const result = serializeEventReceipt(event.kind, receipt);
1088
1717
  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
- })
1099
- );
1718
+ io2.emitJson({
1719
+ command,
1720
+ repoAddr: addr,
1721
+ identity,
1722
+ kind: result.kind,
1723
+ executed: true,
1724
+ feeEstimate: fee,
1725
+ result
1726
+ });
1100
1727
  } else {
1101
- for (const line of renderEventReceipt(action, result)) io.out(line);
1728
+ for (const line of renderEventReceipt(action, result)) io2.out(line);
1102
1729
  }
1103
1730
  return 0;
1104
1731
  } catch (err) {
1105
- const described = describeError(err, command);
1106
- if (flags.json) {
1107
- io.out(JSON.stringify({ command, ...described.json }, null, 2));
1108
- } else {
1109
- for (const line of described.lines) io.err(line);
1110
- }
1111
- return 1;
1732
+ return emitCliError(io2, flags.json, command, err);
1112
1733
  } finally {
1113
1734
  if (standaloneCtx) {
1114
1735
  try {
@@ -1118,21 +1739,18 @@ async function runEvent(opts) {
1118
1739
  }
1119
1740
  }
1120
1741
  }
1121
- function jsonOut2(output) {
1122
- return JSON.stringify(output, null, 2);
1123
- }
1124
1742
  async function runIssue(args, deps) {
1125
- const { io } = deps;
1743
+ const { io: io2 } = deps;
1126
1744
  const [sub, ...rest] = args;
1127
1745
  if (sub === "--help" || sub === "-h" || sub === "help") {
1128
- io.out(ISSUE_USAGE);
1746
+ io2.out(ISSUE_USAGE);
1129
1747
  return 0;
1130
1748
  }
1131
1749
  if (sub !== "create") {
1132
- io.err(
1750
+ io2.err(
1133
1751
  sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
1134
1752
  );
1135
- io.err(ISSUE_USAGE);
1753
+ io2.err(ISSUE_USAGE);
1136
1754
  return 2;
1137
1755
  }
1138
1756
  let flags;
@@ -1141,7 +1759,7 @@ async function runIssue(args, deps) {
1141
1759
  let bodyFile;
1142
1760
  let labels;
1143
1761
  try {
1144
- const { values } = parseArgs3({
1762
+ const { values } = parseArgs5({
1145
1763
  args: rest,
1146
1764
  options: {
1147
1765
  ...COMMON_OPTIONS,
@@ -1164,12 +1782,12 @@ async function runIssue(args, deps) {
1164
1782
  bodyFile = values["body-file"];
1165
1783
  labels = values.label ?? [];
1166
1784
  } catch (err) {
1167
- io.err(err instanceof Error ? err.message : String(err));
1168
- io.err(ISSUE_USAGE);
1785
+ io2.err(err instanceof Error ? err.message : String(err));
1786
+ io2.err(ISSUE_USAGE);
1169
1787
  return 2;
1170
1788
  }
1171
1789
  if (flags.help) {
1172
- io.out(ISSUE_USAGE);
1790
+ io2.out(ISSUE_USAGE);
1173
1791
  return 0;
1174
1792
  }
1175
1793
  let body;
@@ -1179,20 +1797,20 @@ async function runIssue(args, deps) {
1179
1797
  try {
1180
1798
  body = await readFile(bodyFile, "utf-8");
1181
1799
  } catch (err) {
1182
- io.err(
1800
+ io2.err(
1183
1801
  `cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
1184
1802
  );
1185
1803
  return 2;
1186
1804
  }
1187
- } else if (!io.isInteractive) {
1805
+ } else if (!io2.isInteractive) {
1188
1806
  body = await (deps.readStdin ?? defaultReadStdin)();
1189
1807
  } else {
1190
- io.err("an issue body is required: pass --body/--body-file or pipe stdin");
1191
- io.err(ISSUE_USAGE);
1808
+ io2.err("an issue body is required: pass --body/--body-file or pipe stdin");
1809
+ io2.err(ISSUE_USAGE);
1192
1810
  return 2;
1193
1811
  }
1194
1812
  if (body.trim() === "") {
1195
- io.err("the issue body is empty \u2014 nothing to publish");
1813
+ io2.err("the issue body is empty \u2014 nothing to publish");
1196
1814
  return 2;
1197
1815
  }
1198
1816
  return runEvent({
@@ -1204,14 +1822,14 @@ async function runIssue(args, deps) {
1204
1822
  });
1205
1823
  }
1206
1824
  async function runComment(args, deps) {
1207
- const { io } = deps;
1825
+ const { io: io2 } = deps;
1208
1826
  let flags;
1209
1827
  let rootEventId;
1210
1828
  let body;
1211
1829
  let parentAuthor;
1212
1830
  let marker;
1213
1831
  try {
1214
- const { values, positionals } = parseArgs3({
1832
+ const { values, positionals } = parseArgs5({
1215
1833
  args,
1216
1834
  options: {
1217
1835
  ...COMMON_OPTIONS,
@@ -1223,7 +1841,7 @@ async function runComment(args, deps) {
1223
1841
  });
1224
1842
  flags = pickCommon(values);
1225
1843
  if (flags.help) {
1226
- io.out(COMMENT_USAGE);
1844
+ io2.out(COMMENT_USAGE);
1227
1845
  return 0;
1228
1846
  }
1229
1847
  if (positionals.length !== 1) {
@@ -1245,8 +1863,8 @@ async function runComment(args, deps) {
1245
1863
  }
1246
1864
  marker = rawMarker;
1247
1865
  } catch (err) {
1248
- io.err(err instanceof Error ? err.message : String(err));
1249
- io.err(COMMENT_USAGE);
1866
+ io2.err(err instanceof Error ? err.message : String(err));
1867
+ io2.err(COMMENT_USAGE);
1250
1868
  return 2;
1251
1869
  }
1252
1870
  return runEvent({
@@ -1269,7 +1887,7 @@ function extractPatchShas(patchText) {
1269
1887
  return [...patchText.matchAll(PATCH_FROM_RE)].map((m) => m[1]);
1270
1888
  }
1271
1889
  async function runPr(args, deps) {
1272
- const { io } = deps;
1890
+ const { io: io2 } = deps;
1273
1891
  const [sub, ...rest] = args;
1274
1892
  switch (sub) {
1275
1893
  case "create":
@@ -1279,25 +1897,25 @@ async function runPr(args, deps) {
1279
1897
  case "--help":
1280
1898
  case "-h":
1281
1899
  case "help":
1282
- io.out(PR_USAGE);
1900
+ io2.out(PR_USAGE);
1283
1901
  return 0;
1284
1902
  default:
1285
- io.err(
1903
+ io2.err(
1286
1904
  sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
1287
1905
  );
1288
- io.err(PR_USAGE);
1906
+ io2.err(PR_USAGE);
1289
1907
  return 2;
1290
1908
  }
1291
1909
  }
1292
1910
  async function runPrCreate(rest, deps) {
1293
- const { io } = deps;
1911
+ const { io: io2 } = deps;
1294
1912
  let flags;
1295
1913
  let title;
1296
1914
  let range;
1297
1915
  let patchFile;
1298
1916
  let branch;
1299
1917
  try {
1300
- const { values } = parseArgs3({
1918
+ const { values } = parseArgs5({
1301
1919
  args: rest,
1302
1920
  options: {
1303
1921
  ...COMMON_OPTIONS,
@@ -1310,7 +1928,7 @@ async function runPrCreate(rest, deps) {
1310
1928
  });
1311
1929
  flags = pickCommon(values);
1312
1930
  if (flags.help) {
1313
- io.out(PR_CREATE_USAGE);
1931
+ io2.out(PR_CREATE_USAGE);
1314
1932
  return 0;
1315
1933
  }
1316
1934
  if (values.title === void 0 || values.title === "") {
@@ -1324,8 +1942,8 @@ async function runPrCreate(rest, deps) {
1324
1942
  patchFile = values["patch-file"];
1325
1943
  branch = values.branch;
1326
1944
  } catch (err) {
1327
- io.err(err instanceof Error ? err.message : String(err));
1328
- io.err(PR_CREATE_USAGE);
1945
+ io2.err(err instanceof Error ? err.message : String(err));
1946
+ io2.err(PR_CREATE_USAGE);
1329
1947
  return 2;
1330
1948
  }
1331
1949
  return runEvent({
@@ -1378,19 +1996,19 @@ function isStatusValue(value) {
1378
1996
  return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
1379
1997
  }
1380
1998
  async function runPrStatus(args, deps) {
1381
- const { io } = deps;
1999
+ const { io: io2 } = deps;
1382
2000
  let flags;
1383
2001
  let targetEventId;
1384
2002
  let status;
1385
2003
  try {
1386
- const { values, positionals } = parseArgs3({
2004
+ const { values, positionals } = parseArgs5({
1387
2005
  args,
1388
2006
  options: COMMON_OPTIONS,
1389
2007
  allowPositionals: true
1390
2008
  });
1391
2009
  flags = pickCommon(values);
1392
2010
  if (flags.help) {
1393
- io.out(PR_STATUS_USAGE);
2011
+ io2.out(PR_STATUS_USAGE);
1394
2012
  return 0;
1395
2013
  }
1396
2014
  if (positionals.length !== 2) {
@@ -1408,8 +2026,8 @@ async function runPrStatus(args, deps) {
1408
2026
  }
1409
2027
  status = rawStatus;
1410
2028
  } catch (err) {
1411
- io.err(err instanceof Error ? err.message : String(err));
1412
- io.err(PR_STATUS_USAGE);
2029
+ io2.err(err instanceof Error ? err.message : String(err));
2030
+ io2.err(PR_STATUS_USAGE);
1413
2031
  return 2;
1414
2032
  }
1415
2033
  return runEvent({
@@ -1428,6 +2046,169 @@ async function runPrStatus(args, deps) {
1428
2046
  });
1429
2047
  }
1430
2048
 
2049
+ // src/cli/fund.ts
2050
+ import { readFileSync } from "fs";
2051
+ import { homedir } from "os";
2052
+ import { join } from "path";
2053
+ import { parseArgs as parseArgs6 } from "util";
2054
+ var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
2055
+ var CHAINS = ["evm", "solana", "mina"];
2056
+ var FUND_USAGE = `Usage: rig fund [options]
2057
+
2058
+ Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
2059
+ pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
2060
+ ~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
2061
+ faucetUrl config field, or the deployed devnet faucet when the configured
2062
+ network is devnet. The faucet drips a FIXED amount per chain (there is no
2063
+ --amount). On a network without a faucet, prints the wallet address(es) to
2064
+ fund externally instead.
2065
+
2066
+ Options:
2067
+ --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
2068
+ \`chain\` config field, else evm)
2069
+ --address <address> fund this address instead of the identity's own
2070
+ --json machine-readable envelope
2071
+ -h, --help show this help`;
2072
+ function readFundConfig(env) {
2073
+ const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
2074
+ const configPath = join(dir, "config.json");
2075
+ try {
2076
+ return {
2077
+ file: JSON.parse(readFileSync(configPath, "utf8")),
2078
+ configPath
2079
+ };
2080
+ } catch (err) {
2081
+ if (err.code === "ENOENT") {
2082
+ return { file: {}, configPath };
2083
+ }
2084
+ throw new Error(
2085
+ `failed to read client config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
2086
+ );
2087
+ }
2088
+ }
2089
+ async function runFund(args, deps) {
2090
+ const { io: io2, env } = deps;
2091
+ let chainFlag;
2092
+ let addressFlag;
2093
+ let json = false;
2094
+ try {
2095
+ const { values } = parseArgs6({
2096
+ args,
2097
+ options: {
2098
+ chain: { type: "string" },
2099
+ address: { type: "string" },
2100
+ json: { type: "boolean", default: false },
2101
+ help: { type: "boolean", short: "h", default: false }
2102
+ }
2103
+ });
2104
+ if (values.help) {
2105
+ io2.out(FUND_USAGE);
2106
+ return 0;
2107
+ }
2108
+ chainFlag = values.chain;
2109
+ addressFlag = values.address;
2110
+ json = values.json ?? false;
2111
+ if (chainFlag !== void 0 && !CHAINS.includes(chainFlag)) {
2112
+ throw new Error(
2113
+ `--chain must be one of ${CHAINS.join(" | ")}, got ${JSON.stringify(chainFlag)}`
2114
+ );
2115
+ }
2116
+ } catch (err) {
2117
+ io2.err(err instanceof Error ? err.message : String(err));
2118
+ io2.err(FUND_USAGE);
2119
+ return 2;
2120
+ }
2121
+ try {
2122
+ const { file } = readFundConfig(env);
2123
+ const chain = chainFlag ?? env["TOON_CLIENT_CHAIN"] ?? file.chain ?? "evm";
2124
+ if (!CHAINS.includes(chain)) {
2125
+ throw new Error(
2126
+ `configured settlement chain ${JSON.stringify(chain)} has no faucet \u2014 pass --chain ${CHAINS.join(" | ")}`
2127
+ );
2128
+ }
2129
+ const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
2130
+ const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (network === "devnet" ? DEVNET_FAUCET_URL : void 0);
2131
+ const resolved = await resolveIdentity({
2132
+ env,
2133
+ cwd: deps.cwd,
2134
+ warn: (line) => io2.err(line)
2135
+ });
2136
+ const identity = {
2137
+ pubkey: resolved.pubkey,
2138
+ source: resolved.source,
2139
+ sourceLabel: resolved.sourceLabel
2140
+ };
2141
+ const client = await import("@toon-protocol/client");
2142
+ const derived = await client.deriveFullIdentity(
2143
+ resolved.mnemonic,
2144
+ resolved.accountIndex
2145
+ );
2146
+ const addresses = {
2147
+ evm: derived.evm.address || null,
2148
+ solana: derived.solana.publicKey || null,
2149
+ mina: derived.mina.publicKey || null
2150
+ };
2151
+ if (!faucetUrl) {
2152
+ const guidance = `no faucet on network ${JSON.stringify(network ?? "custom")} \u2014 fund the wallet externally (send the settlement token + native gas to the address for the chain your channels settle on), or set TOON_CLIENT_FAUCET_URL / the faucetUrl config field if this network has one.`;
2153
+ if (json) {
2154
+ io2.emitJson({
2155
+ command: "fund",
2156
+ identity,
2157
+ funded: false,
2158
+ network: network ?? null,
2159
+ chain,
2160
+ addresses,
2161
+ guidance
2162
+ });
2163
+ return 0;
2164
+ }
2165
+ io2.out(renderIdentityLine(identity));
2166
+ io2.out(guidance);
2167
+ io2.out("Wallet addresses:");
2168
+ io2.out(` evm ${addresses.evm ?? "(no key derived)"}`);
2169
+ io2.out(` solana ${addresses.solana ?? "(no key derived)"}`);
2170
+ io2.out(` mina ${addresses.mina ?? "(no key derived \u2014 optional mina-signer dependency missing)"}`);
2171
+ return 0;
2172
+ }
2173
+ const address = addressFlag ?? addresses[chain];
2174
+ if (!address) {
2175
+ throw new Error(
2176
+ `no ${chain} address could be derived for this identity \u2014 pass an explicit --address (for mina, install the optional mina-signer dependency)`
2177
+ );
2178
+ }
2179
+ const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
2180
+ const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
2181
+ if (!json) {
2182
+ io2.out(
2183
+ `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
2184
+ );
2185
+ }
2186
+ const { response } = await client.fundWallet(faucetUrl, address, chain, {
2187
+ timeout,
2188
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
2189
+ });
2190
+ if (json) {
2191
+ io2.emitJson({
2192
+ command: "fund",
2193
+ identity,
2194
+ funded: true,
2195
+ network: network ?? null,
2196
+ chain,
2197
+ address,
2198
+ faucetUrl,
2199
+ response
2200
+ });
2201
+ return 0;
2202
+ }
2203
+ io2.out(`Faucet drip succeeded: ${chain} \u2192 ${address}`);
2204
+ io2.out(renderIdentityLine(identity));
2205
+ io2.out("Re-check with `rig balance` (a drip can take a few blocks to land).");
2206
+ return 0;
2207
+ } catch (err) {
2208
+ return emitCliError(io2, json, "fund", err);
2209
+ }
2210
+ }
2211
+
1431
2212
  // src/cli/git-passthrough.ts
1432
2213
  import { spawn } from "child_process";
1433
2214
  import { constants as osConstants } from "os";
@@ -1437,11 +2218,11 @@ function signalExitCode(signal) {
1437
2218
  const num = osConstants.signals[signal];
1438
2219
  return num === void 0 ? 1 : 128 + num;
1439
2220
  }
1440
- var runGitPassthrough = (argv, options = {}) => {
2221
+ var runGitPassthrough = (argv2, options = {}) => {
1441
2222
  const err = options.err ?? ((line) => process.stderr.write(`${line}
1442
2223
  `));
1443
2224
  return new Promise((resolve) => {
1444
- const child = spawn("git", argv, {
2225
+ const child = spawn("git", argv2, {
1445
2226
  stdio: "inherit",
1446
2227
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1447
2228
  env: options.env ?? process.env
@@ -1461,7 +2242,7 @@ var runGitPassthrough = (argv, options = {}) => {
1461
2242
  restoreSignals();
1462
2243
  if (spawnErr.code === "ENOENT") {
1463
2244
  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.`
2245
+ `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
2246
  );
1466
2247
  resolve(GIT_NOT_FOUND_EXIT);
1467
2248
  return;
@@ -1478,7 +2259,7 @@ var runGitPassthrough = (argv, options = {}) => {
1478
2259
 
1479
2260
  // src/cli/init.ts
1480
2261
  import { basename } from "path";
1481
- import { parseArgs as parseArgs4 } from "util";
2262
+ import { parseArgs as parseArgs7 } from "util";
1482
2263
  var INIT_USAGE = `Usage: rig init [options]
1483
2264
 
1484
2265
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -1498,7 +2279,7 @@ Options:
1498
2279
  --json machine-readable report
1499
2280
  -h, --help show this help`;
1500
2281
  function parseInitArgs(args) {
1501
- const { values, positionals } = parseArgs4({
2282
+ const { values, positionals } = parseArgs7({
1502
2283
  args,
1503
2284
  options: {
1504
2285
  "repo-id": { type: "string" },
@@ -1522,17 +2303,17 @@ function parseInitArgs(args) {
1522
2303
  return flags;
1523
2304
  }
1524
2305
  async function runInit(args, deps) {
1525
- const { io, env } = deps;
2306
+ const { io: io2, env } = deps;
1526
2307
  let flags;
1527
2308
  try {
1528
2309
  flags = parseInitArgs(args);
1529
2310
  } catch (err) {
1530
- io.err(err instanceof Error ? err.message : String(err));
1531
- io.err(INIT_USAGE);
2311
+ io2.err(err instanceof Error ? err.message : String(err));
2312
+ io2.err(INIT_USAGE);
1532
2313
  return 2;
1533
2314
  }
1534
2315
  if (flags.help) {
1535
- io.out(INIT_USAGE);
2316
+ io2.out(INIT_USAGE);
1536
2317
  return 0;
1537
2318
  }
1538
2319
  try {
@@ -1545,7 +2326,7 @@ async function runInit(args, deps) {
1545
2326
  const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
1546
2327
  env,
1547
2328
  cwd: deps.cwd,
1548
- warn: (line) => io.err(line)
2329
+ warn: (line) => io2.err(line)
1549
2330
  });
1550
2331
  const existing = await readToonConfig(repoRoot);
1551
2332
  const repoId = flags.repoId ?? existing.repoId ?? basename(repoRoot);
@@ -1582,50 +2363,44 @@ async function runInit(args, deps) {
1582
2363
  migratedToonRelay,
1583
2364
  changed
1584
2365
  };
1585
- io.out(JSON.stringify(output, null, 2));
2366
+ io2.emitJson(output);
1586
2367
  return 0;
1587
2368
  }
1588
- io.out(`Initialized rig for ${repoRoot}`);
1589
- io.out(renderIdentityLine(identity));
1590
- io.out(
2369
+ io2.out(`Initialized rig for ${repoRoot}`);
2370
+ io2.out(renderIdentityLine(identity));
2371
+ io2.out(
1591
2372
  ` toon.repoid = ${repoId}` + (changed.repoId ? "" : " (unchanged)") + (existing.repoId && changed.repoId ? ` (was ${existing.repoId})` : "")
1592
2373
  );
1593
- io.out(
2374
+ io2.out(
1594
2375
  ` toon.owner = ${identity.pubkey}` + (changed.owner ? "" : " (unchanged)") + (existing.owner && changed.owner ? ` (was ${existing.owner})` : "")
1595
2376
  );
1596
2377
  if (originRelay !== void 0) {
1597
- io.out(
2378
+ io2.out(
1598
2379
  ` origin = ${originRelay}` + (migratedToonRelay ? " (migrated from git config toon.relay)" : "")
1599
2380
  );
1600
2381
  if (migratedToonRelay) {
1601
- io.out(
2382
+ io2.out(
1602
2383
  "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
2384
  );
1604
2385
  }
1605
- io.out('Ready: `rig push` publishes this repo via remote "origin".');
2386
+ io2.out('Ready: `rig push` publishes this repo via remote "origin".');
1606
2387
  } else if (existing.relays.length > 0) {
1607
- io.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
1608
- io.out(
2388
+ io2.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
2389
+ io2.out(
1609
2390
  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
2391
  );
1611
2392
  } else if (origin !== void 0) {
1612
- io.out(
2393
+ io2.out(
1613
2394
  `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
2395
  );
1615
2396
  } else {
1616
- io.out(
2397
+ io2.out(
1617
2398
  "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
2399
  );
1619
2400
  }
1620
2401
  return 0;
1621
2402
  } 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;
2403
+ return emitCliError(io2, flags.json, "init", err);
1629
2404
  }
1630
2405
  }
1631
2406
 
@@ -1651,15 +2426,35 @@ Commands rig owns:
1651
2426
  \`git format-patch\` content
1652
2427
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
1653
2428
  open | applied | closed | draft
2429
+ fund drip devnet faucet funds to this identity's
2430
+ wallet (free); on other networks prints the
2431
+ address(es) to fund externally
2432
+ balance wallet balances + payment-channel holdings
2433
+ (free \u2014 chain reads and local state only)
2434
+ channel list show the payment channels paid commands hold
2435
+ (free \u2014 reads local state)
2436
+ channel open explicitly open (or resume) the channel for a
2437
+ peer; --deposit adds collateral (on-chain)
2438
+ channel close <channelId> start the settlement challenge window (on-chain)
2439
+ channel settle <channelId> release collateral after the window (on-chain)
1654
2440
 
1655
2441
  Any other command is passed through to git verbatim: \`rig status\` runs
1656
2442
  \`git status\`, and \`rig add -p\`, \`rig commit\`, \`rig log --oneline\`,
1657
2443
  \`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
1658
2444
 
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).`;
2445
+ Run \`rig <command> --help\` for a rig command's flags. \`rig init\`,
2446
+ \`rig remote\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\` are
2447
+ free; push/issue/comment/pr are paid writes \u2014 permanent and non-refundable \u2014
2448
+ and channel open/close/settle are on-chain wallet transactions; each states
2449
+ what it will spend and asks for confirmation before doing so (--yes skips,
2450
+ --json emits machine output).
2451
+
2452
+ With --json, stdout carries exactly ONE JSON document (everything human-facing
2453
+ goes to stderr), so \`rig <command> --json | jq\` always parses. --json is a
2454
+ per-subcommand flag on the commands rig owns, NOT a global rig flag: it does
2455
+ not apply to the git passthrough (\`rig status --json\` runs
2456
+ \`git status --json\`), and flags placed before the subcommand
2457
+ (\`rig --json status\`) pass through to git untouched.`;
1663
2458
  function rigVersion() {
1664
2459
  const require2 = createRequire(import.meta.url);
1665
2460
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
@@ -1671,9 +2466,9 @@ function rigVersion() {
1671
2466
  }
1672
2467
  return "unknown";
1673
2468
  }
1674
- async function dispatch(argv, deps) {
1675
- const [command, ...rest] = argv;
1676
- const { io } = deps;
2469
+ async function dispatch(argv2, deps) {
2470
+ const [command, ...rest] = argv2;
2471
+ const { io: io2 } = deps;
1677
2472
  switch (command) {
1678
2473
  case "init":
1679
2474
  return runInit(rest, deps);
@@ -1687,35 +2482,124 @@ async function dispatch(argv, deps) {
1687
2482
  return runComment(rest, deps);
1688
2483
  case "pr":
1689
2484
  return runPr(rest, deps);
2485
+ case "channel":
2486
+ return runChannel(rest, deps);
2487
+ case "fund":
2488
+ return runFund(rest, deps);
2489
+ case "balance":
2490
+ return runBalance(rest, deps);
1690
2491
  case "help":
1691
2492
  case "--help":
1692
2493
  case "-h":
1693
- io.out(USAGE);
1694
- io.out("");
1695
- io.out(PUSH_USAGE);
2494
+ io2.out(USAGE);
2495
+ io2.out("");
2496
+ io2.out(PUSH_USAGE);
1696
2497
  return 0;
1697
2498
  case "--version":
1698
- io.out(`rig ${rigVersion()}`);
2499
+ io2.out(`rig ${rigVersion()}`);
1699
2500
  return 0;
1700
2501
  case void 0:
1701
- io.err(USAGE);
2502
+ io2.err(USAGE);
1702
2503
  return 2;
1703
2504
  default:
1704
- return (deps.runGit ?? runGitPassthrough)(argv, {
2505
+ return (deps.runGit ?? runGitPassthrough)(argv2, {
1705
2506
  cwd: deps.cwd,
1706
2507
  env: deps.env,
1707
- err: (line) => io.err(line)
2508
+ err: (line) => io2.err(line)
1708
2509
  });
1709
2510
  }
1710
2511
  }
1711
2512
 
1712
- // src/cli/rig.ts
1713
- function makeIo() {
2513
+ // src/cli/output.ts
2514
+ function makeCliIo(options) {
2515
+ const { jsonMode, writeStdout, writeStderr } = options;
2516
+ let emittedJson = false;
2517
+ const humanLines = [];
2518
+ const toStderr = (line) => {
2519
+ if (jsonMode) humanLines.push(line);
2520
+ writeStderr(`${line}
2521
+ `);
2522
+ };
2523
+ return {
2524
+ jsonMode,
2525
+ get emittedJson() {
2526
+ return emittedJson;
2527
+ },
2528
+ out: (line) => {
2529
+ if (jsonMode) {
2530
+ toStderr(line);
2531
+ } else {
2532
+ writeStdout(`${line}
2533
+ `);
2534
+ }
2535
+ },
2536
+ err: toStderr,
2537
+ emitJson: (payload) => {
2538
+ emittedJson = true;
2539
+ writeStdout(`${JSON.stringify(payload, null, 2)}
2540
+ `);
2541
+ },
2542
+ isInteractive: options.isInteractive,
2543
+ confirm: options.confirm,
2544
+ ensureSingleJsonDoc(exitCode) {
2545
+ if (!jsonMode || emittedJson) return;
2546
+ const detail = humanLines.join("\n") || (exitCode === 0 ? "the command produced no machine output" : "the command failed before emitting machine output \u2014 see stderr");
2547
+ this.emitJson(
2548
+ exitCode === 0 ? { error: null, exitCode, detail } : { error: "error", exitCode, detail }
2549
+ );
2550
+ }
2551
+ };
2552
+ }
2553
+ var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
2554
+ "init",
2555
+ "remote",
2556
+ "push",
2557
+ "issue",
2558
+ "comment",
2559
+ "pr",
2560
+ "channel",
2561
+ "fund",
2562
+ "balance"
2563
+ ]);
2564
+ function isJsonInvocation(argv2) {
2565
+ const [verb, ...rest] = argv2;
2566
+ if (verb === void 0 || !RIG_OWNED_VERBS.has(verb)) return false;
2567
+ for (const arg of rest) {
2568
+ if (arg === "--") return false;
2569
+ if (arg === "--json") return true;
2570
+ }
2571
+ return false;
2572
+ }
2573
+ function redirectStdoutToStderr(realWrite) {
2574
+ const original = process.stdout.write;
2575
+ const real = realWrite ?? original.bind(process.stdout);
2576
+ const patched = ((chunk, encodingOrCb, cb) => typeof encodingOrCb === "function" ? process.stderr.write(chunk, encodingOrCb) : process.stderr.write(
2577
+ chunk,
2578
+ encodingOrCb,
2579
+ cb
2580
+ ));
2581
+ process.stdout.write = patched;
1714
2582
  return {
1715
- out: (line) => process.stdout.write(`${line}
1716
- `),
1717
- err: (line) => process.stderr.write(`${line}
1718
- `),
2583
+ write: (text) => {
2584
+ real(text);
2585
+ },
2586
+ restore: () => {
2587
+ if (process.stdout.write === patched) process.stdout.write = original;
2588
+ }
2589
+ };
2590
+ }
2591
+
2592
+ // src/cli/rig.ts
2593
+ function makeIo(jsonMode) {
2594
+ const guard = jsonMode ? redirectStdoutToStderr() : void 0;
2595
+ return makeCliIo({
2596
+ jsonMode,
2597
+ writeStdout: guard ? guard.write : (text) => {
2598
+ process.stdout.write(text);
2599
+ },
2600
+ writeStderr: (text) => {
2601
+ process.stderr.write(text);
2602
+ },
1719
2603
  isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1720
2604
  confirm: async (question) => {
1721
2605
  const rl = createInterface({
@@ -1729,14 +2613,17 @@ function makeIo() {
1729
2613
  rl.close();
1730
2614
  }
1731
2615
  }
1732
- };
2616
+ });
1733
2617
  }
1734
- dispatch(process.argv.slice(2), {
1735
- io: makeIo(),
2618
+ var argv = process.argv.slice(2);
2619
+ var io = makeIo(isJsonInvocation(argv));
2620
+ dispatch(argv, {
2621
+ io,
1736
2622
  env: process.env,
1737
2623
  cwd: process.cwd()
1738
2624
  }).then(
1739
2625
  (code) => {
2626
+ io.ensureSingleJsonDoc(code);
1740
2627
  process.exitCode = code;
1741
2628
  },
1742
2629
  (err) => {
@@ -1744,6 +2631,7 @@ dispatch(process.argv.slice(2), {
1744
2631
  `rig: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
1745
2632
  `
1746
2633
  );
2634
+ io.ensureSingleJsonDoc(1);
1747
2635
  process.exitCode = 1;
1748
2636
  }
1749
2637
  );