@toon-protocol/rig 2.2.0 → 2.3.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,23 @@ import {
17
17
  setHeadSymref,
18
18
  updateRef,
19
19
  writeGitObjects
20
- } from "../chunk-PS5QOT62.js";
20
+ } from "../chunk-W2SDL2PE.js";
21
21
  import {
22
22
  MissingIdentityError,
23
23
  resolveIdentity
24
24
  } from "../chunk-CW4HJNMU.js";
25
25
  import {
26
26
  COMMENT_KIND,
27
+ authorizedStatusAuthors,
27
28
  buildComment,
28
29
  buildIssue,
29
30
  buildPatch,
31
+ buildRepoAnnouncement,
30
32
  buildStatus,
31
33
  fetchRemoteState,
34
+ parseMaintainers,
32
35
  queryRelay
33
- } from "../chunk-JBB7HBQC.js";
36
+ } from "../chunk-3HRFDH7H.js";
34
37
  import {
35
38
  ChannelMapStore,
36
39
  channelStatus,
@@ -897,7 +900,7 @@ function loadPaidSession(deps, relayUrl) {
897
900
  });
898
901
  }
899
902
  var defaultLoadStandalone = async (options) => {
900
- const mod = await import("../standalone-mode-64CKUVU2.js");
903
+ const mod = await import("../standalone-mode-JLGAUMRF.js");
901
904
  return mod.createStandaloneContext(options);
902
905
  };
