@toon-protocol/rig 2.2.0 → 2.4.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
@@ -17,20 +17,28 @@ import {
17
17
  setHeadSymref,
18
18
  updateRef,
19
19
  writeGitObjects
20
- } from "../chunk-PS5QOT62.js";
20
+ } from "../chunk-W2SDL2PE.js";
21
21
  import {
22
+ DEFAULT_KEYSTORE_PASSWORD,
22
23
  MissingIdentityError,
24
+ clientConfigPath,
25
+ clientKeystorePath,
26
+ linkKeystoreInClientConfig,
27
+ readClientConfigFile,
23
28
  resolveIdentity
24
- } from "../chunk-CW4HJNMU.js";
29
+ } from "../chunk-XGFBDUQX.js";
25
30
  import {
26
31
  COMMENT_KIND,
32
+ authorizedStatusAuthors,
27
33
  buildComment,
28
34
  buildIssue,
29
35
  buildPatch,
36
+ buildRepoAnnouncement,
30
37
  buildStatus,
31
38
  fetchRemoteState,
39
+ parseMaintainers,
32
40
  queryRelay
33
- } from "../chunk-JBB7HBQC.js";
41
+ } from "../chunk-3HRFDH7H.js";
34
42
  import {
35
43
  ChannelMapStore,
36
44
  channelStatus,
@@ -42,7 +50,7 @@ import {
42
50
  } from "../chunk-X2CZPPDM.js";
43
51
 
44
52
  // src/cli/rig.ts
45
- import { createInterface } from "readline/promises";
53
+ import { createInterface as createInterface2 } from "readline/promises";
46
54
 
47
55
  // src/cli/dispatch.ts
48
56
  import { createRequire } from "module";
@@ -897,7 +905,7 @@ function loadPaidSession(deps, relayUrl) {
897
905
  });
898
906
  }
899
907
  var defaultLoadStandalone = async (options) => {
900
- const mod = await import("../standalone-mode-64CKUVU2.js");
908
+ const mod = await import("../standalone-mode-VH2XPOPK.js");
901
909
  return mod.createStandaloneContext(options);
902
910
  };
