@toon-protocol/rig 1.0.0 → 2.0.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
@@ -10,15 +10,16 @@ import {
10
10
  serializePushPlan,
11
11
  serializePushResult
12
12
  } from "../chunk-LFGDLD6J.js";
13
+ import {
14
+ MissingIdentityError,
15
+ resolveIdentity
16
+ } from "../chunk-QD437XAW.js";
13
17
  import {
14
18
  buildComment,
15
19
  buildIssue,
16
20
  buildPatch,
17
21
  buildStatus
18
22
  } from "../chunk-HPSOQP7Q.js";
19
- import {
20
- DEFAULT_DAEMON_PORT
21
- } from "../chunk-D3HGS44Y.js";
22
23
  import {
23
24
  MAX_OBJECT_SIZE
24
25
  } from "../chunk-X2CZPPDM.js";
@@ -26,9 +27,12 @@ import {
26
27
  // src/cli/rig.ts
27
28
  import { createInterface } from "readline/promises";
28
29
 
30
+ // src/cli/dispatch.ts
31
+ import { createRequire } from "module";
32
+
29
33
  // src/cli/events.ts
30
34
  import { readFile } from "fs/promises";
31
- import { parseArgs as parseArgs2 } from "util";
35
+ import { parseArgs as parseArgs3 } from "util";
32
36
  import {
33
37
  REPOSITORY_ANNOUNCEMENT_KIND,
34
38
  STATUS_APPLIED_KIND,
@@ -37,179 +41,66 @@ import {
37
41
  STATUS_OPEN_KIND
38
42
  } from "@toon-protocol/core/nip34";
39
43
 
40
- // src/cli/daemon.ts
41
- var DaemonRouteError = class extends Error {
42
- constructor(status, envelope) {
43
- super(envelope.detail ?? envelope.error);
44
- this.status = status;
45
- this.envelope = envelope;
46
- this.name = "DaemonRouteError";
44
+ // src/cli/errors.ts
45
+ var UnconfiguredRepoAddressError = class extends Error {
46
+ constructor(missing) {
47
+ super(
48
+ `no ${missing} configured \u2014 this command addresses the repo as 30617:<ownerPubkey>:<repoId>. Run \`rig init\` once inside the repo (it writes toon.repoid/toon.owner to the local git config), or pass ${missing === "repository id" ? "--repo-id <id>" : "--owner <pubkey>"} explicitly (use --owner for repos you don't own).`
49
+ );
50
+ this.missing = missing;
51
+ this.name = "UnconfiguredRepoAddressError";
47
52
  }
48
- status;
49
- envelope;
53
+ missing;
50
54
  };
51
- var DaemonUnreachableError = class extends Error {
52
- constructor(baseUrl, cause) {
55
+ var InvalidRelayUrlError = class extends Error {
56
+ constructor(url, context) {
53
57
  super(
54
- `toon-clientd control API is not reachable at ${baseUrl} \u2014 start the daemon (\`toon-clientd\`, shipped by @toon-protocol/client-mcp) and re-run, or use --standalone with TOON_CLIENT_MNEMONIC set` + (cause instanceof Error ? ` (${cause.message})` : "")
58
+ `${context}: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
55
59
  );
56
- this.baseUrl = baseUrl;
57
- this.name = "DaemonUnreachableError";
60
+ this.url = url;
61
+ this.name = "InvalidRelayUrlError";
58
62
  }
59
- baseUrl;
63
+ url;
60
64
  };
61
- var DaemonGitClient = class {
62
- constructor(baseUrl, fetchImpl) {
63
- this.baseUrl = baseUrl;
64
- this.fetchImpl = fetchImpl;
65
- }
66
- baseUrl;
67
- fetchImpl;
68
- gitEstimate(req) {
69
- return this.post("/git/estimate", req);
70
- }
71
- gitPush(req) {
72
- return this.post("/git/push", req);
73
- }
74
- gitIssue(req) {
75
- return this.post("/git/issue", req);
76
- }
77
- gitComment(req) {
78
- return this.post("/git/comment", req);
79
- }
80
- gitPatch(req) {
81
- return this.post("/git/patch", req);
82
- }
83
- gitStatus(req) {
84
- return this.post("/git/status", req);
85
- }
86
- async post(path, body) {
87
- let res;
88
- try {
89
- res = await this.fetchImpl(`${this.baseUrl}${path}`, {
90
- method: "POST",
91
- headers: { "content-type": "application/json" },
92
- body: JSON.stringify(body)
93
- });
94
- } catch (err) {
95
- throw new DaemonUnreachableError(this.baseUrl, err);
96
- }
97
- const text = await res.text();
98
- let parsed;
99
- try {
100
- parsed = text === "" ? {} : JSON.parse(text);
101
- } catch {
102
- throw new DaemonRouteError(res.status, {
103
- error: "invalid_response",
104
- detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
105
- });
106
- }
107
- if (!res.ok) {
108
- const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
109
- throw new DaemonRouteError(res.status, envelope);
110
- }
111
- return parsed;
65
+ var UnknownRemoteError = class extends Error {
66
+ constructor(remote) {
67
+ super(
68
+ `no remote named ${JSON.stringify(remote)} is configured \u2014 \`rig remote list\` shows configured remotes; add it with \`rig remote add ${remote} <relay-url>\``
69
+ );
70
+ this.remote = remote;
71
+ this.name = "UnknownRemoteError";
112
72
  }
73
+ remote;
113
74
  };
114
-
115
- // src/cli/mode.ts
116
- import { existsSync, readFileSync } from "fs";
117
- import { homedir } from "os";
118
- import { join } from "path";
119
- function daemonBaseUrl(env) {
120
- const raw = env["TOON_CLIENT_HTTP_PORT"];
121
- const parsed = raw ? Number(raw) : NaN;
122
- const port = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
123
- return `http://127.0.0.1:${port}`;
124
- }
125
- async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
126
- const baseUrl = daemonBaseUrl(env);
127
- try {
128
- const res = await fetchImpl(`${baseUrl}/status`, {
129
- signal: AbortSignal.timeout(timeoutMs)
130
- });
131
- if (!res.ok) return { baseUrl, reachable: false };
132
- const body = await res.json();
133
- const probe = { baseUrl, reachable: true };
134
- const pubkey = body?.identity?.nostrPubkey;
135
- if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
136
- if (typeof body?.ready === "boolean") probe.ready = body.ready;
137
- if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
138
- probe.relayUrl = body.relay.url;
139
- }
140
- if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
141
- probe.feePerEvent = body.feePerEvent;
142
- }
143
- return probe;
144
- } catch {
145
- return { baseUrl, reachable: false };
146
- }
147
- }
148
- function clientConfigPath(env) {
149
- const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
150
- return join(dir, "config.json");
151
- }
152
- function standaloneAvailability(env) {
153
- const configPath = clientConfigPath(env);
154
- if (env["TOON_CLIENT_MNEMONIC"]) {
155
- return { available: true, source: "env", configPath };
156
- }
157
- if (existsSync(configPath)) {
158
- try {
159
- const file = JSON.parse(readFileSync(configPath, "utf8"));
160
- if (typeof file.mnemonic === "string" && file.mnemonic !== "" || typeof file.keystorePath === "string" && file.keystorePath !== "") {
161
- return { available: true, source: "config", configPath };
162
- }
163
- } catch {
164
- }
165
- }
166
- return { available: false, configPath };
167
- }
168
- var NoPublisherError = class extends Error {
169
- constructor(probe, standalone) {
75
+ var MultiUrlRemoteError = class extends Error {
76
+ constructor(remote, urls) {
170
77
  super(
171
- `no way to pay for this push \u2014 neither publisher mode is available:
172
- \u2022 daemon: no toon-clientd control API at ${probe.baseUrl}/status \u2014 start it (\`toon-clientd\`, shipped by @toon-protocol/client-mcp) and re-run, or pass --daemon once it is up
173
- \u2022 standalone: no identity found \u2014 set TOON_CLIENT_MNEMONIC (BIP-39 seed phrase) or configure ${standalone.configPath} (mnemonic / keystorePath), then re-run (or pass --standalone)`
78
+ `remote ${JSON.stringify(remote)} has ${urls.length} URLs (${urls.join(", ")}) \u2014 rig supports one relay URL per remote. Fix it with \`git remote set-url ${remote} <relay-url>\`. Nothing was uploaded, published, or paid.`
174
79
  );
175
- this.name = "NoPublisherError";
80
+ this.remote = remote;
81
+ this.urls = urls;
82
+ this.name = "MultiUrlRemoteError";
176
83
  }
84
+ remote;
85
+ urls;
177
86
  };
178
- async function selectMode(options) {
179
- const { env, fetchImpl } = options;
180
- if (options.daemon && options.standalone) {
181
- throw new Error("--daemon and --standalone are mutually exclusive");
182
- }
183
- if (options.standalone) {
184
- return {
185
- mode: "standalone",
186
- probe: { baseUrl: daemonBaseUrl(env), reachable: false }
187
- };
87
+ var NoOriginConfiguredError = class extends Error {
88
+ constructor(nonRelayOriginUrl) {
89
+ super(
90
+ "no origin configured \u2014 run `rig remote add origin <relay-url>` (or pass --relay <url> for a one-off publish)." + (nonRelayOriginUrl !== void 0 ? `
91
+ The existing "origin" remote (${nonRelayOriginUrl}) is not a relay URL, so rig ignores it \u2014 add the relay under another name (\`rig remote add toon <relay-url>\`) and target it explicitly (\`rig push toon\` / \`--remote toon\`).` : "")
92
+ );
93
+ this.name = "NoOriginConfiguredError";
188
94
  }
189
- const probe = await probeDaemon(env, fetchImpl);
190
- if (options.daemon) return { mode: "daemon", probe };
191
- if (probe.reachable && probe.identity) return { mode: "daemon", probe };
192
- const standalone = standaloneAvailability(env);
193
- if (standalone.available) return { mode: "standalone", probe };
194
- throw new NoPublisherError(probe, standalone);
195
- }
196
-
197
- // src/cli/errors.ts
198
- var FUNDING_REMEDIATION = (command) => [
199
- "Remediation:",
200
- " \u2022 fund the settlement wallet: run the toon_fund_wallet MCP tool (devnet faucet), or send gas/tokens to the wallet yourself",
201
- " \u2022 open (or top up) a payment channel: toon_open_channel / toon_channel_deposit",
202
- ` \u2022 then re-run rig ${command}`
203
- ];
204
- var UnconfiguredRepoAddressError = class extends Error {
205
- constructor(missing) {
95
+ };
96
+ var NotAGitRepositoryError = class extends Error {
97
+ constructor(cwd) {
206
98
  super(
207
- `no ${missing} configured \u2014 this command addresses the repo as 30617:<ownerPubkey>:<repoId>. Run \`rig push\` once inside the repo (it persists toon.repoid/toon.owner/toon.relay to git config), or pass ${missing === "repository id" ? "--repo-id <id>" : "--owner <pubkey>"} explicitly (use --owner for repos you don't own).`
99
+ `not a git repository: ${cwd}
100
+ rig works inside an existing repo \u2014 create one first with \`git init\` (rig never runs it for you), then re-run.`
208
101
  );
209
- this.missing = missing;
210
- this.name = "UnconfiguredRepoAddressError";
102
+ this.name = "NotAGitRepositoryError";
211
103
  }
212
- missing;
213
104
  };
214
105
  function nonFastForwardLines(refs) {
215
106
  return [
@@ -231,49 +122,7 @@ function oversizeLines(objects) {
231
122
  "Large-object support is tracked in toon-client#235."
232
123
  ];
233
124
  }
234
- function fromDaemonEnvelope(err, command) {
235
- const { envelope, status } = err;
236
- const json = { ...envelope, status };
237
- switch (envelope.error) {
238
- case "non_fast_forward": {
239
- const refs = Array.isArray(envelope["refs"]) ? envelope["refs"] : [];
240
- return { code: "non_fast_forward", lines: nonFastForwardLines(refs), json };
241
- }
242
- case "oversize_objects": {
243
- const objects = Array.isArray(envelope["objects"]) ? envelope["objects"] : [];
244
- return { code: "oversize_objects", lines: oversizeLines(objects), json };
245
- }
246
- case "bootstrapping":
247
- return {
248
- code: "bootstrapping",
249
- lines: [
250
- `toon-clientd is still bootstrapping: ${envelope.detail ?? "transport/channel coming up"}`,
251
- "Retry in a few seconds. If it never becomes ready, check the toon-clientd logs."
252
- ],
253
- json
254
- };
255
- case "insufficient_gas":
256
- return {
257
- code: "insufficient_gas",
258
- lines: [
259
- `Payment failed: ${envelope.detail ?? "the settlement wallet cannot fund the channel"}`,
260
- ...FUNDING_REMEDIATION(command)
261
- ],
262
- json
263
- };
264
- default:
265
- return {
266
- code: envelope.error,
267
- lines: [
268
- `rig ${command} failed (${envelope.error}, HTTP ${status})` + (envelope.detail ? `: ${envelope.detail}` : ""),
269
- ...envelope.retryable ? ["This error is retryable \u2014 try again shortly."] : []
270
- ],
271
- json
272
- };
273
- }
274
- }
275
125
  function describeError(err, command = "push") {
276
- if (err instanceof DaemonRouteError) return fromDaemonEnvelope(err, command);
277
126
  if (err instanceof UnconfiguredRepoAddressError) {
278
127
  return {
279
128
  code: "unconfigured_repo_address",
@@ -281,18 +130,51 @@ function describeError(err, command = "push") {
281
130
  json: { error: "unconfigured_repo_address", detail: err.message }
282
131
  };
283
132
  }
284
- if (err instanceof DaemonUnreachableError) {
133
+ if (err instanceof NotAGitRepositoryError) {
134
+ return {
135
+ code: "not_a_git_repository",
136
+ lines: err.message.split("\n"),
137
+ json: { error: "not_a_git_repository", detail: err.message }
138
+ };
139
+ }
140
+ if (err instanceof InvalidRelayUrlError) {
285
141
  return {
286
- code: "daemon_unreachable",
287
- lines: [err.message],
288
- json: { error: "daemon_unreachable", detail: err.message }
142
+ code: "invalid_relay_url",
143
+ lines: err.message.split("\n"),
144
+ json: { error: "invalid_relay_url", detail: err.message, url: err.url }
145
+ };
146
+ }
147
+ if (err instanceof UnknownRemoteError) {
148
+ return {
149
+ code: "unknown_remote",
150
+ lines: err.message.split("\n"),
151
+ json: { error: "unknown_remote", detail: err.message, remote: err.remote }
152
+ };
153
+ }
154
+ if (err instanceof MultiUrlRemoteError) {
155
+ return {
156
+ code: "multi_url_remote",
157
+ lines: err.message.split("\n"),
158
+ json: {
159
+ error: "multi_url_remote",
160
+ detail: err.message,
161
+ remote: err.remote,
162
+ urls: err.urls
163
+ }
289
164
  };
290
165
  }
291
- if (err instanceof NoPublisherError) {
166
+ if (err instanceof NoOriginConfiguredError) {
292
167
  return {
293
- code: "no_publisher",
168
+ code: "no_origin_configured",
294
169
  lines: err.message.split("\n"),
295
- json: { error: "no_publisher", detail: err.message }
170
+ json: { error: "no_origin_configured", detail: err.message }
171
+ };
172
+ }
173
+ if (err instanceof MissingIdentityError) {
174
+ return {
175
+ code: "missing_identity",
176
+ lines: err.message.split("\n"),
177
+ json: { error: "missing_identity", detail: err.message }
296
178
  };
297
179
  }
298
180
  if (err instanceof NonFastForwardError) {
@@ -325,15 +207,18 @@ function describeError(err, command = "push") {
325
207
  if (name === "DaemonIdentityConflictError") {
326
208
  return {
327
209
  code: "daemon_identity_conflict",
328
- lines: [message, "Re-run without --standalone (or with --daemon) to push through the running daemon."],
210
+ lines: [
211
+ message,
212
+ "Stop the toon-clientd daemon (or publish through its toon_git_* MCP tools) and re-run."
213
+ ],
329
214
  json: { error: "daemon_identity_conflict", detail: message }
330
215
  };
331
216
  }
332
- if (name === "MissingMnemonicError" || name === "MissingUplinkError") {
217
+ if (name === "MissingUplinkError") {
333
218
  return {
334
- code: name === "MissingMnemonicError" ? "missing_mnemonic" : "missing_uplink",
219
+ code: "missing_uplink",
335
220
  lines: [message],
336
- json: { error: "standalone_unavailable", detail: message }
221
+ json: { error: "missing_uplink", detail: message }
337
222
  };
338
223
  }
339
224
  return {
@@ -397,6 +282,29 @@ async function readToonConfig(repoPath) {
397
282
  if (owner.exitCode === 0 && own) config.owner = own;
398
283
  return config;
399
284
  }
285
+ async function getGitRemoteUrls(repoPath, name) {
286
+ const { stdout } = await git(
287
+ repoPath,
288
+ ["config", "--get-all", `remote.${name}.url`],
289
+ [1]
290
+ );
291
+ return stdout.split("\n").map((l) => l.trim()).filter(Boolean);
292
+ }
293
+ async function listGitRemotes(repoPath) {
294
+ const { stdout } = await git(repoPath, ["remote"]);
295
+ const names = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
296
+ const remotes = [];
297
+ for (const name of names) {
298
+ remotes.push({ name, urls: await getGitRemoteUrls(repoPath, name) });
299
+ }
300
+ return remotes;
301
+ }
302
+ async function addGitRemote(repoPath, name, url) {
303
+ await git(repoPath, ["remote", "add", name, url]);
304
+ }
305
+ async function removeGitRemote(repoPath, name) {
306
+ await git(repoPath, ["remote", "remove", name]);
307
+ }
400
308
  async function writeToonConfig(repoPath, config) {
401
309
  if (config.repoId !== void 0) {
402
310
  await git(repoPath, ["config", "toon.repoid", config.repoId]);
@@ -413,8 +321,7 @@ async function writeToonConfig(repoPath, config) {
413
321
  }
414
322
 
415
323
  // src/cli/push.ts
416
- import { basename } from "path";
417
- import { parseArgs } from "util";
324
+ import { parseArgs as parseArgs2 } from "util";
418
325
 
419
326
  // src/cli/render.ts
420
327
  function formatNumber(value) {
@@ -427,10 +334,13 @@ function refLine(update) {
427
334
  const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
428
335
  return ` ${update.refname} ${arrow} (${update.kind})`;
429
336
  }
430
- function renderPlan(plan, mode) {
337
+ function renderIdentityLine(identity) {
338
+ return `Identity: ${identity.pubkey} (from ${identity.sourceLabel})`;
339
+ }
340
+ function renderPlan(plan) {
431
341
  const lines = [];
432
342
  lines.push(
433
- `Push plan for repo "${plan.repoId}" (${mode} mode)` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
343
+ `Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
434
344
  );
435
345
  lines.push("Refs:");
436
346
  for (const update of plan.refUpdates) lines.push(refLine(update));
@@ -453,8 +363,9 @@ function feeLabel(fee) {
453
363
  }
454
364
  function renderEventPlan(opts) {
455
365
  return [
456
- `Publish ${opts.action} (${opts.mode} mode)`,
366
+ `Publish ${opts.action}`,
457
367
  `Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
368
+ renderIdentityLine(opts.identity),
458
369
  `Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
459
370
  ];
460
371
  }
@@ -496,18 +407,248 @@ function renderResult(result) {
496
407
  return lines;
497
408
  }
498
409
 
410
+ // src/cli/remote.ts
411
+ import { parseArgs } from "util";
412
+ var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
413
+ function isRelayUrl(url) {
414
+ try {
415
+ return RELAY_PROTOCOLS.has(new URL(url).protocol);
416
+ } catch {
417
+ return false;
418
+ }
419
+ }
420
+ function assertRelayUrl(url, context) {
421
+ if (!isRelayUrl(url)) throw new InvalidRelayUrlError(url, context);
422
+ }
423
+ async function resolveRelays(opts) {
424
+ if (opts.relayFlags.length > 0) {
425
+ return { relays: opts.relayFlags, source: "relay-flag" };
426
+ }
427
+ if (opts.remoteName !== void 0) {
428
+ const urls = opts.repoRoot !== void 0 ? await getGitRemoteUrls(opts.repoRoot, opts.remoteName) : [];
429
+ if (urls.length === 0) throw new UnknownRemoteError(opts.remoteName);
430
+ if (urls.length > 1) throw new MultiUrlRemoteError(opts.remoteName, urls);
431
+ const url = urls[0];
432
+ assertRelayUrl(url, `remote ${JSON.stringify(opts.remoteName)}`);
433
+ return { relays: urls, source: "remote", remoteName: opts.remoteName };
434
+ }
435
+ let nonRelayOriginUrl;
436
+ if (opts.repoRoot !== void 0) {
437
+ const urls = await getGitRemoteUrls(opts.repoRoot, "origin");
438
+ if (urls.length === 1 && isRelayUrl(urls[0])) {
439
+ return { relays: urls, source: "remote", remoteName: "origin" };
440
+ }
441
+ if (urls.length > 1 && urls.some(isRelayUrl)) {
442
+ throw new MultiUrlRemoteError("origin", urls);
443
+ }
444
+ if (urls.length > 0) nonRelayOriginUrl = urls[0];
445
+ }
446
+ if (opts.toonRelays.length > 0) {
447
+ return {
448
+ relays: opts.toonRelays,
449
+ source: "toon.relay",
450
+ nudge: `note: git config toon.relay is deprecated (removed in v0.3) \u2014 migrate: rig remote add origin ${opts.toonRelays[0]}`
451
+ };
452
+ }
453
+ throw new NoOriginConfiguredError(nonRelayOriginUrl);
454
+ }
455
+ function singleRelayRefusal(resolved, nothingHappened) {
456
+ const fix = resolved.source === "relay-flag" ? "pass exactly one --relay <url>" : "trim git config toon.relay to one URL (better: migrate \u2014 `rig remote add origin <relay-url>`)";
457
+ return `rig publishes to a single relay, but ${resolved.relays.length} are configured (${resolved.relays.join(", ")}) \u2014 ${fix}. ${nothingHappened}`;
458
+ }
459
+ var REMOTE_USAGE = `Usage: rig remote <add|remove|list> [args] [options]
460
+
461
+ Manage the relays this repo publishes to. Remotes are stored as REAL git
462
+ remotes (\`git remote -v\` shows them; plain git tooling round-trips). Paid
463
+ commands publish via the "origin" remote by default; \`rig push <remote>\`
464
+ and \`--remote <name>\` target another one. Free \u2014 nothing is published or
465
+ paid.
466
+
467
+ Commands:
468
+ add <name> <relay-url> add a remote pointing at a relay (the URL must be
469
+ ws://, wss://, http://, or https://)
470
+ remove <name> remove a remote
471
+ list list remote names + URLs (the default subcommand)
472
+
473
+ Options:
474
+ --json machine-readable output (list)
475
+ -h, --help show this help`;
476
+ async function runRemote(args, deps) {
477
+ const { io } = deps;
478
+ let sub;
479
+ let rest;
480
+ let json;
481
+ try {
482
+ const { values, positionals } = parseArgs({
483
+ args,
484
+ options: {
485
+ json: { type: "boolean", default: false },
486
+ help: { type: "boolean", short: "h", default: false }
487
+ },
488
+ allowPositionals: true
489
+ });
490
+ if (values.help) {
491
+ io.out(REMOTE_USAGE);
492
+ return 0;
493
+ }
494
+ json = values.json ?? false;
495
+ [sub, ...rest] = positionals;
496
+ } catch (err) {
497
+ io.err(err instanceof Error ? err.message : String(err));
498
+ io.err(REMOTE_USAGE);
499
+ return 2;
500
+ }
501
+ switch (sub) {
502
+ case void 0:
503
+ case "list":
504
+ if (rest.length > 0) {
505
+ io.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
506
+ io.err(REMOTE_USAGE);
507
+ return 2;
508
+ }
509
+ break;
510
+ case "add": {
511
+ if (rest.length !== 2) {
512
+ io.err("usage: rig remote add <name> <relay-url>");
513
+ io.err(REMOTE_USAGE);
514
+ return 2;
515
+ }
516
+ const url = rest[1];
517
+ if (!isRelayUrl(url)) {
518
+ io.err(
519
+ `cannot add remote: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
520
+ );
521
+ return 2;
522
+ }
523
+ break;
524
+ }
525
+ case "remove":
526
+ if (rest.length !== 1) {
527
+ io.err("usage: rig remote remove <name>");
528
+ io.err(REMOTE_USAGE);
529
+ return 2;
530
+ }
531
+ break;
532
+ default:
533
+ io.err(`unknown rig remote subcommand: ${sub}`);
534
+ io.err(REMOTE_USAGE);
535
+ return 2;
536
+ }
537
+ try {
538
+ let repoRoot;
539
+ try {
540
+ repoRoot = await resolveRepoRoot(deps.cwd);
541
+ } catch {
542
+ throw new NotAGitRepositoryError(deps.cwd);
543
+ }
544
+ switch (sub) {
545
+ case void 0:
546
+ case "list": {
547
+ const remotes = await listGitRemotes(repoRoot);
548
+ if (json) {
549
+ io.out(JSON.stringify({ command: "remote", remotes }, null, 2));
550
+ return 0;
551
+ }
552
+ if (remotes.length === 0) {
553
+ io.out(
554
+ "no remotes configured \u2014 add one: rig remote add origin <relay-url>"
555
+ );
556
+ return 0;
557
+ }
558
+ for (const remote of remotes) {
559
+ for (const url of remote.urls) {
560
+ io.out(
561
+ `${remote.name} ${url}` + (isRelayUrl(url) ? "" : " (not a relay URL \u2014 ignored by rig)")
562
+ );
563
+ }
564
+ if (remote.urls.length > 1) {
565
+ io.err(
566
+ `warning: remote "${remote.name}" has ${remote.urls.length} URLs \u2014 rig supports one relay URL per remote (fix with \`git remote set-url ${remote.name} <relay-url>\`)`
567
+ );
568
+ }
569
+ }
570
+ return 0;
571
+ }
572
+ case "add": {
573
+ const [name, url] = rest;
574
+ const existing = await getGitRemoteUrls(repoRoot, name);
575
+ if (existing.length > 0) {
576
+ io.err(
577
+ `remote ${JSON.stringify(name)} already exists (${existing.join(", ")}) \u2014 nothing changed. Point it somewhere else with \`git remote set-url ${name} <relay-url>\`, or \`rig remote remove ${name}\` first.`
578
+ );
579
+ return 1;
580
+ }
581
+ await addGitRemote(repoRoot, name, url);
582
+ io.out(`Added remote ${name} \u2192 ${url}`);
583
+ if (name === "origin") {
584
+ io.out(
585
+ "`rig push` and the event commands now publish here by default."
586
+ );
587
+ const toonConfig = await readToonConfig(repoRoot);
588
+ if (toonConfig.relays.length > 0) {
589
+ io.err(
590
+ `note: git config toon.relay (${toonConfig.relays.join(", ")}) is deprecated and now shadowed by the origin remote \u2014 drop it with \`git config --unset-all toon.relay\` (the key is removed in v0.3).`
591
+ );
592
+ }
593
+ }
594
+ return 0;
595
+ }
596
+ case "remove": {
597
+ const name = rest[0];
598
+ const existing = await getGitRemoteUrls(repoRoot, name);
599
+ if (existing.length === 0) {
600
+ io.err(
601
+ `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`
602
+ );
603
+ return 1;
604
+ }
605
+ await removeGitRemote(repoRoot, name);
606
+ io.out(`Removed remote ${name}`);
607
+ return 0;
608
+ }
609
+ /* v8 ignore next 2 -- unreachable: validated above */
610
+ default:
611
+ return 2;
612
+ }
613
+ } catch (err) {
614
+ const described = describeError(err, "remote");
615
+ if (json) {
616
+ io.out(JSON.stringify({ command: "remote", ...described.json }, null, 2));
617
+ } else {
618
+ for (const line of described.lines) io.err(line);
619
+ }
620
+ return 1;
621
+ }
622
+ }
623
+
499
624
  // src/cli/push.ts
500
- var defaultLoadStandalone = async (env) => {
501
- const mod = await import("../standalone-mode-IK2RXILS.js");
502
- return mod.createStandaloneContext(env);
625
+ var defaultLoadStandalone = async (options) => {
626
+ const mod = await import("../standalone-mode-CAYWOURK.js");
627
+ return mod.createStandaloneContext(options);
503
628
  };
504
- var PUSH_USAGE = `Usage: rig push [refspecs...] [options]
629
+ function identityReport(ctx) {
630
+ return {
631
+ pubkey: ctx.ownerPubkey,
632
+ source: ctx.identitySource,
633
+ sourceLabel: ctx.identitySourceLabel
634
+ };
635
+ }
636
+ var PUSH_USAGE = `Usage: rig push [remote] [refspecs...] [options]
505
637
 
506
638
  Push local git refs to TOON: uploads the object delta to Arweave (paid) and
507
639
  publishes the NIP-34 refs event (kind:30618; kind:30617 announce on first
508
640
  push). Writes are permanent and non-refundable.
509
641
 
510
- Refspecs are branch/tag names or full refnames; default is the current branch.
642
+ The repo must be set up once with \`rig init\` (writes toon.repoid/toon.owner
643
+ to the local git config); the identity comes from RIG_MNEMONIC (env or a
644
+ project .env), or the ~/.toon-client keystore/config.
645
+
646
+ The relay is a git remote: \`rig push\` publishes via "origin" (set up with
647
+ \`rig remote add origin <relay-url>\`); \`rig push <remote> [refspecs...]\`
648
+ publishes via a named remote. When the first positional matches a configured
649
+ remote name it is the remote; otherwise it is a refspec and the remote
650
+ defaults to origin. Refspecs are branch/tag names or full refnames; default
651
+ is the current branch.
511
652
 
512
653
  Options:
513
654
  --force allow non-fast-forward ref updates (overwrites remote history)
@@ -516,13 +657,12 @@ Options:
516
657
  --yes skip the fee confirmation (required when not a TTY)
517
658
  --json machine-readable plan/receipts; without --yes it is a pure
518
659
  estimate (nothing executed)
519
- --relay <url> relay URL (repeatable in daemon mode; standalone mode
520
- supports exactly one; default: git config toon.relay,
521
- then the mode's default relay)
660
+ --relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
661
+ configured remotes; every positional is then a refspec.
662
+ The deprecated v0.1 \`git config toon.relay\` still works
663
+ as a fallback, with a migration nudge
522
664
  --repo-id <id> repository id / NIP-34 d-tag (default: git config
523
- toon.repoid, then the repo directory name)
524
- --daemon force daemon mode (toon-clientd control API)
525
- --standalone force standalone mode (embedded client from TOON_CLIENT_MNEMONIC)
665
+ toon.repoid \u2014 run \`rig init\` to set it)
526
666
  -h, --help show this help`;
527
667
  async function selectRefspecs(reader, positionals, all, tags) {
528
668
  const { head, refs } = await reader.listRefs();
@@ -565,7 +705,7 @@ async function selectRefspecs(reader, positionals, all, tags) {
565
705
  return selected;
566
706
  }
567
707
  function parsePushArgs(args) {
568
- const { values, positionals } = parseArgs({
708
+ const { values, positionals } = parseArgs2({
569
709
  args,
570
710
  options: {
571
711
  force: { type: "boolean", default: false },
@@ -573,8 +713,6 @@ function parsePushArgs(args) {
573
713
  tags: { type: "boolean", default: false },
574
714
  yes: { type: "boolean", default: false },
575
715
  json: { type: "boolean", default: false },
576
- daemon: { type: "boolean", default: false },
577
- standalone: { type: "boolean", default: false },
578
716
  relay: { type: "string", multiple: true },
579
717
  "repo-id": { type: "string" },
580
718
  help: { type: "boolean", short: "h", default: false }
@@ -587,8 +725,6 @@ function parsePushArgs(args) {
587
725
  tags: values.tags ?? false,
588
726
  yes: values.yes ?? false,
589
727
  json: values.json ?? false,
590
- daemon: values.daemon ?? false,
591
- standalone: values.standalone ?? false,
592
728
  relay: values.relay ?? [],
593
729
  help: values.help ?? false,
594
730
  positionals
@@ -598,7 +734,7 @@ function parsePushArgs(args) {
598
734
  return flags;
599
735
  }
600
736
  async function runPush(args, deps) {
601
- const { io, env, fetchImpl } = deps;
737
+ const { io, env } = deps;
602
738
  let flags;
603
739
  try {
604
740
  flags = parsePushArgs(args);
@@ -612,91 +748,98 @@ async function runPush(args, deps) {
612
748
  return 0;
613
749
  }
614
750
  let standaloneCtx;
615
- let standalonePlan;
616
751
  try {
617
752
  const repoRoot = await resolveRepoRoot(deps.cwd);
618
753
  const toonConfig = await readToonConfig(repoRoot);
619
- const repoId = flags.repoId ?? toonConfig.repoId ?? basename(repoRoot);
754
+ const repoId = flags.repoId ?? toonConfig.repoId;
755
+ if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
620
756
  const reader = new GitRepoReader(repoRoot);
757
+ let remoteName;
758
+ let refspecArgs = flags.positionals;
759
+ if (flags.relay.length === 0 && refspecArgs.length > 0) {
760
+ const first = refspecArgs[0];
761
+ const remoteNames = new Set(
762
+ (await listGitRemotes(repoRoot)).map((r) => r.name)
763
+ );
764
+ if (remoteNames.has(first)) {
765
+ remoteName = first;
766
+ refspecArgs = refspecArgs.slice(1);
767
+ } else {
768
+ const { refs } = await reader.listRefs();
769
+ const known = new Set(refs.map((r) => r.refname));
770
+ if (!known.has(first) && !known.has(`refs/heads/${first}`) && !known.has(`refs/tags/${first}`)) {
771
+ throw new Error(
772
+ `${JSON.stringify(first)} is neither a configured remote nor a local branch/tag \u2014 add the relay remote (\`rig remote add ${first} <relay-url>\`; \`rig remote list\` shows configured remotes) or fix the refspec`
773
+ );
774
+ }
775
+ }
776
+ }
621
777
  const refspecs = await selectRefspecs(
622
778
  reader,
623
- flags.positionals,
779
+ refspecArgs,
624
780
  flags.all,
625
781
  flags.tags
626
782
  );
627
- const explicitRelays = flags.relay.length > 0 ? flags.relay : toonConfig.relays.length > 0 ? toonConfig.relays : void 0;
628
- const { mode, probe } = await selectMode({
629
- daemon: flags.daemon,
630
- standalone: flags.standalone,
631
- env,
632
- fetchImpl
783
+ const resolved = await resolveRelays({
784
+ relayFlags: flags.relay,
785
+ remoteName,
786
+ repoRoot,
787
+ toonRelays: toonConfig.relays
633
788
  });
634
- let plan;
635
- let identity;
636
- let relaysUsed = explicitRelays;
637
- const daemonClient = new DaemonGitClient(probe.baseUrl, fetchImpl);
638
- const daemonRequest = {
639
- repoPath: repoRoot,
640
- repoId,
641
- refspecs,
642
- force: flags.force,
643
- ...explicitRelays ? { relayUrls: explicitRelays } : {}
644
- };
645
- if (mode === "daemon") {
646
- identity = probe.identity;
647
- relaysUsed ??= probe.relayUrl ? [probe.relayUrl] : void 0;
648
- plan = await daemonClient.gitEstimate(daemonRequest);
649
- } else {
650
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)(env);
651
- identity = standaloneCtx.ownerPubkey;
652
- relaysUsed ??= standaloneCtx.defaultRelayUrls;
653
- if (relaysUsed && relaysUsed.length > 1) {
654
- io.err(
655
- `standalone mode publishes to a single relay, but ${relaysUsed.length} are configured (${relaysUsed.join(", ")}) \u2014 re-run with exactly one --relay <url> (or trim git config toon.relay). Nothing was uploaded or paid.`
656
- );
657
- return 1;
658
- }
659
- const remoteState = await standaloneCtx.fetchRemote({
660
- ownerPubkey: standaloneCtx.ownerPubkey,
661
- repoId,
662
- relayUrls: relaysUsed
663
- });
664
- const feeRates = await standaloneCtx.publisher.getFeeRates();
665
- const pushPlan = await planPush({
666
- repoReader: reader,
667
- remoteState,
668
- feeRates,
669
- repoId,
670
- refs: refspecs,
671
- force: flags.force
672
- });
673
- plan = serializePushPlan(pushPlan);
674
- standalonePlan = { pushPlan, remoteState, reader, relaysUsed };
789
+ if (resolved.nudge !== void 0) io.err(resolved.nudge);
790
+ const relaysUsed = resolved.relays;
791
+ if (relaysUsed.length > 1) {
792
+ io.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
793
+ return 1;
675
794
  }
676
- if (toonConfig.owner && identity && toonConfig.owner !== identity) {
795
+ standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
796
+ env,
797
+ cwd: deps.cwd,
798
+ warn: (line) => io.err(line)
799
+ });
800
+ const identity = identityReport(standaloneCtx);
801
+ if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
677
802
  io.err(
678
- `warning: git config toon.owner (${toonConfig.owner.slice(0, 8)}\u2026) differs from the active ${mode} identity (${identity.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's`
803
+ `warning: git config toon.owner (${toonConfig.owner.slice(0, 8)}\u2026) differs from the active identity (${identity.pubkey.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's. Re-run \`rig init\` to adopt the active identity.`
679
804
  );
680
805
  }
806
+ const remoteState = await standaloneCtx.fetchRemote({
807
+ ownerPubkey: standaloneCtx.ownerPubkey,
808
+ repoId,
809
+ relayUrls: relaysUsed
810
+ });
811
+ const feeRates = await standaloneCtx.publisher.getFeeRates();
812
+ const pushPlan = await planPush({
813
+ repoReader: reader,
814
+ remoteState,
815
+ feeRates,
816
+ repoId,
817
+ refs: refspecs,
818
+ force: flags.force
819
+ });
820
+ const plan = serializePushPlan(pushPlan);
681
821
  const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
682
822
  if (upToDate) {
683
823
  if (flags.json) {
684
- io.out(jsonOut({ command: "push", mode, repoId, executed: false, upToDate: true, plan }));
824
+ io.out(
825
+ jsonOut({ command: "push", repoId, identity, executed: false, upToDate: true, plan })
826
+ );
685
827
  } else {
686
828
  io.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
687
829
  }
688
830
  return 0;
689
831
  }
690
832
  if (!flags.json) {
691
- for (const line of renderPlan(plan, mode)) io.out(line);
833
+ for (const line of renderPlan(plan)) io.out(line);
834
+ io.out(renderIdentityLine(identity));
692
835
  }
693
836
  if (!flags.yes) {
694
837
  if (flags.json) {
695
838
  io.out(
696
839
  jsonOut({
697
840
  command: "push",
698
- mode,
699
841
  repoId,
842
+ identity,
700
843
  executed: false,
701
844
  upToDate: false,
702
845
  plan,
@@ -719,41 +862,21 @@ async function runPush(args, deps) {
719
862
  return 1;
720
863
  }
721
864
  }
722
- let result;
723
- if (mode === "daemon") {
724
- result = await daemonClient.gitPush({ ...daemonRequest, confirm: true });
725
- } else {
726
- const cached = standalonePlan;
727
- if (!cached || !standaloneCtx) {
728
- throw new Error("internal: standalone plan cache missing");
729
- }
730
- const pushResult = await executePush({
731
- plan: cached.pushPlan,
732
- publisher: standaloneCtx.publisher,
733
- remoteState: cached.remoteState,
734
- repoReader: cached.reader,
735
- relayUrls: cached.relaysUsed
736
- });
737
- result = serializePushResult(cached.pushPlan, pushResult);
738
- }
865
+ const pushResult = await executePush({
866
+ plan: pushPlan,
867
+ publisher: standaloneCtx.publisher,
868
+ remoteState,
869
+ repoReader: reader,
870
+ relayUrls: relaysUsed
871
+ });
872
+ const result = serializePushResult(pushPlan, pushResult);
739
873
  if (flags.json) {
740
874
  io.out(
741
- jsonOut({ command: "push", mode, repoId, executed: true, upToDate: false, plan, result })
875
+ jsonOut({ command: "push", repoId, identity, executed: true, upToDate: false, plan, result })
742
876
  );
743
877
  } else {
744
878
  for (const line of renderResult(result)) io.out(line);
745
879
  }
746
- try {
747
- await writeToonConfig(repoRoot, {
748
- repoId,
749
- ...identity ? { owner: identity } : {},
750
- ...relaysUsed ? { relays: relaysUsed } : {}
751
- });
752
- } catch (err) {
753
- io.err(
754
- `warning: push succeeded but persisting git config failed: ${err instanceof Error ? err.message : String(err)}`
755
- );
756
- }
757
880
  return 0;
758
881
  } catch (err) {
759
882
  const described = describeError(err);
@@ -783,23 +906,24 @@ var defaultReadStdin = async () => {
783
906
  for await (const chunk of process.stdin) chunks.push(chunk);
784
907
  return Buffer.concat(chunks).toString("utf-8");
785
908
  };
786
- var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config toon.repoid)
909
+ var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
910
+ toon.repoid \u2014 run \`rig init\` to set it)
787
911
  --owner <pubkey> repository owner pubkey, 64-char hex (default: git config
788
- toon.owner, then the active publisher identity)
789
- --relay <url> relay to publish to in standalone mode (default: git config
790
- toon.relay, then the mode's default; standalone supports
791
- exactly one relay \u2014 daemon mode publishes via its apex)
912
+ toon.owner, then the active identity)
913
+ --remote <name> publish via this configured git remote (default: the
914
+ "origin" remote \u2014 \`rig remote add origin <relay-url>\`)
915
+ --relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
916
+ configured remotes. The deprecated v0.1 \`git config
917
+ toon.relay\` still works as a fallback, with a nudge
792
918
  --yes skip the fee confirmation (required when not a TTY)
793
919
  --json machine-readable receipt; without --yes it is a pure
794
920
  estimate (nothing published, exit 0)
795
- --daemon force daemon mode (toon-clientd control API)
796
- --standalone force standalone mode (embedded client from TOON_CLIENT_MNEMONIC)
797
921
  -h, --help show this help`;
798
922
  var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
799
923
 
800
924
  File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
801
925
  permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
802
- from the toon.* git config keys \`rig push\` persists.
926
+ from the toon.* git config keys \`rig init\` writes.
803
927
 
804
928
  Body source (exactly one): --body, --body-file, or piped stdin.
805
929
 
@@ -822,7 +946,7 @@ Options:
822
946
  --marker <root|reply> e-tag marker (default: root \u2014 commenting directly
823
947
  on the issue/patch)
824
948
  ${COMMON_FLAGS_USAGE}`;
825
- var PR_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
949
+ var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
826
950
 
827
951
  Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
828
952
  a paid publish; writes are permanent and non-refundable. --range runs
@@ -839,16 +963,20 @@ Options:
839
963
  --patch-file <path> literal patch text to publish
840
964
  --branch <name> branch name (t tag)
841
965
  ${COMMON_FLAGS_USAGE}`;
842
- var STATUS_USAGE = `Usage: rig status <target-event-id> <open|applied|closed|draft> [options]
966
+ var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
843
967
 
844
968
  Set the status of an issue or patch \u2014 a paid publish; writes are permanent
845
969
  and non-refundable. Publishes kind:1630 (open), 1631 (applied), 1632
846
970
  (closed), or 1633 (draft) against the 64-char hex id of the target event,
847
971
  with the repo a-tag attached so readers can scope a status stream to the
848
- repository.
972
+ repository. (This command was \`rig status\` before v2 \u2014 bare \`rig status\`
973
+ now passes through to \`git status\`.)
849
974
 
850
975
  Options:
851
976
  ${COMMON_FLAGS_USAGE}`;
977
+ var PR_USAGE = `${PR_CREATE_USAGE}
978
+
979
+ ${PR_STATUS_USAGE}`;
852
980
  var HEX64_RE = /^[0-9a-f]{64}$/;
853
981
  function assertHex64(value, what) {
854
982
  if (!HEX64_RE.test(value)) {
@@ -860,9 +988,8 @@ function assertHex64(value, what) {
860
988
  var COMMON_OPTIONS = {
861
989
  yes: { type: "boolean", default: false },
862
990
  json: { type: "boolean", default: false },
863
- daemon: { type: "boolean", default: false },
864
- standalone: { type: "boolean", default: false },
865
991
  relay: { type: "string", multiple: true },
992
+ remote: { type: "string" },
866
993
  "repo-id": { type: "string" },
867
994
  owner: { type: "string" },
868
995
  help: { type: "boolean", short: "h", default: false }
@@ -871,11 +998,11 @@ function pickCommon(values) {
871
998
  const flags = {
872
999
  yes: values["yes"] === true,
873
1000
  json: values["json"] === true,
874
- daemon: values["daemon"] === true,
875
- standalone: values["standalone"] === true,
876
1001
  relay: Array.isArray(values["relay"]) ? values["relay"] : [],
877
1002
  help: values["help"] === true
878
1003
  };
1004
+ const remote = values["remote"];
1005
+ if (typeof remote === "string") flags.remote = remote;
879
1006
  const repoId = values["repo-id"];
880
1007
  if (typeof repoId === "string") flags.repoId = repoId;
881
1008
  const owner = values["owner"];
@@ -887,54 +1014,43 @@ function pickCommon(values) {
887
1014
  }
888
1015
  async function runEvent(opts) {
889
1016
  const { command, flags, deps, actionLabel } = opts;
890
- const { io, env, fetchImpl } = deps;
1017
+ const { io, env } = deps;
891
1018
  let standaloneCtx;
892
1019
  try {
1020
+ let repoRoot;
893
1021
  let toonConfig = { relays: [] };
894
1022
  try {
895
- toonConfig = await readToonConfig(await resolveRepoRoot(deps.cwd));
1023
+ repoRoot = await resolveRepoRoot(deps.cwd);
1024
+ toonConfig = await readToonConfig(repoRoot);
896
1025
  } catch {
897
1026
  }
898
1027
  const repoId = flags.repoId ?? toonConfig.repoId;
899
1028
  if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
900
- const explicitRelays = flags.relay.length > 0 ? flags.relay : toonConfig.relays.length > 0 ? toonConfig.relays : void 0;
901
- const { mode, probe } = await selectMode({
902
- daemon: flags.daemon,
903
- standalone: flags.standalone,
904
- env,
905
- fetchImpl
1029
+ const resolved = await resolveRelays({
1030
+ relayFlags: flags.relay,
1031
+ remoteName: flags.remote,
1032
+ repoRoot,
1033
+ toonRelays: toonConfig.relays
906
1034
  });
907
- const daemonClient = new DaemonGitClient(probe.baseUrl, fetchImpl);
908
- let identity;
909
- let fee;
910
- let relaysUsed = explicitRelays;
911
- if (mode === "daemon") {
912
- identity = probe.identity;
913
- fee = probe.feePerEvent;
914
- } else {
915
- standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)(env);
916
- identity = standaloneCtx.ownerPubkey;
917
- relaysUsed ??= standaloneCtx.defaultRelayUrls;
918
- if (relaysUsed.length > 1) {
919
- io.err(
920
- `standalone mode publishes to a single relay, but ${relaysUsed.length} are configured (${relaysUsed.join(", ")}) \u2014 re-run with exactly one --relay <url> (or trim git config toon.relay). Nothing was published or paid.`
921
- );
922
- return 1;
923
- }
924
- fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
1035
+ if (resolved.nudge !== void 0) io.err(resolved.nudge);
1036
+ const relaysUsed = resolved.relays;
1037
+ if (relaysUsed.length > 1) {
1038
+ io.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
1039
+ return 1;
925
1040
  }
926
- const owner = flags.owner ?? toonConfig.owner ?? identity;
927
- if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
1041
+ standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)({
1042
+ env,
1043
+ cwd: deps.cwd,
1044
+ warn: (line) => io.err(line)
1045
+ });
1046
+ const identity = identityReport(standaloneCtx);
1047
+ const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
1048
+ const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
928
1049
  const addr = { ownerPubkey: owner, repoId };
929
1050
  const event = await opts.buildEvent(addr);
930
1051
  const action = `kind:${event.kind} ${actionLabel}`;
931
1052
  if (!flags.json) {
932
- for (const line of renderEventPlan({
933
- action,
934
- addr,
935
- mode,
936
- ...fee !== void 0 ? { fee } : {}
937
- })) {
1053
+ for (const line of renderEventPlan({ action, addr, identity, fee })) {
938
1054
  io.out(line);
939
1055
  }
940
1056
  }
@@ -943,11 +1059,11 @@ async function runEvent(opts) {
943
1059
  io.out(
944
1060
  jsonOut2({
945
1061
  command,
946
- mode,
947
1062
  repoAddr: addr,
1063
+ identity,
948
1064
  kind: event.kind,
949
1065
  executed: false,
950
- feeEstimate: fee ?? null,
1066
+ feeEstimate: fee,
951
1067
  hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
952
1068
  })
953
1069
  );
@@ -967,26 +1083,17 @@ async function runEvent(opts) {
967
1083
  return 1;
968
1084
  }
969
1085
  }
970
- let result;
971
- if (mode === "daemon") {
972
- result = await opts.sendDaemon(daemonClient, addr);
973
- } else {
974
- if (!standaloneCtx) throw new Error("internal: standalone context missing");
975
- const receipt = await standaloneCtx.publisher.publishEvent(
976
- event,
977
- relaysUsed ?? []
978
- );
979
- result = serializeEventReceipt(event.kind, receipt);
980
- }
1086
+ const receipt = await standaloneCtx.publisher.publishEvent(event, relaysUsed);
1087
+ const result = serializeEventReceipt(event.kind, receipt);
981
1088
  if (flags.json) {
982
1089
  io.out(
983
1090
  jsonOut2({
984
1091
  command,
985
- mode,
986
1092
  repoAddr: addr,
1093
+ identity,
987
1094
  kind: result.kind,
988
1095
  executed: true,
989
- feeEstimate: fee ?? null,
1096
+ feeEstimate: fee,
990
1097
  result
991
1098
  })
992
1099
  );
@@ -1034,7 +1141,7 @@ async function runIssue(args, deps) {
1034
1141
  let bodyFile;
1035
1142
  let labels;
1036
1143
  try {
1037
- const { values } = parseArgs2({
1144
+ const { values } = parseArgs3({
1038
1145
  args: rest,
1039
1146
  options: {
1040
1147
  ...COMMON_OPTIONS,
@@ -1093,13 +1200,7 @@ async function runIssue(args, deps) {
1093
1200
  flags,
1094
1201
  deps,
1095
1202
  actionLabel: `issue ${JSON.stringify(title)}`,
1096
- buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
1097
- sendDaemon: (client, addr) => client.gitIssue({
1098
- repoAddr: addr,
1099
- title,
1100
- body,
1101
- ...labels.length > 0 ? { labels } : {}
1102
- })
1203
+ buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels)
1103
1204
  });
1104
1205
  }
1105
1206
  async function runComment(args, deps) {
@@ -1110,7 +1211,7 @@ async function runComment(args, deps) {
1110
1211
  let parentAuthor;
1111
1212
  let marker;
1112
1213
  try {
1113
- const { values, positionals } = parseArgs2({
1214
+ const { values, positionals } = parseArgs3({
1114
1215
  args,
1115
1216
  options: {
1116
1217
  ...COMMON_OPTIONS,
@@ -1160,14 +1261,7 @@ async function runComment(args, deps) {
1160
1261
  parentAuthor ?? addr.ownerPubkey,
1161
1262
  body,
1162
1263
  marker
1163
- ),
1164
- sendDaemon: (client, addr) => client.gitComment({
1165
- repoAddr: addr,
1166
- rootEventId,
1167
- body,
1168
- marker,
1169
- ...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {}
1170
- })
1264
+ )
1171
1265
  });
1172
1266
  }
1173
1267
  var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
@@ -1177,24 +1271,33 @@ function extractPatchShas(patchText) {
1177
1271
  async function runPr(args, deps) {
1178
1272
  const { io } = deps;
1179
1273
  const [sub, ...rest] = args;
1180
- if (sub === "--help" || sub === "-h" || sub === "help") {
1181
- io.out(PR_USAGE);
1182
- return 0;
1183
- }
1184
- if (sub !== "create") {
1185
- io.err(
1186
- sub === void 0 ? "missing subcommand: rig pr create" : `unknown rig pr subcommand: ${sub}`
1187
- );
1188
- io.err(PR_USAGE);
1189
- return 2;
1274
+ switch (sub) {
1275
+ case "create":
1276
+ return runPrCreate(rest, deps);
1277
+ case "status":
1278
+ return runPrStatus(rest, deps);
1279
+ case "--help":
1280
+ case "-h":
1281
+ case "help":
1282
+ io.out(PR_USAGE);
1283
+ return 0;
1284
+ default:
1285
+ io.err(
1286
+ sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
1287
+ );
1288
+ io.err(PR_USAGE);
1289
+ return 2;
1190
1290
  }
1291
+ }
1292
+ async function runPrCreate(rest, deps) {
1293
+ const { io } = deps;
1191
1294
  let flags;
1192
1295
  let title;
1193
1296
  let range;
1194
1297
  let patchFile;
1195
1298
  let branch;
1196
1299
  try {
1197
- const { values } = parseArgs2({
1300
+ const { values } = parseArgs3({
1198
1301
  args: rest,
1199
1302
  options: {
1200
1303
  ...COMMON_OPTIONS,
@@ -1207,7 +1310,7 @@ async function runPr(args, deps) {
1207
1310
  });
1208
1311
  flags = pickCommon(values);
1209
1312
  if (flags.help) {
1210
- io.out(PR_USAGE);
1313
+ io.out(PR_CREATE_USAGE);
1211
1314
  return 0;
1212
1315
  }
1213
1316
  if (values.title === void 0 || values.title === "") {
@@ -1222,43 +1325,38 @@ async function runPr(args, deps) {
1222
1325
  branch = values.branch;
1223
1326
  } catch (err) {
1224
1327
  io.err(err instanceof Error ? err.message : String(err));
1225
- io.err(PR_USAGE);
1328
+ io.err(PR_CREATE_USAGE);
1226
1329
  return 2;
1227
1330
  }
1228
- let prepared;
1229
- const prepare = async () => {
1230
- if (prepared) return prepared;
1231
- if (range !== void 0) {
1232
- const reader = new GitRepoReader(await resolveRepoRoot(deps.cwd));
1233
- const patchText = await reader.formatPatch(range);
1234
- if (patchText === "") {
1235
- throw new Error(
1236
- `range ${JSON.stringify(range)} selects no commits \u2014 nothing to publish`
1237
- );
1238
- }
1239
- const shas = extractPatchShas(patchText);
1240
- const parents = await reader.commitParents(shas);
1241
- const commits = shas.flatMap((sha) => {
1242
- const parentSha = parents.get(sha)?.[0];
1243
- return parentSha ? [{ sha, parentSha }] : [];
1244
- });
1245
- prepared = { patchText, commits };
1246
- } else {
1247
- const patchText = await readFile(patchFile, "utf-8");
1248
- if (patchText.trim() === "") {
1249
- throw new Error(`--patch-file ${patchFile} is empty \u2014 nothing to publish`);
1250
- }
1251
- prepared = { patchText, commits: [] };
1252
- }
1253
- return prepared;
1254
- };
1255
1331
  return runEvent({
1256
1332
  command: "pr",
1257
1333
  flags,
1258
1334
  deps,
1259
1335
  actionLabel: `patch ${JSON.stringify(title)}`,
1260
1336
  buildEvent: async (addr) => {
1261
- const { patchText, commits } = await prepare();
1337
+ let patchText;
1338
+ let commits;
1339
+ if (range !== void 0) {
1340
+ const reader = new GitRepoReader(await resolveRepoRoot(deps.cwd));
1341
+ patchText = await reader.formatPatch(range);
1342
+ if (patchText === "") {
1343
+ throw new Error(
1344
+ `range ${JSON.stringify(range)} selects no commits \u2014 nothing to publish`
1345
+ );
1346
+ }
1347
+ const shas = extractPatchShas(patchText);
1348
+ const parents = await reader.commitParents(shas);
1349
+ commits = shas.flatMap((sha) => {
1350
+ const parentSha = parents.get(sha)?.[0];
1351
+ return parentSha ? [{ sha, parentSha }] : [];
1352
+ });
1353
+ } else {
1354
+ patchText = await readFile(patchFile, "utf-8");
1355
+ if (patchText.trim() === "") {
1356
+ throw new Error(`--patch-file ${patchFile} is empty \u2014 nothing to publish`);
1357
+ }
1358
+ commits = [];
1359
+ }
1262
1360
  return buildPatch(
1263
1361
  addr.ownerPubkey,
1264
1362
  addr.repoId,
@@ -1267,16 +1365,6 @@ async function runPr(args, deps) {
1267
1365
  branch,
1268
1366
  patchText
1269
1367
  );
1270
- },
1271
- sendDaemon: async (client, addr) => {
1272
- const { patchText, commits } = await prepare();
1273
- return client.gitPatch({
1274
- repoAddr: addr,
1275
- title,
1276
- patchText,
1277
- ...commits.length > 0 ? { commits } : {},
1278
- ...branch !== void 0 ? { branch } : {}
1279
- });
1280
1368
  }
1281
1369
  });
1282
1370
  }
@@ -1289,20 +1377,20 @@ var STATUS_KIND_BY_VALUE = {
1289
1377
  function isStatusValue(value) {
1290
1378
  return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
1291
1379
  }
1292
- async function runStatus(args, deps) {
1380
+ async function runPrStatus(args, deps) {
1293
1381
  const { io } = deps;
1294
1382
  let flags;
1295
1383
  let targetEventId;
1296
1384
  let status;
1297
1385
  try {
1298
- const { values, positionals } = parseArgs2({
1386
+ const { values, positionals } = parseArgs3({
1299
1387
  args,
1300
1388
  options: COMMON_OPTIONS,
1301
1389
  allowPositionals: true
1302
1390
  });
1303
1391
  flags = pickCommon(values);
1304
1392
  if (flags.help) {
1305
- io.out(STATUS_USAGE);
1393
+ io.out(PR_STATUS_USAGE);
1306
1394
  return 0;
1307
1395
  }
1308
1396
  if (positionals.length !== 2) {
@@ -1321,11 +1409,11 @@ async function runStatus(args, deps) {
1321
1409
  status = rawStatus;
1322
1410
  } catch (err) {
1323
1411
  io.err(err instanceof Error ? err.message : String(err));
1324
- io.err(STATUS_USAGE);
1412
+ io.err(PR_STATUS_USAGE);
1325
1413
  return 2;
1326
1414
  }
1327
1415
  return runEvent({
1328
- command: "status",
1416
+ command: "pr status",
1329
1417
  flags,
1330
1418
  deps,
1331
1419
  actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
@@ -1336,59 +1424,261 @@ async function runStatus(args, deps) {
1336
1424
  `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
1337
1425
  ]);
1338
1426
  return event;
1427
+ }
1428
+ });
1429
+ }
1430
+
1431
+ // src/cli/git-passthrough.ts
1432
+ import { spawn } from "child_process";
1433
+ import { constants as osConstants } from "os";
1434
+ var GIT_NOT_FOUND_EXIT = 127;
1435
+ var RELAYED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
1436
+ function signalExitCode(signal) {
1437
+ const num = osConstants.signals[signal];
1438
+ return num === void 0 ? 1 : 128 + num;
1439
+ }
1440
+ var runGitPassthrough = (argv, options = {}) => {
1441
+ const err = options.err ?? ((line) => process.stderr.write(`${line}
1442
+ `));
1443
+ return new Promise((resolve) => {
1444
+ const child = spawn("git", argv, {
1445
+ stdio: "inherit",
1446
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1447
+ env: options.env ?? process.env
1448
+ });
1449
+ const relay = /* @__PURE__ */ new Map();
1450
+ for (const signal of RELAYED_SIGNALS) {
1451
+ const handler = () => {
1452
+ child.kill(signal);
1453
+ };
1454
+ relay.set(signal, handler);
1455
+ process.on(signal, handler);
1456
+ }
1457
+ const restoreSignals = () => {
1458
+ for (const [signal, handler] of relay) process.removeListener(signal, handler);
1459
+ };
1460
+ child.on("error", (spawnErr) => {
1461
+ restoreSignals();
1462
+ if (spawnErr.code === "ENOENT") {
1463
+ err(
1464
+ `rig: git not found \u2014 \`rig ${argv[0] ?? ""}\` is not a rig command, so it is passed through to the system \`git\`, which is not on your PATH. Install git (https://git-scm.com) or fix your PATH.`
1465
+ );
1466
+ resolve(GIT_NOT_FOUND_EXIT);
1467
+ return;
1468
+ }
1469
+ err(`rig: failed to run git: ${spawnErr.message}`);
1470
+ resolve(1);
1471
+ });
1472
+ child.on("close", (code, signal) => {
1473
+ restoreSignals();
1474
+ resolve(signal !== null ? signalExitCode(signal) : code ?? 1);
1475
+ });
1476
+ });
1477
+ };
1478
+
1479
+ // src/cli/init.ts
1480
+ import { basename } from "path";
1481
+ import { parseArgs as parseArgs4 } from "util";
1482
+ var INIT_USAGE = `Usage: rig init [options]
1483
+
1484
+ Set up the current git repository for rig (one-shot, idempotent, free):
1485
+ resolves your identity (RIG_MNEMONIC env \u2192 project .env \u2192 ~/.toon-client
1486
+ keystore/config), then writes toon.repoid and toon.owner to the repo's
1487
+ LOCAL git config. Re-running updates and reports; it never errors on an
1488
+ already-initialized repo. The seed phrase is never written anywhere.
1489
+
1490
+ The follow-up step is adding a relay: \`rig remote add origin <relay-url>\`
1491
+ (a real git remote \u2014 \`git remote -v\` shows it). A deprecated v0.1
1492
+ \`git config toon.relay\` is migrated to the origin remote automatically
1493
+ when no origin exists (the old key stays readable and is removed in v0.3).
1494
+
1495
+ Options:
1496
+ --repo-id <id> repository id / NIP-34 d-tag (default: the existing
1497
+ toon.repoid, then the repo directory basename)
1498
+ --json machine-readable report
1499
+ -h, --help show this help`;
1500
+ function parseInitArgs(args) {
1501
+ const { values, positionals } = parseArgs4({
1502
+ args,
1503
+ options: {
1504
+ "repo-id": { type: "string" },
1505
+ json: { type: "boolean", default: false },
1506
+ help: { type: "boolean", short: "h", default: false }
1339
1507
  },
1340
- sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
1508
+ allowPositionals: false
1341
1509
  });
1510
+ if (positionals.length > 0) {
1511
+ throw new Error(`rig init takes no positional arguments`);
1512
+ }
1513
+ const flags = {
1514
+ json: values.json ?? false,
1515
+ help: values.help ?? false
1516
+ };
1517
+ const repoId = values["repo-id"];
1518
+ if (repoId !== void 0) {
1519
+ if (repoId.trim() === "") throw new Error("--repo-id must not be empty");
1520
+ flags.repoId = repoId;
1521
+ }
1522
+ return flags;
1523
+ }
1524
+ async function runInit(args, deps) {
1525
+ const { io, env } = deps;
1526
+ let flags;
1527
+ try {
1528
+ flags = parseInitArgs(args);
1529
+ } catch (err) {
1530
+ io.err(err instanceof Error ? err.message : String(err));
1531
+ io.err(INIT_USAGE);
1532
+ return 2;
1533
+ }
1534
+ if (flags.help) {
1535
+ io.out(INIT_USAGE);
1536
+ return 0;
1537
+ }
1538
+ try {
1539
+ let repoRoot;
1540
+ try {
1541
+ repoRoot = await resolveRepoRoot(deps.cwd);
1542
+ } catch {
1543
+ throw new NotAGitRepositoryError(deps.cwd);
1544
+ }
1545
+ const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
1546
+ env,
1547
+ cwd: deps.cwd,
1548
+ warn: (line) => io.err(line)
1549
+ });
1550
+ const existing = await readToonConfig(repoRoot);
1551
+ const repoId = flags.repoId ?? existing.repoId ?? basename(repoRoot);
1552
+ const changed = {
1553
+ repoId: existing.repoId !== repoId,
1554
+ owner: existing.owner !== identity.pubkey
1555
+ };
1556
+ await writeToonConfig(repoRoot, { repoId, owner: identity.pubkey });
1557
+ let remotes = await listGitRemotes(repoRoot);
1558
+ let migratedToonRelay = false;
1559
+ const legacyRelay = existing.relays[0];
1560
+ if (!remotes.some((r) => r.name === "origin") && existing.relays.length === 1 && legacyRelay !== void 0 && isRelayUrl(legacyRelay)) {
1561
+ await addGitRemote(repoRoot, "origin", legacyRelay);
1562
+ migratedToonRelay = true;
1563
+ remotes = await listGitRemotes(repoRoot);
1564
+ }
1565
+ const origin = remotes.find((r) => r.name === "origin");
1566
+ const originRelay = origin !== void 0 && origin.urls.length === 1 && isRelayUrl(origin.urls[0]) ? origin.urls[0] : void 0;
1567
+ if (flags.json) {
1568
+ const output = {
1569
+ command: "init",
1570
+ repoRoot,
1571
+ repoId,
1572
+ owner: identity.pubkey,
1573
+ identity: {
1574
+ source: identity.source,
1575
+ sourceLabel: identity.sourceLabel,
1576
+ pubkey: identity.pubkey
1577
+ },
1578
+ relays: existing.relays,
1579
+ relayConfigured: originRelay !== void 0 || existing.relays.length > 0,
1580
+ remotes,
1581
+ origin: originRelay ?? null,
1582
+ migratedToonRelay,
1583
+ changed
1584
+ };
1585
+ io.out(JSON.stringify(output, null, 2));
1586
+ return 0;
1587
+ }
1588
+ io.out(`Initialized rig for ${repoRoot}`);
1589
+ io.out(renderIdentityLine(identity));
1590
+ io.out(
1591
+ ` toon.repoid = ${repoId}` + (changed.repoId ? "" : " (unchanged)") + (existing.repoId && changed.repoId ? ` (was ${existing.repoId})` : "")
1592
+ );
1593
+ io.out(
1594
+ ` toon.owner = ${identity.pubkey}` + (changed.owner ? "" : " (unchanged)") + (existing.owner && changed.owner ? ` (was ${existing.owner})` : "")
1595
+ );
1596
+ if (originRelay !== void 0) {
1597
+ io.out(
1598
+ ` origin = ${originRelay}` + (migratedToonRelay ? " (migrated from git config toon.relay)" : "")
1599
+ );
1600
+ if (migratedToonRelay) {
1601
+ io.out(
1602
+ "note: toon.relay is deprecated \u2014 it stays readable as a fallback and is removed in v0.3; drop it now with `git config --unset-all toon.relay`."
1603
+ );
1604
+ }
1605
+ io.out('Ready: `rig push` publishes this repo via remote "origin".');
1606
+ } else if (existing.relays.length > 0) {
1607
+ io.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
1608
+ io.out(
1609
+ origin !== void 0 ? `The "origin" remote (${origin.urls.join(", ")}) is not a single relay URL, so toon.relay stays the fallback \u2014 add the relay under another name (\`rig remote add toon <relay-url>\`) and push with \`rig push toon\`.` : existing.relays.length > 1 ? `toon.relay has ${existing.relays.length} values and rig publishes to one relay \u2014 migrate the right one: \`rig remote add origin <relay-url>\`.` : "This value is not a relay URL (ws://, wss://, http://, or https://) \u2014 set a real one: `rig remote add origin <relay-url>`."
1610
+ );
1611
+ } else if (origin !== void 0) {
1612
+ io.out(
1613
+ `No relay configured yet \u2014 "origin" (${origin.urls.join(", ")}) is not a relay URL, so add the relay under another name: \`rig remote add toon <relay-url>\`, then \`rig push toon\`.`
1614
+ );
1615
+ } else {
1616
+ io.out(
1617
+ "No relay configured yet \u2014 add one as the follow-up step: `rig remote add origin <relay-url>` (a real git remote; `git remote -v` shows it). One-off pushes can pass --relay <url>."
1618
+ );
1619
+ }
1620
+ return 0;
1621
+ } catch (err) {
1622
+ const described = describeError(err, "init");
1623
+ if (flags.json) {
1624
+ io.out(JSON.stringify({ command: "init", ...described.json }, null, 2));
1625
+ } else {
1626
+ for (const line of described.lines) io.err(line);
1627
+ }
1628
+ return 1;
1629
+ }
1342
1630
  }
1343
1631
 
1344
- // src/cli/rig.ts
1345
- var USAGE = `rig \u2014 push git repos to TOON (pay-to-write Nostr + Arweave)
1632
+ // src/cli/dispatch.ts
1633
+ var USAGE = `rig \u2014 git with a TOON remote (pay-to-write Nostr + Arweave)
1346
1634
 
1347
1635
  Usage: rig <command> [options]
1348
1636
 
1349
- Commands:
1350
- push [refspecs...] plan, price, confirm, and execute a paid push
1637
+ Commands rig owns:
1638
+ init set up this repo: resolve your identity
1639
+ (RIG_MNEMONIC) and write the toon.* git config
1640
+ remote add <name> <url> add a relay as a REAL git remote ("origin" is
1641
+ remote remove <name> the default publish target); remove/list manage
1642
+ remote list them \u2014 \`git remote -v\` shows the same data
1643
+ push [remote] [refspecs...] plan, price, confirm, and execute a paid push
1644
+ to TOON (defaults to the "origin" remote). rig
1645
+ push is the TOON transport and shadows git push;
1646
+ plain-git pushes remain available by running
1647
+ \`git push\` directly
1351
1648
  issue create file an issue (kind:1621) against a repo
1352
1649
  comment <root-event-id> comment (kind:1622) on an issue or patch
1353
1650
  pr create publish a patch (kind:1617) with real
1354
1651
  \`git format-patch\` content
1355
- status <event-id> <state> set an issue/patch status (kind:1630-1633):
1652
+ pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
1356
1653
  open | applied | closed | draft
1357
1654
 
1358
- Run \`rig <command> --help\` for the command's flags. All writes are paid,
1359
- permanent, and non-refundable; each command quotes its fee and asks for
1360
- confirmation before spending (--yes skips, --json emits machine output).`;
1361
- function makeIo() {
1362
- return {
1363
- out: (line) => process.stdout.write(`${line}
1364
- `),
1365
- err: (line) => process.stderr.write(`${line}
1366
- `),
1367
- isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1368
- confirm: async (question) => {
1369
- const rl = createInterface({
1370
- input: process.stdin,
1371
- output: process.stderr
1372
- });
1373
- try {
1374
- const answer = (await rl.question(question)).trim().toLowerCase();
1375
- return answer === "y" || answer === "yes";
1376
- } finally {
1377
- rl.close();
1378
- }
1655
+ Any other command is passed through to git verbatim: \`rig status\` runs
1656
+ \`git status\`, and \`rig add -p\`, \`rig commit\`, \`rig log --oneline\`,
1657
+ \`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
1658
+
1659
+ Run \`rig <command> --help\` for a rig command's flags. \`rig init\` and
1660
+ \`rig remote\` are free; the other rig commands are paid writes \u2014 permanent
1661
+ and non-refundable; each quotes its fee and asks for confirmation before
1662
+ spending (--yes skips, --json emits machine output).`;
1663
+ function rigVersion() {
1664
+ const require2 = createRequire(import.meta.url);
1665
+ for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
1666
+ try {
1667
+ const pkg = require2(rel);
1668
+ if (pkg.name === "@toon-protocol/rig" && pkg.version) return pkg.version;
1669
+ } catch {
1379
1670
  }
1380
- };
1671
+ }
1672
+ return "unknown";
1381
1673
  }
1382
- async function main() {
1383
- const [command, ...rest] = process.argv.slice(2);
1384
- const io = makeIo();
1385
- const deps = {
1386
- io,
1387
- env: process.env,
1388
- cwd: process.cwd(),
1389
- fetchImpl: fetch
1390
- };
1674
+ async function dispatch(argv, deps) {
1675
+ const [command, ...rest] = argv;
1676
+ const { io } = deps;
1391
1677
  switch (command) {
1678
+ case "init":
1679
+ return runInit(rest, deps);
1680
+ case "remote":
1681
+ return runRemote(rest, deps);
1392
1682
  case "push":
1393
1683
  return runPush(rest, deps);
1394
1684
  case "issue":
@@ -1397,8 +1687,6 @@ async function main() {
1397
1687
  return runComment(rest, deps);
1398
1688
  case "pr":
1399
1689
  return runPr(rest, deps);
1400
- case "status":
1401
- return runStatus(rest, deps);
1402
1690
  case "help":
1403
1691
  case "--help":
1404
1692
  case "-h":
@@ -1406,16 +1694,48 @@ async function main() {
1406
1694
  io.out("");
1407
1695
  io.out(PUSH_USAGE);
1408
1696
  return 0;
1697
+ case "--version":
1698
+ io.out(`rig ${rigVersion()}`);
1699
+ return 0;
1409
1700
  case void 0:
1410
1701
  io.err(USAGE);
1411
1702
  return 2;
1412
1703
  default:
1413
- io.err(`unknown command: ${command}`);
1414
- io.err(USAGE);
1415
- return 2;
1704
+ return (deps.runGit ?? runGitPassthrough)(argv, {
1705
+ cwd: deps.cwd,
1706
+ env: deps.env,
1707
+ err: (line) => io.err(line)
1708
+ });
1416
1709
  }
1417
1710
  }
1418
- main().then(
1711
+
1712
+ // src/cli/rig.ts
1713
+ function makeIo() {
1714
+ return {
1715
+ out: (line) => process.stdout.write(`${line}
1716
+ `),
1717
+ err: (line) => process.stderr.write(`${line}
1718
+ `),
1719
+ isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1720
+ confirm: async (question) => {
1721
+ const rl = createInterface({
1722
+ input: process.stdin,
1723
+ output: process.stderr
1724
+ });
1725
+ try {
1726
+ const answer = (await rl.question(question)).trim().toLowerCase();
1727
+ return answer === "y" || answer === "yes";
1728
+ } finally {
1729
+ rl.close();
1730
+ }
1731
+ }
1732
+ };
1733
+ }
1734
+ dispatch(process.argv.slice(2), {
1735
+ io: makeIo(),
1736
+ env: process.env,
1737
+ cwd: process.cwd()
1738
+ }).then(
1419
1739
  (code) => {
1420
1740
  process.exitCode = code;
1421
1741
  },