903
906
  function identityReport(ctx) {
@@ -2100,10 +2103,11 @@ function tagValue(tags, name) {
2100
2103
  function tagValues(tags, name) {
2101
2104
  return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
2102
2105
  }
2103
- function deriveStatus(targetEventId, statusEvents) {
2106
+ function deriveStatus(targetEventId, statusEvents, authorized) {
2104
2107
  let winner = null;
2105
2108
  for (const event of statusEvents) {
2106
2109
  if (STATUS_BY_KIND[event.kind] === void 0) continue;
2110
+ if (!authorized.has(event.pubkey.toLowerCase())) continue;
2107
2111
  if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
2108
2112
  continue;
2109
2113
  if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
@@ -2184,12 +2188,13 @@ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2184
2188
  } catch {
2185
2189
  }
2186
2190
  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");
2191
+ const repoId = flags.repoId ?? toonConfig.repoId;
2192
+ const owner = flags.owner ?? toonConfig.owner;
2193
+ if (repoId && owner) {
2192
2194
  repoAddr = { ownerPubkey: owner, repoId };
2195
+ } else if (needRepoAddr) {
2196
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
2197
+ throw new UnconfiguredRepoAddressError("repository owner");
2193
2198
  }
2194
2199
  const resolved = await resolveRelays({
2195
2200
  relayFlags: flags.relay,
@@ -2213,6 +2218,34 @@ async function resolveTrackerContext(flags, deps, needRepoAddr) {
2213
2218
  function repoATag(addr) {
2214
2219
  return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
2215
2220
  }
2221
+ async function fetchAuthorizedAuthors(ctx, ownerPubkey, repoId) {
2222
+ let announceTags = [];
2223
+ try {
2224
+ const events = await queryAll(
2225
+ ctx.relays,
2226
+ [
2227
+ {
2228
+ kinds: [REPOSITORY_ANNOUNCEMENT_KIND],
2229
+ authors: [ownerPubkey],
2230
+ "#d": [repoId]
2231
+ }
2232
+ ],
2233
+ ctx.webSocketFactory
2234
+ );
2235
+ let latest = null;
2236
+ for (const event of events.values()) {
2237
+ if (event.kind !== REPOSITORY_ANNOUNCEMENT_KIND) continue;
2238
+ if (event.pubkey !== ownerPubkey) continue;
2239
+ if (!event.tags.some((t) => t[0] === "d" && t[1] === repoId)) continue;
2240
+ if (latest === null || event.created_at > latest.created_at || event.created_at === latest.created_at && event.id < latest.id) {
2241
+ latest = event;
2242
+ }
2243
+ }
2244
+ if (latest) announceTags = latest.tags;
2245
+ } catch {
2246
+ }
2247
+ return authorizedStatusAuthors(ownerPubkey, announceTags);
2248
+ }
2216
2249
  function parseTrackerItem(event, status) {
2217
2250
  const item = {
2218
2251
  eventId: event.id,
@@ -2237,8 +2270,12 @@ function isoDate(createdAt) {
2237
2270
  return new Date(createdAt * 1e3).toISOString().slice(0, 10);
2238
2271
  }
2239
2272
  async function fetchItems(ctx, kind) {
2240
- const aTag = repoATag(
2241
- ctx.repoAddr
2273
+ const addr = ctx.repoAddr;
2274
+ const aTag = repoATag(addr);
2275
+ const authorizedPromise = fetchAuthorizedAuthors(
2276
+ ctx,
2277
+ addr.ownerPubkey,
2278
+ addr.repoId
2242
2279
  );
2243
2280
  const [itemEvents, aStatusEvents] = await Promise.all([
2244
2281
  queryAll(
@@ -2265,8 +2302,9 @@ async function fetchItems(ctx, kind) {
2265
2302
  for (const [id, event] of byE)
2266
2303
  if (!statuses.has(id)) statuses.set(id, event);
2267
2304
  }
2305
+ const authorized = await authorizedPromise;
2268
2306
  return items.map(
2269
- (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values()))
2307
+ (event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values(), authorized))
2270
2308
  ).sort((a, b) => b.createdAt - a.createdAt);
2271
2309
  }
2272
2310
  async function fetchItem(ctx, eventId, expectedKind) {
@@ -2305,12 +2343,27 @@ async function fetchItem(ctx, eventId, expectedKind) {
2305
2343
  createdAt: e.created_at,
2306
2344
  content: e.content
2307
2345
  }));
2346
+ const repoATag2 = tagValue(event.tags, "a") ?? null;
2347
+ const parsedAddr = parseRepoATag(repoATag2) ?? ctx.repoAddr;
2348
+ const authorized = parsedAddr ? await fetchAuthorizedAuthors(ctx, parsedAddr.ownerPubkey, parsedAddr.repoId) : /* @__PURE__ */ new Set();
2308
2349
  return {
2309
- item: parseTrackerItem(event, deriveStatus(eventId, statusEvents.values())),
2350
+ item: parseTrackerItem(
2351
+ event,
2352
+ deriveStatus(eventId, statusEvents.values(), authorized)
2353
+ ),
2310
2354
  comments,
2311
- repoATag: tagValue(event.tags, "a") ?? null
2355
+ repoATag: repoATag2
2312
2356
  };
2313
2357
  }
2358
+ function parseRepoATag(aTag) {
2359
+ if (!aTag) return null;
2360
+ const [kind, ownerPubkey, ...repoIdParts] = aTag.split(":");
2361
+ const repoId = repoIdParts.join(":");
2362
+ if (kind !== String(REPOSITORY_ANNOUNCEMENT_KIND) || !ownerPubkey || !repoId) {
2363
+ return null;
2364
+ }
2365
+ return { ownerPubkey, repoId };
2366
+ }
2314
2367
  async function runList(args, deps, spec) {
2315
2368
  const { io: io2 } = deps;
2316
2369
  let flags;
@@ -2663,6 +2716,9 @@ async function runEvent(opts) {
2663
2716
  const addr = { ownerPubkey: owner, repoId };
2664
2717
  const event = await opts.buildEvent(addr);
2665
2718
  const action = `kind:${event.kind} ${actionLabel}`;
2719
+ if (opts.preConfirm) {
2720
+ await opts.preConfirm({ identity, addr, relayUrl: relaysUsed[0] });
2721
+ }
2666
2722
  if (!flags.json) {
2667
2723
  for (const line of renderEventPlan({
2668
2724
  action,
@@ -3109,9 +3165,41 @@ async function runPrStatus(args, deps) {
3109
3165
  ]);
3110
3166
  return event;
3111
3167
  },
3168
+ // Authority warning (#287): a status event only moves an issue/PR's
3169
+ // resolved state for repo-authority-honoring clients if its author is the
3170
+ // owner or a declared maintainer. Warn clearly when the active identity is
3171
+ // neither — the publish still happens (permissionless relay; the caller
3172
+ // may be acting on their own repo/fork), but the futility is made obvious.
3173
+ preConfirm: (args2) => warnIfNotMaintainer(deps, args2),
3112
3174
  sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
3113
3175
  });
3114
3176
  }
3177
+ async function warnIfNotMaintainer(deps, args) {
3178
+ const { io: io2 } = deps;
3179
+ const { identity, addr, relayUrl } = args;
3180
+ if (identity.pubkey.toLowerCase() === addr.ownerPubkey.toLowerCase()) return;
3181
+ if (relayUrl === void 0) return;
3182
+ try {
3183
+ const remote = await fetchRemoteState({
3184
+ relayUrls: [relayUrl],
3185
+ ownerPubkey: addr.ownerPubkey,
3186
+ repoId: addr.repoId,
3187
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
3188
+ });
3189
+ const authorized = authorizedStatusAuthors(
3190
+ addr.ownerPubkey,
3191
+ remote.announceEvent?.tags ?? []
3192
+ );
3193
+ if (authorized.has(identity.pubkey.toLowerCase())) return;
3194
+ io2.err(
3195
+ `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.`
3196
+ );
3197
+ } catch {
3198
+ io2.err(
3199
+ "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."
3200
+ );
3201
+ }
3202
+ }
3115
3203
 
3116
3204
  // src/cli/fetch.ts
3117
3205
  import { parseArgs as parseArgs8 } from "util";
@@ -3344,10 +3432,11 @@ var FUND_USAGE = `Usage: rig fund [options]
3344
3432
  Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
3345
3433
  pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
3346
3434
  ~/.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.
3435
+ faucetUrl config field, or the deployed devnet faucet when the network is
3436
+ devnet \u2014 including when a configured *.devnet.toonprotocol.dev origin infers
3437
+ it. The faucet drips a FIXED amount per chain (there is no --amount). On a
3438
+ network without a faucet, prints the wallet address(es) to fund externally
3439
+ instead.
3351
3440
 
3352
3441
  Options:
3353
3442
  --chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
@@ -3440,7 +3529,10 @@ async function runFund(args, deps) {
3440
3529
  );
3441
3530
  }
3442
3531
  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);
3532
+ const devnetOrigin = sharedDevnetOrigin(env, file);
3533
+ const inferredDevnet = devnetOrigin !== void 0 && (network === void 0 || network === "custom");
3534
+ const effectiveNetwork = inferredDevnet ? "devnet" : network;
3535
+ const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (effectiveNetwork === "devnet" ? DEVNET_FAUCET_URL : void 0);
3444
3536
  const resolved = await resolveIdentity({
3445
3537
  env,
3446
3538
  cwd: deps.cwd,
@@ -3462,7 +3554,7 @@ async function runFund(args, deps) {
3462
3554
  mina: derived.mina.publicKey || null
3463
3555
  };
3464
3556
  if (!faucetUrl) {
3465
- const guidance = noFaucetGuidance(network, sharedDevnetOrigin(env, file));
3557
+ const guidance = noFaucetGuidance(network, devnetOrigin);
3466
3558
  if (json) {
3467
3559
  io2.emitJson({
3468
3560
  command: "fund",
@@ -3491,6 +3583,11 @@ async function runFund(args, deps) {
3491
3583
  }
3492
3584
  const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
3493
3585
  const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
3586
+ if (!json && inferredDevnet) {
3587
+ io2.out(
3588
+ `Inferred network 'devnet' from the configured origin ${devnetOrigin} (network was ${JSON.stringify(network ?? "custom")}). Set TOON_CLIENT_NETWORK explicitly to override.`
3589
+ );
3590
+ }
3494
3591
  if (!json) {
3495
3592
  io2.out(
3496
3593
  `Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
@@ -3505,11 +3602,12 @@ async function runFund(args, deps) {
3505
3602
  command: "fund",
3506
3603
  identity,
3507
3604
  funded: true,
3508
- network: network ?? null,
3605
+ network: effectiveNetwork ?? null,
3509
3606
  chain,
3510
3607
  address,
3511
3608
  faucetUrl,
3512
- response
3609
+ response,
3610
+ ...inferredDevnet ? { inferredDevnetFrom: devnetOrigin } : {}
3513
3611
  });
3514
3612
  return 0;
3515
3613
  }
@@ -3717,6 +3815,326 @@ async function runInit(args, deps) {
3717
3815
  }
3718
3816
  }
3719
3817
 
3818
+ // src/cli/maintainers.ts
3819
+ import { parseArgs as parseArgs11 } from "util";
3820
+ var HEX64_RE3 = /^[0-9a-f]{64}$/;
3821
+ var WS_URL_RE4 = /^wss?:\/\//i;
3822
+ var MAINTAINERS_USAGE = `Usage: rig maintainers <list|add|remove> [<pubkey>] [options]
3823
+
3824
+ Manage a repo's declared maintainers (#287). Status authority is consumer-side:
3825
+ rig and rig-web honor an issue/PR status (kind:1630-1633) ONLY when its author
3826
+ is the repo owner or a declared maintainer. The owner is always an implicit
3827
+ maintainer; this command edits the explicit set on the kind:30617 announcement.
3828
+
3829
+ Subcommands:
3830
+ list show the owner + declared maintainers \u2014 FREE (relay read)
3831
+ add <pubkey> add a maintainer (npub or 64-char hex) \u2014 PAID: republishes
3832
+ the kind:30617 (permanent, non-refundable)
3833
+ remove <pubkey> remove a maintainer \u2014 PAID: republishes the kind:30617
3834
+
3835
+ add/remove must run under the repo OWNER's identity (only the owner's
3836
+ announcement is authoritative).
3837
+
3838
+ Options:
3839
+ --repo-id <id> repository id / NIP-34 d-tag (default: git config)
3840
+ --owner <pubkey> repository owner (npub or hex; default: git config)
3841
+ --remote <name> publish/read via this configured git remote (default: origin)
3842
+ --relay <url> ad-hoc relay override (exactly one for add/remove)
3843
+ --yes skip the fee confirmation (required when not a TTY)
3844
+ --json machine-readable envelope
3845
+ -h, --help show this help`;
3846
+ var MAINT_OPTIONS = {
3847
+ json: { type: "boolean", default: false },
3848
+ yes: { type: "boolean", default: false },
3849
+ relay: { type: "string", multiple: true },
3850
+ remote: { type: "string" },
3851
+ "repo-id": { type: "string" },
3852
+ owner: { type: "string" },
3853
+ help: { type: "boolean", short: "h", default: false }
3854
+ };
3855
+ function pickFlags(values) {
3856
+ const flags = {
3857
+ json: values["json"] === true,
3858
+ yes: values["yes"] === true,
3859
+ help: values["help"] === true,
3860
+ relay: Array.isArray(values["relay"]) ? values["relay"] : []
3861
+ };
3862
+ if (typeof values["remote"] === "string") flags.remote = values["remote"];
3863
+ if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
3864
+ if (typeof values["owner"] === "string")
3865
+ flags.owner = ownerToHex(values["owner"]);
3866
+ return flags;
3867
+ }
3868
+ async function resolveContext(flags, deps) {
3869
+ let repoRoot;
3870
+ let toonConfig = {
3871
+ relays: []
3872
+ };
3873
+ try {
3874
+ repoRoot = await resolveRepoRoot(deps.cwd);
3875
+ toonConfig = await readToonConfig(repoRoot);
3876
+ } catch {
3877
+ }
3878
+ const repoId = flags.repoId ?? toonConfig.repoId;
3879
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
3880
+ const owner = flags.owner ?? toonConfig.owner;
3881
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
3882
+ const resolved = await resolveRelays({
3883
+ relayFlags: flags.relay,
3884
+ remoteName: flags.remote,
3885
+ repoRoot,
3886
+ toonRelays: toonConfig.relays
3887
+ });
3888
+ if (resolved.nudge !== void 0) deps.io.err(resolved.nudge);
3889
+ return {
3890
+ repoId,
3891
+ owner,
3892
+ relays: resolved.relays,
3893
+ resolved,
3894
+ ...repoRoot !== void 0 ? { repoRoot } : {}
3895
+ };
3896
+ }
3897
+ async function runMaintainers(args, deps) {
3898
+ const { io: io2 } = deps;
3899
+ const [sub, ...rest] = args;
3900
+ switch (sub) {
3901
+ case "list":
3902
+ return runList2(rest, deps);
3903
+ case "add":
3904
+ return runMutate("add", rest, deps);
3905
+ case "remove":
3906
+ return runMutate("remove", rest, deps);
3907
+ case "--help":
3908
+ case "-h":
3909
+ case "help":
3910
+ io2.out(MAINTAINERS_USAGE);
3911
+ return 0;
3912
+ default:
3913
+ io2.err(
3914
+ sub === void 0 ? "missing subcommand: rig maintainers <list|add|remove>" : `unknown rig maintainers subcommand: ${sub}`
3915
+ );
3916
+ io2.err(MAINTAINERS_USAGE);
3917
+ return 2;
3918
+ }
3919
+ }
3920
+ async function runList2(args, deps) {
3921
+ const { io: io2 } = deps;
3922
+ let flags;
3923
+ try {
3924
+ const { values, positionals } = parseArgs11({
3925
+ args,
3926
+ options: MAINT_OPTIONS,
3927
+ allowPositionals: true
3928
+ });
3929
+ if (positionals.length > 0) {
3930
+ throw new Error("rig maintainers list takes no positional arguments");
3931
+ }
3932
+ flags = pickFlags(values);
3933
+ } catch (err) {
3934
+ io2.err(err instanceof Error ? err.message : String(err));
3935
+ io2.err(MAINTAINERS_USAGE);
3936
+ return 2;
3937
+ }
3938
+ if (flags.help) {
3939
+ io2.out(MAINTAINERS_USAGE);
3940
+ return 0;
3941
+ }
3942
+ try {
3943
+ const ctx = await resolveContext(flags, deps);
3944
+ const wsRelays = ctx.relays.filter((url) => WS_URL_RE4.test(url));
3945
+ if (wsRelays.length === 0) {
3946
+ throw new InvalidRelayUrlError(
3947
+ ctx.relays[0] ?? "",
3948
+ "reads need a ws:// or wss:// relay"
3949
+ );
3950
+ }
3951
+ const remote = await fetchRemoteState({
3952
+ relayUrls: wsRelays,
3953
+ ownerPubkey: ctx.owner,
3954
+ repoId: ctx.repoId,
3955
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
3956
+ });
3957
+ const maintainers = remote.maintainers;
3958
+ if (flags.json) {
3959
+ io2.emitJson({
3960
+ command: "maintainers list",
3961
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
3962
+ announced: remote.announced,
3963
+ owner: ctx.owner,
3964
+ maintainers
3965
+ });
3966
+ return 0;
3967
+ }
3968
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
3969
+ io2.out(`Owner: ${ctx.owner} (implicit maintainer)`);
3970
+ if (!remote.announced) {
3971
+ io2.out("No kind:30617 announcement found \u2014 owner-only authority.");
3972
+ return 0;
3973
+ }
3974
+ if (maintainers.length === 0) {
3975
+ io2.out("Maintainers: (none declared \u2014 owner-only authority)");
3976
+ } else {
3977
+ io2.out(`Maintainers (${maintainers.length}):`);
3978
+ for (const m of maintainers) io2.out(` ${m}`);
3979
+ }
3980
+ return 0;
3981
+ } catch (err) {
3982
+ return emitCliError(io2, flags.json, "maintainers list", err);
3983
+ }
3984
+ }
3985
+ async function runMutate(op, args, deps) {
3986
+ const { io: io2 } = deps;
3987
+ const command = `maintainers ${op}`;
3988
+ let flags;
3989
+ let pubkey;
3990
+ try {
3991
+ const { values, positionals } = parseArgs11({
3992
+ args,
3993
+ options: MAINT_OPTIONS,
3994
+ allowPositionals: true
3995
+ });
3996
+ flags = pickFlags(values);
3997
+ if (flags.help) {
3998
+ io2.out(MAINTAINERS_USAGE);
3999
+ return 0;
4000
+ }
4001
+ if (positionals.length !== 1) {
4002
+ throw new Error(`expected exactly one <pubkey> to ${op}`);
4003
+ }
4004
+ pubkey = ownerToHex(positionals[0]).toLowerCase();
4005
+ if (!HEX64_RE3.test(pubkey)) {
4006
+ throw new Error(`<pubkey> must resolve to 64-char hex (got ${JSON.stringify(positionals[0])})`);
4007
+ }
4008
+ } catch (err) {
4009
+ io2.err(err instanceof Error ? err.message : String(err));
4010
+ io2.err(MAINTAINERS_USAGE);
4011
+ return 2;
4012
+ }
4013
+ let standaloneCtx;
4014
+ try {
4015
+ const ctx = await resolveContext(flags, deps);
4016
+ if (ctx.relays.length > 1) {
4017
+ io2.err(singleRelayRefusal(ctx.resolved, "Nothing was published or paid."));
4018
+ return 1;
4019
+ }
4020
+ const relayUrl = ctx.relays[0];
4021
+ if (relayUrl === void 0 || !WS_URL_RE4.test(relayUrl)) {
4022
+ throw new InvalidRelayUrlError(
4023
+ relayUrl ?? "",
4024
+ "a paid publish needs a ws:// or wss:// relay"
4025
+ );
4026
+ }
4027
+ const load = deps.loadStandalone ?? defaultLoadStandalone;
4028
+ standaloneCtx = await load({
4029
+ env: deps.env,
4030
+ cwd: deps.cwd,
4031
+ warn: (line) => io2.err(line),
4032
+ relayUrl
4033
+ });
4034
+ const identity = identityReport(standaloneCtx);
4035
+ if (identity.pubkey.toLowerCase() !== ctx.owner.toLowerCase()) {
4036
+ io2.err(
4037
+ `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.`
4038
+ );
4039
+ return 1;
4040
+ }
4041
+ const remote = await fetchRemoteState({
4042
+ relayUrls: [relayUrl],
4043
+ ownerPubkey: ctx.owner,
4044
+ repoId: ctx.repoId,
4045
+ ...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
4046
+ });
4047
+ if (!remote.announced) {
4048
+ io2.err(
4049
+ `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.`
4050
+ );
4051
+ return 1;
4052
+ }
4053
+ const current = remote.announceEvent ? parseMaintainers(remote.announceEvent.tags) : [];
4054
+ const currentSet = new Set(current);
4055
+ if (op === "add" && currentSet.has(pubkey)) {
4056
+ io2.err(
4057
+ `rig: ${pubkey.slice(0, 8)}\u2026 is already a maintainer \u2014 nothing to do (not published).`
4058
+ );
4059
+ return 0;
4060
+ }
4061
+ if (op === "remove" && !currentSet.has(pubkey)) {
4062
+ io2.err(
4063
+ `rig: ${pubkey.slice(0, 8)}\u2026 is not a declared maintainer \u2014 nothing to do (not published).`
4064
+ );
4065
+ return 0;
4066
+ }
4067
+ const next = op === "add" ? [...current, pubkey] : current.filter((m) => m !== pubkey);
4068
+ const name = remote.name ?? ctx.repoId;
4069
+ const description = remote.description ?? "";
4070
+ const event = buildRepoAnnouncement(ctx.repoId, name, description, next);
4071
+ const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
4072
+ const action = `kind:30617 maintainers ${op} ${pubkey.slice(0, 8)}\u2026`;
4073
+ if (!flags.json) {
4074
+ io2.out(`Republish ${action}`);
4075
+ io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
4076
+ io2.out(`Maintainers after: ${next.length === 0 ? "(none)" : next.join(", ")}`);
4077
+ io2.out(`Fee: ${feeLabel(fee)}. Writes are permanent and non-refundable.`);
4078
+ }
4079
+ if (!flags.yes) {
4080
+ if (flags.json) {
4081
+ io2.emitJson({
4082
+ command,
4083
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4084
+ identity,
4085
+ executed: false,
4086
+ feeEstimate: fee,
4087
+ maintainers: next,
4088
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
4089
+ });
4090
+ return 0;
4091
+ }
4092
+ if (!io2.isInteractive) {
4093
+ io2.err(
4094
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
4095
+ );
4096
+ return 1;
4097
+ }
4098
+ const proceed = await io2.confirm(
4099
+ `Proceed with paid republish (${feeLabel(fee)})? [y/N] `
4100
+ );
4101
+ if (!proceed) {
4102
+ io2.err("aborted \u2014 nothing was published.");
4103
+ return 1;
4104
+ }
4105
+ }
4106
+ const receipt = await standaloneCtx.publisher.publishEvent(event, [
4107
+ relayUrl
4108
+ ]);
4109
+ const result = serializeEventReceipt(event.kind, receipt);
4110
+ if (flags.json) {
4111
+ io2.emitJson({
4112
+ command,
4113
+ repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
4114
+ identity,
4115
+ executed: true,
4116
+ feeEstimate: fee,
4117
+ maintainers: next,
4118
+ result
4119
+ });
4120
+ } else {
4121
+ io2.out(
4122
+ `Published ${action}: ${result.eventId} paid ${result.feePaid} base units`
4123
+ );
4124
+ }
4125
+ return 0;
4126
+ } catch (err) {
4127
+ return emitCliError(io2, flags.json, command, err);
4128
+ } finally {
4129
+ if (standaloneCtx) {
4130
+ try {
4131
+ await standaloneCtx.stop();
4132
+ } catch {
4133
+ }
4134
+ }
4135
+ }
4136
+ }
4137
+
3720
4138
  // src/cli/dispatch.ts
3721
4139
  var USAGE = `rig \u2014 git with a TOON remote (pay-to-write Nostr + Arweave)
3722
4140
 
@@ -3749,6 +4167,10 @@ Commands rig owns:
3749
4167
  \`git format-patch\` content
3750
4168
  pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
3751
4169
  open | applied | closed | draft
4170
+ maintainers list show the repo's declared maintainers (free); add/
4171
+ maintainers add <pubkey> remove republish the kind:30617 to change who may
4172
+ maintainers remove <pubkey> author authoritative issue/PR status (owner is
4173
+ always an implicit maintainer)
3752
4174
  fund drip devnet faucet funds to this identity's
3753
4175
  wallet (free); on other networks prints the
3754
4176
  address(es) to fund externally
@@ -3812,6 +4234,8 @@ async function dispatch(argv2, deps) {
3812
4234
  return runComment(rest, deps);
3813
4235
  case "pr":
3814
4236
  return runPr(rest, deps);
4237
+ case "maintainers":
4238
+ return runMaintainers(rest, deps);
3815
4239
  case "channel":
3816
4240
  return runChannel(rest, deps);
3817
4241
  case "fund":
@@ -3889,6 +4313,7 @@ var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
3889
4313
  "issue",
3890
4314
  "comment",
3891
4315
  "pr",
4316
+ "maintainers",
3892
4317
  "channel",
3893
4318
  "fund",
3894
4319
  "balance"