903
911
  function identityReport(ctx) {
@@ -2100,10 +2108,11 @@ function tagValue(tags, name) {
2100
2108
  function tagValues(tags, name) {
2101
2109
  return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
2102
2110
  }
2103
- function deriveStatus(targetEventId, statusEvents) {
2111
+ function deriveStatus(targetEventId, statusEvents, authorized) {
2104
2112
  let winner = null;
2105
2113
  for (const event of statusEvents) {
2106
2114
  if (STATUS_BY_KIND[event.kind] === void 0) continue;
2115
+ if (!authorized.has(event.pubkey.toLowerCase())) continue;
2107
2116
  if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
2108
2117
  continue;
2109
2118
  if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
@@ -2184,12 +2193,13 @@ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2184
2193
  } catch {
2185
2194
  }
2186
2195
  let repoAddr = null;
2187
- if (needRepoAddr) {
2188
- const repoId = flags.repoId ?? toonConfig.repoId;
2189
- if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2190
- const owner = flags.owner ?? toonConfig.owner;
2191
- if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
2196
+ const repoId = flags.repoId ?? toonConfig.repoId;
2197
+ const owner = flags.owner ?? toonConfig.owner;
2198
+ if (repoId && owner) {
2192
2199
  repoAddr = { ownerPubkey: owner, repoId };
2200
+ } else if (needRepoAddr) {
2201
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2202
+ throw new UnconfiguredRepoAddressError("repository owner");
2193
2203
  }
2194
2204
  const resolved = await resolveRelays({
2195
2205
  relayFlags: flags.relay,
@@ -2213,6 +2223,34 @@ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2213
2223
  function repoATag(addr) {
2214
2224
  return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
2215
2225
  }
2226
+ async function fetchAuthorizedAuthors(ctx, ownerPubkey, repoId) {
2227
+ let announceTags = [];
2228
+ try {
2229
+ const events = await queryAll(
2230
+ ctx.relays,
2231
+ [
2232
+ {
2233
+ kinds: [REPOSITORY_ANNOUNCEMENT_KIND],
2234
+ authors: [ownerPubkey],
2235
+ "#d": [repoId]
2236
+ }
2237
+ ],
2238
+ ctx.webSocketFactory
2239
+ );
2240
+ let latest = null;
2241
+ for (const event of events.values()) {
2242
+ if (event.kind !== REPOSITORY_ANNOUNCEMENT_KIND) continue;
2243
+ if (event.pubkey !== ownerPubkey) continue;
2244
+ if (!event.tags.some((t) => t[0] === "d" && t[1] === repoId)) continue;
2245
+ if (latest === null || event.created_at > latest.created_at || event.created_at === latest.created_at && event.id < latest.id) {
2246
+ latest = event;
2247
+ }
2248
+ }
2249
+ if (latest) announceTags = latest.tags;
2250
+ } catch {
2251
+ }
2252
+ return authorizedStatusAuthors(ownerPubkey, announceTags);
2253
+ }
2216
2254
  function parseTrackerItem(event, status) {
2217
2255
  const item = {
2218
2256
  eventId: event.id,
@@ -2237,8 +2275,12 @@ function isoDate(createdAt) {
2237
2275
  return new Date(createdAt * 1e3).toISOString().slice(0, 10);
2238
2276
  }
2239
2277
  async function fetchItems(ctx, kind) {
2240
- const aTag = repoATag(
2241
- ctx.repoAddr
2278
+ const addr = ctx.repoAddr;
2279
+ const aTag = repoATag(addr);
2280
+ const authorizedPromise = fetchAuthorizedAuthors(
2281
+ ctx,
2282
+ addr.ownerPubkey,
2283
+ addr.repoId
2242
2284
  );
2243
2285
  const [itemEvents, aStatusEvents] = await Promise.all([
2244
2286
  queryAll(
@@ -2265,8 +2307,9 @@ async function fetchItems(ctx, kind) {
2265
2307
  for (const [id, event] of byE)
2266
2308
  if (!statuses.has(id)) statuses.set(id, event);
2267
2309
  }
2310
+ const authorized = await authorizedPromise;
2268
2311
  return items.map(
2269
- (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values()))
2312
+ (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values(), authorized))
2270
2313
  ).sort((a, b) => b.createdAt - a.createdAt);
2271
2314
  }
2272
2315
  async function fetchItem(ctx, eventId, expectedKind) {
@@ -2305,12 +2348,27 @@ async function fetchItem(ctx, eventId, expectedKind) {
2305
2348
  createdAt: e.created_at,
2306
2349
  content: e.content
2307
2350
  }));
2351
+ const repoATag2 = tagValue(event.tags, "a") ?? null;
2352
+ const parsedAddr = parseRepoATag(repoATag2) ?? ctx.repoAddr;
2353
+ const authorized = parsedAddr ? await fetchAuthorizedAuthors(ctx, parsedAddr.ownerPubkey, parsedAddr.repoId) : /* @__PURE__ */ new Set();
2308
2354
  return {
2309
- item: parseTrackerItem(event, deriveStatus(eventId, statusEvents.values())),
2355
+ item: parseTrackerItem(
2356
+ event,
2357
+ deriveStatus(eventId, statusEvents.values(), authorized)
2358
+ ),
2310
2359
  comments,
2311
- repoATag: tagValue(event.tags, "a") ?? null
2360
+ repoATag: repoATag2
2312
2361
  };
2313
2362
  }
2363
+ function parseRepoATag(aTag) {
2364
+ if (!aTag) return null;
2365
+ const [kind, ownerPubkey, ...repoIdParts] = aTag.split(":");
2366
+ const repoId = repoIdParts.join(":");
2367
+ if (kind !== String(REPOSITORY_ANNOUNCEMENT_KIND) || !ownerPubkey || !repoId) {
2368
+ return null;
2369
+ }
2370
+ return { ownerPubkey, repoId };
2371
+ }
2314
2372
  async function runList(args, deps, spec) {
2315
2373
  const { io: io2 } = deps;
2316
2374
  let flags;
@@ -2663,6 +2721,9 @@ async function runEvent(opts) {
2663
2721
  const addr = { ownerPubkey: owner, repoId };
2664
2722
  const event = await opts.buildEvent(addr);
2665
2723
  const action = `kind:${event.kind} ${actionLabel}`;
2724
+ if (opts.preConfirm) {
2725
+ await opts.preConfirm({ identity, addr, relayUrl: relaysUsed[0] });
2726
+ }
2666
2727
  if (!flags.json) {
2667
2728
  for (const line of renderEventPlan({
2668
2729
  action,
@@ -3109,9 +3170,41 @@ async function runPrStatus(args, deps) {
3109
3170
  ]);
3110
3171
  return event;
3111
3172
  },
3173
+ // Authority warning (#287): a status event only moves an issue/PR's
3174
+ // resolved state for repo-authority-honoring clients if its author is the
3175
+ // owner or a declared maintainer. Warn clearly when the active identity is
3176
+ // neither — the publish still happens (permissionless relay; the caller
3177
+ // may be acting on their own repo/fork), but the futility is made obvious.
3178
+ preConfirm: (args2) => warnIfNotMaintainer(deps, args2),
3112
3179
  sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
3113
3180
  });
3114
3181
  }
3182
+ async function warnIfNotMaintainer(deps, args) {
3183
+ const { io: io2 } = deps;
3184
+ const { identity, addr, relayUrl } = args;
3185
+ if (identity.pubkey.toLowerCase() === addr.ownerPubkey.toLowerCase()) return;
3186
+ if (relayUrl === void 0) return;
3187
+ try {
3188
+ const remote = await fetchRemoteState({
3189
+ relayUrls: [relayUrl],
3190
+ ownerPubkey: addr.ownerPubkey,
3191
+ repoId: addr.repoId,
3192
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
3193
+ });
3194
+ const authorized = authorizedStatusAuthors(
3195
+ addr.ownerPubkey,
3196
+ remote.announceEvent?.tags ?? []
3197
+ );
3198
+ if (authorized.has(identity.pubkey.toLowerCase())) return;
3199
+ io2.err(
3200
+ `warning: you (${identity.pubkey.slice(0, 8)}\u2026) are not a maintainer of 30617:${addr.ownerPubkey.slice(0, 8)}\u2026:${addr.repoId} \u2014 clients honoring repo authority (rig, rig-web) will IGNORE this status when resolving the issue/PR's state. Publishing anyway (the relay is permissionless); ask the owner to \`rig maintainers add\` you if this should stick.`
3201
+ );
3202
+ } catch {
3203
+ io2.err(
3204
+ "warning: could not read the repo announcement to verify your maintainer status \u2014 if you are not the owner or a declared maintainer, authority-honoring clients will ignore this status."
3205
+ );
3206
+ }
3207
+ }
3115
3208
 
3116
3209
  // src/cli/fetch.ts
3117
3210
  import { parseArgs as parseArgs8 } from "util";
@@ -3344,10 +3437,11 @@ var FUND_USAGE = `Usage: rig fund [options]
3344
3437
  Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
3345
3438
  pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
3346
3439
  ~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
3347
- faucetUrl config field, or the deployed devnet faucet when the configured
3348
- network is devnet. The faucet drips a FIXED amount per chain (there is no
3349
- --amount). On a network without a faucet, prints the wallet address(es) to
3350
- fund externally instead.
3440
+ faucetUrl config field, or the deployed devnet faucet when the network is
3441
+ devnet \u2014 including when a configured *.devnet.toonprotocol.dev origin infers
3442
+ it. The faucet drips a FIXED amount per chain (there is no --amount). On a
3443
+ network without a faucet, prints the wallet address(es) to fund externally
3444
+ instead.
3351
3445
 
3352
3446
  Options:
3353
3447
  --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
@@ -3440,7 +3534,10 @@ async function runFund(args, deps) {
3440
3534
  );
3441
3535
  }
3442
3536
  const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
3443
- const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (network === "devnet" ? DEVNET_FAUCET_URL : void 0);
3537
+ const devnetOrigin = sharedDevnetOrigin(env, file);
3538
+ const inferredDevnet = devnetOrigin !== void 0 && (network === void 0 || network === "custom");
3539
+ const effectiveNetwork = inferredDevnet ? "devnet" : network;
3540
+ const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (effectiveNetwork === "devnet" ? DEVNET_FAUCET_URL : void 0);
3444
3541
  const resolved = await resolveIdentity({
3445
3542
  env,
3446
3543
  cwd: deps.cwd,
@@ -3462,7 +3559,7 @@ async function runFund(args, deps) {
3462
3559
  mina: derived.mina.publicKey || null
3463
3560
  };
3464
3561
  if (!faucetUrl) {
3465
- const guidance = noFaucetGuidance(network, sharedDevnetOrigin(env, file));
3562
+ const guidance = noFaucetGuidance(network, devnetOrigin);
3466
3563
  if (json) {
3467
3564
  io2.emitJson({
3468
3565
  command: "fund",
@@ -3491,6 +3588,11 @@ async function runFund(args, deps) {
3491
3588
  }
3492
3589
  const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
3493
3590
  const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
3591
+ if (!json && inferredDevnet) {
3592
+ io2.out(
3593
+ `Inferred network 'devnet' from the configured origin ${devnetOrigin} (network was ${JSON.stringify(network ?? "custom")}). Set TOON_CLIENT_NETWORK explicitly to override.`
3594
+ );
3595
+ }
3494
3596
  if (!json) {
3495
3597
  io2.out(
3496
3598
  `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
@@ -3505,11 +3607,12 @@ async function runFund(args, deps) {
3505
3607
  command: "fund",
3506
3608
  identity,
3507
3609
  funded: true,
3508
- network: network ?? null,
3610
+ network: effectiveNetwork ?? null,
3509
3611
  chain,
3510
3612
  address,
3511
3613
  faucetUrl,
3512
- response
3614
+ response,
3615
+ ...inferredDevnet ? { inferredDevnetFrom: devnetOrigin } : {}
3513
3616
  });
3514
3617
  return 0;
3515
3618
  }
@@ -3570,9 +3673,396 @@ var runGitPassthrough = (argv2, options = {}) => {
3570
3673
  });
3571
3674
  };
3572
3675
 
3676
+ // src/cli/identity-cmd.ts
3677
+ import { existsSync, mkdirSync } from "fs";
3678
+ import { dirname as dirname2 } from "path";
3679
+ import { createInterface } from "readline/promises";
3680
+ import { parseArgs as parseArgs10 } from "util";
3681
+ async function loadKeyOps() {
3682
+ return await import("@toon-protocol/client");
3683
+ }
3684
+ var IdentityExistsError = class extends Error {
3685
+ constructor(pubkey, sourceLabel) {
3686
+ super(
3687
+ `an identity already resolves (${pubkey}, from ${sourceLabel}) \u2014 rig will not overwrite it. Inspect it with \`rig identity show\`, or pass \`--force\` to replace the keystore with a brand-new identity (the old phrase becomes unrecoverable unless you backed it up).`
3688
+ );
3689
+ this.pubkey = pubkey;
3690
+ this.sourceLabel = sourceLabel;
3691
+ this.name = "IdentityExistsError";
3692
+ }
3693
+ pubkey;
3694
+ sourceLabel;
3695
+ };
3696
+ var KeystoreFileExistsError = class extends Error {
3697
+ constructor(keystorePath) {
3698
+ super(
3699
+ `a keystore file already exists at ${keystorePath} \u2014 rig will not overwrite it. Link it via the client config, or pass \`--force\` to replace it with a new identity.`
3700
+ );
3701
+ this.keystorePath = keystorePath;
3702
+ this.name = "KeystoreFileExistsError";
3703
+ }
3704
+ keystorePath;
3705
+ };
3706
+ async function prepareKeystoreTarget(input) {
3707
+ const keyOps = await (input.loadKeyOps ?? loadKeyOps)();
3708
+ const resolveImpl = input.resolveIdentityImpl ?? resolveIdentity;
3709
+ let existing;
3710
+ try {
3711
+ existing = await resolveImpl({
3712
+ env: input.env,
3713
+ cwd: input.cwd,
3714
+ warn: input.warn
3715
+ });
3716
+ } catch (err) {
3717
+ if (!(err instanceof MissingIdentityError)) throw err;
3718
+ }
3719
+ if (existing && !input.force) {
3720
+ throw new IdentityExistsError(existing.pubkey, existing.sourceLabel);
3721
+ }
3722
+ const keystorePath = clientKeystorePath(input.env);
3723
+ if (existsSync(keystorePath) && !input.force) {
3724
+ throw new KeystoreFileExistsError(keystorePath);
3725
+ }
3726
+ const envPassword = input.env["TOON_CLIENT_KEYSTORE_PASSWORD"]?.trim() || void 0;
3727
+ const autoPassword = envPassword === void 0;
3728
+ const password = envPassword ?? DEFAULT_KEYSTORE_PASSWORD;
3729
+ mkdirSync(dirname2(keystorePath), { recursive: true });
3730
+ return { keystorePath, password, autoPassword, existing, keyOps };
3731
+ }
3732
+ function readAccountIndex(env) {
3733
+ const file = readClientConfigFile(clientConfigPath(env));
3734
+ return typeof file.mnemonicAccountIndex === "number" ? file.mnemonicAccountIndex : 0;
3735
+ }
3736
+ function finishMint(input, target, mnemonic) {
3737
+ const configPath = linkKeystoreInClientConfig(
3738
+ input.env,
3739
+ target.keystorePath,
3740
+ target.autoPassword
3741
+ );
3742
+ const accountIndex = readAccountIndex(input.env);
3743
+ const { pubkey } = target.keyOps.deriveNostrKeyFromMnemonic(
3744
+ mnemonic,
3745
+ accountIndex
3746
+ );
3747
+ const result = {
3748
+ mnemonic,
3749
+ pubkey,
3750
+ keystorePath: target.keystorePath,
3751
+ configPath,
3752
+ autoPassword: target.autoPassword,
3753
+ accountIndex
3754
+ };
3755
+ if (target.existing && target.existing.pubkey !== pubkey) {
3756
+ result.shadowedBy = {
3757
+ sourceLabel: target.existing.sourceLabel,
3758
+ pubkey: target.existing.pubkey
3759
+ };
3760
+ }
3761
+ return result;
3762
+ }
3763
+ async function createIdentity(input) {
3764
+ const target = await prepareKeystoreTarget(input);
3765
+ const { mnemonic } = target.keyOps.generateKeystore(
3766
+ target.keystorePath,
3767
+ target.password
3768
+ );
3769
+ return finishMint(input, target, mnemonic);
3770
+ }
3771
+ async function importIdentity(input) {
3772
+ const target = await prepareKeystoreTarget(input);
3773
+ target.keyOps.importKeystore(
3774
+ target.keystorePath,
3775
+ input.mnemonic,
3776
+ target.password
3777
+ );
3778
+ return finishMint(input, target, input.mnemonic);
3779
+ }
3780
+ function createdIdentityBanner(result) {
3781
+ return [
3782
+ "",
3783
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
3784
+ " rig: generated a new identity",
3785
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
3786
+ ` Nostr pubkey : ${result.pubkey}`,
3787
+ "",
3788
+ " Seed phrase \u2014 this phrase CONTROLS YOUR FUNDS. Write it down and",
3789
+ " store it safely. It is shown ONCE and CANNOT be recovered:",
3790
+ "",
3791
+ ` ${result.mnemonic}`,
3792
+ "",
3793
+ ` Encrypted keystore: ${result.keystorePath}`,
3794
+ result.autoPassword ? " Encrypted with the default password, so the identity reloads with no\n env var. To use your own password: set TOON_CLIENT_KEYSTORE_PASSWORD\n and re-import with `rig identity import`." : " Encrypted with your TOON_CLIENT_KEYSTORE_PASSWORD.",
3795
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
3796
+ ""
3797
+ ];
3798
+ }
3799
+ function shadowNote(shadowedBy) {
3800
+ return `note: a higher-precedence identity (${shadowedBy.pubkey}, from ${shadowedBy.sourceLabel}) still shadows the new keystore \u2014 rig keeps using it until you remove that source (unset the env var / edit .env).`;
3801
+ }
3802
+ var IDENTITY_USAGE = `Usage: rig identity <command> [options]
3803
+
3804
+ Manage the BIP-39 signing identity rig uses to sign and pay:
3805
+
3806
+ create generate a fresh identity into the encrypted keystore under
3807
+ TOON_CLIENT_HOME. The seed phrase is shown ONCE \u2014 back it up.
3808
+ Refuses to overwrite an existing identity without --force.
3809
+ show report the ACTIVE identity's source + derived pubkey (never the
3810
+ phrase); errors with the resolution remediation when none exists.
3811
+ import read an EXISTING phrase from stdin (never a CLI argument) and write
3812
+ it to the encrypted keystore.
3813
+
3814
+ Options:
3815
+ --force (create/import) replace an existing identity/keystore
3816
+ --json machine-readable output. NOTE: \`identity create --json\`
3817
+ DELIBERATELY emits the seed phrase in a \`mnemonic\`
3818
+ field \u2014 treat that output as SECRET. \`identity show\`
3819
+ and \`identity import\` never emit the phrase.
3820
+ -h, --help show this help
3821
+
3822
+ The keystore is encrypted with TOON_CLIENT_KEYSTORE_PASSWORD when that env var
3823
+ is set, else a default password so the identity reloads with no configuration.
3824
+ rig deliberately does NOT accept the keystore password as a CLI flag \u2014 that
3825
+ would leak the value that decrypts your keys to shell history and \`ps\`.`;
3826
+ async function runIdentity(args, deps) {
3827
+ const [sub, ...rest] = args;
3828
+ switch (sub) {
3829
+ case "create":
3830
+ return runIdentityCreate(rest, deps);
3831
+ case "show":
3832
+ return runIdentityShow(rest, deps);
3833
+ case "import":
3834
+ return runIdentityImport(rest, deps);
3835
+ case "help":
3836
+ case "--help":
3837
+ case "-h":
3838
+ deps.io.out(IDENTITY_USAGE);
3839
+ return 0;
3840
+ case void 0:
3841
+ deps.io.err("rig identity needs a subcommand: create | show | import");
3842
+ deps.io.err(IDENTITY_USAGE);
3843
+ return 2;
3844
+ default:
3845
+ deps.io.err(`unknown rig identity subcommand: ${sub}`);
3846
+ deps.io.err(IDENTITY_USAGE);
3847
+ return 2;
3848
+ }
3849
+ }
3850
+ function parseMintArgs(args, verb) {
3851
+ const { values, positionals } = parseArgs10({
3852
+ args,
3853
+ options: {
3854
+ force: { type: "boolean", default: false },
3855
+ json: { type: "boolean", default: false },
3856
+ help: { type: "boolean", short: "h", default: false }
3857
+ },
3858
+ allowPositionals: false
3859
+ });
3860
+ if (positionals.length > 0) {
3861
+ throw new Error(`rig identity ${verb} takes no positional arguments`);
3862
+ }
3863
+ return {
3864
+ force: values.force ?? false,
3865
+ json: values.json ?? false,
3866
+ help: values.help ?? false
3867
+ };
3868
+ }
3869
+ async function runIdentityCreate(args, deps) {
3870
+ const { io: io2 } = deps;
3871
+ let flags;
3872
+ try {
3873
+ flags = parseMintArgs(args, "create");
3874
+ } catch (err) {
3875
+ io2.err(err instanceof Error ? err.message : String(err));
3876
+ io2.err(IDENTITY_USAGE);
3877
+ return 2;
3878
+ }
3879
+ if (flags.help) {
3880
+ io2.out(IDENTITY_USAGE);
3881
+ return 0;
3882
+ }
3883
+ try {
3884
+ const result = await createIdentity({
3885
+ env: deps.env,
3886
+ cwd: deps.cwd,
3887
+ force: flags.force,
3888
+ warn: (line) => io2.err(line),
3889
+ ...deps.resolveIdentityImpl ? { resolveIdentityImpl: deps.resolveIdentityImpl } : {},
3890
+ ...deps.loadKeyOps ? { loadKeyOps: deps.loadKeyOps } : {}
3891
+ });
3892
+ emitCreated(io2, flags.json, "identity create", result);
3893
+ return 0;
3894
+ } catch (err) {
3895
+ return emitMintError(io2, flags.json, "identity create", err);
3896
+ }
3897
+ }
3898
+ async function runIdentityImport(args, deps) {
3899
+ const { io: io2 } = deps;
3900
+ let flags;
3901
+ try {
3902
+ flags = parseMintArgs(args, "import");
3903
+ } catch (err) {
3904
+ io2.err(err instanceof Error ? err.message : String(err));
3905
+ io2.err(IDENTITY_USAGE);
3906
+ return 2;
3907
+ }
3908
+ if (flags.help) {
3909
+ io2.out(IDENTITY_USAGE);
3910
+ return 0;
3911
+ }
3912
+ try {
3913
+ const read = deps.readSecretLine ?? defaultReadSecretLine;
3914
+ const phrase = (await read(
3915
+ "Paste your BIP-39 seed phrase (read from stdin, not the terminal history): ",
3916
+ io2.isInteractive
3917
+ )).trim();
3918
+ if (phrase === "") {
3919
+ throw new Error(
3920
+ "no seed phrase provided on stdin \u2014 pipe it in or type it at the prompt (never pass a phrase as a CLI argument; it leaks to shell history and `ps`)."
3921
+ );
3922
+ }
3923
+ const result = await importIdentity({
3924
+ env: deps.env,
3925
+ cwd: deps.cwd,
3926
+ force: flags.force,
3927
+ mnemonic: phrase,
3928
+ warn: (line) => io2.err(line),
3929
+ ...deps.resolveIdentityImpl ? { resolveIdentityImpl: deps.resolveIdentityImpl } : {},
3930
+ ...deps.loadKeyOps ? { loadKeyOps: deps.loadKeyOps } : {}
3931
+ });
3932
+ if (flags.json) {
3933
+ io2.emitJson({
3934
+ command: "identity import",
3935
+ imported: true,
3936
+ pubkey: result.pubkey,
3937
+ keystorePath: result.keystorePath,
3938
+ autoPassword: result.autoPassword,
3939
+ ...result.shadowedBy ? { shadowedBy: result.shadowedBy } : {}
3940
+ });
3941
+ } else {
3942
+ io2.out(`Imported identity ${result.pubkey}`);
3943
+ io2.out(` Encrypted keystore: ${result.keystorePath}`);
3944
+ }
3945
+ if (result.shadowedBy) io2.err(shadowNote(result.shadowedBy));
3946
+ return 0;
3947
+ } catch (err) {
3948
+ return emitMintError(io2, flags.json, "identity import", err);
3949
+ }
3950
+ }
3951
+ async function runIdentityShow(args, deps) {
3952
+ const { io: io2 } = deps;
3953
+ let json = false;
3954
+ let help = false;
3955
+ try {
3956
+ const parsed = parseArgs10({
3957
+ args,
3958
+ options: {
3959
+ json: { type: "boolean", default: false },
3960
+ help: { type: "boolean", short: "h", default: false }
3961
+ },
3962
+ allowPositionals: false
3963
+ });
3964
+ json = parsed.values.json ?? false;
3965
+ help = parsed.values.help ?? false;
3966
+ } catch (err) {
3967
+ io2.err(err instanceof Error ? err.message : String(err));
3968
+ io2.err(IDENTITY_USAGE);
3969
+ return 2;
3970
+ }
3971
+ if (help) {
3972
+ io2.out(IDENTITY_USAGE);
3973
+ return 0;
3974
+ }
3975
+ try {
3976
+ const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
3977
+ env: deps.env,
3978
+ cwd: deps.cwd,
3979
+ warn: (line) => io2.err(line)
3980
+ });
3981
+ if (json) {
3982
+ io2.emitJson({
3983
+ command: "identity show",
3984
+ source: identity.source,
3985
+ sourceLabel: identity.sourceLabel,
3986
+ pubkey: identity.pubkey,
3987
+ accountIndex: identity.accountIndex
3988
+ });
3989
+ } else {
3990
+ io2.out(renderIdentityLine(identity));
3991
+ io2.out(` source : ${identity.source} (${identity.sourceLabel})`);
3992
+ io2.out(` pubkey : ${identity.pubkey}`);
3993
+ }
3994
+ return 0;
3995
+ } catch (err) {
3996
+ return emitCliError(io2, json, "identity show", err);
3997
+ }
3998
+ }
3999
+ function emitCreated(io2, json, command, result) {
4000
+ if (json) {
4001
+ io2.emitJson({
4002
+ command,
4003
+ created: true,
4004
+ pubkey: result.pubkey,
4005
+ keystorePath: result.keystorePath,
4006
+ autoPassword: result.autoPassword,
4007
+ // DELIBERATE, documented exception to "never print the phrase": the
4008
+ // scripting/agent path needs the generated phrase to back it up.
4009
+ mnemonic: result.mnemonic,
4010
+ ...result.shadowedBy ? { shadowedBy: result.shadowedBy } : {}
4011
+ });
4012
+ io2.err(
4013
+ "rig: `--json` emitted your seed phrase in the `mnemonic` field \u2014 this output is SECRET; store it safely and do not log or share it."
4014
+ );
4015
+ } else {
4016
+ for (const line of createdIdentityBanner(result)) io2.out(line);
4017
+ }
4018
+ if (result.shadowedBy) io2.err(shadowNote(result.shadowedBy));
4019
+ }
4020
+ function emitMintError(io2, json, command, err) {
4021
+ if (err instanceof IdentityExistsError) {
4022
+ if (json) {
4023
+ io2.emitJson({
4024
+ command,
4025
+ error: "identity_exists",
4026
+ pubkey: err.pubkey,
4027
+ detail: err.message
4028
+ });
4029
+ }
4030
+ for (const line of err.message.split("\n")) io2.err(line);
4031
+ return 1;
4032
+ }
4033
+ if (err instanceof KeystoreFileExistsError) {
4034
+ if (json) {
4035
+ io2.emitJson({
4036
+ command,
4037
+ error: "keystore_exists",
4038
+ keystorePath: err.keystorePath,
4039
+ detail: err.message
4040
+ });
4041
+ }
4042
+ for (const line of err.message.split("\n")) io2.err(line);
4043
+ return 1;
4044
+ }
4045
+ return emitCliError(io2, json, command, err);
4046
+ }
4047
+ async function defaultReadSecretLine(prompt, isInteractive) {
4048
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
4049
+ if (isInteractive) {
4050
+ process.stderr.write(prompt);
4051
+ rl._writeToOutput = () => {
4052
+ };
4053
+ }
4054
+ try {
4055
+ const answer = await rl.question("");
4056
+ if (isInteractive) process.stderr.write("\n");
4057
+ return answer;
4058
+ } finally {
4059
+ rl.close();
4060
+ }
4061
+ }
4062
+
3573
4063
  // src/cli/init.ts
3574
4064
  import { basename as basename2 } from "path";
3575
- import { parseArgs as parseArgs10 } from "util";
4065
+ import { parseArgs as parseArgs11 } from "util";
3576
4066
  var INIT_USAGE = `Usage: rig init [options]
3577
4067
 
3578
4068
  Set up the current git repository for rig (one-shot, idempotent, free):
@@ -3586,16 +4076,23 @@ The follow-up step is adding a relay: \`rig remote add origin <relay-url>\`
3586
4076
  \`git config toon.relay\` is migrated to the origin remote automatically
3587
4077
  when no origin exists (the old key stays readable and is removed in v0.3).
3588
4078
 
4079
+ When NO identity resolves, init does not dead-end: in a terminal it offers to
4080
+ generate one (\`Create a new identity now? [y/N]\`), and \`--generate-identity\`
4081
+ does it non-interactively (equivalent to \`rig identity create\` \u2014 the phrase
4082
+ is shown once to back up). Nothing is ever auto-generated without your yes.
4083
+
3589
4084
  Options:
3590
- --repo-id <id> repository id / NIP-34 d-tag (default: the existing
3591
- toon.repoid, then the repo directory basename)
3592
- --json machine-readable report
3593
- -h, --help show this help`;
4085
+ --repo-id <id> repository id / NIP-34 d-tag (default: the existing
4086
+ toon.repoid, then the repo directory basename)
4087
+ --generate-identity when no identity resolves, mint a fresh one (no prompt)
4088
+ --json machine-readable report
4089
+ -h, --help show this help`;
3594
4090
  function parseInitArgs(args) {
3595
- const { values, positionals } = parseArgs10({
4091
+ const { values, positionals } = parseArgs11({
3596
4092
  args,
3597
4093
  options: {
3598
4094
  "repo-id": { type: "string" },
4095
+ "generate-identity": { type: "boolean", default: false },
3599
4096
  json: { type: "boolean", default: false },
3600
4097
  help: { type: "boolean", short: "h", default: false }
3601
4098
  },
@@ -3605,6 +4102,7 @@ function parseInitArgs(args) {
3605
4102
  throw new Error(`rig init takes no positional arguments`);
3606
4103
  }
3607
4104
  const flags = {
4105
+ generateIdentity: values["generate-identity"] ?? false,
3608
4106
  json: values.json ?? false,
3609
4107
  help: values.help ?? false
3610
4108
  };
@@ -3615,8 +4113,38 @@ function parseInitArgs(args) {
3615
4113
  }
3616
4114
  return flags;
3617
4115
  }
3618
- async function runInit(args, deps) {
4116
+ async function resolveOrGenerateIdentity(deps, flags) {
3619
4117
  const { io: io2, env } = deps;
4118
+ const resolve2 = deps.resolveIdentityImpl ?? resolveIdentity;
4119
+ try {
4120
+ return { identity: await resolve2({ env, cwd: deps.cwd, warn: io2.err }) };
4121
+ } catch (err) {
4122
+ if (!(err instanceof MissingIdentityError)) throw err;
4123
+ let generate = flags.generateIdentity;
4124
+ if (!generate && io2.isInteractive && !flags.json) {
4125
+ io2.err("No identity found \u2014 rig needs one to sign and pay.");
4126
+ generate = await io2.confirm("Create a new identity now? [y/N] ");
4127
+ }
4128
+ if (!generate) throw err;
4129
+ const created = await (deps.createIdentityImpl ?? createIdentity)({
4130
+ env,
4131
+ cwd: deps.cwd,
4132
+ force: false,
4133
+ warn: io2.err,
4134
+ ...deps.resolveIdentityImpl ? { resolveIdentityImpl: deps.resolveIdentityImpl } : {}
4135
+ });
4136
+ const identity = {
4137
+ mnemonic: created.mnemonic,
4138
+ accountIndex: created.accountIndex,
4139
+ source: "keystore",
4140
+ sourceLabel: created.keystorePath,
4141
+ pubkey: created.pubkey
4142
+ };
4143
+ return { identity, generated: created };
4144
+ }
4145
+ }
4146
+ async function runInit(args, deps) {
4147
+ const { io: io2 } = deps;
3620
4148
  let flags;
3621
4149
  try {
3622
4150
  flags = parseInitArgs(args);
@@ -3636,11 +4164,7 @@ async function runInit(args, deps) {
3636
4164
  } catch {
3637
4165
  throw new NotAGitRepositoryError(deps.cwd);
3638
4166
  }
3639
- const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
3640
- env,
3641
- cwd: deps.cwd,
3642
- warn: (line) => io2.err(line)
3643
- });
4167
+ const { identity, generated } = await resolveOrGenerateIdentity(deps, flags);
3644
4168
  const existing = await readToonConfig(repoRoot);
3645
4169
  const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
3646
4170
  const changed = {
@@ -3674,11 +4198,29 @@ async function runInit(args, deps) {
3674
4198
  remotes,
3675
4199
  origin: originRelay ?? null,
3676
4200
  migratedToonRelay,
3677
- changed
4201
+ changed,
4202
+ ...generated ? {
4203
+ generatedIdentity: {
4204
+ mnemonic: generated.mnemonic,
4205
+ pubkey: generated.pubkey,
4206
+ keystorePath: generated.keystorePath,
4207
+ autoPassword: generated.autoPassword
4208
+ }
4209
+ } : {}
3678
4210
  };
3679
4211
  io2.emitJson(output);
4212
+ if (generated) {
4213
+ io2.err(
4214
+ "rig: `--json` emitted your new seed phrase in `generatedIdentity.mnemonic` \u2014 this output is SECRET; store it safely and do not log or share it."
4215
+ );
4216
+ if (generated.shadowedBy) io2.err(shadowNote(generated.shadowedBy));
4217
+ }
3680
4218
  return 0;
3681
4219
  }
4220
+ if (generated) {
4221
+ for (const line of createdIdentityBanner(generated)) io2.out(line);
4222
+ if (generated.shadowedBy) io2.err(shadowNote(generated.shadowedBy));
4223
+ }
3682
4224
  io2.out(`Initialized rig for ${repoRoot}`);
3683
4225
  io2.out(renderIdentityLine(identity));
3684
4226
  io2.out(
@@ -3717,14 +4259,341 @@ async function runInit(args, deps) {
3717
4259
  }
3718
4260
  }
3719
4261
 
4262
+ // src/cli/maintainers.ts
4263
+ import { parseArgs as parseArgs12 } from "util";
4264
+ var HEX64_RE3 = /^[0-9a-f]{64}$/;
4265
+ var WS_URL_RE4 = /^wss?:\/\//i;
4266
+ var MAINTAINERS_USAGE = `Usage: rig maintainers <list|add|remove> [<pubkey>] [options]
4267
+
4268
+ Manage a repo's declared maintainers (#287). Status authority is consumer-side:
4269
+ rig and rig-web honor an issue/PR status (kind:1630-1633) ONLY when its author
4270
+ is the repo owner or a declared maintainer. The owner is always an implicit
4271
+ maintainer; this command edits the explicit set on the kind:30617 announcement.
4272
+
4273
+ Subcommands:
4274
+ list show the owner + declared maintainers \u2014 FREE (relay read)
4275
+ add <pubkey> add a maintainer (npub or 64-char hex) \u2014 PAID: republishes
4276
+ the kind:30617 (permanent, non-refundable)
4277
+ remove <pubkey> remove a maintainer \u2014 PAID: republishes the kind:30617
4278
+
4279
+ add/remove must run under the repo OWNER's identity (only the owner's
4280
+ announcement is authoritative).
4281
+
4282
+ Options:
4283
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
4284
+ --owner <pubkey> repository owner (npub or hex; default: git config)
4285
+ --remote <name> publish/read via this configured git remote (default: origin)
4286
+ --relay <url> ad-hoc relay override (exactly one for add/remove)
4287
+ --yes skip the fee confirmation (required when not a TTY)
4288
+ --json machine-readable envelope
4289
+ -h, --help show this help`;
4290
+ var MAINT_OPTIONS = {
4291
+ json: { type: "boolean", default: false },
4292
+ yes: { type: "boolean", default: false },
4293
+ relay: { type: "string", multiple: true },
4294
+ remote: { type: "string" },
4295
+ "repo-id": { type: "string" },
4296
+ owner: { type: "string" },
4297
+ help: { type: "boolean", short: "h", default: false }
4298
+ };
4299
+ function pickFlags(values) {
4300
+ const flags = {
4301
+ json: values["json"] === true,
4302
+ yes: values["yes"] === true,
4303
+ help: values["help"] === true,
4304
+ relay: Array.isArray(values["relay"]) ? values["relay"] : []
4305
+ };
4306
+ if (typeof values["remote"] === "string") flags.remote = values["remote"];
4307
+ if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
4308
+ if (typeof values["owner"] === "string")
4309
+ flags.owner = ownerToHex(values["owner"]);
4310
+ return flags;
4311
+ }
4312
+ async function resolveContext(flags, deps) {
4313
+ let repoRoot;
4314
+ let toonConfig = {
4315
+ relays: []
4316
+ };
4317
+ try {
4318
+ repoRoot = await resolveRepoRoot(deps.cwd);
4319
+ toonConfig = await readToonConfig(repoRoot);
4320
+ } catch {
4321
+ }
4322
+ const repoId = flags.repoId ?? toonConfig.repoId;
4323
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
4324
+ const owner = flags.owner ?? toonConfig.owner;
4325
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
4326
+ const resolved = await resolveRelays({
4327
+ relayFlags: flags.relay,
4328
+ remoteName: flags.remote,
4329
+ repoRoot,
4330
+ toonRelays: toonConfig.relays
4331
+ });
4332
+ if (resolved.nudge !== void 0) deps.io.err(resolved.nudge);
4333
+ return {
4334
+ repoId,
4335
+ owner,
4336
+ relays: resolved.relays,
4337
+ resolved,
4338
+ ...repoRoot !== void 0 ? { repoRoot } : {}
4339
+ };
4340
+ }
4341
+ async function runMaintainers(args, deps) {
4342
+ const { io: io2 } = deps;
4343
+ const [sub, ...rest] = args;
4344
+ switch (sub) {
4345
+ case "list":
4346
+ return runList2(rest, deps);
4347
+ case "add":
4348
+ return runMutate("add", rest, deps);
4349
+ case "remove":
4350
+ return runMutate("remove", rest, deps);
4351
+ case "--help":
4352
+ case "-h":
4353
+ case "help":
4354
+ io2.out(MAINTAINERS_USAGE);
4355
+ return 0;
4356
+ default:
4357
+ io2.err(
4358
+ sub === void 0 ? "missing subcommand: rig maintainers <list|add|remove>" : `unknown rig maintainers subcommand: ${sub}`
4359
+ );
4360
+ io2.err(MAINTAINERS_USAGE);
4361
+ return 2;
4362
+ }
4363
+ }
4364
+ async function runList2(args, deps) {
4365
+ const { io: io2 } = deps;
4366
+ let flags;
4367
+ try {
4368
+ const { values, positionals } = parseArgs12({
4369
+ args,
4370
+ options: MAINT_OPTIONS,
4371
+ allowPositionals: true
4372
+ });
4373
+ if (positionals.length > 0) {
4374
+ throw new Error("rig maintainers list takes no positional arguments");
4375
+ }
4376
+ flags = pickFlags(values);
4377
+ } catch (err) {
4378
+ io2.err(err instanceof Error ? err.message : String(err));
4379
+ io2.err(MAINTAINERS_USAGE);
4380
+ return 2;
4381
+ }
4382
+ if (flags.help) {
4383
+ io2.out(MAINTAINERS_USAGE);
4384
+ return 0;
4385
+ }
4386
+ try {
4387
+ const ctx = await resolveContext(flags, deps);
4388
+ const wsRelays = ctx.relays.filter((url) => WS_URL_RE4.test(url));
4389
+ if (wsRelays.length === 0) {
4390
+ throw new InvalidRelayUrlError(
4391
+ ctx.relays[0] ?? "",
4392
+ "reads need a ws:// or wss:// relay"
4393
+ );
4394
+ }
4395
+ const remote = await fetchRemoteState({
4396
+ relayUrls: wsRelays,
4397
+ ownerPubkey: ctx.owner,
4398
+ repoId: ctx.repoId,
4399
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
4400
+ });
4401
+ const maintainers = remote.maintainers;
4402
+ if (flags.json) {
4403
+ io2.emitJson({
4404
+ command: "maintainers list",
4405
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4406
+ announced: remote.announced,
4407
+ owner: ctx.owner,
4408
+ maintainers
4409
+ });
4410
+ return 0;
4411
+ }
4412
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
4413
+ io2.out(`Owner: ${ctx.owner} (implicit maintainer)`);
4414
+ if (!remote.announced) {
4415
+ io2.out("No kind:30617 announcement found \u2014 owner-only authority.");
4416
+ return 0;
4417
+ }
4418
+ if (maintainers.length === 0) {
4419
+ io2.out("Maintainers: (none declared \u2014 owner-only authority)");
4420
+ } else {
4421
+ io2.out(`Maintainers (${maintainers.length}):`);
4422
+ for (const m of maintainers) io2.out(` ${m}`);
4423
+ }
4424
+ return 0;
4425
+ } catch (err) {
4426
+ return emitCliError(io2, flags.json, "maintainers list", err);
4427
+ }
4428
+ }
4429
+ async function runMutate(op, args, deps) {
4430
+ const { io: io2 } = deps;
4431
+ const command = `maintainers ${op}`;
4432
+ let flags;
4433
+ let pubkey;
4434
+ try {
4435
+ const { values, positionals } = parseArgs12({
4436
+ args,
4437
+ options: MAINT_OPTIONS,
4438
+ allowPositionals: true
4439
+ });
4440
+ flags = pickFlags(values);
4441
+ if (flags.help) {
4442
+ io2.out(MAINTAINERS_USAGE);
4443
+ return 0;
4444
+ }
4445
+ if (positionals.length !== 1) {
4446
+ throw new Error(`expected exactly one <pubkey> to ${op}`);
4447
+ }
4448
+ pubkey = ownerToHex(positionals[0]).toLowerCase();
4449
+ if (!HEX64_RE3.test(pubkey)) {
4450
+ throw new Error(`<pubkey> must resolve to 64-char hex (got ${JSON.stringify(positionals[0])})`);
4451
+ }
4452
+ } catch (err) {
4453
+ io2.err(err instanceof Error ? err.message : String(err));
4454
+ io2.err(MAINTAINERS_USAGE);
4455
+ return 2;
4456
+ }
4457
+ let standaloneCtx;
4458
+ try {
4459
+ const ctx = await resolveContext(flags, deps);
4460
+ if (ctx.relays.length > 1) {
4461
+ io2.err(singleRelayRefusal(ctx.resolved, "Nothing was published or paid."));
4462
+ return 1;
4463
+ }
4464
+ const relayUrl = ctx.relays[0];
4465
+ if (relayUrl === void 0 || !WS_URL_RE4.test(relayUrl)) {
4466
+ throw new InvalidRelayUrlError(
4467
+ relayUrl ?? "",
4468
+ "a paid publish needs a ws:// or wss:// relay"
4469
+ );
4470
+ }
4471
+ const load = deps.loadStandalone ?? defaultLoadStandalone;
4472
+ standaloneCtx = await load({
4473
+ env: deps.env,
4474
+ cwd: deps.cwd,
4475
+ warn: (line) => io2.err(line),
4476
+ relayUrl
4477
+ });
4478
+ const identity = identityReport(standaloneCtx);
4479
+ if (identity.pubkey.toLowerCase() !== ctx.owner.toLowerCase()) {
4480
+ io2.err(
4481
+ `rig: only the repo owner (${ctx.owner.slice(0, 8)}\u2026) can change the maintainer set \u2014 the active identity is ${identity.pubkey.slice(0, 8)}\u2026. A non-owner republish would write your own (ignored) announcement. Nothing was published or paid.`
4482
+ );
4483
+ return 1;
4484
+ }
4485
+ const remote = await fetchRemoteState({
4486
+ relayUrls: [relayUrl],
4487
+ ownerPubkey: ctx.owner,
4488
+ repoId: ctx.repoId,
4489
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
4490
+ });
4491
+ if (!remote.announced) {
4492
+ io2.err(
4493
+ `rig: 30617:${ctx.owner.slice(0, 8)}\u2026:${ctx.repoId} has no announcement yet \u2014 run \`rig push\` to publish the repo (with its real name/description) before managing maintainers. Nothing was published or paid.`
4494
+ );
4495
+ return 1;
4496
+ }
4497
+ const current = remote.announceEvent ? parseMaintainers(remote.announceEvent.tags) : [];
4498
+ const currentSet = new Set(current);
4499
+ if (op === "add" && currentSet.has(pubkey)) {
4500
+ io2.err(
4501
+ `rig: ${pubkey.slice(0, 8)}\u2026 is already a maintainer \u2014 nothing to do (not published).`
4502
+ );
4503
+ return 0;
4504
+ }
4505
+ if (op === "remove" && !currentSet.has(pubkey)) {
4506
+ io2.err(
4507
+ `rig: ${pubkey.slice(0, 8)}\u2026 is not a declared maintainer \u2014 nothing to do (not published).`
4508
+ );
4509
+ return 0;
4510
+ }
4511
+ const next = op === "add" ? [...current, pubkey] : current.filter((m) => m !== pubkey);
4512
+ const name = remote.name ?? ctx.repoId;
4513
+ const description = remote.description ?? "";
4514
+ const event = buildRepoAnnouncement(ctx.repoId, name, description, next);
4515
+ const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
4516
+ const action = `kind:30617 maintainers ${op} ${pubkey.slice(0, 8)}\u2026`;
4517
+ if (!flags.json) {
4518
+ io2.out(`Republish ${action}`);
4519
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
4520
+ io2.out(`Maintainers after: ${next.length === 0 ? "(none)" : next.join(", ")}`);
4521
+ io2.out(`Fee: ${feeLabel(fee)}. Writes are permanent and non-refundable.`);
4522
+ }
4523
+ if (!flags.yes) {
4524
+ if (flags.json) {
4525
+ io2.emitJson({
4526
+ command,
4527
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4528
+ identity,
4529
+ executed: false,
4530
+ feeEstimate: fee,
4531
+ maintainers: next,
4532
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
4533
+ });
4534
+ return 0;
4535
+ }
4536
+ if (!io2.isInteractive) {
4537
+ io2.err(
4538
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
4539
+ );
4540
+ return 1;
4541
+ }
4542
+ const proceed = await io2.confirm(
4543
+ `Proceed with paid republish (${feeLabel(fee)})? [y/N] `
4544
+ );
4545
+ if (!proceed) {
4546
+ io2.err("aborted \u2014 nothing was published.");
4547
+ return 1;
4548
+ }
4549
+ }
4550
+ const receipt = await standaloneCtx.publisher.publishEvent(event, [
4551
+ relayUrl
4552
+ ]);
4553
+ const result = serializeEventReceipt(event.kind, receipt);
4554
+ if (flags.json) {
4555
+ io2.emitJson({
4556
+ command,
4557
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4558
+ identity,
4559
+ executed: true,
4560
+ feeEstimate: fee,
4561
+ maintainers: next,
4562
+ result
4563
+ });
4564
+ } else {
4565
+ io2.out(
4566
+ `Published ${action}: ${result.eventId} paid ${result.feePaid} base units`
4567
+ );
4568
+ }
4569
+ return 0;
4570
+ } catch (err) {
4571
+ return emitCliError(io2, flags.json, command, err);
4572
+ } finally {
4573
+ if (standaloneCtx) {
4574
+ try {
4575
+ await standaloneCtx.stop();
4576
+ } catch {
4577
+ }
4578
+ }
4579
+ }
4580
+ }
4581
+
3720
4582
  // src/cli/dispatch.ts
