@toon-protocol/rig 2.4.1 → 2.6.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
@@ -6,6 +6,7 @@ import {
6
6
  OversizeObjectsError,
7
7
  collectRepoObjects,
8
8
  executePush,
9
+ hexToNpub,
9
10
  isSafeRefname,
10
11
  missingObjectsMessage,
11
12
  ownerToHex,
@@ -17,7 +18,7 @@ import {
17
18
  setHeadSymref,
18
19
  updateRef,
19
20
  writeGitObjects
20
- } from "../chunk-W2SDL2PE.js";
21
+ } from "../chunk-NIRC5LFS.js";
21
22
  import {
22
23
  DEFAULT_KEYSTORE_PASSWORD,
23
24
  MissingIdentityError,
@@ -27,6 +28,15 @@ import {
27
28
  readClientConfigFile,
28
29
  resolveIdentity
29
30
  } from "../chunk-XGFBDUQX.js";
31
+ import {
32
+ ChannelMapStore,
33
+ channelStatus,
34
+ defaultDaemonPort,
35
+ resolveChannelPaths
36
+ } from "../chunk-SW7ZHMGS.js";
37
+ import {
38
+ MAX_OBJECT_SIZE
39
+ } from "../chunk-B5ISARMU.js";
30
40
  import {
31
41
  COMMENT_KIND,
32
42
  authorizedStatusAuthors,
@@ -39,15 +49,6 @@ import {
39
49
  parseMaintainers,
40
50
  queryRelay
41
51
  } from "../chunk-3HRFDH7H.js";
42
- import {
43
- ChannelMapStore,
44
- channelStatus,
45
- defaultDaemonPort,
46
- resolveChannelPaths
47
- } from "../chunk-SW7ZHMGS.js";
48
- import {
49
- MAX_OBJECT_SIZE
50
- } from "../chunk-X2CZPPDM.js";
51
52
 
52
53
  // src/cli/rig.ts
53
54
  import { createInterface as createInterface2 } from "readline/promises";
@@ -59,6 +60,9 @@ import { createRequire } from "module";
59
60
  import { parseArgs as parseArgs3 } from "util";
60
61
 
61
62
  // src/cli/daemon-session.ts
63
+ function daemonSupportsGit(probe) {
64
+ return Array.isArray(probe.capabilities) && probe.capabilities.includes("git");
65
+ }
62
66
  function daemonBaseUrl(env) {
63
67
  const raw = env["TOON_CLIENT_HTTP_PORT"];
64
68
  const parsed = raw ? Number(raw) : NaN;
@@ -83,6 +87,11 @@ async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
83
87
  if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
84
88
  probe.feePerEvent = body.feePerEvent;
85
89
  }
90
+ if (Array.isArray(body?.capabilities)) {
91
+ probe.capabilities = body.capabilities.filter(
92
+ (c) => typeof c === "string"
93
+ );
94
+ }
86
95
  return probe;
87
96
  } catch {
88
97
  return { baseUrl, reachable: false };
@@ -98,6 +107,19 @@ var DaemonRouteError = class extends Error {
98
107
  status;
99
108
  envelope;
100
109
  };
110
+ var DaemonTooOldForGitError = class extends Error {
111
+ constructor(baseUrl, pubkey) {
112
+ const holds = pubkey !== void 0 && pubkey !== "" ? ` holds this identity (${pubkey.slice(0, 8)}\u2026)` : " holds this identity";
113
+ super(
114
+ `toon-clientd at ${baseUrl}${holds} but is too old to handle git operations (missing /git routes). Upgrade the daemon (npm i -g @toon-protocol/client-mcp@latest, then restart it) \u2014 or stop it to let rig run standalone.`
115
+ );
116
+ this.baseUrl = baseUrl;
117
+ this.pubkey = pubkey;
118
+ this.name = "DaemonTooOldForGitError";
119
+ }
120
+ baseUrl;
121
+ pubkey;
122
+ };
101
123
  var DaemonUnreachableError = class extends Error {
102
124
  constructor(baseUrl, cause) {
103
125
  super(
@@ -109,12 +131,14 @@ var DaemonUnreachableError = class extends Error {
109
131
  baseUrl;
110
132
  };
111
133
  var DaemonGitClient = class {
112
- constructor(baseUrl, fetchImpl) {
134
+ constructor(baseUrl, fetchImpl, pubkey) {
113
135
  this.baseUrl = baseUrl;
114
136
  this.fetchImpl = fetchImpl;
137
+ this.pubkey = pubkey;
115
138
  }
116
139
  baseUrl;
117
140
  fetchImpl;
141
+ pubkey;
118
142
  gitEstimate(req) {
119
143
  return this.post("/git/estimate", req);
120
144
  }
@@ -144,6 +168,9 @@ var DaemonGitClient = class {
144
168
  } catch (err) {
145
169
  throw new DaemonUnreachableError(this.baseUrl, err);
146
170
  }
171
+ if (res.status === 404) {
172
+ throw new DaemonTooOldForGitError(this.baseUrl, this.pubkey);
173
+ }
147
174
  const text = await res.text();
148
175
  let parsed;
149
176
  try {
@@ -174,12 +201,15 @@ async function resolvePaidSession(options) {
174
201
  warn: options.warn
175
202
  });
176
203
  if (identity.pubkey === probe.identity) {
204
+ if (!daemonSupportsGit(probe)) {
205
+ throw new DaemonTooOldForGitError(probe.baseUrl, identity.pubkey);
206
+ }
177
207
  options.warn(
178
208
  `rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
179
209
  );
180
210
  return {
181
211
  path: "daemon",
182
- client: new DaemonGitClient(probe.baseUrl, fetchImpl),
212
+ client: new DaemonGitClient(probe.baseUrl, fetchImpl, identity.pubkey),
183
213
  baseUrl: probe.baseUrl,
184
214
  identity: {
185
215
  pubkey: identity.pubkey,
@@ -263,7 +293,7 @@ var NotAGitRepositoryError = class extends Error {
263
293
  constructor(cwd) {
264
294
  super(
265
295
  `not a git repository: ${cwd}
266
- rig works inside an existing repo \u2014 create one first with \`git init\` (rig never runs it for you), then re-run.`
296
+ rig can create one for you: re-run \`rig init --git-init\` (in a terminal, plain \`rig init\` offers the same via a prompt). Or run \`git init\` yourself first, then re-run.`
267
297
  );
268
298
  this.name = "NotAGitRepositoryError";
269
299
  }
@@ -401,6 +431,18 @@ function describeError(err, command = "push") {
401
431
  json: { error: "daemon_unreachable", detail: err.message }
402
432
  };
403
433
  }
434
+ if (err instanceof DaemonTooOldForGitError) {
435
+ return {
436
+ code: "daemon_too_old_for_git",
437
+ lines: err.message.split("\n"),
438
+ json: {
439
+ error: "daemon_too_old_for_git",
440
+ detail: err.message,
441
+ baseUrl: err.baseUrl,
442
+ ...err.pubkey !== void 0 ? { pubkey: err.pubkey } : {}
443
+ }
444
+ };
445
+ }
404
446
  const name = err instanceof Error ? err.name : "";
405
447
  const message = err instanceof Error ? err.message : String(err);
406
448
  if (name === "DaemonIdentityConflictError") {
@@ -507,8 +549,9 @@ function renderPlan(plan) {
507
549
  for (const update of plan.refUpdates) lines.push(refLine(update));
508
550
  const est = plan.estimate;
509
551
  const skipped = Object.keys(plan.knownShaToTxId).length;
552
+ const emptySkipped = est.skippedEmptyCount ?? 0;
510
553
  lines.push(
511
- `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
554
+ `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "") + (emptySkipped > 0 ? `; ${emptySkipped} empty blob${emptySkipped === 1 ? "" : "s"} skipped (reconstructed on clone)` : "")
512
555
  );
513
556
  lines.push("Fees (base units):");
514
557
  lines.push(
@@ -554,6 +597,12 @@ function renderResult(result) {
554
597
  lines.push(
555
598
  `Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
556
599
  );
600
+ const emptySkipped = result.estimate.skippedEmptyCount ?? 0;
601
+ if (emptySkipped > 0) {
602
+ lines.push(
603
+ `Empty blobs: ${emptySkipped} skipped (zero-byte, not uploaded \u2014 reconstructed on clone).`
604
+ );
605
+ }
557
606
  if (result.announceReceipt) {
558
607
  lines.push(
559
608
  `Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
@@ -607,6 +656,14 @@ async function resolveRepoRoot(cwd) {
607
656
  );
608
657
  }
609
658
  }
659
+ async function initGitRepository(dir) {
660
+ try {
661
+ await git(dir, ["init", "--initial-branch=main"]);
662
+ } catch {
663
+ await git(dir, ["init"]);
664
+ await git(dir, ["symbolic-ref", "HEAD", "refs/heads/main"]);
665
+ }
666
+ }
610
667
  async function readToonConfig(repoPath) {
611
668
  const [repoId, owner, relays] = await Promise.all([
612
669
  git(repoPath, ["config", "--get", "toon.repoid"], [1]),
@@ -639,6 +696,10 @@ async function listGitRemotes(repoPath) {
639
696
  }
640
697
  return remotes;
641
698
  }
699
+ async function setGitAuthor(repoPath, author) {
700
+ await git(repoPath, ["config", "--local", "user.name", author.name]);
701
+ await git(repoPath, ["config", "--local", "user.email", author.email]);
702
+ }
642
703
  async function addGitRemote(repoPath, name, url) {
643
704
  await git(repoPath, ["remote", "add", name, url]);
644
705
  }
@@ -905,7 +966,7 @@ function loadPaidSession(deps, relayUrl) {
905
966
  });
906
967
  }
907
968
  var defaultLoadStandalone = async (options) => {
908
- const mod = await import("../standalone-mode-VH2XPOPK.js");
969
+ const mod = await import("../standalone-mode-WEMJBHME.js");
909
970
  return mod.createStandaloneContext(options);
910
971
  };
911
972
  function identityReport(ctx) {
@@ -946,7 +1007,20 @@ Options:
946
1007
  --repo-id <id> repository id / NIP-34 d-tag (default: git config
947
1008
  toon.repoid \u2014 run \`rig init\` to set it)
948
1009
  -h, --help show this help`;
949
- async function selectRefspecs(reader, positionals, all, tags) {
1010
+ function noSuchRefMessage(spec, head, refs, remoteName) {
1011
+ const branches = refs.filter((r) => r.refname.startsWith("refs/heads/")).map((r) => r.refname.slice("refs/heads/".length));
1012
+ const current = head && head.startsWith("refs/heads/") ? head.slice("refs/heads/".length) : void 0;
1013
+ let msg = `no local branch or tag ${JSON.stringify(spec)}`;
1014
+ if (current) {
1015
+ msg += ` \u2014 your current branch is ${JSON.stringify(current)} (did you mean \`rig push ${remoteName} ${current}\`?)`;
1016
+ } else if (branches.length > 0) {
1017
+ msg += ` \u2014 local branches: ${branches.join(", ")}`;
1018
+ } else {
1019
+ msg += " \u2014 this repository has no branches yet (make a commit first)";
1020
+ }
1021
+ return msg;
1022
+ }
1023
+ async function selectRefspecs(reader, positionals, all, tags, remoteName = "origin") {
950
1024
  const { head, refs } = await reader.listRefs();
951
1025
  const byName = new Set(refs.map((r) => r.refname));
952
1026
  const selected = [];
@@ -960,10 +1034,12 @@ async function selectRefspecs(reader, positionals, all, tags) {
960
1034
  add(`refs/heads/${spec}`);
961
1035
  } else if (byName.has(`refs/tags/${spec}`)) {
962
1036
  add(`refs/tags/${spec}`);
963
- } else {
1037
+ } else if (spec.startsWith(":")) {
964
1038
  throw new Error(
965
- `refspec ${JSON.stringify(spec)} matches no local branch or tag (ref deletion is out of scope in v1)`
1039
+ `deleting remote refs (${JSON.stringify(spec)}) is out of scope in v1`
966
1040
  );
1041
+ } else {
1042
+ throw new Error(noSuchRefMessage(spec, head, refs, remoteName));
967
1043
  }
968
1044
  }
969
1045
  if (all) {
@@ -1060,7 +1136,8 @@ async function runPush(args, deps) {
1060
1136
  reader,
1061
1137
  refspecArgs,
1062
1138
  flags.all,
1063
- flags.tags
1139
+ flags.tags,
1140
+ remoteName ?? "origin"
1064
1141
  );
1065
1142
  const resolved = await resolveRelays({
1066
1143
  relayFlags: flags.relay,
@@ -1215,14 +1292,26 @@ async function runPush(args, deps) {
1215
1292
  // src/cli/balance.ts
1216
1293
  var BALANCE_USAGE = `Usage: rig balance [--json]
1217
1294
 
1218
- Show the active identity's money: on-chain wallet balances (per configured
1219
- chain, read from the settlement chain the payment channels actually use) and
1220
- recorded payment-channel holdings (deposited / claimed / available). Free \u2014
1221
- reads chain RPCs and local state only; nothing is signed or paid.
1295
+ Show the active identity's money: the full on-chain wallet view \u2014 native coin
1296
+ plus USDC on every configured chain (EVM / Solana / Mina) \u2014 and recorded
1297
+ payment-channel holdings (deposited / claimed / available). Free \u2014 reads chain
1298
+ RPCs and local state only; nothing is signed or paid. An unreachable chain RPC
1299
+ degrades to a per-chain notice without failing the others.
1222
1300
 
1223
1301
  Options:
1224
1302
  --json machine-readable envelope (base units as strings)
1225
1303
  -h, --help show this help`;
1304
+ function formatUnits(amount, decimals) {
1305
+ if (decimals === void 0 || decimals <= 0) return amount;
1306
+ const neg = amount.startsWith("-");
1307
+ const digits = (neg ? amount.slice(1) : amount).padStart(decimals + 1, "0");
1308
+ const whole = digits.slice(0, digits.length - decimals);
1309
+ const frac = digits.slice(digits.length - decimals).replace(/0+$/, "");
1310
+ return (neg ? "-" : "") + (frac ? `${whole}.${frac}` : whole);
1311
+ }
1312
+ function renderAmount(a) {
1313
+ return `${a.symbol ?? "token"} ${formatUnits(a.amount, a.decimals)}`;
1314
+ }
1226
1315
  async function runBalance(args, deps) {
1227
1316
  const { io: io2, env } = deps;
1228
1317
  let json = false;
@@ -1254,7 +1343,7 @@ async function runBalance(args, deps) {
1254
1343
  requireUplink: false
1255
1344
  });
1256
1345
  const identity = identityReport(ctx);
1257
- const wallet = ctx.money ? await ctx.money.walletBalances() : [];
1346
+ const wallet = ctx.money ? await ctx.money.walletChainBalances() : [];
1258
1347
  const store = new ChannelMapStore(resolveChannelPaths(env));
1259
1348
  const channels = store.list().filter((record) => record.identity === identity.pubkey).map((record) => {
1260
1349
  const watermark = store.readWatermark(record.channelId);
@@ -1291,13 +1380,23 @@ async function runBalance(args, deps) {
1291
1380
  io2.out("Wallet (on-chain):");
1292
1381
  if (wallet.length === 0) {
1293
1382
  io2.out(
1294
- " (no balance readable \u2014 no chain configured, the RPC is unreachable, or the chain's keys derive only during a client start)"
1383
+ " (no chain configured for this identity \u2014 nothing to read)"
1295
1384
  );
1296
1385
  }
1297
- for (const balance of wallet) {
1298
- io2.out(
1299
- ` ${balance.chain.padEnd(7)} ${balance.address} ${balance.amount}` + (balance.asset ? ` ${balance.asset}` : " base units") + (balance.assetScale !== void 0 ? ` (scale ${balance.assetScale})` : "")
1300
- );
1386
+ for (const chain of wallet) {
1387
+ io2.out(` ${chain.chainKey.padEnd(11)} ${chain.address}`);
1388
+ if (chain.unreadable) {
1389
+ io2.out(
1390
+ ` unreadable (RPC unreachable)` + (chain.error ? ` \u2014 ${chain.error}` : "")
1391
+ );
1392
+ continue;
1393
+ }
1394
+ const assets = [...chain.native ? [chain.native] : [], ...chain.tokens];
1395
+ if (assets.length === 0) {
1396
+ io2.out(" (no balance readable)");
1397
+ continue;
1398
+ }
1399
+ io2.out(` ${assets.map(renderAmount).join(" ")}`);
1301
1400
  }
1302
1401
  io2.out("");
1303
1402
  io2.out("Channels (recorded):");
@@ -3432,24 +3531,37 @@ import { join as join2 } from "path";
3432
3531
  import { parseArgs as parseArgs9 } from "util";
3433
3532
  var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
3434
3533
  var CHAINS = ["evm", "solana", "mina"];
3534
+ var CHAIN_CHOICES = [...CHAINS, "all"];
3535
+ var NATIVE_COIN = {
3536
+ evm: "ETH",
3537
+ solana: "SOL",
3538
+ mina: "MINA"
3539
+ };
3435
3540
  var FUND_USAGE = `Usage: rig fund [options]
3436
3541
 
3437
3542
  Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
3438
- pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
3543
+ pays). By default funds ALL supported chains (evm + solana + mina), each drip
3544
+ covering the native coin AND USDC, so the wallet matches the multi-chain
3545
+ \`rig balance\` view in one run. The per-chain drips run in parallel and are
3546
+ independent \u2014 one chain's faucet failing does not abort the others (exit 0
3547
+ only when every targeted chain funded; exit 1 if any failed, with the
3548
+ per-chain breakdown always shown).
3549
+
3550
+ The identity comes from RIG_MNEMONIC (env or a project .env) or the
3439
3551
  ~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
3440
3552
  faucetUrl config field, or the deployed devnet faucet when the network is
3441
3553
  devnet \u2014 including when a configured *.devnet.toonprotocol.dev origin infers
3442
3554
  it (a devnet relay/proxy/btp endpoint, or the git origin remote from
3443
3555
  \`rig remote add origin <devnet relay>\`). The faucet drips a FIXED amount per
3444
- chain (there is no --amount). On a
3445
- network without a faucet, prints the wallet address(es) to fund externally
3446
- instead.
3556
+ chain (there is no --amount). On a network without a faucet, prints the wallet
3557
+ address(es) to fund externally instead.
3447
3558
 
3448
3559
  Options:
3449
- --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
3450
- \`chain\` config field, else evm)
3560
+ --chain <chain> evm | solana | mina | all \u2014 fund one chain, or all
3561
+ (default: all supported chains)
3451
3562
  --address <address> fund this address instead of the identity's own
3452
- --json machine-readable envelope
3563
+ (requires an explicit single --chain)
3564
+ --json machine-readable envelope (per-chain results array)
3453
3565
  -h, --help show this help`;
3454
3566
  var SHARED_DEVNET_SUFFIX = ".devnet.toonprotocol.dev";
3455
3567
  function devnetHost(url) {
@@ -3543,9 +3655,14 @@ async function runFund(args, deps) {
3543
3655
  chainFlag = values.chain;
3544
3656
  addressFlag = values.address;
3545
3657
  json = values.json ?? false;
3546
- if (chainFlag !== void 0 && !CHAINS.includes(chainFlag)) {
3658
+ if (chainFlag !== void 0 && !CHAIN_CHOICES.includes(chainFlag)) {
3547
3659
  throw new Error(
3548
- `--chain must be one of ${CHAINS.join(" | ")}, got ${JSON.stringify(chainFlag)}`
3660
+ `--chain must be one of ${CHAIN_CHOICES.join(" | ")}, got ${JSON.stringify(chainFlag)}`
3661
+ );
3662
+ }
3663
+ if (addressFlag !== void 0 && (chainFlag === void 0 || chainFlag === "all")) {
3664
+ throw new Error(
3665
+ "--address requires an explicit single --chain (evm | solana | mina) \u2014 a single address cannot fund every chain"
3549
3666
  );
3550
3667
  }
3551
3668
  } catch (err) {
@@ -3555,12 +3672,7 @@ async function runFund(args, deps) {
3555
3672
  }
3556
3673
  try {
3557
3674
  const { file } = readFundConfig(env);
3558
- const chain = chainFlag ?? env["TOON_CLIENT_CHAIN"] ?? file.chain ?? "evm";
3559
- if (!CHAINS.includes(chain)) {
3560
- throw new Error(
3561
- `configured settlement chain ${JSON.stringify(chain)} has no faucet \u2014 pass --chain ${CHAINS.join(" | ")}`
3562
- );
3563
- }
3675
+ const targetChains = chainFlag === void 0 || chainFlag === "all" ? [...CHAINS] : [chainFlag];
3564
3676
  const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
3565
3677
  const canInfer = network === void 0 || network === "custom";
3566
3678
  const originRelays = canInfer ? await resolveOriginRelays(deps.cwd) : [];
@@ -3596,7 +3708,6 @@ async function runFund(args, deps) {
3596
3708
  identity,
3597
3709
  funded: false,
3598
3710
  network: network ?? null,
3599
- chain,
3600
3711
  addresses,
3601
3712
  guidance
3602
3713
  });
@@ -3610,46 +3721,85 @@ async function runFund(args, deps) {
3610
3721
  io2.out(` mina ${addresses.mina ?? "(no key derived \u2014 optional mina-signer dependency missing)"}`);
3611
3722
  return 0;
3612
3723
  }
3613
- const address = addressFlag ?? addresses[chain];
3614
- if (!address) {
3615
- throw new Error(
3616
- `no ${chain} address could be derived for this identity \u2014 pass an explicit --address (for mina, install the optional mina-signer dependency)`
3617
- );
3618
- }
3619
3724
  const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
3620
- const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
3725
+ const timeoutFor = (chain) => {
3726
+ if (timeoutEnv && Number.isFinite(Number(timeoutEnv))) return Number(timeoutEnv);
3727
+ if (file.faucetTimeoutMs !== void 0) return file.faucetTimeoutMs;
3728
+ return chain === "mina" ? 13e4 : 9e4;
3729
+ };
3621
3730
  if (!json && inferredDevnet) {
3622
3731
  io2.out(
3623
3732
  `Inferred network 'devnet' from the configured origin ${devnetOrigin} (network was ${JSON.stringify(network ?? "custom")}). Set TOON_CLIENT_NETWORK explicitly to override.`
3624
3733
  );
3625
3734
  }
3626
3735
  if (!json) {
3736
+ const list = targetChains.join(", ");
3627
3737
  io2.out(
3628
- `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
3738
+ `Requesting ${targetChains.length === 1 ? "" : "parallel "}drip${targetChains.length === 1 ? "" : "s"} from ${faucetUrl} for ${list} \u2026` + (targetChains.includes("mina") ? " (mina settles slowly; this can take ~2 minutes)" : "")
3629
3739
  );
3630
3740
  }
3631
- const { response } = await client.fundWallet(faucetUrl, address, chain, {
3632
- timeout,
3633
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
3741
+ const singleChain = targetChains.length === 1;
3742
+ const drips = targetChains.map(async (chain) => {
3743
+ const address = (singleChain ? addressFlag : void 0) ?? addresses[chain];
3744
+ if (!address) {
3745
+ return {
3746
+ chain,
3747
+ funded: false,
3748
+ address: null,
3749
+ error: `no ${chain} address could be derived for this identity` + (chain === "mina" ? " (install the optional mina-signer dependency)" : "")
3750
+ };
3751
+ }
3752
+ const started = Date.now();
3753
+ try {
3754
+ const { response } = await client.fundWallet(faucetUrl, address, chain, {
3755
+ timeout: timeoutFor(chain),
3756
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
3757
+ });
3758
+ return {
3759
+ chain,
3760
+ funded: true,
3761
+ address,
3762
+ response,
3763
+ elapsedMs: Date.now() - started
3764
+ };
3765
+ } catch (err) {
3766
+ return {
3767
+ chain,
3768
+ funded: false,
3769
+ address,
3770
+ error: err instanceof Error ? err.message : String(err),
3771
+ elapsedMs: Date.now() - started
3772
+ };
3773
+ }
3634
3774
  });
3775
+ const results = await Promise.all(drips);
3776
+ const allFunded = results.every((r) => r.funded);
3635
3777
  if (json) {
3636
3778
  io2.emitJson({
3637
3779
  command: "fund",
3638
3780
  identity,
3639
- funded: true,
3781
+ funded: allFunded,
3640
3782
  network: effectiveNetwork ?? null,
3641
- chain,
3642
- address,
3643
3783
  faucetUrl,
3644
- response,
3784
+ results,
3645
3785
  ...inferredDevnet ? { inferredDevnetFrom: devnetOrigin } : {}
3646
3786
  });
3647
- return 0;
3787
+ return allFunded ? 0 : 1;
3788
+ }
3789
+ for (const r of results) {
3790
+ const label = r.chain.padEnd(6);
3791
+ const addr = r.address ? ` \u2192 ${r.address}` : "";
3792
+ if (r.funded) {
3793
+ const slow = r.elapsedMs !== void 0 && r.elapsedMs >= 5e3;
3794
+ const time = slow ? ` (${Math.round(r.elapsedMs / 1e3)}s)` : "";
3795
+ io2.out(` ${label} \u2713 funded (${NATIVE_COIN[r.chain]} + USDC)${addr}${time}`);
3796
+ } else {
3797
+ io2.out(` ${label} \u2717${addr} \u2014 ${r.error ?? "failed"}`);
3798
+ }
3648
3799
  }
3649
- io2.out(`Faucet drip succeeded: ${chain} \u2192 ${address}`);
3650
3800
  io2.out(renderIdentityLine(identity));
3651
3801
  io2.out("Re-check with `rig balance` (a drip can take a few blocks to land).");
3652
- return 0;
3802
+ return allFunded ? 0 : 1;
3653
3803
  } catch (err) {
3654
3804
  return emitCliError(io2, json, "fund", err);
3655
3805
  }
@@ -4093,6 +4243,82 @@ async function defaultReadSecretLine(prompt, isInteractive) {
4093
4243
  // src/cli/init.ts
4094
4244
  import { basename as basename2 } from "path";
4095
4245
  import { parseArgs as parseArgs11 } from "util";
4246
+
4247
+ // src/cli/git-author.ts
4248
+ var PROFILE_KIND = 0;
4249
+ var DEFAULT_PROFILE_TIMEOUT_MS = 3e3;
4250
+ var PROFILE_TIMEOUT_ENV = "RIG_PROFILE_TIMEOUT_MS";
4251
+ function isWebSocketRelay(url) {
4252
+ return /^wss?:\/\//i.test(url);
4253
+ }
4254
+ function displayNameFromKind0(content) {
4255
+ let parsed;
4256
+ try {
4257
+ parsed = JSON.parse(content);
4258
+ } catch {
4259
+ return void 0;
4260
+ }
4261
+ if (typeof parsed !== "object" || parsed === null) return void 0;
4262
+ const fields = parsed;
4263
+ for (const key of ["display_name", "name"]) {
4264
+ const value = fields[key];
4265
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
4266
+ }
4267
+ return void 0;
4268
+ }
4269
+ async function resolveGitAuthor(options) {
4270
+ const npub = hexToNpub(options.pubkey);
4271
+ const email = `${npub}@nostr`;
4272
+ const fallback = { name: npub, email, npub, source: "npub" };
4273
+ const { relayUrl } = options;
4274
+ if (relayUrl === void 0 || !isWebSocketRelay(relayUrl)) {
4275
+ return fallback;
4276
+ }
4277
+ const query = options.queryRelayImpl ?? queryRelay;
4278
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROFILE_TIMEOUT_MS;
4279
+ const factory = options.webSocketFactory ?? defaultWebSocketFactory2;
4280
+ try {
4281
+ const events = await query(
4282
+ relayUrl,
4283
+ { kinds: [PROFILE_KIND], authors: [options.pubkey], limit: 20 },
4284
+ timeoutMs,
4285
+ factory
4286
+ );
4287
+ let latest;
4288
+ for (const event of events) {
4289
+ if (event.kind !== PROFILE_KIND) continue;
4290
+ if (event.pubkey !== options.pubkey) continue;
4291
+ if (!latest || event.created_at > latest.created_at) {
4292
+ latest = { created_at: event.created_at, content: event.content };
4293
+ }
4294
+ }
4295
+ if (latest) {
4296
+ const name = displayNameFromKind0(latest.content);
4297
+ if (name !== void 0) {
4298
+ return { name, email, npub, source: "profile" };
4299
+ }
4300
+ }
4301
+ } catch {
4302
+ }
4303
+ return fallback;
4304
+ }
4305
+ function profileTimeoutFromEnv(env) {
4306
+ const raw = env[PROFILE_TIMEOUT_ENV]?.trim();
4307
+ if (!raw) return void 0;
4308
+ const ms = Number.parseInt(raw, 10);
4309
+ return Number.isFinite(ms) && ms > 0 ? ms : void 0;
4310
+ }
4311
+ function defaultWebSocketFactory2(url) {
4312
+ const ctor = globalThis.WebSocket;
4313
+ if (!ctor) {
4314
+ throw new Error(
4315
+ "No global WebSocket constructor (Node >= 22 required) for the kind:0 read"
4316
+ );
4317
+ }
4318
+ return new ctor(url);
4319
+ }
4320
+
4321
+ // src/cli/init.ts
4096
4322
  var INIT_USAGE = `Usage: rig init [options]
4097
4323
 
4098
4324
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -4101,19 +4327,35 @@ keystore/config), then writes toon.repoid and toon.owner to the repo's
4101
4327
  LOCAL git config. Re-running updates and reports; it never errors on an
4102
4328
  already-initialized repo. The seed phrase is never written anywhere.
4103
4329
 
4330
+ It also sets the repo's LOCAL git commit-author from your nostr identity
4331
+ (never --global) so \`rig commit\`/\`git commit\` work out of the box and every
4332
+ commit is attributed to the signer: user.email = <npub>@nostr, user.name =
4333
+ your kind:0 profile display name when published (read best-effort from a
4334
+ relay, latest-wins) else the npub. Re-running refreshes the name from a
4335
+ now-readable profile.
4336
+
4104
4337
  The follow-up step is adding a relay: \`rig remote add origin <relay-url>\`
4105
4338
  (a real git remote \u2014 \`git remote -v\` shows it). A deprecated v0.1
4106
4339
  \`git config toon.relay\` is migrated to the origin remote automatically
4107
4340
  when no origin exists (the old key stays readable and is removed in v0.3).
4108
4341
 
4109
- When NO identity resolves, init does not dead-end: in a terminal it offers to
4110
- generate one (\`Create a new identity now? [y/N]\`), and \`--generate-identity\`
4111
- does it non-interactively (equivalent to \`rig identity create\` \u2014 the phrase
4112
- is shown once to back up). Nothing is ever auto-generated without your yes.
4342
+ When the directory is NOT inside a git repository, init does not dead-end: in
4343
+ a terminal it offers to create one (\`Initialize a git repository here?
4344
+ [y/N]\`), and \`--git-init\` does it non-interactively. Likewise, when NO
4345
+ identity resolves it offers to generate one (\`Create a new identity now?
4346
+ [y/N]\`), and \`--generate-identity\` does it non-interactively (equivalent to
4347
+ \`rig identity create\` \u2014 the phrase is shown once to back up). Nothing \u2014 repo
4348
+ or identity \u2014 is ever created without your yes. \`--git-init
4349
+ --generate-identity\` is a fully non-interactive fresh setup.
4113
4350
 
4114
4351
  Options:
4115
4352
  --repo-id <id> repository id / NIP-34 d-tag (default: the existing
4116
4353
  toon.repoid, then the repo directory basename)
4354
+ --git-init when the cwd is not a git repo, run \`git init\` here
4355
+ first, then proceed (no prompt)
4356
+ --relay <url> relay to read your kind:0 profile from for the git
4357
+ author name (default: origin/toon.relay, then the
4358
+ genesis seed; skipped if none is resolvable)
4117
4359
  --generate-identity when no identity resolves, mint a fresh one (no prompt)
4118
4360
  --json machine-readable report
4119
4361
  -h, --help show this help`;
@@ -4122,6 +4364,8 @@ function parseInitArgs(args) {
4122
4364
  args,
4123
4365
  options: {
4124
4366
  "repo-id": { type: "string" },
4367
+ "git-init": { type: "boolean", default: false },
4368
+ relay: { type: "string" },
4125
4369
  "generate-identity": { type: "boolean", default: false },
4126
4370
  json: { type: "boolean", default: false },
4127
4371
  help: { type: "boolean", short: "h", default: false }
@@ -4132,6 +4376,7 @@ function parseInitArgs(args) {
4132
4376
  throw new Error(`rig init takes no positional arguments`);
4133
4377
  }
4134
4378
  const flags = {
4379
+ gitInit: values["git-init"] ?? false,
4135
4380
  generateIdentity: values["generate-identity"] ?? false,
4136
4381
  json: values.json ?? false,
4137
4382
  help: values.help ?? false
@@ -4141,8 +4386,46 @@ function parseInitArgs(args) {
4141
4386
  if (repoId.trim() === "") throw new Error("--repo-id must not be empty");
4142
4387
  flags.repoId = repoId;
4143
4388
  }
4389
+ const relay = values.relay;
4390
+ if (relay !== void 0) {
4391
+ if (relay.trim() === "") throw new Error("--relay must not be empty");
4392
+ flags.relay = relay.trim();
4393
+ }
4144
4394
  return flags;
4145
4395
  }
4396
+ function isWebSocketRelay2(url) {
4397
+ return /^wss?:\/\//i.test(url);
4398
+ }
4399
+ async function resolveProfileRelay(flagRelay, originRelay, toonRelays) {
4400
+ const candidates = [
4401
+ flagRelay,
4402
+ originRelay,
4403
+ ...toonRelays
4404
+ ].filter((u) => u !== void 0 && isWebSocketRelay2(u));
4405
+ if (candidates[0] !== void 0) return candidates[0];
4406
+ try {
4407
+ const { loadGenesisSeed } = await import("../network-bootstrap-WNSHRC5P.js");
4408
+ const seed = loadGenesisSeed();
4409
+ if (seed?.relayUrl && isWebSocketRelay2(seed.relayUrl)) return seed.relayUrl;
4410
+ } catch {
4411
+ }
4412
+ return void 0;
4413
+ }
4414
+ async function resolveOrInitGitRepo(deps, flags) {
4415
+ const { io: io2 } = deps;
4416
+ try {
4417
+ return { repoRoot: await resolveRepoRoot(deps.cwd), initialized: false };
4418
+ } catch {
4419
+ let doInit = flags.gitInit;
4420
+ if (!doInit && io2.isInteractive && !flags.json) {
4421
+ io2.err("Not a git repository \u2014 rig keeps its config and objects in one.");
4422
+ doInit = await io2.confirm("Initialize a git repository here? [y/N] ");
4423
+ }
4424
+ if (!doInit) throw new NotAGitRepositoryError(deps.cwd);
4425
+ await initGitRepository(deps.cwd);
4426
+ return { repoRoot: await resolveRepoRoot(deps.cwd), initialized: true };
4427
+ }
4428
+ }
4146
4429
  async function resolveOrGenerateIdentity(deps, flags) {
4147
4430
  const { io: io2, env } = deps;
4148
4431
  const resolve2 = deps.resolveIdentityImpl ?? resolveIdentity;
@@ -4188,12 +4471,7 @@ async function runInit(args, deps) {
4188
4471
  return 0;
4189
4472
  }
4190
4473
  try {
4191
- let repoRoot;
4192
- try {
4193
- repoRoot = await resolveRepoRoot(deps.cwd);
4194
- } catch {
4195
- throw new NotAGitRepositoryError(deps.cwd);
4196
- }
4474
+ const { repoRoot, initialized: initializedGitRepo } = await resolveOrInitGitRepo(deps, flags);
4197
4475
  const { identity, generated } = await resolveOrGenerateIdentity(deps, flags);
4198
4476
  const existing = await readToonConfig(repoRoot);
4199
4477
  const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
@@ -4212,10 +4490,26 @@ async function runInit(args, deps) {
4212
4490
  }
4213
4491
  const origin = remotes.find((r) => r.name === "origin");
4214
4492
  const originRelay = origin !== void 0 && origin.urls.length === 1 && isRelayUrl(origin.urls[0]) ? origin.urls[0] : void 0;
4493
+ const profileRelay = await resolveProfileRelay(
4494
+ flags.relay,
4495
+ originRelay,
4496
+ existing.relays
4497
+ );
4498
+ const resolveAuthor = deps.resolveGitAuthorImpl ?? resolveGitAuthor;
4499
+ const gitAuthor = await resolveAuthor({
4500
+ pubkey: identity.pubkey,
4501
+ relayUrl: profileRelay,
4502
+ ...profileTimeoutFromEnv(deps.env) !== void 0 ? { timeoutMs: profileTimeoutFromEnv(deps.env) } : {}
4503
+ });
4504
+ await setGitAuthor(repoRoot, {
4505
+ name: gitAuthor.name,
4506
+ email: gitAuthor.email
4507
+ });
4215
4508
  if (flags.json) {
4216
4509
  const output = {
4217
4510
  command: "init",
4218
4511
  repoRoot,
4512
+ initializedGitRepo,
4219
4513
  repoId,
4220
4514
  owner: identity.pubkey,
4221
4515
  identity: {
@@ -4228,6 +4522,11 @@ async function runInit(args, deps) {
4228
4522
  remotes,
4229
4523
  origin: originRelay ?? null,
4230
4524
  migratedToonRelay,
4525
+ gitAuthor: {
4526
+ name: gitAuthor.name,
4527
+ email: gitAuthor.email,
4528
+ source: gitAuthor.source
4529
+ },
4231
4530
  changed,
4232
4531
  ...generated ? {
4233
4532
  generatedIdentity: {
@@ -4251,6 +4550,7 @@ async function runInit(args, deps) {
4251
4550
  for (const line of createdIdentityBanner(generated)) io2.out(line);
4252
4551
  if (generated.shadowedBy) io2.err(shadowNote(generated.shadowedBy));
4253
4552
  }
4553
+ if (initializedGitRepo) io2.out(`Created a git repository at ${repoRoot}`);
4254
4554
  io2.out(`Initialized rig for ${repoRoot}`);
4255
4555
  io2.out(renderIdentityLine(identity));
4256
4556
  io2.out(
@@ -4259,6 +4559,9 @@ async function runInit(args, deps) {
4259
4559
  io2.out(
4260
4560
  ` toon.owner = ${identity.pubkey}` + (changed.owner ? "" : " (unchanged)") + (existing.owner && changed.owner ? ` (was ${existing.owner})` : "")
4261
4561
  );
4562
+ io2.out(
4563
+ `Git author: ${gitAuthor.name} <${gitAuthor.email}> (from ${gitAuthor.source === "profile" ? "nostr profile" : "npub"})`
4564
+ );
4262
4565
  if (originRelay !== void 0) {
4263
4566
  io2.out(
4264
4567
  ` origin = ${originRelay}` + (migratedToonRelay ? " (migrated from git config toon.relay)" : "")