@toon-protocol/git 1.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.
@@ -0,0 +1,1430 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GitError,
4
+ GitRepoReader,
5
+ NonFastForwardError,
6
+ OversizeObjectsError,
7
+ executePush,
8
+ planPush,
9
+ serializeEventReceipt,
10
+ serializePushPlan,
11
+ serializePushResult
12
+ } from "../chunk-4WFGAICZ.js";
13
+ import {
14
+ buildComment,
15
+ buildIssue,
16
+ buildPatch,
17
+ buildStatus
18
+ } from "../chunk-KXXHAUXL.js";
19
+ import {
20
+ DEFAULT_DAEMON_PORT
21
+ } from "../chunk-LJA7PPZI.js";
22
+ import {
23
+ MAX_OBJECT_SIZE
24
+ } from "../chunk-M7O4SEVW.js";
25
+
26
+ // src/cli/rig.ts
27
+ import { createInterface } from "readline/promises";
28
+
29
+ // src/cli/events.ts
30
+ import { readFile } from "fs/promises";
31
+ import { parseArgs as parseArgs2 } from "util";
32
+ import {
33
+ REPOSITORY_ANNOUNCEMENT_KIND,
34
+ STATUS_APPLIED_KIND,
35
+ STATUS_CLOSED_KIND,
36
+ STATUS_DRAFT_KIND,
37
+ STATUS_OPEN_KIND
38
+ } from "@toon-protocol/core/nip34";
39
+
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";
47
+ }
48
+ status;
49
+ envelope;
50
+ };
51
+ var DaemonUnreachableError = class extends Error {
52
+ constructor(baseUrl, cause) {
53
+ 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})` : "")
55
+ );
56
+ this.baseUrl = baseUrl;
57
+ this.name = "DaemonUnreachableError";
58
+ }
59
+ baseUrl;
60
+ };
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;
112
+ }
113
+ };
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) {
170
+ 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)`
174
+ );
175
+ this.name = "NoPublisherError";
176
+ }
177
+ };
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
+ };
188
+ }
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) {
206
+ 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).`
208
+ );
209
+ this.missing = missing;
210
+ this.name = "UnconfiguredRepoAddressError";
211
+ }
212
+ missing;
213
+ };
214
+ function nonFastForwardLines(refs) {
215
+ return [
216
+ "Push rejected: non-fast-forward update for:",
217
+ ...refs.map(
218
+ (r) => ` ${r.refname} remote ${r.remoteSha.slice(0, 7)} is not an ancestor of local ${r.localSha.slice(0, 7)}`
219
+ ),
220
+ "The remote moved since your last push. Re-run with --force to overwrite it",
221
+ "WARNING: --force rewrites the published ref history for every reader of this repo."
222
+ ];
223
+ }
224
+ function oversizeLines(objects) {
225
+ return [
226
+ `Push rejected: ${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE}-byte (95KB) upload limit:`,
227
+ ...objects.map(
228
+ (o) => ` ${o.path ?? o.sha} ${o.type}, ${o.size} bytes`
229
+ ),
230
+ "Objects over 95KB are a hard error in v1 \u2014 split or remove the file(s) from history to push.",
231
+ "Large-object support is tracked in toon-client#235."
232
+ ];
233
+ }
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
+ function describeError(err, command = "push") {
276
+ if (err instanceof DaemonRouteError) return fromDaemonEnvelope(err, command);
277
+ if (err instanceof UnconfiguredRepoAddressError) {
278
+ return {
279
+ code: "unconfigured_repo_address",
280
+ lines: err.message.split("\n"),
281
+ json: { error: "unconfigured_repo_address", detail: err.message }
282
+ };
283
+ }
284
+ if (err instanceof DaemonUnreachableError) {
285
+ return {
286
+ code: "daemon_unreachable",
287
+ lines: [err.message],
288
+ json: { error: "daemon_unreachable", detail: err.message }
289
+ };
290
+ }
291
+ if (err instanceof NoPublisherError) {
292
+ return {
293
+ code: "no_publisher",
294
+ lines: err.message.split("\n"),
295
+ json: { error: "no_publisher", detail: err.message }
296
+ };
297
+ }
298
+ if (err instanceof NonFastForwardError) {
299
+ return {
300
+ code: "non_fast_forward",
301
+ lines: nonFastForwardLines(err.refs),
302
+ json: { error: "non_fast_forward", detail: err.message, refs: err.refs }
303
+ };
304
+ }
305
+ if (err instanceof OversizeObjectsError) {
306
+ return {
307
+ code: "oversize_objects",
308
+ lines: oversizeLines(err.objects),
309
+ json: {
310
+ error: "oversize_objects",
311
+ detail: err.message,
312
+ objects: err.objects
313
+ }
314
+ };
315
+ }
316
+ if (err instanceof GitError) {
317
+ return {
318
+ code: "git_error",
319
+ lines: [`git failed: ${err.message}`],
320
+ json: { error: "git_error", detail: err.message }
321
+ };
322
+ }
323
+ const name = err instanceof Error ? err.name : "";
324
+ const message = err instanceof Error ? err.message : String(err);
325
+ if (name === "DaemonIdentityConflictError") {
326
+ return {
327
+ code: "daemon_identity_conflict",
328
+ lines: [message, "Re-run without --standalone (or with --daemon) to push through the running daemon."],
329
+ json: { error: "daemon_identity_conflict", detail: message }
330
+ };
331
+ }
332
+ if (name === "MissingMnemonicError" || name === "MissingUplinkError") {
333
+ return {
334
+ code: name === "MissingMnemonicError" ? "missing_mnemonic" : "missing_uplink",
335
+ lines: [message],
336
+ json: { error: "standalone_unavailable", detail: message }
337
+ };
338
+ }
339
+ return {
340
+ code: "error",
341
+ lines: [`rig ${command} failed: ${message}`],
342
+ json: { error: "error", detail: message }
343
+ };
344
+ }
345
+
346
+ // src/cli/git-config.ts
347
+ import { execFile } from "child_process";
348
+ import { promisify } from "util";
349
+ var execFileAsync = promisify(execFile);
350
+ async function git(repoPath, args, allowExitCodes = []) {
351
+ try {
352
+ const { stdout } = await execFileAsync("git", args, {
353
+ cwd: repoPath,
354
+ encoding: "utf-8"
355
+ });
356
+ return { stdout, exitCode: 0 };
357
+ } catch (err) {
358
+ const e = err;
359
+ const exitCode = typeof e.code === "number" ? e.code : void 0;
360
+ if (exitCode !== void 0 && allowExitCodes.includes(exitCode)) {
361
+ return { stdout: e.stdout ?? "", exitCode };
362
+ }
363
+ throw new Error(
364
+ `git ${args.join(" ")} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`
365
+ );
366
+ }
367
+ }
368
+ async function resolveRepoRoot(cwd) {
369
+ try {
370
+ const { stdout } = await execFileAsync(
371
+ "git",
372
+ ["rev-parse", "--show-toplevel"],
373
+ { cwd, encoding: "utf-8" }
374
+ );
375
+ const root = stdout.trim();
376
+ if (!root) throw new Error("empty rev-parse output");
377
+ return root;
378
+ } catch (err) {
379
+ const e = err;
380
+ throw new Error(
381
+ `not a git repository (run rig inside a repo): ${(e.stderr ?? e.message ?? "").trim()}`
382
+ );
383
+ }
384
+ }
385
+ async function readToonConfig(repoPath) {
386
+ const [repoId, owner, relays] = await Promise.all([
387
+ git(repoPath, ["config", "--get", "toon.repoid"], [1]),
388
+ git(repoPath, ["config", "--get", "toon.owner"], [1]),
389
+ git(repoPath, ["config", "--get-all", "toon.relay"], [1])
390
+ ]);
391
+ const config = {
392
+ relays: relays.stdout.split("\n").map((l) => l.trim()).filter(Boolean)
393
+ };
394
+ const id = repoId.stdout.trim();
395
+ if (repoId.exitCode === 0 && id) config.repoId = id;
396
+ const own = owner.stdout.trim();
397
+ if (owner.exitCode === 0 && own) config.owner = own;
398
+ return config;
399
+ }
400
+ async function writeToonConfig(repoPath, config) {
401
+ if (config.repoId !== void 0) {
402
+ await git(repoPath, ["config", "toon.repoid", config.repoId]);
403
+ }
404
+ if (config.owner !== void 0) {
405
+ await git(repoPath, ["config", "toon.owner", config.owner]);
406
+ }
407
+ if (config.relays !== void 0 && config.relays.length > 0) {
408
+ await git(repoPath, ["config", "--unset-all", "toon.relay"], [5]);
409
+ for (const relay of config.relays) {
410
+ await git(repoPath, ["config", "--add", "toon.relay", relay]);
411
+ }
412
+ }
413
+ }
414
+
415
+ // src/cli/push.ts
416
+ import { basename } from "path";
417
+ import { parseArgs } from "util";
418
+
419
+ // src/cli/render.ts
420
+ function formatNumber(value) {
421
+ return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
422
+ }
423
+ function shortSha(sha) {
424
+ return sha ? sha.slice(0, 7) : "(none)";
425
+ }
426
+ function refLine(update) {
427
+ const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
428
+ return ` ${update.refname} ${arrow} (${update.kind})`;
429
+ }
430
+ function renderPlan(plan, mode) {
431
+ const lines = [];
432
+ lines.push(
433
+ `Push plan for repo "${plan.repoId}" (${mode} mode)` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
434
+ );
435
+ lines.push("Refs:");
436
+ for (const update of plan.refUpdates) lines.push(refLine(update));
437
+ const est = plan.estimate;
438
+ const skipped = Object.keys(plan.knownShaToTxId).length;
439
+ lines.push(
440
+ `Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
441
+ );
442
+ lines.push("Fees (base units):");
443
+ lines.push(
444
+ ` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
445
+ );
446
+ lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
447
+ lines.push(` total ${formatNumber(est.totalFee)}`);
448
+ lines.push("Writes are permanent and non-refundable.");
449
+ return lines;
450
+ }
451
+ function feeLabel(fee) {
452
+ return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
453
+ }
454
+ function renderEventPlan(opts) {
455
+ return [
456
+ `Publish ${opts.action} (${opts.mode} mode)`,
457
+ `Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
458
+ `Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
459
+ ];
460
+ }
461
+ function renderEventReceipt(action, result) {
462
+ const lines = [
463
+ `Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
464
+ ];
465
+ if (result.channelBalanceAfter !== void 0) {
466
+ lines.push(
467
+ `Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
468
+ );
469
+ }
470
+ return lines;
471
+ }
472
+ function renderResult(result) {
473
+ const lines = [];
474
+ lines.push(`Pushed "${result.repoId}":`);
475
+ const paid = result.uploads.filter((u) => !u.skipped);
476
+ const skipped = result.uploads.filter((u) => u.skipped);
477
+ for (const upload of result.uploads) {
478
+ lines.push(
479
+ ` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
480
+ );
481
+ }
482
+ lines.push(
483
+ `Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
484
+ );
485
+ if (result.announceReceipt) {
486
+ lines.push(
487
+ `Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
488
+ );
489
+ }
490
+ lines.push(
491
+ `Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
492
+ );
493
+ lines.push(
494
+ `Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
495
+ );
496
+ return lines;
497
+ }
498
+
499
+ // src/cli/push.ts
500
+ var defaultLoadStandalone = async (env) => {
501
+ const mod = await import("../standalone-mode-UFMHGUOM.js");
502
+ return mod.createStandaloneContext(env);
503
+ };
504
+ var PUSH_USAGE = `Usage: rig push [refspecs...] [options]
505
+
506
+ Push local git refs to TOON: uploads the object delta to Arweave (paid) and
507
+ publishes the NIP-34 refs event (kind:30618; kind:30617 announce on first
508
+ push). Writes are permanent and non-refundable.
509
+
510
+ Refspecs are branch/tag names or full refnames; default is the current branch.
511
+
512
+ Options:
513
+ --force allow non-fast-forward ref updates (overwrites remote history)
514
+ --all push all local branches
515
+ --tags push all local tags
516
+ --yes skip the fee confirmation (required when not a TTY)
517
+ --json machine-readable plan/receipts; without --yes it is a pure
518
+ 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)
522
+ --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)
526
+ -h, --help show this help`;
527
+ async function selectRefspecs(reader, positionals, all, tags) {
528
+ const { head, refs } = await reader.listRefs();
529
+ const byName = new Set(refs.map((r) => r.refname));
530
+ const selected = [];
531
+ const add = (refname) => {
532
+ if (!selected.includes(refname)) selected.push(refname);
533
+ };
534
+ for (const spec of positionals) {
535
+ if (byName.has(spec)) {
536
+ add(spec);
537
+ } else if (byName.has(`refs/heads/${spec}`)) {
538
+ add(`refs/heads/${spec}`);
539
+ } else if (byName.has(`refs/tags/${spec}`)) {
540
+ add(`refs/tags/${spec}`);
541
+ } else {
542
+ throw new Error(
543
+ `refspec ${JSON.stringify(spec)} matches no local branch or tag (ref deletion is out of scope in v1)`
544
+ );
545
+ }
546
+ }
547
+ if (all) {
548
+ for (const ref of refs) {
549
+ if (ref.refname.startsWith("refs/heads/")) add(ref.refname);
550
+ }
551
+ }
552
+ if (tags) {
553
+ for (const ref of refs) {
554
+ if (ref.refname.startsWith("refs/tags/")) add(ref.refname);
555
+ }
556
+ }
557
+ if (selected.length === 0) {
558
+ if (!head) {
559
+ throw new Error(
560
+ "HEAD is detached and no refspec was given \u2014 pass a branch/tag name, --all, or --tags"
561
+ );
562
+ }
563
+ add(head);
564
+ }
565
+ return selected;
566
+ }
567
+ function parsePushArgs(args) {
568
+ const { values, positionals } = parseArgs({
569
+ args,
570
+ options: {
571
+ force: { type: "boolean", default: false },
572
+ all: { type: "boolean", default: false },
573
+ tags: { type: "boolean", default: false },
574
+ yes: { type: "boolean", default: false },
575
+ json: { type: "boolean", default: false },
576
+ daemon: { type: "boolean", default: false },
577
+ standalone: { type: "boolean", default: false },
578
+ relay: { type: "string", multiple: true },
579
+ "repo-id": { type: "string" },
580
+ help: { type: "boolean", short: "h", default: false }
581
+ },
582
+ allowPositionals: true
583
+ });
584
+ const flags = {
585
+ force: values.force ?? false,
586
+ all: values.all ?? false,
587
+ tags: values.tags ?? false,
588
+ yes: values.yes ?? false,
589
+ json: values.json ?? false,
590
+ daemon: values.daemon ?? false,
591
+ standalone: values.standalone ?? false,
592
+ relay: values.relay ?? [],
593
+ help: values.help ?? false,
594
+ positionals
595
+ };
596
+ const repoId = values["repo-id"];
597
+ if (repoId !== void 0) flags.repoId = repoId;
598
+ return flags;
599
+ }
600
+ async function runPush(args, deps) {
601
+ const { io, env, fetchImpl } = deps;
602
+ let flags;
603
+ try {
604
+ flags = parsePushArgs(args);
605
+ } catch (err) {
606
+ io.err(err instanceof Error ? err.message : String(err));
607
+ io.err(PUSH_USAGE);
608
+ return 2;
609
+ }
610
+ if (flags.help) {
611
+ io.out(PUSH_USAGE);
612
+ return 0;
613
+ }
614
+ let standaloneCtx;
615
+ let standalonePlan;
616
+ try {
617
+ const repoRoot = await resolveRepoRoot(deps.cwd);
618
+ const toonConfig = await readToonConfig(repoRoot);
619
+ const repoId = flags.repoId ?? toonConfig.repoId ?? basename(repoRoot);
620
+ const reader = new GitRepoReader(repoRoot);
621
+ const refspecs = await selectRefspecs(
622
+ reader,
623
+ flags.positionals,
624
+ flags.all,
625
+ flags.tags
626
+ );
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
633
+ });
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 };
675
+ }
676
+ if (toonConfig.owner && identity && toonConfig.owner !== identity) {
677
+ 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`
679
+ );
680
+ }
681
+ const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
682
+ if (upToDate) {
683
+ if (flags.json) {
684
+ io.out(jsonOut({ command: "push", mode, repoId, executed: false, upToDate: true, plan }));
685
+ } else {
686
+ io.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
687
+ }
688
+ return 0;
689
+ }
690
+ if (!flags.json) {
691
+ for (const line of renderPlan(plan, mode)) io.out(line);
692
+ }
693
+ if (!flags.yes) {
694
+ if (flags.json) {
695
+ io.out(
696
+ jsonOut({
697
+ command: "push",
698
+ mode,
699
+ repoId,
700
+ executed: false,
701
+ upToDate: false,
702
+ plan,
703
+ hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
704
+ })
705
+ );
706
+ return 0;
707
+ }
708
+ if (!io.isInteractive) {
709
+ io.err(
710
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
711
+ );
712
+ return 1;
713
+ }
714
+ const proceed = await io.confirm(
715
+ `Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
716
+ );
717
+ if (!proceed) {
718
+ io.err("aborted \u2014 nothing was uploaded or published.");
719
+ return 1;
720
+ }
721
+ }
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
+ }
739
+ if (flags.json) {
740
+ io.out(
741
+ jsonOut({ command: "push", mode, repoId, executed: true, upToDate: false, plan, result })
742
+ );
743
+ } else {
744
+ for (const line of renderResult(result)) io.out(line);
745
+ }
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
+ return 0;
758
+ } catch (err) {
759
+ const described = describeError(err);
760
+ if (flags.json) {
761
+ io.out(JSON.stringify({ command: "push", ...described.json }, null, 2));
762
+ } else {
763
+ for (const line of described.lines) io.err(line);
764
+ }
765
+ return 1;
766
+ } finally {
767
+ if (standaloneCtx) {
768
+ try {
769
+ await standaloneCtx.stop();
770
+ } catch {
771
+ }
772
+ }
773
+ }
774
+ }
775
+ function jsonOut(output) {
776
+ return JSON.stringify(output, null, 2);
777
+ }
778
+
779
+ // src/cli/events.ts
780
+ var defaultReadStdin = async () => {
781
+ if (process.stdin.isTTY) return "";
782
+ const chunks = [];
783
+ for await (const chunk of process.stdin) chunks.push(chunk);
784
+ return Buffer.concat(chunks).toString("utf-8");
785
+ };
786
+ var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config toon.repoid)
787
+ --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)
792
+ --yes skip the fee confirmation (required when not a TTY)
793
+ --json machine-readable receipt; without --yes it is a pure
794
+ 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
+ -h, --help show this help`;
798
+ var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
799
+
800
+ File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
801
+ permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
802
+ from the toon.* git config keys \`rig push\` persists.
803
+
804
+ Body source (exactly one): --body, --body-file, or piped stdin.
805
+
806
+ Options:
807
+ --title <title> issue title (subject tag) [required]
808
+ --body <text> issue body (Markdown)
809
+ --body-file <path> read the issue body from a file
810
+ --label <label> label (t tag); repeatable
811
+ ${COMMON_FLAGS_USAGE}`;
812
+ var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
813
+
814
+ Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
815
+ permanent and non-refundable. <root-event-id> is the 64-char hex id of the
816
+ kind:1621 issue / kind:1617 patch being commented on.
817
+
818
+ Options:
819
+ --body <text> comment body (Markdown) [required]
820
+ --parent-author <pubkey> pubkey of the TARGET event's author (p threading
821
+ tag; default: the repo owner)
822
+ --marker <root|reply> e-tag marker (default: root \u2014 commenting directly
823
+ on the issue/patch)
824
+ ${COMMON_FLAGS_USAGE}`;
825
+ var PR_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
826
+
827
+ Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
828
+ a paid publish; writes are permanent and non-refundable. --range runs
829
+ \`git format-patch --stdout <range>\` in the local repository and derives the
830
+ commit/parent-commit tags; --patch-file publishes a pre-generated patch
831
+ verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
832
+ full series text \u2014 cover-letter threading (one event per commit) is out of
833
+ scope in v1.
834
+
835
+ Options:
836
+ --title <title> patch/PR title (subject tag) [required]
837
+ --range <range> revision range for format-patch: <rev>, <rev>..<rev>,
838
+ or <rev>...<rev> (mutually exclusive with --patch-file)
839
+ --patch-file <path> literal patch text to publish
840
+ --branch <name> branch name (t tag)
841
+ ${COMMON_FLAGS_USAGE}`;
842
+ var STATUS_USAGE = `Usage: rig status <target-event-id> <open|applied|closed|draft> [options]
843
+
844
+ Set the status of an issue or patch \u2014 a paid publish; writes are permanent
845
+ and non-refundable. Publishes kind:1630 (open), 1631 (applied), 1632
846
+ (closed), or 1633 (draft) against the 64-char hex id of the target event,
847
+ with the repo a-tag attached so readers can scope a status stream to the
848
+ repository.
849
+
850
+ Options:
851
+ ${COMMON_FLAGS_USAGE}`;
852
+ var HEX64_RE = /^[0-9a-f]{64}$/;
853
+ function assertHex64(value, what) {
854
+ if (!HEX64_RE.test(value)) {
855
+ throw new Error(
856
+ `${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
857
+ );
858
+ }
859
+ }
860
+ var COMMON_OPTIONS = {
861
+ yes: { type: "boolean", default: false },
862
+ json: { type: "boolean", default: false },
863
+ daemon: { type: "boolean", default: false },
864
+ standalone: { type: "boolean", default: false },
865
+ relay: { type: "string", multiple: true },
866
+ "repo-id": { type: "string" },
867
+ owner: { type: "string" },
868
+ help: { type: "boolean", short: "h", default: false }
869
+ };
870
+ function pickCommon(values) {
871
+ const flags = {
872
+ yes: values["yes"] === true,
873
+ json: values["json"] === true,
874
+ daemon: values["daemon"] === true,
875
+ standalone: values["standalone"] === true,
876
+ relay: Array.isArray(values["relay"]) ? values["relay"] : [],
877
+ help: values["help"] === true
878
+ };
879
+ const repoId = values["repo-id"];
880
+ if (typeof repoId === "string") flags.repoId = repoId;
881
+ const owner = values["owner"];
882
+ if (typeof owner === "string") {
883
+ assertHex64(owner, "--owner");
884
+ flags.owner = owner;
885
+ }
886
+ return flags;
887
+ }
888
+ async function runEvent(opts) {
889
+ const { command, flags, deps, actionLabel } = opts;
890
+ const { io, env, fetchImpl } = deps;
891
+ let standaloneCtx;
892
+ try {
893
+ let toonConfig = { relays: [] };
894
+ try {
895
+ toonConfig = await readToonConfig(await resolveRepoRoot(deps.cwd));
896
+ } catch {
897
+ }
898
+ const repoId = flags.repoId ?? toonConfig.repoId;
899
+ 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
906
+ });
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();
925
+ }
926
+ const owner = flags.owner ?? toonConfig.owner ?? identity;
927
+ if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
928
+ const addr = { ownerPubkey: owner, repoId };
929
+ const event = await opts.buildEvent(addr);
930
+ const action = `kind:${event.kind} ${actionLabel}`;
931
+ if (!flags.json) {
932
+ for (const line of renderEventPlan({
933
+ action,
934
+ addr,
935
+ mode,
936
+ ...fee !== void 0 ? { fee } : {}
937
+ })) {
938
+ io.out(line);
939
+ }
940
+ }
941
+ if (!flags.yes) {
942
+ if (flags.json) {
943
+ io.out(
944
+ jsonOut2({
945
+ command,
946
+ mode,
947
+ repoAddr: addr,
948
+ kind: event.kind,
949
+ executed: false,
950
+ feeEstimate: fee ?? null,
951
+ hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
952
+ })
953
+ );
954
+ return 0;
955
+ }
956
+ if (!io.isInteractive) {
957
+ io.err(
958
+ "refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
959
+ );
960
+ return 1;
961
+ }
962
+ const proceed = await io.confirm(
963
+ `Proceed with paid publish (${feeLabel(fee)})? [y/N] `
964
+ );
965
+ if (!proceed) {
966
+ io.err("aborted \u2014 nothing was published.");
967
+ return 1;
968
+ }
969
+ }
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
+ }
981
+ if (flags.json) {
982
+ io.out(
983
+ jsonOut2({
984
+ command,
985
+ mode,
986
+ repoAddr: addr,
987
+ kind: result.kind,
988
+ executed: true,
989
+ feeEstimate: fee ?? null,
990
+ result
991
+ })
992
+ );
993
+ } else {
994
+ for (const line of renderEventReceipt(action, result)) io.out(line);
995
+ }
996
+ return 0;
997
+ } catch (err) {
998
+ const described = describeError(err, command);
999
+ if (flags.json) {
1000
+ io.out(JSON.stringify({ command, ...described.json }, null, 2));
1001
+ } else {
1002
+ for (const line of described.lines) io.err(line);
1003
+ }
1004
+ return 1;
1005
+ } finally {
1006
+ if (standaloneCtx) {
1007
+ try {
1008
+ await standaloneCtx.stop();
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ function jsonOut2(output) {
1015
+ return JSON.stringify(output, null, 2);
1016
+ }
1017
+ async function runIssue(args, deps) {
1018
+ const { io } = deps;
1019
+ const [sub, ...rest] = args;
1020
+ if (sub === "--help" || sub === "-h" || sub === "help") {
1021
+ io.out(ISSUE_USAGE);
1022
+ return 0;
1023
+ }
1024
+ if (sub !== "create") {
1025
+ io.err(
1026
+ sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
1027
+ );
1028
+ io.err(ISSUE_USAGE);
1029
+ return 2;
1030
+ }
1031
+ let flags;
1032
+ let title;
1033
+ let bodyFlag;
1034
+ let bodyFile;
1035
+ let labels;
1036
+ try {
1037
+ const { values } = parseArgs2({
1038
+ args: rest,
1039
+ options: {
1040
+ ...COMMON_OPTIONS,
1041
+ title: { type: "string" },
1042
+ body: { type: "string" },
1043
+ "body-file": { type: "string" },
1044
+ label: { type: "string", multiple: true }
1045
+ },
1046
+ allowPositionals: false
1047
+ });
1048
+ flags = pickCommon(values);
1049
+ if (!flags.help && (values.title === void 0 || values.title === "")) {
1050
+ throw new Error("--title is required");
1051
+ }
1052
+ if (values.body !== void 0 && values["body-file"] !== void 0) {
1053
+ throw new Error("--body and --body-file are mutually exclusive");
1054
+ }
1055
+ title = values.title ?? "";
1056
+ bodyFlag = values.body;
1057
+ bodyFile = values["body-file"];
1058
+ labels = values.label ?? [];
1059
+ } catch (err) {
1060
+ io.err(err instanceof Error ? err.message : String(err));
1061
+ io.err(ISSUE_USAGE);
1062
+ return 2;
1063
+ }
1064
+ if (flags.help) {
1065
+ io.out(ISSUE_USAGE);
1066
+ return 0;
1067
+ }
1068
+ let body;
1069
+ if (bodyFlag !== void 0) {
1070
+ body = bodyFlag;
1071
+ } else if (bodyFile !== void 0) {
1072
+ try {
1073
+ body = await readFile(bodyFile, "utf-8");
1074
+ } catch (err) {
1075
+ io.err(
1076
+ `cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
1077
+ );
1078
+ return 2;
1079
+ }
1080
+ } else if (!io.isInteractive) {
1081
+ body = await (deps.readStdin ?? defaultReadStdin)();
1082
+ } else {
1083
+ io.err("an issue body is required: pass --body/--body-file or pipe stdin");
1084
+ io.err(ISSUE_USAGE);
1085
+ return 2;
1086
+ }
1087
+ if (body.trim() === "") {
1088
+ io.err("the issue body is empty \u2014 nothing to publish");
1089
+ return 2;
1090
+ }
1091
+ return runEvent({
1092
+ command: "issue",
1093
+ flags,
1094
+ deps,
1095
+ 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
+ })
1103
+ });
1104
+ }
1105
+ async function runComment(args, deps) {
1106
+ const { io } = deps;
1107
+ let flags;
1108
+ let rootEventId;
1109
+ let body;
1110
+ let parentAuthor;
1111
+ let marker;
1112
+ try {
1113
+ const { values, positionals } = parseArgs2({
1114
+ args,
1115
+ options: {
1116
+ ...COMMON_OPTIONS,
1117
+ body: { type: "string" },
1118
+ "parent-author": { type: "string" },
1119
+ marker: { type: "string" }
1120
+ },
1121
+ allowPositionals: true
1122
+ });
1123
+ flags = pickCommon(values);
1124
+ if (flags.help) {
1125
+ io.out(COMMENT_USAGE);
1126
+ return 0;
1127
+ }
1128
+ if (positionals.length !== 1) {
1129
+ throw new Error(
1130
+ positionals.length === 0 ? "<root-event-id> is required" : `expected exactly one <root-event-id>, got ${positionals.length} positionals`
1131
+ );
1132
+ }
1133
+ rootEventId = positionals[0];
1134
+ assertHex64(rootEventId, "<root-event-id>");
1135
+ if (values.body === void 0 || values.body === "") {
1136
+ throw new Error("--body is required");
1137
+ }
1138
+ body = values.body;
1139
+ parentAuthor = values["parent-author"];
1140
+ if (parentAuthor !== void 0) assertHex64(parentAuthor, "--parent-author");
1141
+ const rawMarker = values.marker ?? "root";
1142
+ if (rawMarker !== "root" && rawMarker !== "reply") {
1143
+ throw new Error(`--marker must be root or reply (got ${JSON.stringify(rawMarker)})`);
1144
+ }
1145
+ marker = rawMarker;
1146
+ } catch (err) {
1147
+ io.err(err instanceof Error ? err.message : String(err));
1148
+ io.err(COMMENT_USAGE);
1149
+ return 2;
1150
+ }
1151
+ return runEvent({
1152
+ command: "comment",
1153
+ flags,
1154
+ deps,
1155
+ actionLabel: `comment on ${rootEventId.slice(0, 8)}\u2026`,
1156
+ buildEvent: async (addr) => buildComment(
1157
+ addr.ownerPubkey,
1158
+ addr.repoId,
1159
+ rootEventId,
1160
+ parentAuthor ?? addr.ownerPubkey,
1161
+ body,
1162
+ marker
1163
+ ),
1164
+ sendDaemon: (client, addr) => client.gitComment({
1165
+ repoAddr: addr,
1166
+ rootEventId,
1167
+ body,
1168
+ marker,
1169
+ ...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {}
1170
+ })
1171
+ });
1172
+ }
1173
+ var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
1174
+ function extractPatchShas(patchText) {
1175
+ return [...patchText.matchAll(PATCH_FROM_RE)].map((m) => m[1]);
1176
+ }
1177
+ async function runPr(args, deps) {
1178
+ const { io } = deps;
1179
+ 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;
1190
+ }
1191
+ let flags;
1192
+ let title;
1193
+ let range;
1194
+ let patchFile;
1195
+ let branch;
1196
+ try {
1197
+ const { values } = parseArgs2({
1198
+ args: rest,
1199
+ options: {
1200
+ ...COMMON_OPTIONS,
1201
+ title: { type: "string" },
1202
+ range: { type: "string" },
1203
+ "patch-file": { type: "string" },
1204
+ branch: { type: "string" }
1205
+ },
1206
+ allowPositionals: false
1207
+ });
1208
+ flags = pickCommon(values);
1209
+ if (flags.help) {
1210
+ io.out(PR_USAGE);
1211
+ return 0;
1212
+ }
1213
+ if (values.title === void 0 || values.title === "") {
1214
+ throw new Error("--title is required");
1215
+ }
1216
+ if (values.range === void 0 === (values["patch-file"] === void 0)) {
1217
+ throw new Error("exactly one of --range or --patch-file is required");
1218
+ }
1219
+ title = values.title;
1220
+ range = values.range;
1221
+ patchFile = values["patch-file"];
1222
+ branch = values.branch;
1223
+ } catch (err) {
1224
+ io.err(err instanceof Error ? err.message : String(err));
1225
+ io.err(PR_USAGE);
1226
+ return 2;
1227
+ }
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
+ return runEvent({
1256
+ command: "pr",
1257
+ flags,
1258
+ deps,
1259
+ actionLabel: `patch ${JSON.stringify(title)}`,
1260
+ buildEvent: async (addr) => {
1261
+ const { patchText, commits } = await prepare();
1262
+ return buildPatch(
1263
+ addr.ownerPubkey,
1264
+ addr.repoId,
1265
+ title,
1266
+ commits,
1267
+ branch,
1268
+ patchText
1269
+ );
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
+ }
1281
+ });
1282
+ }
1283
+ var STATUS_KIND_BY_VALUE = {
1284
+ open: STATUS_OPEN_KIND,
1285
+ applied: STATUS_APPLIED_KIND,
1286
+ closed: STATUS_CLOSED_KIND,
1287
+ draft: STATUS_DRAFT_KIND
1288
+ };
1289
+ function isStatusValue(value) {
1290
+ return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
1291
+ }
1292
+ async function runStatus(args, deps) {
1293
+ const { io } = deps;
1294
+ let flags;
1295
+ let targetEventId;
1296
+ let status;
1297
+ try {
1298
+ const { values, positionals } = parseArgs2({
1299
+ args,
1300
+ options: COMMON_OPTIONS,
1301
+ allowPositionals: true
1302
+ });
1303
+ flags = pickCommon(values);
1304
+ if (flags.help) {
1305
+ io.out(STATUS_USAGE);
1306
+ return 0;
1307
+ }
1308
+ if (positionals.length !== 2) {
1309
+ throw new Error(
1310
+ "expected exactly two arguments: <target-event-id> <open|applied|closed|draft>"
1311
+ );
1312
+ }
1313
+ targetEventId = positionals[0];
1314
+ assertHex64(targetEventId, "<target-event-id>");
1315
+ const rawStatus = positionals[1];
1316
+ if (!isStatusValue(rawStatus)) {
1317
+ throw new Error(
1318
+ `status must be one of open | applied | closed | draft (got ${JSON.stringify(rawStatus)})`
1319
+ );
1320
+ }
1321
+ status = rawStatus;
1322
+ } catch (err) {
1323
+ io.err(err instanceof Error ? err.message : String(err));
1324
+ io.err(STATUS_USAGE);
1325
+ return 2;
1326
+ }
1327
+ return runEvent({
1328
+ command: "status",
1329
+ flags,
1330
+ deps,
1331
+ actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
1332
+ buildEvent: async (addr) => {
1333
+ const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
1334
+ event.tags.push([
1335
+ "a",
1336
+ `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
1337
+ ]);
1338
+ return event;
1339
+ },
1340
+ sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
1341
+ });
1342
+ }
1343
+
1344
+ // src/cli/rig.ts
1345
+ var USAGE = `rig \u2014 push git repos to TOON (pay-to-write Nostr + Arweave)
1346
+
1347
+ Usage: rig <command> [options]
1348
+
1349
+ Commands:
1350
+ push [refspecs...] plan, price, confirm, and execute a paid push
1351
+ issue create file an issue (kind:1621) against a repo
1352
+ comment <root-event-id> comment (kind:1622) on an issue or patch
1353
+ pr create publish a patch (kind:1617) with real
1354
+ \`git format-patch\` content
1355
+ status <event-id> <state> set an issue/patch status (kind:1630-1633):
1356
+ open | applied | closed | draft
1357
+
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
+ }
1379
+ }
1380
+ };
1381
+ }
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
+ };
1391
+ switch (command) {
1392
+ case "push":
1393
+ return runPush(rest, deps);
1394
+ case "issue":
1395
+ return runIssue(rest, deps);
1396
+ case "comment":
1397
+ return runComment(rest, deps);
1398
+ case "pr":
1399
+ return runPr(rest, deps);
1400
+ case "status":
1401
+ return runStatus(rest, deps);
1402
+ case "help":
1403
+ case "--help":
1404
+ case "-h":
1405
+ io.out(USAGE);
1406
+ io.out("");
1407
+ io.out(PUSH_USAGE);
1408
+ return 0;
1409
+ case void 0:
1410
+ io.err(USAGE);
1411
+ return 2;
1412
+ default:
1413
+ io.err(`unknown command: ${command}`);
1414
+ io.err(USAGE);
1415
+ return 2;
1416
+ }
1417
+ }
1418
+ main().then(
1419
+ (code) => {
1420
+ process.exitCode = code;
1421
+ },
1422
+ (err) => {
1423
+ process.stderr.write(
1424
+ `rig: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
1425
+ `
1426
+ );
1427
+ process.exitCode = 1;
1428
+ }
1429
+ );
1430
+ //# sourceMappingURL=rig.js.map