3721
4583
  var USAGE = `rig \u2014 git with a TOON remote (pay-to-write Nostr + Arweave)
3722
4584
 
3723
4585
  Usage: rig <command> [options]
3724
4586
 
3725
4587
  Commands rig owns:
3726
- init set up this repo: resolve your identity
3727
- (RIG_MNEMONIC) and write the toon.* git config
4588
+ identity create generate a fresh identity (BIP-39) into the
4589
+ encrypted keystore \u2014 the phrase is shown ONCE;
4590
+ back it up. This is your first step on a new box
4591
+ identity show the active identity's source + derived pubkey
4592
+ (never the phrase)
4593
+ identity import write an EXISTING phrase (read from stdin) to the
4594
+ encrypted keystore
4595
+ init set up this repo: resolve your identity (or offer
4596
+ to generate one) and write the toon.* git config
3728
4597
  remote add <name> <url> add a relay as a REAL git remote ("origin" is
3729
4598
  remote remove <name> the default publish target); remove/list manage
3730
4599
  remote list them \u2014 \`git remote -v\` shows the same data
@@ -3749,6 +4618,10 @@ Commands rig owns:
3749
4618
  \`git format-patch\` content
3750
4619
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
3751
4620
  open | applied | closed | draft
4621
+ maintainers list show the repo's declared maintainers (free); add/
4622
+ maintainers add <pubkey> remove republish the kind:30617 to change who may
4623
+ maintainers remove <pubkey> author authoritative issue/PR status (owner is
4624
+ always an implicit maintainer)
3752
4625
  fund drip devnet faucet funds to this identity's
3753
4626
  wallet (free); on other networks prints the
3754
4627
  address(es) to fund externally
@@ -3797,6 +4670,8 @@ async function dispatch(argv2, deps) {
3797
4670
  switch (command) {
3798
4671
  case "init":
3799
4672
  return runInit(rest, deps);
4673
+ case "identity":
4674
+ return runIdentity(rest, deps);
3800
4675
  case "remote":
3801
4676
  return runRemote(rest, deps);
3802
4677
  // The #278 read path — FREE (relay + Arweave gateway reads, no payment).
@@ -3812,6 +4687,8 @@ async function dispatch(argv2, deps) {
3812
4687
  return runComment(rest, deps);
3813
4688
  case "pr":
3814
4689
  return runPr(rest, deps);
4690
+ case "maintainers":
4691
+ return runMaintainers(rest, deps);
3815
4692
  case "channel":
3816
4693
  return runChannel(rest, deps);
3817
4694
  case "fund":
@@ -3882,6 +4759,7 @@ function makeCliIo(options) {
3882
4759
  }
3883
4760
  var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
3884
4761
  "init",
4762
+ "identity",
3885
4763
  "remote",
3886
4764
  "clone",
3887
4765
  "fetch",
@@ -3889,6 +4767,7 @@ var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
3889
4767
  "issue",
3890
4768
  "comment",
3891
4769
  "pr",
4770
+ "maintainers",
3892
4771
  "channel",
3893
4772
  "fund",
3894
4773
  "balance"
@@ -3958,7 +4837,7 @@ function makeIo(jsonMode) {
3958
4837
  },
3959
4838
  isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
3960
4839
  confirm: async (question) => {
3961
- const rl = createInterface({
4840
+ const rl = createInterface2({
3962
4841
  input: process.stdin,
3963
4842
  output: process.stderr
3964
4843
  });