@toon-protocol/rig 2.0.1 → 2.2.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/README.md +98 -13
- package/dist/{chunk-G4W4MH6G.js → chunk-JBB7HBQC.js} +120 -6
- package/dist/chunk-JBB7HBQC.js.map +1 -0
- package/dist/chunk-OIJNRACA.js +588 -0
- package/dist/chunk-OIJNRACA.js.map +1 -0
- package/dist/{chunk-LFGDLD6J.js → chunk-PS5QOT62.js} +464 -4
- package/dist/chunk-PS5QOT62.js.map +1 -0
- package/dist/chunk-SW7ZHMGS.js +358 -0
- package/dist/chunk-SW7ZHMGS.js.map +1 -0
- package/dist/cli/rig.js +2664 -410
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +271 -3
- package/dist/index.js +44 -8
- package/dist/{publisher-CClD9OIZ.d.ts → publisher-CrwaNQrK.d.ts} +10 -1
- package/dist/standalone/index.d.ts +410 -3
- package/dist/standalone/index.js +20 -6
- package/dist/standalone-mode-64CKUVU2.js +865 -0
- package/dist/standalone-mode-64CKUVU2.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-G4W4MH6G.js.map +0 -1
- package/dist/chunk-HPSOQP7Q.js +0 -109
- package/dist/chunk-HPSOQP7Q.js.map +0 -1
- package/dist/chunk-LFGDLD6J.js.map +0 -1
- package/dist/chunk-XRRU6YBQ.js +0 -402
- package/dist/chunk-XRRU6YBQ.js.map +0 -1
- package/dist/standalone-mode-Q3FFZUXE.js +0 -111
- package/dist/standalone-mode-Q3FFZUXE.js.map +0 -1
package/dist/cli/rig.js
CHANGED
|
@@ -4,22 +4,39 @@ import {
|
|
|
4
4
|
GitRepoReader,
|
|
5
5
|
NonFastForwardError,
|
|
6
6
|
OversizeObjectsError,
|
|
7
|
+
collectRepoObjects,
|
|
7
8
|
executePush,
|
|
9
|
+
isSafeRefname,
|
|
10
|
+
missingObjectsMessage,
|
|
11
|
+
ownerToHex,
|
|
8
12
|
planPush,
|
|
13
|
+
runGit,
|
|
9
14
|
serializeEventReceipt,
|
|
10
15
|
serializePushPlan,
|
|
11
|
-
serializePushResult
|
|
12
|
-
|
|
16
|
+
serializePushResult,
|
|
17
|
+
setHeadSymref,
|
|
18
|
+
updateRef,
|
|
19
|
+
writeGitObjects
|
|
20
|
+
} from "../chunk-PS5QOT62.js";
|
|
13
21
|
import {
|
|
14
22
|
MissingIdentityError,
|
|
15
23
|
resolveIdentity
|
|
16
24
|
} from "../chunk-CW4HJNMU.js";
|
|
17
25
|
import {
|
|
26
|
+
COMMENT_KIND,
|
|
18
27
|
buildComment,
|
|
19
28
|
buildIssue,
|
|
20
29
|
buildPatch,
|
|
21
|
-
buildStatus
|
|
22
|
-
|
|
30
|
+
buildStatus,
|
|
31
|
+
fetchRemoteState,
|
|
32
|
+
queryRelay
|
|
33
|
+
} from "../chunk-JBB7HBQC.js";
|
|
34
|
+
import {
|
|
35
|
+
ChannelMapStore,
|
|
36
|
+
channelStatus,
|
|
37
|
+
defaultDaemonPort,
|
|
38
|
+
resolveChannelPaths
|
|
39
|
+
} from "../chunk-SW7ZHMGS.js";
|
|
23
40
|
import {
|
|
24
41
|
MAX_OBJECT_SIZE
|
|
25
42
|
} from "../chunk-X2CZPPDM.js";
|
|
@@ -30,16 +47,157 @@ import { createInterface } from "readline/promises";
|
|
|
30
47
|
// src/cli/dispatch.ts
|
|
31
48
|
import { createRequire } from "module";
|
|
32
49
|
|
|
33
|
-
// src/cli/
|
|
34
|
-
import { readFile } from "fs/promises";
|
|
50
|
+
// src/cli/balance.ts
|
|
35
51
|
import { parseArgs as parseArgs3 } from "util";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
52
|
+
|
|
53
|
+
// src/cli/daemon-session.ts
|
|
54
|
+
function daemonBaseUrl(env) {
|
|
55
|
+
const raw = env["TOON_CLIENT_HTTP_PORT"];
|
|
56
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
57
|
+
const port = Number.isFinite(parsed) && parsed > 0 ? parsed : defaultDaemonPort();
|
|
58
|
+
return `http://127.0.0.1:${port}`;
|
|
59
|
+
}
|
|
60
|
+
async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
|
|
61
|
+
const baseUrl = daemonBaseUrl(env);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetchImpl(`${baseUrl}/status`, {
|
|
64
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) return { baseUrl, reachable: false };
|
|
67
|
+
const body = await res.json();
|
|
68
|
+
const probe = { baseUrl, reachable: true };
|
|
69
|
+
const pubkey = body?.identity?.nostrPubkey;
|
|
70
|
+
if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
|
|
71
|
+
if (typeof body?.ready === "boolean") probe.ready = body.ready;
|
|
72
|
+
if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
|
|
73
|
+
probe.relayUrl = body.relay.url;
|
|
74
|
+
}
|
|
75
|
+
if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
|
|
76
|
+
probe.feePerEvent = body.feePerEvent;
|
|
77
|
+
}
|
|
78
|
+
return probe;
|
|
79
|
+
} catch {
|
|
80
|
+
return { baseUrl, reachable: false };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var DaemonRouteError = class extends Error {
|
|
84
|
+
constructor(status, envelope) {
|
|
85
|
+
super(envelope.detail ?? envelope.error);
|
|
86
|
+
this.status = status;
|
|
87
|
+
this.envelope = envelope;
|
|
88
|
+
this.name = "DaemonRouteError";
|
|
89
|
+
}
|
|
90
|
+
status;
|
|
91
|
+
envelope;
|
|
92
|
+
};
|
|
93
|
+
var DaemonUnreachableError = class extends Error {
|
|
94
|
+
constructor(baseUrl, cause) {
|
|
95
|
+
super(
|
|
96
|
+
`toon-clientd stopped answering at ${baseUrl} after the identity probe \u2014 nothing was paid. Re-run: rig falls back to standalone automatically when no daemon responds` + (cause instanceof Error ? ` (${cause.message})` : "")
|
|
97
|
+
);
|
|
98
|
+
this.baseUrl = baseUrl;
|
|
99
|
+
this.name = "DaemonUnreachableError";
|
|
100
|
+
}
|
|
101
|
+
baseUrl;
|
|
102
|
+
};
|
|
103
|
+
var DaemonGitClient = class {
|
|
104
|
+
constructor(baseUrl, fetchImpl) {
|
|
105
|
+
this.baseUrl = baseUrl;
|
|
106
|
+
this.fetchImpl = fetchImpl;
|
|
107
|
+
}
|
|
108
|
+
baseUrl;
|
|
109
|
+
fetchImpl;
|
|
110
|
+
gitEstimate(req) {
|
|
111
|
+
return this.post("/git/estimate", req);
|
|
112
|
+
}
|
|
113
|
+
gitPush(req) {
|
|
114
|
+
return this.post("/git/push", req);
|
|
115
|
+
}
|
|
116
|
+
gitIssue(req) {
|
|
117
|
+
return this.post("/git/issue", req);
|
|
118
|
+
}
|
|
119
|
+
gitComment(req) {
|
|
120
|
+
return this.post("/git/comment", req);
|
|
121
|
+
}
|
|
122
|
+
gitPatch(req) {
|
|
123
|
+
return this.post("/git/patch", req);
|
|
124
|
+
}
|
|
125
|
+
gitStatus(req) {
|
|
126
|
+
return this.post("/git/status", req);
|
|
127
|
+
}
|
|
128
|
+
async post(path, body) {
|
|
129
|
+
let res;
|
|
130
|
+
try {
|
|
131
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "content-type": "application/json" },
|
|
134
|
+
body: JSON.stringify(body)
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new DaemonUnreachableError(this.baseUrl, err);
|
|
138
|
+
}
|
|
139
|
+
const text = await res.text();
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = text === "" ? {} : JSON.parse(text);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new DaemonRouteError(res.status, {
|
|
145
|
+
error: "invalid_response",
|
|
146
|
+
detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
|
|
151
|
+
throw new DaemonRouteError(res.status, envelope);
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
async function resolvePaidSession(options) {
|
|
157
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
158
|
+
const probe = await (options.probeDaemon ?? probeDaemon)(
|
|
159
|
+
options.env,
|
|
160
|
+
fetchImpl
|
|
161
|
+
);
|
|
162
|
+
if (probe.reachable && probe.identity !== void 0) {
|
|
163
|
+
const identity = await resolveIdentity({
|
|
164
|
+
env: options.env,
|
|
165
|
+
cwd: options.cwd,
|
|
166
|
+
warn: options.warn
|
|
167
|
+
});
|
|
168
|
+
if (identity.pubkey === probe.identity) {
|
|
169
|
+
options.warn(
|
|
170
|
+
`rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
|
|
171
|
+
);
|
|
172
|
+
return {
|
|
173
|
+
path: "daemon",
|
|
174
|
+
client: new DaemonGitClient(probe.baseUrl, fetchImpl),
|
|
175
|
+
baseUrl: probe.baseUrl,
|
|
176
|
+
identity: {
|
|
177
|
+
pubkey: identity.pubkey,
|
|
178
|
+
source: identity.source,
|
|
179
|
+
sourceLabel: identity.sourceLabel
|
|
180
|
+
},
|
|
181
|
+
...probe.feePerEvent !== void 0 ? { feePerEvent: probe.feePerEvent } : {},
|
|
182
|
+
...probe.relayUrl !== void 0 ? { daemonRelayUrl: probe.relayUrl } : {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
options.warn(
|
|
186
|
+
`rig: paid path: standalone \u2014 the toon-clientd at ${probe.baseUrl} runs a different identity (${probe.identity.slice(0, 8)}\u2026), no shared channel state`
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
options.warn(
|
|
190
|
+
`rig: paid path: standalone (no toon-clientd at ${probe.baseUrl})`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const ctx = await options.loadStandalone({
|
|
194
|
+
env: options.env,
|
|
195
|
+
cwd: options.cwd,
|
|
196
|
+
warn: options.warn,
|
|
197
|
+
...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
|
|
198
|
+
});
|
|
199
|
+
return { path: "standalone", ctx };
|
|
200
|
+
}
|
|
43
201
|
|
|
44
202
|
// src/cli/errors.ts
|
|
45
203
|
var UnconfiguredRepoAddressError = class extends Error {
|
|
@@ -202,6 +360,39 @@ function describeError(err, command = "push") {
|
|
|
202
360
|
json: { error: "git_error", detail: err.message }
|
|
203
361
|
};
|
|
204
362
|
}
|
|
363
|
+
if (err instanceof DaemonRouteError) {
|
|
364
|
+
const envelope = err.envelope;
|
|
365
|
+
if (envelope.error === "non_fast_forward" && Array.isArray(envelope["refs"])) {
|
|
366
|
+
return {
|
|
367
|
+
code: "non_fast_forward",
|
|
368
|
+
lines: nonFastForwardLines(envelope["refs"]),
|
|
369
|
+
json: { ...envelope }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (envelope.error === "oversize_objects" && Array.isArray(envelope["objects"])) {
|
|
373
|
+
return {
|
|
374
|
+
code: "oversize_objects",
|
|
375
|
+
lines: oversizeLines(envelope["objects"]),
|
|
376
|
+
json: { ...envelope }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const retryHint = envelope.retryable === true ? ["The daemon reports this as retryable \u2014 re-run shortly."] : [];
|
|
380
|
+
return {
|
|
381
|
+
code: envelope.error,
|
|
382
|
+
lines: [
|
|
383
|
+
`daemon rejected the operation (HTTP ${err.status}): ` + (envelope.detail ?? envelope.error),
|
|
384
|
+
...retryHint
|
|
385
|
+
],
|
|
386
|
+
json: { ...envelope }
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (err instanceof DaemonUnreachableError) {
|
|
390
|
+
return {
|
|
391
|
+
code: "daemon_unreachable",
|
|
392
|
+
lines: err.message.split("\n"),
|
|
393
|
+
json: { error: "daemon_unreachable", detail: err.message }
|
|
394
|
+
};
|
|
395
|
+
}
|
|
205
396
|
const name = err instanceof Error ? err.name : "";
|
|
206
397
|
const message = err instanceof Error ? err.message : String(err);
|
|
207
398
|
if (name === "DaemonIdentityConflictError") {
|
|
@@ -209,7 +400,7 @@ function describeError(err, command = "push") {
|
|
|
209
400
|
code: "daemon_identity_conflict",
|
|
210
401
|
lines: [
|
|
211
402
|
message,
|
|
212
|
-
"
|
|
403
|
+
"Paid writes delegate to a same-identity daemon automatically; seeing this means the daemon appeared mid-run or this operation has no daemon route \u2014 stop the daemon and re-run."
|
|
213
404
|
],
|
|
214
405
|
json: { error: "daemon_identity_conflict", detail: message }
|
|
215
406
|
};
|
|
@@ -221,12 +412,153 @@ function describeError(err, command = "push") {
|
|
|
221
412
|
json: { error: "missing_uplink", detail: message }
|
|
222
413
|
};
|
|
223
414
|
}
|
|
415
|
+
if (name === "ChannelMapCorruptError") {
|
|
416
|
+
return {
|
|
417
|
+
code: "channel_map_corrupt",
|
|
418
|
+
lines: [message],
|
|
419
|
+
json: { error: "channel_map_corrupt", detail: message }
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (name === "SettleTooEarlyError") {
|
|
423
|
+
const settleableAt = err.settleableAt;
|
|
424
|
+
return {
|
|
425
|
+
code: "settle_too_early",
|
|
426
|
+
lines: [
|
|
427
|
+
message,
|
|
428
|
+
"The settlement challenge window is still open \u2014 nothing was spent. Re-run `rig channel settle` after the settleable time."
|
|
429
|
+
],
|
|
430
|
+
json: {
|
|
431
|
+
error: "settle_too_early",
|
|
432
|
+
detail: message,
|
|
433
|
+
...typeof settleableAt === "string" ? { settleableAt } : {}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (name === "RepoNotFoundError") {
|
|
438
|
+
return {
|
|
439
|
+
code: "repo_not_found",
|
|
440
|
+
lines: message.split("\n"),
|
|
441
|
+
json: { error: "repo_not_found", detail: message }
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (name === "MissingRemoteObjectsError") {
|
|
445
|
+
const missing = err.missing;
|
|
446
|
+
return {
|
|
447
|
+
code: "missing_remote_objects",
|
|
448
|
+
lines: message.split("\n"),
|
|
449
|
+
json: {
|
|
450
|
+
error: "missing_remote_objects",
|
|
451
|
+
detail: message,
|
|
452
|
+
...Array.isArray(missing) ? { missing } : {}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (name === "ObjectIntegrityError" || name === "ObjectWriteMismatchError") {
|
|
457
|
+
return {
|
|
458
|
+
code: "object_integrity",
|
|
459
|
+
lines: message.split("\n"),
|
|
460
|
+
json: { error: "object_integrity", detail: message }
|
|
461
|
+
};
|
|
462
|
+
}
|
|
224
463
|
return {
|
|
225
464
|
code: "error",
|
|
226
465
|
lines: [`rig ${command} failed: ${message}`],
|
|
227
466
|
json: { error: "error", detail: message }
|
|
228
467
|
};
|
|
229
468
|
}
|
|
469
|
+
function emitCliError(io2, json, command, err) {
|
|
470
|
+
const described = describeError(err, command);
|
|
471
|
+
if (json) io2.emitJson({ command, ...described.json });
|
|
472
|
+
for (const line of described.lines) io2.err(line);
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/cli/push.ts
|
|
477
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
478
|
+
|
|
479
|
+
// src/cli/render.ts
|
|
480
|
+
function formatNumber(value) {
|
|
481
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
482
|
+
}
|
|
483
|
+
function shortSha(sha) {
|
|
484
|
+
return sha ? sha.slice(0, 7) : "(none)";
|
|
485
|
+
}
|
|
486
|
+
function refLine(update) {
|
|
487
|
+
const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
|
|
488
|
+
return ` ${update.refname} ${arrow} (${update.kind})`;
|
|
489
|
+
}
|
|
490
|
+
function renderIdentityLine(identity) {
|
|
491
|
+
return `Identity: ${identity.pubkey} (from ${identity.sourceLabel})`;
|
|
492
|
+
}
|
|
493
|
+
function renderPlan(plan) {
|
|
494
|
+
const lines = [];
|
|
495
|
+
lines.push(
|
|
496
|
+
`Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
|
|
497
|
+
);
|
|
498
|
+
lines.push("Refs:");
|
|
499
|
+
for (const update of plan.refUpdates) lines.push(refLine(update));
|
|
500
|
+
const est = plan.estimate;
|
|
501
|
+
const skipped = Object.keys(plan.knownShaToTxId).length;
|
|
502
|
+
lines.push(
|
|
503
|
+
`Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
|
|
504
|
+
);
|
|
505
|
+
lines.push("Fees (base units):");
|
|
506
|
+
lines.push(
|
|
507
|
+
` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
|
|
508
|
+
);
|
|
509
|
+
lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
|
|
510
|
+
lines.push(` total ${formatNumber(est.totalFee)}`);
|
|
511
|
+
lines.push("Writes are permanent and non-refundable.");
|
|
512
|
+
return lines;
|
|
513
|
+
}
|
|
514
|
+
function feeLabel(fee) {
|
|
515
|
+
return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
|
|
516
|
+
}
|
|
517
|
+
function renderEventPlan(opts) {
|
|
518
|
+
return [
|
|
519
|
+
`Publish ${opts.action}`,
|
|
520
|
+
`Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
|
|
521
|
+
renderIdentityLine(opts.identity),
|
|
522
|
+
`Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
function renderEventReceipt(action, result) {
|
|
526
|
+
const lines = [
|
|
527
|
+
`Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
|
|
528
|
+
];
|
|
529
|
+
if (result.channelBalanceAfter !== void 0) {
|
|
530
|
+
lines.push(
|
|
531
|
+
`Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return lines;
|
|
535
|
+
}
|
|
536
|
+
function renderResult(result) {
|
|
537
|
+
const lines = [];
|
|
538
|
+
lines.push(`Pushed "${result.repoId}":`);
|
|
539
|
+
const paid = result.uploads.filter((u) => !u.skipped);
|
|
540
|
+
const skipped = result.uploads.filter((u) => u.skipped);
|
|
541
|
+
for (const upload of result.uploads) {
|
|
542
|
+
lines.push(
|
|
543
|
+
` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
lines.push(
|
|
547
|
+
`Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
|
|
548
|
+
);
|
|
549
|
+
if (result.announceReceipt) {
|
|
550
|
+
lines.push(
|
|
551
|
+
`Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
lines.push(
|
|
555
|
+
`Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
|
|
556
|
+
);
|
|
557
|
+
lines.push(
|
|
558
|
+
`Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
|
|
559
|
+
);
|
|
560
|
+
return lines;
|
|
561
|
+
}
|
|
230
562
|
|
|
231
563
|
// src/cli/git-config.ts
|
|
232
564
|
import { execFile } from "child_process";
|
|
@@ -320,141 +652,54 @@ async function writeToonConfig(repoPath, config) {
|
|
|
320
652
|
}
|
|
321
653
|
}
|
|
322
654
|
|
|
323
|
-
// src/cli/
|
|
324
|
-
import { parseArgs
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
655
|
+
// src/cli/remote.ts
|
|
656
|
+
import { parseArgs } from "util";
|
|
657
|
+
var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
|
|
658
|
+
function isRelayUrl(url) {
|
|
659
|
+
try {
|
|
660
|
+
return RELAY_PROTOCOLS.has(new URL(url).protocol);
|
|
661
|
+
} catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
329
664
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
665
|
+
function assertRelayUrl(url, context) {
|
|
666
|
+
if (!isRelayUrl(url)) throw new InvalidRelayUrlError(url, context);
|
|
332
667
|
}
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
|
|
668
|
+
async function resolveRelays(opts) {
|
|
669
|
+
if (opts.relayFlags.length > 0) {
|
|
670
|
+
return { relays: opts.relayFlags, source: "relay-flag" };
|
|
671
|
+
}
|
|
672
|
+
if (opts.remoteName !== void 0) {
|
|
673
|
+
const urls = opts.repoRoot !== void 0 ? await getGitRemoteUrls(opts.repoRoot, opts.remoteName) : [];
|
|
674
|
+
if (urls.length === 0) throw new UnknownRemoteError(opts.remoteName);
|
|
675
|
+
if (urls.length > 1) throw new MultiUrlRemoteError(opts.remoteName, urls);
|
|
676
|
+
const url = urls[0];
|
|
677
|
+
assertRelayUrl(url, `remote ${JSON.stringify(opts.remoteName)}`);
|
|
678
|
+
return { relays: urls, source: "remote", remoteName: opts.remoteName };
|
|
679
|
+
}
|
|
680
|
+
let nonRelayOriginUrl;
|
|
681
|
+
if (opts.repoRoot !== void 0) {
|
|
682
|
+
const urls = await getGitRemoteUrls(opts.repoRoot, "origin");
|
|
683
|
+
if (urls.length === 1 && isRelayUrl(urls[0])) {
|
|
684
|
+
return { relays: urls, source: "remote", remoteName: "origin" };
|
|
685
|
+
}
|
|
686
|
+
if (urls.length > 1 && urls.some(isRelayUrl)) {
|
|
687
|
+
throw new MultiUrlRemoteError("origin", urls);
|
|
688
|
+
}
|
|
689
|
+
if (urls.length > 0) nonRelayOriginUrl = urls[0];
|
|
690
|
+
}
|
|
691
|
+
if (opts.toonRelays.length > 0) {
|
|
692
|
+
return {
|
|
693
|
+
relays: opts.toonRelays,
|
|
694
|
+
source: "toon.relay",
|
|
695
|
+
nudge: `note: git config toon.relay is deprecated (removed in v0.3) \u2014 migrate: rig remote add origin ${opts.toonRelays[0]}`
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
throw new NoOriginConfiguredError(nonRelayOriginUrl);
|
|
336
699
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
function renderPlan(plan) {
|
|
341
|
-
const lines = [];
|
|
342
|
-
lines.push(
|
|
343
|
-
`Push plan for repo "${plan.repoId}"` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
|
|
344
|
-
);
|
|
345
|
-
lines.push("Refs:");
|
|
346
|
-
for (const update of plan.refUpdates) lines.push(refLine(update));
|
|
347
|
-
const est = plan.estimate;
|
|
348
|
-
const skipped = Object.keys(plan.knownShaToTxId).length;
|
|
349
|
-
lines.push(
|
|
350
|
-
`Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
|
|
351
|
-
);
|
|
352
|
-
lines.push("Fees (base units):");
|
|
353
|
-
lines.push(
|
|
354
|
-
` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
|
|
355
|
-
);
|
|
356
|
-
lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
|
|
357
|
-
lines.push(` total ${formatNumber(est.totalFee)}`);
|
|
358
|
-
lines.push("Writes are permanent and non-refundable.");
|
|
359
|
-
return lines;
|
|
360
|
-
}
|
|
361
|
-
function feeLabel(fee) {
|
|
362
|
-
return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
|
|
363
|
-
}
|
|
364
|
-
function renderEventPlan(opts) {
|
|
365
|
-
return [
|
|
366
|
-
`Publish ${opts.action}`,
|
|
367
|
-
`Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
|
|
368
|
-
renderIdentityLine(opts.identity),
|
|
369
|
-
`Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
|
|
370
|
-
];
|
|
371
|
-
}
|
|
372
|
-
function renderEventReceipt(action, result) {
|
|
373
|
-
const lines = [
|
|
374
|
-
`Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
|
|
375
|
-
];
|
|
376
|
-
if (result.channelBalanceAfter !== void 0) {
|
|
377
|
-
lines.push(
|
|
378
|
-
`Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
return lines;
|
|
382
|
-
}
|
|
383
|
-
function renderResult(result) {
|
|
384
|
-
const lines = [];
|
|
385
|
-
lines.push(`Pushed "${result.repoId}":`);
|
|
386
|
-
const paid = result.uploads.filter((u) => !u.skipped);
|
|
387
|
-
const skipped = result.uploads.filter((u) => u.skipped);
|
|
388
|
-
for (const upload of result.uploads) {
|
|
389
|
-
lines.push(
|
|
390
|
-
` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
lines.push(
|
|
394
|
-
`Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
|
|
395
|
-
);
|
|
396
|
-
if (result.announceReceipt) {
|
|
397
|
-
lines.push(
|
|
398
|
-
`Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
lines.push(
|
|
402
|
-
`Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
|
|
403
|
-
);
|
|
404
|
-
lines.push(
|
|
405
|
-
`Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
|
|
406
|
-
);
|
|
407
|
-
return lines;
|
|
408
|
-
}
|
|
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}`;
|
|
700
|
+
function singleRelayRefusal(resolved, nothingHappened) {
|
|
701
|
+
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>`)";
|
|
702
|
+
return `rig publishes to a single relay, but ${resolved.relays.length} are configured (${resolved.relays.join(", ")}) \u2014 ${fix}. ${nothingHappened}`;
|
|
458
703
|
}
|
|
459
704
|
var REMOTE_USAGE = `Usage: rig remote <add|remove|list> [args] [options]
|
|
460
705
|
|
|
@@ -471,10 +716,10 @@ Commands:
|
|
|
471
716
|
list list remote names + URLs (the default subcommand)
|
|
472
717
|
|
|
473
718
|
Options:
|
|
474
|
-
--json machine-readable
|
|
719
|
+
--json machine-readable envelope (all subcommands)
|
|
475
720
|
-h, --help show this help`;
|
|
476
721
|
async function runRemote(args, deps) {
|
|
477
|
-
const { io } = deps;
|
|
722
|
+
const { io: io2 } = deps;
|
|
478
723
|
let sub;
|
|
479
724
|
let rest;
|
|
480
725
|
let json;
|
|
@@ -488,34 +733,34 @@ async function runRemote(args, deps) {
|
|
|
488
733
|
allowPositionals: true
|
|
489
734
|
});
|
|
490
735
|
if (values.help) {
|
|
491
|
-
|
|
736
|
+
io2.out(REMOTE_USAGE);
|
|
492
737
|
return 0;
|
|
493
738
|
}
|
|
494
739
|
json = values.json ?? false;
|
|
495
740
|
[sub, ...rest] = positionals;
|
|
496
741
|
} catch (err) {
|
|
497
|
-
|
|
498
|
-
|
|
742
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
743
|
+
io2.err(REMOTE_USAGE);
|
|
499
744
|
return 2;
|
|
500
745
|
}
|
|
501
746
|
switch (sub) {
|
|
502
747
|
case void 0:
|
|
503
748
|
case "list":
|
|
504
749
|
if (rest.length > 0) {
|
|
505
|
-
|
|
506
|
-
|
|
750
|
+
io2.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
|
|
751
|
+
io2.err(REMOTE_USAGE);
|
|
507
752
|
return 2;
|
|
508
753
|
}
|
|
509
754
|
break;
|
|
510
755
|
case "add": {
|
|
511
756
|
if (rest.length !== 2) {
|
|
512
|
-
|
|
513
|
-
|
|
757
|
+
io2.err("usage: rig remote add <name> <relay-url>");
|
|
758
|
+
io2.err(REMOTE_USAGE);
|
|
514
759
|
return 2;
|
|
515
760
|
}
|
|
516
761
|
const url = rest[1];
|
|
517
762
|
if (!isRelayUrl(url)) {
|
|
518
|
-
|
|
763
|
+
io2.err(
|
|
519
764
|
`cannot add remote: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
|
|
520
765
|
);
|
|
521
766
|
return 2;
|
|
@@ -524,14 +769,14 @@ async function runRemote(args, deps) {
|
|
|
524
769
|
}
|
|
525
770
|
case "remove":
|
|
526
771
|
if (rest.length !== 1) {
|
|
527
|
-
|
|
528
|
-
|
|
772
|
+
io2.err("usage: rig remote remove <name>");
|
|
773
|
+
io2.err(REMOTE_USAGE);
|
|
529
774
|
return 2;
|
|
530
775
|
}
|
|
531
776
|
break;
|
|
532
777
|
default:
|
|
533
|
-
|
|
534
|
-
|
|
778
|
+
io2.err(`unknown rig remote subcommand: ${sub}`);
|
|
779
|
+
io2.err(REMOTE_USAGE);
|
|
535
780
|
return 2;
|
|
536
781
|
}
|
|
537
782
|
try {
|
|
@@ -546,23 +791,23 @@ async function runRemote(args, deps) {
|
|
|
546
791
|
case "list": {
|
|
547
792
|
const remotes = await listGitRemotes(repoRoot);
|
|
548
793
|
if (json) {
|
|
549
|
-
|
|
794
|
+
io2.emitJson({ command: "remote", remotes });
|
|
550
795
|
return 0;
|
|
551
796
|
}
|
|
552
797
|
if (remotes.length === 0) {
|
|
553
|
-
|
|
798
|
+
io2.out(
|
|
554
799
|
"no remotes configured \u2014 add one: rig remote add origin <relay-url>"
|
|
555
800
|
);
|
|
556
801
|
return 0;
|
|
557
802
|
}
|
|
558
803
|
for (const remote of remotes) {
|
|
559
804
|
for (const url of remote.urls) {
|
|
560
|
-
|
|
805
|
+
io2.out(
|
|
561
806
|
`${remote.name} ${url}` + (isRelayUrl(url) ? "" : " (not a relay URL \u2014 ignored by rig)")
|
|
562
807
|
);
|
|
563
808
|
}
|
|
564
809
|
if (remote.urls.length > 1) {
|
|
565
|
-
|
|
810
|
+
io2.err(
|
|
566
811
|
`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
812
|
);
|
|
568
813
|
}
|
|
@@ -573,37 +818,61 @@ async function runRemote(args, deps) {
|
|
|
573
818
|
const [name, url] = rest;
|
|
574
819
|
const existing = await getGitRemoteUrls(repoRoot, name);
|
|
575
820
|
if (existing.length > 0) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
821
|
+
const detail = `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.`;
|
|
822
|
+
if (json) {
|
|
823
|
+
io2.emitJson({
|
|
824
|
+
command: "remote",
|
|
825
|
+
error: "remote_exists",
|
|
826
|
+
detail,
|
|
827
|
+
remote: name,
|
|
828
|
+
urls: existing
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
io2.err(detail);
|
|
579
832
|
return 1;
|
|
580
833
|
}
|
|
581
834
|
await addGitRemote(repoRoot, name, url);
|
|
582
|
-
|
|
835
|
+
if (!json) io2.out(`Added remote ${name} \u2192 ${url}`);
|
|
583
836
|
if (name === "origin") {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
837
|
+
if (!json) {
|
|
838
|
+
io2.out(
|
|
839
|
+
"`rig push` and the event commands now publish here by default."
|
|
840
|
+
);
|
|
841
|
+
}
|
|
587
842
|
const toonConfig = await readToonConfig(repoRoot);
|
|
588
843
|
if (toonConfig.relays.length > 0) {
|
|
589
|
-
|
|
844
|
+
io2.err(
|
|
590
845
|
`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
846
|
);
|
|
592
847
|
}
|
|
593
848
|
}
|
|
849
|
+
if (json) {
|
|
850
|
+
io2.emitJson({ command: "remote", action: "add", name, url });
|
|
851
|
+
}
|
|
594
852
|
return 0;
|
|
595
853
|
}
|
|
596
854
|
case "remove": {
|
|
597
855
|
const name = rest[0];
|
|
598
856
|
const existing = await getGitRemoteUrls(repoRoot, name);
|
|
599
857
|
if (existing.length === 0) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
858
|
+
const detail = `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`;
|
|
859
|
+
if (json) {
|
|
860
|
+
io2.emitJson({
|
|
861
|
+
command: "remote",
|
|
862
|
+
error: "unknown_remote",
|
|
863
|
+
detail,
|
|
864
|
+
remote: name
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
io2.err(detail);
|
|
603
868
|
return 1;
|
|
604
869
|
}
|
|
605
870
|
await removeGitRemote(repoRoot, name);
|
|
606
|
-
|
|
871
|
+
if (json) {
|
|
872
|
+
io2.emitJson({ command: "remote", action: "remove", name });
|
|
873
|
+
} else {
|
|
874
|
+
io2.out(`Removed remote ${name}`);
|
|
875
|
+
}
|
|
607
876
|
return 0;
|
|
608
877
|
}
|
|
609
878
|
/* v8 ignore next 2 -- unreachable: validated above */
|
|
@@ -611,19 +880,24 @@ async function runRemote(args, deps) {
|
|
|
611
880
|
return 2;
|
|
612
881
|
}
|
|
613
882
|
} catch (err) {
|
|
614
|
-
|
|
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;
|
|
883
|
+
return emitCliError(io2, json, "remote", err);
|
|
621
884
|
}
|
|
622
885
|
}
|
|
623
886
|
|
|
624
887
|
// src/cli/push.ts
|
|
888
|
+
function loadPaidSession(deps, relayUrl) {
|
|
889
|
+
return resolvePaidSession({
|
|
890
|
+
env: deps.env,
|
|
891
|
+
cwd: deps.cwd,
|
|
892
|
+
warn: (line) => deps.io.err(line),
|
|
893
|
+
loadStandalone: deps.loadStandalone ?? defaultLoadStandalone,
|
|
894
|
+
...relayUrl !== void 0 ? { relayUrl } : {},
|
|
895
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
896
|
+
...deps.probeDaemon ? { probeDaemon: deps.probeDaemon } : {}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
625
899
|
var defaultLoadStandalone = async (options) => {
|
|
626
|
-
const mod = await import("../standalone-mode-
|
|
900
|
+
const mod = await import("../standalone-mode-64CKUVU2.js");
|
|
627
901
|
return mod.createStandaloneContext(options);
|
|
628
902
|
};
|
|
629
903
|
function identityReport(ctx) {
|
|
@@ -734,17 +1008,17 @@ function parsePushArgs(args) {
|
|
|
734
1008
|
return flags;
|
|
735
1009
|
}
|
|
736
1010
|
async function runPush(args, deps) {
|
|
737
|
-
const { io
|
|
1011
|
+
const { io: io2 } = deps;
|
|
738
1012
|
let flags;
|
|
739
1013
|
try {
|
|
740
1014
|
flags = parsePushArgs(args);
|
|
741
1015
|
} catch (err) {
|
|
742
|
-
|
|
743
|
-
|
|
1016
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1017
|
+
io2.err(PUSH_USAGE);
|
|
744
1018
|
return 2;
|
|
745
1019
|
}
|
|
746
1020
|
if (flags.help) {
|
|
747
|
-
|
|
1021
|
+
io2.out(PUSH_USAGE);
|
|
748
1022
|
return 0;
|
|
749
1023
|
}
|
|
750
1024
|
let standaloneCtx;
|
|
@@ -786,106 +1060,140 @@ async function runPush(args, deps) {
|
|
|
786
1060
|
repoRoot,
|
|
787
1061
|
toonRelays: toonConfig.relays
|
|
788
1062
|
});
|
|
789
|
-
if (resolved.nudge !== void 0)
|
|
1063
|
+
if (resolved.nudge !== void 0) io2.err(resolved.nudge);
|
|
790
1064
|
const relaysUsed = resolved.relays;
|
|
791
1065
|
if (relaysUsed.length > 1) {
|
|
792
|
-
|
|
1066
|
+
io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
|
|
793
1067
|
return 1;
|
|
794
1068
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1069
|
+
const session = await loadPaidSession(deps, relaysUsed[0]);
|
|
1070
|
+
const path = session.path;
|
|
1071
|
+
let identity;
|
|
1072
|
+
if (session.path === "standalone") {
|
|
1073
|
+
standaloneCtx = session.ctx;
|
|
1074
|
+
identity = identityReport(standaloneCtx);
|
|
1075
|
+
} else {
|
|
1076
|
+
identity = session.identity;
|
|
1077
|
+
}
|
|
801
1078
|
if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
|
|
802
|
-
|
|
1079
|
+
io2.err(
|
|
803
1080
|
`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.`
|
|
804
1081
|
);
|
|
805
1082
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
feeRates
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1083
|
+
let plan;
|
|
1084
|
+
let execute;
|
|
1085
|
+
if (session.path === "standalone") {
|
|
1086
|
+
const ctx = session.ctx;
|
|
1087
|
+
const remoteState = await ctx.fetchRemote({
|
|
1088
|
+
ownerPubkey: ctx.ownerPubkey,
|
|
1089
|
+
repoId,
|
|
1090
|
+
relayUrls: relaysUsed
|
|
1091
|
+
});
|
|
1092
|
+
const feeRates = await ctx.publisher.getFeeRates();
|
|
1093
|
+
const pushPlan = await planPush({
|
|
1094
|
+
repoReader: reader,
|
|
1095
|
+
remoteState,
|
|
1096
|
+
feeRates,
|
|
1097
|
+
repoId,
|
|
1098
|
+
refs: refspecs,
|
|
1099
|
+
force: flags.force
|
|
1100
|
+
});
|
|
1101
|
+
plan = serializePushPlan(pushPlan);
|
|
1102
|
+
execute = async () => serializePushResult(
|
|
1103
|
+
pushPlan,
|
|
1104
|
+
await executePush({
|
|
1105
|
+
plan: pushPlan,
|
|
1106
|
+
publisher: ctx.publisher,
|
|
1107
|
+
remoteState,
|
|
1108
|
+
repoReader: reader,
|
|
1109
|
+
relayUrls: relaysUsed
|
|
1110
|
+
})
|
|
1111
|
+
);
|
|
1112
|
+
} else {
|
|
1113
|
+
const client = session.client;
|
|
1114
|
+
plan = await client.gitEstimate({
|
|
1115
|
+
repoPath: repoRoot,
|
|
1116
|
+
repoId,
|
|
1117
|
+
refspecs,
|
|
1118
|
+
force: flags.force,
|
|
1119
|
+
relayUrls: relaysUsed
|
|
1120
|
+
});
|
|
1121
|
+
execute = () => client.gitPush({
|
|
1122
|
+
repoPath: repoRoot,
|
|
1123
|
+
repoId,
|
|
1124
|
+
refspecs,
|
|
1125
|
+
force: flags.force,
|
|
1126
|
+
relayUrls: relaysUsed,
|
|
1127
|
+
confirm: true
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
821
1130
|
const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
|
|
822
1131
|
if (upToDate) {
|
|
823
1132
|
if (flags.json) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1133
|
+
io2.emitJson({
|
|
1134
|
+
command: "push",
|
|
1135
|
+
repoId,
|
|
1136
|
+
path,
|
|
1137
|
+
identity,
|
|
1138
|
+
executed: false,
|
|
1139
|
+
upToDate: true,
|
|
1140
|
+
plan
|
|
1141
|
+
});
|
|
827
1142
|
} else {
|
|
828
|
-
|
|
1143
|
+
io2.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
|
|
829
1144
|
}
|
|
830
1145
|
return 0;
|
|
831
1146
|
}
|
|
832
1147
|
if (!flags.json) {
|
|
833
|
-
for (const line of renderPlan(plan))
|
|
834
|
-
|
|
1148
|
+
for (const line of renderPlan(plan)) io2.out(line);
|
|
1149
|
+
io2.out(renderIdentityLine(identity));
|
|
835
1150
|
}
|
|
836
1151
|
if (!flags.yes) {
|
|
837
1152
|
if (flags.json) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
);
|
|
1153
|
+
io2.emitJson({
|
|
1154
|
+
command: "push",
|
|
1155
|
+
repoId,
|
|
1156
|
+
path,
|
|
1157
|
+
identity,
|
|
1158
|
+
executed: false,
|
|
1159
|
+
upToDate: false,
|
|
1160
|
+
plan,
|
|
1161
|
+
hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
|
|
1162
|
+
});
|
|
849
1163
|
return 0;
|
|
850
1164
|
}
|
|
851
|
-
if (!
|
|
852
|
-
|
|
1165
|
+
if (!io2.isInteractive) {
|
|
1166
|
+
io2.err(
|
|
853
1167
|
"refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
|
|
854
1168
|
);
|
|
855
1169
|
return 1;
|
|
856
1170
|
}
|
|
857
|
-
const proceed = await
|
|
1171
|
+
const proceed = await io2.confirm(
|
|
858
1172
|
`Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
|
|
859
1173
|
);
|
|
860
1174
|
if (!proceed) {
|
|
861
|
-
|
|
1175
|
+
io2.err("aborted \u2014 nothing was uploaded or published.");
|
|
862
1176
|
return 1;
|
|
863
1177
|
}
|
|
864
1178
|
}
|
|
865
|
-
const
|
|
866
|
-
plan: pushPlan,
|
|
867
|
-
publisher: standaloneCtx.publisher,
|
|
868
|
-
remoteState,
|
|
869
|
-
repoReader: reader,
|
|
870
|
-
relayUrls: relaysUsed
|
|
871
|
-
});
|
|
872
|
-
const result = serializePushResult(pushPlan, pushResult);
|
|
1179
|
+
const result = await execute();
|
|
873
1180
|
if (flags.json) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1181
|
+
io2.emitJson({
|
|
1182
|
+
command: "push",
|
|
1183
|
+
repoId,
|
|
1184
|
+
path,
|
|
1185
|
+
identity,
|
|
1186
|
+
executed: true,
|
|
1187
|
+
upToDate: false,
|
|
1188
|
+
plan,
|
|
1189
|
+
result
|
|
1190
|
+
});
|
|
877
1191
|
} else {
|
|
878
|
-
for (const line of renderResult(result))
|
|
1192
|
+
for (const line of renderResult(result)) io2.out(line);
|
|
879
1193
|
}
|
|
880
1194
|
return 0;
|
|
881
1195
|
} catch (err) {
|
|
882
|
-
|
|
883
|
-
if (flags.json) {
|
|
884
|
-
io.out(JSON.stringify({ command: "push", ...described.json }, null, 2));
|
|
885
|
-
} else {
|
|
886
|
-
for (const line of described.lines) io.err(line);
|
|
887
|
-
}
|
|
888
|
-
return 1;
|
|
1196
|
+
return emitCliError(io2, flags.json, "push", err);
|
|
889
1197
|
} finally {
|
|
890
1198
|
if (standaloneCtx) {
|
|
891
1199
|
try {
|
|
@@ -895,8 +1203,1289 @@ async function runPush(args, deps) {
|
|
|
895
1203
|
}
|
|
896
1204
|
}
|
|
897
1205
|
}
|
|
898
|
-
|
|
899
|
-
|
|
1206
|
+
|
|
1207
|
+
// src/cli/balance.ts
|
|
1208
|
+
var BALANCE_USAGE = `Usage: rig balance [--json]
|
|
1209
|
+
|
|
1210
|
+
Show the active identity's money: on-chain wallet balances (per configured
|
|
1211
|
+
chain, read from the settlement chain the payment channels actually use) and
|
|
1212
|
+
recorded payment-channel holdings (deposited / claimed / available). Free \u2014
|
|
1213
|
+
reads chain RPCs and local state only; nothing is signed or paid.
|
|
1214
|
+
|
|
1215
|
+
Options:
|
|
1216
|
+
--json machine-readable envelope (base units as strings)
|
|
1217
|
+
-h, --help show this help`;
|
|
1218
|
+
async function runBalance(args, deps) {
|
|
1219
|
+
const { io: io2, env } = deps;
|
|
1220
|
+
let json = false;
|
|
1221
|
+
try {
|
|
1222
|
+
const { values } = parseArgs3({
|
|
1223
|
+
args,
|
|
1224
|
+
options: {
|
|
1225
|
+
json: { type: "boolean", default: false },
|
|
1226
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
if (values.help) {
|
|
1230
|
+
io2.out(BALANCE_USAGE);
|
|
1231
|
+
return 0;
|
|
1232
|
+
}
|
|
1233
|
+
json = values.json ?? false;
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1236
|
+
io2.err(BALANCE_USAGE);
|
|
1237
|
+
return 2;
|
|
1238
|
+
}
|
|
1239
|
+
let ctx;
|
|
1240
|
+
try {
|
|
1241
|
+
ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
|
|
1242
|
+
env,
|
|
1243
|
+
cwd: deps.cwd,
|
|
1244
|
+
warn: (line) => io2.err(line),
|
|
1245
|
+
// Free read: works without a proxy/BTP write uplink.
|
|
1246
|
+
requireUplink: false
|
|
1247
|
+
});
|
|
1248
|
+
const identity = identityReport(ctx);
|
|
1249
|
+
const wallet = ctx.money ? await ctx.money.walletBalances() : [];
|
|
1250
|
+
const store = new ChannelMapStore(resolveChannelPaths(env));
|
|
1251
|
+
const channels = store.list().filter((record) => record.identity === identity.pubkey).map((record) => {
|
|
1252
|
+
const watermark = store.readWatermark(record.channelId);
|
|
1253
|
+
const deposited = record.depositTotal;
|
|
1254
|
+
const claimed = watermark?.cumulativeAmount;
|
|
1255
|
+
let available = null;
|
|
1256
|
+
if (deposited !== void 0 && claimed !== void 0) {
|
|
1257
|
+
const remaining = BigInt(deposited) - BigInt(claimed);
|
|
1258
|
+
available = (remaining > 0n ? remaining : 0n).toString();
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
channelId: record.channelId,
|
|
1262
|
+
destination: record.destination,
|
|
1263
|
+
peerId: record.peerId,
|
|
1264
|
+
chain: record.chain,
|
|
1265
|
+
status: channelStatus(watermark),
|
|
1266
|
+
depositTotal: deposited ?? null,
|
|
1267
|
+
cumulativeClaimed: claimed ?? null,
|
|
1268
|
+
nonce: watermark?.nonce ?? null,
|
|
1269
|
+
available
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
if (json) {
|
|
1273
|
+
io2.emitJson({
|
|
1274
|
+
command: "balance",
|
|
1275
|
+
identity,
|
|
1276
|
+
wallet,
|
|
1277
|
+
channels
|
|
1278
|
+
});
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
io2.out(renderIdentityLine(identity));
|
|
1282
|
+
io2.out("");
|
|
1283
|
+
io2.out("Wallet (on-chain):");
|
|
1284
|
+
if (wallet.length === 0) {
|
|
1285
|
+
io2.out(
|
|
1286
|
+
" (no balance readable \u2014 no chain configured, the RPC is unreachable, or the chain's keys derive only during a client start)"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
for (const balance of wallet) {
|
|
1290
|
+
io2.out(
|
|
1291
|
+
` ${balance.chain.padEnd(7)} ${balance.address} ${balance.amount}` + (balance.asset ? ` ${balance.asset}` : " base units") + (balance.assetScale !== void 0 ? ` (scale ${balance.assetScale})` : "")
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
io2.out("");
|
|
1295
|
+
io2.out("Channels (recorded):");
|
|
1296
|
+
if (channels.length === 0) {
|
|
1297
|
+
io2.out(
|
|
1298
|
+
" none \u2014 paid commands record their channel on first use; `rig channel open` records one explicitly."
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
for (const c of channels) {
|
|
1302
|
+
io2.out(
|
|
1303
|
+
` ${c.channelId} [${c.status}] deposited ${c.depositTotal ?? "?"} claimed ${c.cumulativeClaimed ?? "?"} available ${c.available ?? "?"} (${c.chain} \u2192 ${c.destination})`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return 0;
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
return emitCliError(io2, json, "balance", err);
|
|
1309
|
+
} finally {
|
|
1310
|
+
if (ctx) {
|
|
1311
|
+
try {
|
|
1312
|
+
await ctx.stop();
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/cli/channel.ts
|
|
1320
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
1321
|
+
var CHANNEL_USAGE = `Usage: rig channel <subcommand>
|
|
1322
|
+
|
|
1323
|
+
Manage the payment channels paid rig commands hold with relay/store peers.
|
|
1324
|
+
Paid commands open a channel lazily on first use and record it under
|
|
1325
|
+
TOON_CLIENT_HOME (default ~/.toon-client, rig-channels.json), so later
|
|
1326
|
+
invocations resume the same channel instead of opening a new one per run.
|
|
1327
|
+
|
|
1328
|
+
Subcommands:
|
|
1329
|
+
list [--json] show recorded channels \u2014 peer, chain, channel id, deposit,
|
|
1330
|
+
cumulative claimed, status. Free: reads local state only.
|
|
1331
|
+
|
|
1332
|
+
open [--peer <ilp-destination>] [--deposit <base-units>]
|
|
1333
|
+
explicitly open the payment channel for a peer \u2014 the SAME
|
|
1334
|
+
path paid commands use lazily: resumes the recorded live
|
|
1335
|
+
channel if one exists (no on-chain spend), else opens and
|
|
1336
|
+
records a fresh one (locks the peer-negotiated initial
|
|
1337
|
+
deposit on-chain). --peer is the ILP destination to anchor
|
|
1338
|
+
to (default: the configured destination, e.g. the devnet
|
|
1339
|
+
apex); --deposit adds that much extra collateral after the
|
|
1340
|
+
open/resume. On-chain: asks for confirmation (--yes skips).
|
|
1341
|
+
|
|
1342
|
+
close <channelId>
|
|
1343
|
+
close a recorded channel \u2014 an on-chain tx that starts the
|
|
1344
|
+
settlement challenge window (the peer's settlementTimeout).
|
|
1345
|
+
The channel stops paying immediately; the remaining
|
|
1346
|
+
collateral stays locked until \`rig channel settle\` after
|
|
1347
|
+
the window elapses. Asks for confirmation (--yes skips).
|
|
1348
|
+
|
|
1349
|
+
settle <channelId>
|
|
1350
|
+
settle a closed channel once its challenge window elapsed \u2014
|
|
1351
|
+
an on-chain tx that releases the remaining collateral back
|
|
1352
|
+
to your wallet. Refused (without spending gas) while the
|
|
1353
|
+
window is still open. Asks for confirmation (--yes skips).
|
|
1354
|
+
|
|
1355
|
+
Common options: --json (machine-readable envelopes; without --yes lifecycle
|
|
1356
|
+
commands emit a pure plan and execute nothing), --yes, -h/--help.`;
|
|
1357
|
+
async function runChannel(args, deps) {
|
|
1358
|
+
const { io: io2 } = deps;
|
|
1359
|
+
const [sub, ...rest] = args;
|
|
1360
|
+
switch (sub) {
|
|
1361
|
+
case "list":
|
|
1362
|
+
return runChannelList(rest, deps);
|
|
1363
|
+
case "open":
|
|
1364
|
+
return runChannelOpen(rest, deps);
|
|
1365
|
+
case "close":
|
|
1366
|
+
return runChannelWithdrawStep(rest, deps, "close");
|
|
1367
|
+
case "settle":
|
|
1368
|
+
return runChannelWithdrawStep(rest, deps, "settle");
|
|
1369
|
+
case "help":
|
|
1370
|
+
case "--help":
|
|
1371
|
+
case "-h":
|
|
1372
|
+
io2.out(CHANNEL_USAGE);
|
|
1373
|
+
return 0;
|
|
1374
|
+
case void 0:
|
|
1375
|
+
io2.err(CHANNEL_USAGE);
|
|
1376
|
+
return 2;
|
|
1377
|
+
default:
|
|
1378
|
+
io2.err(`rig channel: unknown subcommand ${JSON.stringify(sub)}`);
|
|
1379
|
+
io2.err(CHANNEL_USAGE);
|
|
1380
|
+
return 2;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function runChannelList(args, deps) {
|
|
1384
|
+
const { io: io2, env } = deps;
|
|
1385
|
+
let json = false;
|
|
1386
|
+
try {
|
|
1387
|
+
const { values } = parseArgs4({
|
|
1388
|
+
args,
|
|
1389
|
+
options: {
|
|
1390
|
+
json: { type: "boolean", default: false },
|
|
1391
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
if (values.help) {
|
|
1395
|
+
io2.out(CHANNEL_USAGE);
|
|
1396
|
+
return 0;
|
|
1397
|
+
}
|
|
1398
|
+
json = values.json ?? false;
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1401
|
+
io2.err(CHANNEL_USAGE);
|
|
1402
|
+
return 2;
|
|
1403
|
+
}
|
|
1404
|
+
try {
|
|
1405
|
+
const paths = resolveChannelPaths(env);
|
|
1406
|
+
const store = new ChannelMapStore(paths);
|
|
1407
|
+
const records = store.list();
|
|
1408
|
+
const rows = records.map((record) => describeChannel(record, store));
|
|
1409
|
+
if (json) {
|
|
1410
|
+
io2.emitJson({ command: "channel list", channels: rows });
|
|
1411
|
+
return 0;
|
|
1412
|
+
}
|
|
1413
|
+
if (rows.length === 0) {
|
|
1414
|
+
io2.out(
|
|
1415
|
+
"No payment channels recorded \u2014 paid rig commands (push, issue, comment, pr) record the channel they open on first use, and `rig channel open` records an explicit one."
|
|
1416
|
+
);
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
io2.out(
|
|
1420
|
+
`${rows.length} payment channel${rows.length === 1 ? "" : "s"} recorded in ${paths.mapPath}:`
|
|
1421
|
+
);
|
|
1422
|
+
for (const row of rows) {
|
|
1423
|
+
io2.out("");
|
|
1424
|
+
for (const line of renderChannel(row)) io2.out(line);
|
|
1425
|
+
}
|
|
1426
|
+
return 0;
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
return emitCliError(io2, json, "channel list", err);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function describeChannel(record, store) {
|
|
1432
|
+
const watermark = store.readWatermark(
|
|
1433
|
+
record.channelId
|
|
1434
|
+
);
|
|
1435
|
+
return {
|
|
1436
|
+
channelId: record.channelId,
|
|
1437
|
+
peerId: record.peerId,
|
|
1438
|
+
identity: record.identity,
|
|
1439
|
+
destination: record.destination,
|
|
1440
|
+
chain: record.chain,
|
|
1441
|
+
tokenNetwork: record.tokenNetwork,
|
|
1442
|
+
depositTotal: record.depositTotal ?? null,
|
|
1443
|
+
cumulativeClaimed: watermark?.cumulativeAmount ?? null,
|
|
1444
|
+
nonce: watermark?.nonce ?? null,
|
|
1445
|
+
status: channelStatus(watermark),
|
|
1446
|
+
openedAt: record.openedAt,
|
|
1447
|
+
lastUsedAt: record.lastUsedAt
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function renderChannel(row) {
|
|
1451
|
+
const claimed = row.cumulativeClaimed === null ? "unknown (no local claim state)" : `${row.cumulativeClaimed} base units (nonce ${row.nonce})`;
|
|
1452
|
+
return [
|
|
1453
|
+
`channel ${row.channelId} [${row.status}]`,
|
|
1454
|
+
` peer ${row.destination} (${row.peerId})`,
|
|
1455
|
+
` identity ${row.identity.slice(0, 8)}\u2026`,
|
|
1456
|
+
` chain ${row.chain}` + (row.tokenNetwork ? ` token-network ${row.tokenNetwork}` : ""),
|
|
1457
|
+
` deposited ${row.depositTotal ?? "unrecorded"}` + (row.depositTotal ? " base units" : ""),
|
|
1458
|
+
` claimed ${claimed}`,
|
|
1459
|
+
` opened ${row.openedAt} (last used ${row.lastUsedAt})`
|
|
1460
|
+
];
|
|
1461
|
+
}
|
|
1462
|
+
async function loadMoneyContext(deps, options) {
|
|
1463
|
+
const ctx = await (deps.loadStandalone ?? defaultLoadStandalone)({
|
|
1464
|
+
env: deps.env,
|
|
1465
|
+
cwd: deps.cwd,
|
|
1466
|
+
warn: (line) => deps.io.err(line),
|
|
1467
|
+
...options?.channelDestination ? { channelDestination: options.channelDestination } : {}
|
|
1468
|
+
});
|
|
1469
|
+
const money = ctx.money;
|
|
1470
|
+
if (!money) {
|
|
1471
|
+
await ctx.stop().catch(() => void 0);
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
"this standalone loader does not expose money operations \u2014 channel open/close/settle need a loader with the money lifecycle"
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
return { ctx, money };
|
|
1477
|
+
}
|
|
1478
|
+
async function confirmOnChain(io2, flags, question) {
|
|
1479
|
+
if (flags.yes) return "proceed";
|
|
1480
|
+
if (flags.json) return "json-plan";
|
|
1481
|
+
if (!io2.isInteractive) {
|
|
1482
|
+
io2.err(
|
|
1483
|
+
"refusing to move on-chain funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for a plan)"
|
|
1484
|
+
);
|
|
1485
|
+
return 1;
|
|
1486
|
+
}
|
|
1487
|
+
const proceed = await io2.confirm(question);
|
|
1488
|
+
if (proceed) return "proceed";
|
|
1489
|
+
io2.err("aborted \u2014 no on-chain transaction was sent.");
|
|
1490
|
+
return 1;
|
|
1491
|
+
}
|
|
1492
|
+
function formatUnixSeconds(value) {
|
|
1493
|
+
const ms = Number(value) * 1e3;
|
|
1494
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : `t=${value}s`;
|
|
1495
|
+
}
|
|
1496
|
+
function secondsUntil(value, nowSec) {
|
|
1497
|
+
return Number(value) - nowSec;
|
|
1498
|
+
}
|
|
1499
|
+
async function runChannelOpen(args, deps) {
|
|
1500
|
+
const { io: io2 } = deps;
|
|
1501
|
+
let peer;
|
|
1502
|
+
let deposit;
|
|
1503
|
+
let yes = false;
|
|
1504
|
+
let json = false;
|
|
1505
|
+
try {
|
|
1506
|
+
const { values } = parseArgs4({
|
|
1507
|
+
args,
|
|
1508
|
+
options: {
|
|
1509
|
+
peer: { type: "string" },
|
|
1510
|
+
deposit: { type: "string" },
|
|
1511
|
+
yes: { type: "boolean", default: false },
|
|
1512
|
+
json: { type: "boolean", default: false },
|
|
1513
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
if (values.help) {
|
|
1517
|
+
io2.out(CHANNEL_USAGE);
|
|
1518
|
+
return 0;
|
|
1519
|
+
}
|
|
1520
|
+
peer = values.peer;
|
|
1521
|
+
yes = values.yes ?? false;
|
|
1522
|
+
json = values.json ?? false;
|
|
1523
|
+
if (values.deposit !== void 0) {
|
|
1524
|
+
if (!/^\d+$/.test(values.deposit) || BigInt(values.deposit) <= 0n) {
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
`--deposit must be a positive base-unit integer, got ${JSON.stringify(values.deposit)}`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
deposit = BigInt(values.deposit);
|
|
1530
|
+
}
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1533
|
+
io2.err(CHANNEL_USAGE);
|
|
1534
|
+
return 2;
|
|
1535
|
+
}
|
|
1536
|
+
let ctx;
|
|
1537
|
+
try {
|
|
1538
|
+
const loaded = await loadMoneyContext(deps, {
|
|
1539
|
+
...peer ? { channelDestination: peer } : {}
|
|
1540
|
+
});
|
|
1541
|
+
ctx = loaded.ctx;
|
|
1542
|
+
const identity = identityReport(ctx);
|
|
1543
|
+
const plan = {
|
|
1544
|
+
destination: peer ?? null,
|
|
1545
|
+
deposit: deposit?.toString() ?? null
|
|
1546
|
+
};
|
|
1547
|
+
if (!json) {
|
|
1548
|
+
io2.out("Channel open plan:");
|
|
1549
|
+
io2.out(` peer (ILP) ${peer ?? "(configured default destination)"}`);
|
|
1550
|
+
io2.out(
|
|
1551
|
+
` deposit ${deposit !== void 0 ? `+${deposit} base units of extra collateral after the open/resume` : "none beyond the peer-negotiated initial deposit"}`
|
|
1552
|
+
);
|
|
1553
|
+
io2.out(renderIdentityLine(identity));
|
|
1554
|
+
io2.out(
|
|
1555
|
+
"If a live channel is already recorded for this peer it is RESUMED (no on-chain spend); otherwise an on-chain channel open locks the peer-negotiated initial deposit from your wallet (plus gas)."
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
const gate = await confirmOnChain(
|
|
1559
|
+
io2,
|
|
1560
|
+
{ yes, json },
|
|
1561
|
+
"Proceed with on-chain channel open? [y/N] "
|
|
1562
|
+
);
|
|
1563
|
+
if (gate === "json-plan") {
|
|
1564
|
+
io2.emitJson({
|
|
1565
|
+
command: "channel open",
|
|
1566
|
+
identity,
|
|
1567
|
+
executed: false,
|
|
1568
|
+
plan,
|
|
1569
|
+
hint: "plan only \u2014 re-run with --yes to open (on-chain: locks collateral and spends gas)"
|
|
1570
|
+
});
|
|
1571
|
+
return 0;
|
|
1572
|
+
}
|
|
1573
|
+
if (gate !== "proceed") return gate;
|
|
1574
|
+
const outcome = await loaded.money.openChannel(
|
|
1575
|
+
deposit !== void 0 ? { deposit } : void 0
|
|
1576
|
+
);
|
|
1577
|
+
if (json) {
|
|
1578
|
+
io2.emitJson({
|
|
1579
|
+
command: "channel open",
|
|
1580
|
+
identity,
|
|
1581
|
+
executed: true,
|
|
1582
|
+
plan,
|
|
1583
|
+
result: {
|
|
1584
|
+
channelId: outcome.channelId,
|
|
1585
|
+
resumed: outcome.resumed,
|
|
1586
|
+
destination: outcome.destination,
|
|
1587
|
+
chain: outcome.chain ?? null,
|
|
1588
|
+
peerId: outcome.peerId ?? null,
|
|
1589
|
+
depositTotal: outcome.depositTotal ?? null,
|
|
1590
|
+
depositAdded: outcome.depositAdded ?? null,
|
|
1591
|
+
depositTxHash: outcome.depositTxHash ?? null
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
return 0;
|
|
1595
|
+
}
|
|
1596
|
+
io2.out(
|
|
1597
|
+
outcome.resumed ? `Resumed recorded channel ${outcome.channelId} \u2014 no on-chain open was needed.` : `Opened channel ${outcome.channelId}${outcome.chain ? ` on ${outcome.chain}` : ""} and recorded it for reuse.`
|
|
1598
|
+
);
|
|
1599
|
+
io2.out(` peer ${outcome.destination}${outcome.peerId ? ` (${outcome.peerId})` : ""}`);
|
|
1600
|
+
if (outcome.depositAdded) {
|
|
1601
|
+
io2.out(
|
|
1602
|
+
` deposited +${outcome.depositAdded} base units` + (outcome.depositTxHash ? ` (tx ${outcome.depositTxHash})` : "")
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
if (outcome.depositTotal) {
|
|
1606
|
+
io2.out(` collateral ${outcome.depositTotal} base units total on-chain`);
|
|
1607
|
+
}
|
|
1608
|
+
return 0;
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
return emitCliError(io2, json, "channel open", err);
|
|
1611
|
+
} finally {
|
|
1612
|
+
await stopQuietly(ctx);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function withdrawPrecheck(step, channelId, watermark, nowSec) {
|
|
1616
|
+
const status = channelStatus(watermark, nowSec);
|
|
1617
|
+
if (status === "settled") {
|
|
1618
|
+
return `channel ${channelId} is already settled \u2014 nothing left to ${step}.`;
|
|
1619
|
+
}
|
|
1620
|
+
if (step === "close") {
|
|
1621
|
+
if (status === "closing" || status === "settleable") {
|
|
1622
|
+
const at = watermark?.settleableAt;
|
|
1623
|
+
return `channel ${channelId} is already closing \u2014 ` + (status === "settleable" ? "its challenge window has elapsed; run `rig channel settle` to release the collateral." : `settleable at ${at ? formatUnixSeconds(at) : "the end of its challenge window"} (run \`rig channel settle\` then).`);
|
|
1624
|
+
}
|
|
1625
|
+
return void 0;
|
|
1626
|
+
}
|
|
1627
|
+
if (status === "open") {
|
|
1628
|
+
return `channel ${channelId} is not closed \u2014 run \`rig channel close ${channelId}\` first (settle only releases collateral after the challenge window of a closed channel).`;
|
|
1629
|
+
}
|
|
1630
|
+
if (status === "closing" && watermark?.settleableAt !== void 0) {
|
|
1631
|
+
const remain = secondsUntil(watermark.settleableAt, nowSec);
|
|
1632
|
+
return `channel ${channelId} is not settleable yet \u2014 the challenge window is still open (${remain}s remain, settleable at ${formatUnixSeconds(watermark.settleableAt)}). Nothing was spent; re-run after that time.`;
|
|
1633
|
+
}
|
|
1634
|
+
return void 0;
|
|
1635
|
+
}
|
|
1636
|
+
async function runChannelWithdrawStep(args, deps, step) {
|
|
1637
|
+
const { io: io2, env } = deps;
|
|
1638
|
+
const command = `channel ${step}`;
|
|
1639
|
+
let channelId;
|
|
1640
|
+
let yes = false;
|
|
1641
|
+
let json = false;
|
|
1642
|
+
try {
|
|
1643
|
+
const { values, positionals } = parseArgs4({
|
|
1644
|
+
args,
|
|
1645
|
+
options: {
|
|
1646
|
+
yes: { type: "boolean", default: false },
|
|
1647
|
+
json: { type: "boolean", default: false },
|
|
1648
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1649
|
+
},
|
|
1650
|
+
allowPositionals: true
|
|
1651
|
+
});
|
|
1652
|
+
if (values.help) {
|
|
1653
|
+
io2.out(CHANNEL_USAGE);
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1656
|
+
yes = values.yes ?? false;
|
|
1657
|
+
json = values.json ?? false;
|
|
1658
|
+
if (positionals.length !== 1) {
|
|
1659
|
+
throw new Error(
|
|
1660
|
+
`rig channel ${step} takes exactly one <channelId> (got ${positionals.length}) \u2014 \`rig channel list\` shows recorded channels`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
channelId = positionals[0];
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1666
|
+
io2.err(CHANNEL_USAGE);
|
|
1667
|
+
return 2;
|
|
1668
|
+
}
|
|
1669
|
+
let ctx;
|
|
1670
|
+
try {
|
|
1671
|
+
const store = new ChannelMapStore(resolveChannelPaths(env));
|
|
1672
|
+
const record = store.list().find((r) => r.channelId === channelId);
|
|
1673
|
+
if (!record) {
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`no recorded channel ${JSON.stringify(channelId)} \u2014 \`rig channel list\` shows the channels this identity holds`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const watermark = store.readWatermark(channelId);
|
|
1679
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1680
|
+
const refusal = withdrawPrecheck(step, channelId, watermark, nowSec);
|
|
1681
|
+
if (refusal) throw new Error(refusal);
|
|
1682
|
+
const loaded = await loadMoneyContext(deps);
|
|
1683
|
+
ctx = loaded.ctx;
|
|
1684
|
+
const identity = identityReport(ctx);
|
|
1685
|
+
if (record.identity !== identity.pubkey) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`channel ${channelId} was opened by identity ${record.identity.slice(0, 8)}\u2026 but the active identity is ${identity.pubkey.slice(0, 8)}\u2026 (from ${identity.sourceLabel}) \u2014 on-chain ${step} must be signed by the opener's wallet key. Switch RIG_MNEMONIC (or the keystore) to that identity.`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
if (!json) {
|
|
1691
|
+
const claimed = watermark?.cumulativeAmount;
|
|
1692
|
+
io2.out(`Channel ${step} plan:`);
|
|
1693
|
+
io2.out(` channel ${channelId} [${channelStatus(watermark, nowSec)}]`);
|
|
1694
|
+
io2.out(` peer ${record.destination} (${record.peerId})`);
|
|
1695
|
+
io2.out(` chain ${record.chain}`);
|
|
1696
|
+
io2.out(
|
|
1697
|
+
` deposited ${record.depositTotal ?? "unrecorded"}` + (claimed !== void 0 ? ` claimed ${claimed} base units` : "")
|
|
1698
|
+
);
|
|
1699
|
+
io2.out(renderIdentityLine(identity));
|
|
1700
|
+
if (step === "close") {
|
|
1701
|
+
io2.out(
|
|
1702
|
+
"Closing is an ON-CHAIN transaction (gas) that starts the settlement challenge window: the channel stops paying immediately, and the remaining collateral stays locked until `rig channel settle` succeeds after the window elapses."
|
|
1703
|
+
);
|
|
1704
|
+
} else {
|
|
1705
|
+
io2.out(
|
|
1706
|
+
"Settling is an ON-CHAIN transaction (gas) that releases the remaining collateral back to your wallet. The chain's challenge window is authoritative \u2014 a too-early settle is refused before any gas is spent."
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
const gate = await confirmOnChain(
|
|
1711
|
+
io2,
|
|
1712
|
+
{ yes, json },
|
|
1713
|
+
`Proceed with on-chain channel ${step}? [y/N] `
|
|
1714
|
+
);
|
|
1715
|
+
if (gate === "json-plan") {
|
|
1716
|
+
io2.emitJson({
|
|
1717
|
+
command,
|
|
1718
|
+
identity,
|
|
1719
|
+
executed: false,
|
|
1720
|
+
channelId,
|
|
1721
|
+
hint: `plan only \u2014 re-run with --yes to ${step} (on-chain transaction)`
|
|
1722
|
+
});
|
|
1723
|
+
return 0;
|
|
1724
|
+
}
|
|
1725
|
+
if (gate !== "proceed") return gate;
|
|
1726
|
+
if (step === "close") {
|
|
1727
|
+
const result2 = await loaded.money.closeChannel(record);
|
|
1728
|
+
if (json) {
|
|
1729
|
+
io2.emitJson({
|
|
1730
|
+
command,
|
|
1731
|
+
identity,
|
|
1732
|
+
executed: true,
|
|
1733
|
+
channelId,
|
|
1734
|
+
result: {
|
|
1735
|
+
txHash: result2.txHash ?? null,
|
|
1736
|
+
closedAt: result2.closedAt,
|
|
1737
|
+
settleableAt: result2.settleableAt
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
return 0;
|
|
1741
|
+
}
|
|
1742
|
+
io2.out(
|
|
1743
|
+
`Channel ${channelId} is closing` + (result2.txHash ? ` (tx ${result2.txHash})` : "") + "."
|
|
1744
|
+
);
|
|
1745
|
+
io2.out(
|
|
1746
|
+
` challenge window: settleable at ${formatUnixSeconds(result2.settleableAt)} (~${Math.max(0, secondsUntil(result2.settleableAt, nowSec))}s from now)`
|
|
1747
|
+
);
|
|
1748
|
+
io2.out(
|
|
1749
|
+
` run \`rig channel settle ${channelId}\` after that time to release the collateral.`
|
|
1750
|
+
);
|
|
1751
|
+
return 0;
|
|
1752
|
+
}
|
|
1753
|
+
const result = await loaded.money.settleChannel(record);
|
|
1754
|
+
if (json) {
|
|
1755
|
+
io2.emitJson({
|
|
1756
|
+
command,
|
|
1757
|
+
identity,
|
|
1758
|
+
executed: true,
|
|
1759
|
+
channelId,
|
|
1760
|
+
result: { txHash: result.txHash ?? null }
|
|
1761
|
+
});
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
io2.out(
|
|
1765
|
+
`Channel ${channelId} settled` + (result.txHash ? ` (tx ${result.txHash})` : "") + " \u2014 the remaining collateral was released to your wallet."
|
|
1766
|
+
);
|
|
1767
|
+
return 0;
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
return emitCliError(io2, json, command, err);
|
|
1770
|
+
} finally {
|
|
1771
|
+
await stopQuietly(ctx);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function stopQuietly(ctx) {
|
|
1775
|
+
if (!ctx) return;
|
|
1776
|
+
try {
|
|
1777
|
+
await ctx.stop();
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/cli/clone.ts
|
|
1783
|
+
import { mkdtemp, mkdir, readdir, rename, rm, rmdir } from "fs/promises";
|
|
1784
|
+
import { basename, dirname, join, resolve } from "path";
|
|
1785
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1786
|
+
var CLONE_USAGE = `Usage: rig clone <relay-url> <owner>/<repo-id> [dir] [options]
|
|
1787
|
+
|
|
1788
|
+
Clone a TOON repository \u2014 FREE (relay reads + Arweave gateway downloads; no
|
|
1789
|
+
payments, no channel, no identity needed). <relay-url> is a ws:// or wss://
|
|
1790
|
+
NIP-01 relay; <owner> is the repo owner's pubkey as npub1\u2026 or 64-char hex;
|
|
1791
|
+
[dir] defaults to <repo-id>.
|
|
1792
|
+
|
|
1793
|
+
Fetches the kind:30617/30618 repository state, downloads every git object
|
|
1794
|
+
the refs need from Arweave gateways (SHA-1 verified \u2014 corrupt content is
|
|
1795
|
+
rejected), and materializes a real git repository: objects via git plumbing,
|
|
1796
|
+
refs, HEAD, checked-out worktree, toon.* config, and the relay preconfigured
|
|
1797
|
+
as the "origin" remote \u2014 so \`rig fetch\`, \`rig push\`, and the issue/pr
|
|
1798
|
+
commands work immediately.
|
|
1799
|
+
|
|
1800
|
+
Note: recently pushed objects can take 10-20 minutes to become fetchable
|
|
1801
|
+
from Arweave gateways. A clone right after a push may report missing
|
|
1802
|
+
objects \u2014 retry after propagation.
|
|
1803
|
+
|
|
1804
|
+
Options:
|
|
1805
|
+
--concurrency <n> parallel gateway downloads (default 8)
|
|
1806
|
+
--json machine-readable result envelope
|
|
1807
|
+
-h, --help show this help`;
|
|
1808
|
+
var RepoNotFoundError = class extends Error {
|
|
1809
|
+
constructor(relay, owner, repoId) {
|
|
1810
|
+
super(
|
|
1811
|
+
`repository 30617:${owner}:${repoId} not found on ${relay} \u2014 no kind:30617 announcement or kind:30618 state event exists there. Check the relay URL, the owner pubkey (npub or hex), and the repo id.`
|
|
1812
|
+
);
|
|
1813
|
+
this.name = "RepoNotFoundError";
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
var MissingRemoteObjectsError = class extends Error {
|
|
1817
|
+
constructor(missing, context) {
|
|
1818
|
+
super(missingObjectsMessage(missing, context));
|
|
1819
|
+
this.missing = missing;
|
|
1820
|
+
this.name = "MissingRemoteObjectsError";
|
|
1821
|
+
}
|
|
1822
|
+
missing;
|
|
1823
|
+
};
|
|
1824
|
+
var WS_URL_RE = /^wss?:\/\//i;
|
|
1825
|
+
function parseCloneArgs(args) {
|
|
1826
|
+
const { values, positionals } = parseArgs5({
|
|
1827
|
+
args,
|
|
1828
|
+
options: {
|
|
1829
|
+
json: { type: "boolean", default: false },
|
|
1830
|
+
concurrency: { type: "string" },
|
|
1831
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1832
|
+
},
|
|
1833
|
+
allowPositionals: true
|
|
1834
|
+
});
|
|
1835
|
+
if (values.help) return { help: true };
|
|
1836
|
+
if (positionals.length < 2 || positionals.length > 3) {
|
|
1837
|
+
throw new Error("expected: rig clone <relay-url> <owner>/<repo-id> [dir]");
|
|
1838
|
+
}
|
|
1839
|
+
const [relayUrl, addr, dir] = positionals;
|
|
1840
|
+
if (!WS_URL_RE.test(relayUrl)) {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
`<relay-url> must be ws:// or wss:// (got ${JSON.stringify(relayUrl)})`
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const slash = addr.indexOf("/");
|
|
1846
|
+
if (slash <= 0 || slash === addr.length - 1) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`expected <owner>/<repo-id> (npub or 64-char hex owner), got ${JSON.stringify(addr)}`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
const owner = ownerToHex(addr.slice(0, slash));
|
|
1852
|
+
const repoId = addr.slice(slash + 1);
|
|
1853
|
+
let concurrency;
|
|
1854
|
+
if (values.concurrency !== void 0) {
|
|
1855
|
+
concurrency = Number.parseInt(values.concurrency, 10);
|
|
1856
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
1857
|
+
throw new Error(`--concurrency must be a positive integer`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
relayUrl,
|
|
1862
|
+
owner,
|
|
1863
|
+
repoId,
|
|
1864
|
+
dir,
|
|
1865
|
+
concurrency,
|
|
1866
|
+
json: values.json === true
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
async function isUsableDestination(dir) {
|
|
1870
|
+
try {
|
|
1871
|
+
const entries = await readdir(dir);
|
|
1872
|
+
return entries.length === 0;
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
return err.code === "ENOENT";
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function initialBranch(headSymref) {
|
|
1878
|
+
if (headSymref?.startsWith("refs/heads/")) {
|
|
1879
|
+
const name = headSymref.slice("refs/heads/".length);
|
|
1880
|
+
if (name && isSafeRefname(headSymref)) return name;
|
|
1881
|
+
}
|
|
1882
|
+
return "main";
|
|
1883
|
+
}
|
|
1884
|
+
async function runClone(args, deps) {
|
|
1885
|
+
const { io: io2 } = deps;
|
|
1886
|
+
let parsed;
|
|
1887
|
+
try {
|
|
1888
|
+
const result = parseCloneArgs(args);
|
|
1889
|
+
if ("help" in result) {
|
|
1890
|
+
io2.out(CLONE_USAGE);
|
|
1891
|
+
return 0;
|
|
1892
|
+
}
|
|
1893
|
+
parsed = result;
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1896
|
+
io2.err(CLONE_USAGE);
|
|
1897
|
+
return 2;
|
|
1898
|
+
}
|
|
1899
|
+
const { relayUrl, owner, repoId, json } = parsed;
|
|
1900
|
+
const dest = resolve(deps.cwd, parsed.dir ?? repoId);
|
|
1901
|
+
let tempDir;
|
|
1902
|
+
try {
|
|
1903
|
+
if (!await isUsableDestination(dest)) {
|
|
1904
|
+
throw new Error(
|
|
1905
|
+
`destination path ${JSON.stringify(dest)} already exists and is not an empty directory`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
if (!json) io2.out(`Cloning into '${basename(dest)}'...`);
|
|
1909
|
+
const remoteState = await fetchRemoteState({
|
|
1910
|
+
relayUrls: [relayUrl],
|
|
1911
|
+
ownerPubkey: owner,
|
|
1912
|
+
repoId,
|
|
1913
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
|
|
1914
|
+
...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
|
|
1915
|
+
});
|
|
1916
|
+
if (!remoteState.announced && remoteState.refsEvent === null) {
|
|
1917
|
+
throw new RepoNotFoundError(relayUrl, owner, repoId);
|
|
1918
|
+
}
|
|
1919
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1920
|
+
for (const [refname, sha] of remoteState.refs) {
|
|
1921
|
+
if (!isSafeRefname(refname)) {
|
|
1922
|
+
io2.err(
|
|
1923
|
+
`warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
|
|
1924
|
+
);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
refs.set(refname, sha);
|
|
1928
|
+
}
|
|
1929
|
+
if (refs.size === 0) {
|
|
1930
|
+
io2.err(
|
|
1931
|
+
"warning: the repository has no refs yet (announced but never pushed) \u2014 cloning an empty repository"
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
const tips = [...new Set(refs.values())];
|
|
1935
|
+
const collected = await collectRepoObjects({
|
|
1936
|
+
tips,
|
|
1937
|
+
shaToTxId: remoteState.shaToTxId,
|
|
1938
|
+
resolveMissing: (shas) => remoteState.resolveMissing(shas),
|
|
1939
|
+
...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
|
|
1940
|
+
...parsed.concurrency !== void 0 ? { concurrency: parsed.concurrency } : {}
|
|
1941
|
+
});
|
|
1942
|
+
if (collected.missing.length > 0) {
|
|
1943
|
+
throw new MissingRemoteObjectsError(
|
|
1944
|
+
collected.missing,
|
|
1945
|
+
`cannot clone ${owner.slice(0, 8)}\u2026/${repoId}`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
for (const { sha, txId } of collected.skippedUnavailable) {
|
|
1949
|
+
io2.err(
|
|
1950
|
+
`warning: unreachable object ${sha} (tx ${txId}) could not be downloaded \u2014 not needed by any ref, continuing`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
const parent = dirname(dest);
|
|
1954
|
+
await mkdir(parent, { recursive: true });
|
|
1955
|
+
tempDir = await mkdtemp(join(parent, `.${basename(dest)}.rig-clone-`));
|
|
1956
|
+
await runGit(tempDir, [
|
|
1957
|
+
"init",
|
|
1958
|
+
"--quiet",
|
|
1959
|
+
`--initial-branch=${initialBranch(remoteState.headSymref)}`
|
|
1960
|
+
]);
|
|
1961
|
+
const written = await writeGitObjects(tempDir, collected.objects.values());
|
|
1962
|
+
for (const [refname, sha] of refs) {
|
|
1963
|
+
await updateRef(tempDir, refname, sha);
|
|
1964
|
+
if (refname.startsWith("refs/heads/")) {
|
|
1965
|
+
const branch = refname.slice("refs/heads/".length);
|
|
1966
|
+
await updateRef(tempDir, `refs/remotes/origin/${branch}`, sha);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const head = remoteState.headSymref !== null && isSafeRefname(remoteState.headSymref) && refs.has(remoteState.headSymref) ? remoteState.headSymref : [...refs.keys()].find((r) => r.startsWith("refs/heads/")) ?? null;
|
|
1970
|
+
if (head !== null) {
|
|
1971
|
+
await setHeadSymref(tempDir, head);
|
|
1972
|
+
await runGit(tempDir, ["reset", "--hard", "--quiet"]);
|
|
1973
|
+
}
|
|
1974
|
+
await runGit(tempDir, ["config", "toon.repoid", repoId]);
|
|
1975
|
+
await runGit(tempDir, ["config", "toon.owner", owner]);
|
|
1976
|
+
await runGit(tempDir, ["remote", "add", "origin", relayUrl]);
|
|
1977
|
+
if (head !== null) {
|
|
1978
|
+
const branch = head.slice("refs/heads/".length);
|
|
1979
|
+
await runGit(tempDir, ["config", `branch.${branch}.remote`, "origin"]);
|
|
1980
|
+
await runGit(tempDir, ["config", `branch.${branch}.merge`, head]);
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
await rmdir(dest);
|
|
1984
|
+
} catch {
|
|
1985
|
+
}
|
|
1986
|
+
await rename(tempDir, dest);
|
|
1987
|
+
tempDir = void 0;
|
|
1988
|
+
if (json) {
|
|
1989
|
+
io2.emitJson({
|
|
1990
|
+
command: "clone",
|
|
1991
|
+
repoAddr: { ownerPubkey: owner, repoId },
|
|
1992
|
+
relay: relayUrl,
|
|
1993
|
+
directory: dest,
|
|
1994
|
+
head,
|
|
1995
|
+
refs: Object.fromEntries(refs),
|
|
1996
|
+
name: remoteState.name,
|
|
1997
|
+
objectsDownloaded: written,
|
|
1998
|
+
executed: true
|
|
1999
|
+
});
|
|
2000
|
+
} else {
|
|
2001
|
+
io2.out(`Downloaded ${written} object(s) from Arweave (SHA-1 verified).`);
|
|
2002
|
+
for (const [refname, sha] of refs) {
|
|
2003
|
+
io2.out(` ${sha.slice(0, 7)} ${refname}`);
|
|
2004
|
+
}
|
|
2005
|
+
if (head !== null) io2.out(`HEAD is now at ${head}`);
|
|
2006
|
+
io2.out(
|
|
2007
|
+
`Configured toon.repoid=${repoId}, toon.owner=${owner.slice(0, 8)}\u2026, origin \u2192 ${relayUrl}`
|
|
2008
|
+
);
|
|
2009
|
+
io2.out("Done.");
|
|
2010
|
+
}
|
|
2011
|
+
return 0;
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
return emitCliError(io2, json, "clone", err);
|
|
2014
|
+
} finally {
|
|
2015
|
+
if (tempDir !== void 0) {
|
|
2016
|
+
await rm(tempDir, { recursive: true, force: true }).catch(
|
|
2017
|
+
() => void 0
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/cli/events.ts
|
|
2024
|
+
import { readFile } from "fs/promises";
|
|
2025
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
2026
|
+
import {
|
|
2027
|
+
REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2,
|
|
2028
|
+
STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2,
|
|
2029
|
+
STATUS_CLOSED_KIND as STATUS_CLOSED_KIND2,
|
|
2030
|
+
STATUS_DRAFT_KIND as STATUS_DRAFT_KIND2,
|
|
2031
|
+
STATUS_OPEN_KIND as STATUS_OPEN_KIND2
|
|
2032
|
+
} from "@toon-protocol/core/nip34";
|
|
2033
|
+
|
|
2034
|
+
// src/cli/tracker.ts
|
|
2035
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
2036
|
+
import {
|
|
2037
|
+
ISSUE_KIND,
|
|
2038
|
+
PATCH_KIND,
|
|
2039
|
+
REPOSITORY_ANNOUNCEMENT_KIND,
|
|
2040
|
+
STATUS_APPLIED_KIND,
|
|
2041
|
+
STATUS_CLOSED_KIND,
|
|
2042
|
+
STATUS_DRAFT_KIND,
|
|
2043
|
+
STATUS_OPEN_KIND
|
|
2044
|
+
} from "@toon-protocol/core/nip34";
|
|
2045
|
+
var READ_COMMON_FLAGS = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
2046
|
+
--owner <pubkey> repository owner (npub or 64-char hex; default: git config)
|
|
2047
|
+
--remote <name> read via this configured git remote (default: origin)
|
|
2048
|
+
--relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
|
|
2049
|
+
--json machine-readable envelope
|
|
2050
|
+
-h, --help show this help`;
|
|
2051
|
+
var ISSUE_LIST_USAGE = `Usage: rig issue list [--state open|closed|all] [options]
|
|
2052
|
+
|
|
2053
|
+
List the repo's issues (kind:1621) with their derived state \u2014 FREE (relay
|
|
2054
|
+
reads only). State comes from kind:1630-1633 status events: the latest status
|
|
2055
|
+
wins; an issue with no status events is open.
|
|
2056
|
+
|
|
2057
|
+
Options:
|
|
2058
|
+
--state <state> open | closed | all (default: all)
|
|
2059
|
+
${READ_COMMON_FLAGS}`;
|
|
2060
|
+
var ISSUE_SHOW_USAGE = `Usage: rig issue show <event-id> [options]
|
|
2061
|
+
|
|
2062
|
+
Show one issue (kind:1621): metadata, derived state, body, and its
|
|
2063
|
+
kind:1622 comments \u2014 FREE (relay reads only). <event-id> is the 64-char hex
|
|
2064
|
+
id \`rig issue create\` printed (also visible in \`rig issue list\`).
|
|
2065
|
+
|
|
2066
|
+
Options:
|
|
2067
|
+
${READ_COMMON_FLAGS}`;
|
|
2068
|
+
var PR_LIST_USAGE = `Usage: rig pr list [--state open|applied|closed|draft|all] [options]
|
|
2069
|
+
|
|
2070
|
+
List the repo's patches/PRs (kind:1617) with their derived state \u2014 FREE
|
|
2071
|
+
(relay reads only). State comes from kind:1630-1633 status events: the
|
|
2072
|
+
latest status wins; a patch with no status events is open.
|
|
2073
|
+
|
|
2074
|
+
Options:
|
|
2075
|
+
--state <state> open | applied | closed | draft | all (default: all)
|
|
2076
|
+
${READ_COMMON_FLAGS}`;
|
|
2077
|
+
var PR_SHOW_USAGE = `Usage: rig pr show <event-id> [options]
|
|
2078
|
+
|
|
2079
|
+
Show one patch/PR (kind:1617): metadata, derived state, the FULL patch text
|
|
2080
|
+
(real \`git format-patch\` output \u2014 pipe it to \`git am\` to apply), and its
|
|
2081
|
+
kind:1622 comments \u2014 FREE (relay reads only).
|
|
2082
|
+
|
|
2083
|
+
Options:
|
|
2084
|
+
${READ_COMMON_FLAGS}`;
|
|
2085
|
+
var STATUS_BY_KIND = {
|
|
2086
|
+
[STATUS_OPEN_KIND]: "open",
|
|
2087
|
+
[STATUS_APPLIED_KIND]: "applied",
|
|
2088
|
+
[STATUS_CLOSED_KIND]: "closed",
|
|
2089
|
+
[STATUS_DRAFT_KIND]: "draft"
|
|
2090
|
+
};
|
|
2091
|
+
var STATUS_KINDS = [
|
|
2092
|
+
STATUS_OPEN_KIND,
|
|
2093
|
+
STATUS_APPLIED_KIND,
|
|
2094
|
+
STATUS_CLOSED_KIND,
|
|
2095
|
+
STATUS_DRAFT_KIND
|
|
2096
|
+
];
|
|
2097
|
+
function tagValue(tags, name) {
|
|
2098
|
+
return tags.find((t) => t[0] === name)?.[1];
|
|
2099
|
+
}
|
|
2100
|
+
function tagValues(tags, name) {
|
|
2101
|
+
return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
|
|
2102
|
+
}
|
|
2103
|
+
function deriveStatus(targetEventId, statusEvents) {
|
|
2104
|
+
let winner = null;
|
|
2105
|
+
for (const event of statusEvents) {
|
|
2106
|
+
if (STATUS_BY_KIND[event.kind] === void 0) continue;
|
|
2107
|
+
if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
|
|
2108
|
+
continue;
|
|
2109
|
+
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
2110
|
+
winner = event;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return winner === null ? "open" : STATUS_BY_KIND[winner.kind];
|
|
2114
|
+
}
|
|
2115
|
+
var RELAY_TIMEOUT_MS = 1e4;
|
|
2116
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
2117
|
+
var WS_URL_RE2 = /^wss?:\/\//i;
|
|
2118
|
+
function defaultWebSocketFactory(url) {
|
|
2119
|
+
const ctor = globalThis.WebSocket;
|
|
2120
|
+
if (!ctor) {
|
|
2121
|
+
throw new Error(
|
|
2122
|
+
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
return new ctor(url);
|
|
2126
|
+
}
|
|
2127
|
+
async function queryAll(relays, filters, webSocketFactory) {
|
|
2128
|
+
const jobs = relays.flatMap(
|
|
2129
|
+
(relay) => filters.map(
|
|
2130
|
+
(filter) => queryRelay(relay, filter, RELAY_TIMEOUT_MS, webSocketFactory)
|
|
2131
|
+
)
|
|
2132
|
+
);
|
|
2133
|
+
const results = await Promise.allSettled(jobs);
|
|
2134
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2135
|
+
let failures = 0;
|
|
2136
|
+
let firstError;
|
|
2137
|
+
for (const result of results) {
|
|
2138
|
+
if (result.status === "rejected") {
|
|
2139
|
+
failures += 1;
|
|
2140
|
+
firstError ??= result.reason;
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
for (const event of result.value) {
|
|
2144
|
+
if (typeof event.id === "string" && !byId.has(event.id)) {
|
|
2145
|
+
byId.set(event.id, event);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (failures === results.length && results.length > 0) {
|
|
2150
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
2151
|
+
}
|
|
2152
|
+
return byId;
|
|
2153
|
+
}
|
|
2154
|
+
var TRACKER_OPTIONS = {
|
|
2155
|
+
json: { type: "boolean", default: false },
|
|
2156
|
+
state: { type: "string" },
|
|
2157
|
+
relay: { type: "string", multiple: true },
|
|
2158
|
+
remote: { type: "string" },
|
|
2159
|
+
"repo-id": { type: "string" },
|
|
2160
|
+
owner: { type: "string" },
|
|
2161
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2162
|
+
};
|
|
2163
|
+
function pickTrackerFlags(values) {
|
|
2164
|
+
const flags = {
|
|
2165
|
+
json: values["json"] === true,
|
|
2166
|
+
help: values["help"] === true,
|
|
2167
|
+
relay: Array.isArray(values["relay"]) ? values["relay"] : []
|
|
2168
|
+
};
|
|
2169
|
+
if (typeof values["state"] === "string") flags.state = values["state"];
|
|
2170
|
+
if (typeof values["remote"] === "string") flags.remote = values["remote"];
|
|
2171
|
+
if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
|
|
2172
|
+
if (typeof values["owner"] === "string")
|
|
2173
|
+
flags.owner = ownerToHex(values["owner"]);
|
|
2174
|
+
return flags;
|
|
2175
|
+
}
|
|
2176
|
+
async function resolveTrackerContext(flags, deps, needRepoAddr) {
|
|
2177
|
+
let repoRoot;
|
|
2178
|
+
let toonConfig = {
|
|
2179
|
+
relays: []
|
|
2180
|
+
};
|
|
2181
|
+
try {
|
|
2182
|
+
repoRoot = await resolveRepoRoot(deps.cwd);
|
|
2183
|
+
toonConfig = await readToonConfig(repoRoot);
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
let repoAddr = null;
|
|
2187
|
+
if (needRepoAddr) {
|
|
2188
|
+
const repoId = flags.repoId ?? toonConfig.repoId;
|
|
2189
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
2190
|
+
const owner = flags.owner ?? toonConfig.owner;
|
|
2191
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
2192
|
+
repoAddr = { ownerPubkey: owner, repoId };
|
|
2193
|
+
}
|
|
2194
|
+
const resolved = await resolveRelays({
|
|
2195
|
+
relayFlags: flags.relay,
|
|
2196
|
+
remoteName: flags.remote,
|
|
2197
|
+
repoRoot,
|
|
2198
|
+
toonRelays: toonConfig.relays
|
|
2199
|
+
});
|
|
2200
|
+
const wsRelays = resolved.relays.filter((url) => WS_URL_RE2.test(url));
|
|
2201
|
+
if (wsRelays.length === 0) {
|
|
2202
|
+
throw new InvalidRelayUrlError(
|
|
2203
|
+
resolved.relays[0] ?? "",
|
|
2204
|
+
"reads need a ws:// or wss:// relay"
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
relays: wsRelays,
|
|
2209
|
+
repoAddr,
|
|
2210
|
+
webSocketFactory: deps.webSocketFactory ?? defaultWebSocketFactory
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
function repoATag(addr) {
|
|
2214
|
+
return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
|
|
2215
|
+
}
|
|
2216
|
+
function parseTrackerItem(event, status) {
|
|
2217
|
+
const item = {
|
|
2218
|
+
eventId: event.id,
|
|
2219
|
+
kind: event.kind,
|
|
2220
|
+
title: tagValue(event.tags, "subject") ?? (event.content.split("\n")[0] ?? "").slice(0, 120),
|
|
2221
|
+
status,
|
|
2222
|
+
authorPubkey: event.pubkey,
|
|
2223
|
+
createdAt: event.created_at,
|
|
2224
|
+
labels: tagValues(event.tags, "t"),
|
|
2225
|
+
content: event.content
|
|
2226
|
+
};
|
|
2227
|
+
if (event.kind === PATCH_KIND) {
|
|
2228
|
+
item.commitShas = tagValues(event.tags, "commit");
|
|
2229
|
+
const branch = tagValue(event.tags, "branch");
|
|
2230
|
+
if (branch !== void 0) item.branch = branch;
|
|
2231
|
+
const description = tagValue(event.tags, "description");
|
|
2232
|
+
if (description !== void 0) item.description = description;
|
|
2233
|
+
}
|
|
2234
|
+
return item;
|
|
2235
|
+
}
|
|
2236
|
+
function isoDate(createdAt) {
|
|
2237
|
+
return new Date(createdAt * 1e3).toISOString().slice(0, 10);
|
|
2238
|
+
}
|
|
2239
|
+
async function fetchItems(ctx, kind) {
|
|
2240
|
+
const aTag = repoATag(
|
|
2241
|
+
ctx.repoAddr
|
|
2242
|
+
);
|
|
2243
|
+
const [itemEvents, aStatusEvents] = await Promise.all([
|
|
2244
|
+
queryAll(
|
|
2245
|
+
ctx.relays,
|
|
2246
|
+
[{ kinds: [kind], "#a": [aTag] }],
|
|
2247
|
+
ctx.webSocketFactory
|
|
2248
|
+
),
|
|
2249
|
+
queryAll(
|
|
2250
|
+
ctx.relays,
|
|
2251
|
+
[{ kinds: STATUS_KINDS, "#a": [aTag] }],
|
|
2252
|
+
ctx.webSocketFactory
|
|
2253
|
+
)
|
|
2254
|
+
]);
|
|
2255
|
+
const items = [...itemEvents.values()].filter(
|
|
2256
|
+
(e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag)
|
|
2257
|
+
);
|
|
2258
|
+
const statuses = new Map(aStatusEvents);
|
|
2259
|
+
if (items.length > 0) {
|
|
2260
|
+
const byE = await queryAll(
|
|
2261
|
+
ctx.relays,
|
|
2262
|
+
[{ kinds: STATUS_KINDS, "#e": items.map((i) => i.id) }],
|
|
2263
|
+
ctx.webSocketFactory
|
|
2264
|
+
);
|
|
2265
|
+
for (const [id, event] of byE)
|
|
2266
|
+
if (!statuses.has(id)) statuses.set(id, event);
|
|
2267
|
+
}
|
|
2268
|
+
return items.map(
|
|
2269
|
+
(event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values()))
|
|
2270
|
+
).sort((a, b) => b.createdAt - a.createdAt);
|
|
2271
|
+
}
|
|
2272
|
+
async function fetchItem(ctx, eventId, expectedKind) {
|
|
2273
|
+
const found = await queryAll(
|
|
2274
|
+
ctx.relays,
|
|
2275
|
+
[{ ids: [eventId] }],
|
|
2276
|
+
ctx.webSocketFactory
|
|
2277
|
+
);
|
|
2278
|
+
const event = found.get(eventId);
|
|
2279
|
+
if (!event) {
|
|
2280
|
+
throw new Error(`event ${eventId} not found on ${ctx.relays.join(", ")}`);
|
|
2281
|
+
}
|
|
2282
|
+
if (event.kind !== expectedKind) {
|
|
2283
|
+
const hint = event.kind === PATCH_KIND ? " (it is a kind:1617 patch \u2014 use `rig pr show`)" : event.kind === ISSUE_KIND ? " (it is a kind:1621 issue \u2014 use `rig issue show`)" : "";
|
|
2284
|
+
throw new Error(
|
|
2285
|
+
`event ${eventId} has kind ${event.kind}, expected ${expectedKind}${hint}`
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
const [statusEvents, commentEvents] = await Promise.all([
|
|
2289
|
+
queryAll(
|
|
2290
|
+
ctx.relays,
|
|
2291
|
+
[{ kinds: STATUS_KINDS, "#e": [eventId] }],
|
|
2292
|
+
ctx.webSocketFactory
|
|
2293
|
+
),
|
|
2294
|
+
queryAll(
|
|
2295
|
+
ctx.relays,
|
|
2296
|
+
[{ kinds: [COMMENT_KIND], "#e": [eventId] }],
|
|
2297
|
+
ctx.webSocketFactory
|
|
2298
|
+
)
|
|
2299
|
+
]);
|
|
2300
|
+
const comments = [...commentEvents.values()].filter(
|
|
2301
|
+
(e) => e.kind === COMMENT_KIND && e.tags.some((t) => t[0] === "e" && t[1] === eventId)
|
|
2302
|
+
).sort((a, b) => a.created_at - b.created_at).map((e) => ({
|
|
2303
|
+
eventId: e.id,
|
|
2304
|
+
authorPubkey: e.pubkey,
|
|
2305
|
+
createdAt: e.created_at,
|
|
2306
|
+
content: e.content
|
|
2307
|
+
}));
|
|
2308
|
+
return {
|
|
2309
|
+
item: parseTrackerItem(event, deriveStatus(eventId, statusEvents.values())),
|
|
2310
|
+
comments,
|
|
2311
|
+
repoATag: tagValue(event.tags, "a") ?? null
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
async function runList(args, deps, spec) {
|
|
2315
|
+
const { io: io2 } = deps;
|
|
2316
|
+
let flags;
|
|
2317
|
+
try {
|
|
2318
|
+
const { values, positionals } = parseArgs6({
|
|
2319
|
+
args,
|
|
2320
|
+
options: TRACKER_OPTIONS,
|
|
2321
|
+
allowPositionals: true
|
|
2322
|
+
});
|
|
2323
|
+
if (positionals.length > 0) {
|
|
2324
|
+
throw new Error(`rig ${spec.command} takes no positional arguments`);
|
|
2325
|
+
}
|
|
2326
|
+
flags = pickTrackerFlags(values);
|
|
2327
|
+
if (flags.state !== void 0 && flags.state !== "all" && !spec.states.includes(flags.state)) {
|
|
2328
|
+
throw new Error(
|
|
2329
|
+
`--state must be one of ${[...spec.states, "all"].join(" | ")} (got ${JSON.stringify(flags.state)})`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2334
|
+
io2.err(spec.usage);
|
|
2335
|
+
return 2;
|
|
2336
|
+
}
|
|
2337
|
+
if (flags.help) {
|
|
2338
|
+
io2.out(spec.usage);
|
|
2339
|
+
return 0;
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
const ctx = await resolveTrackerContext(flags, deps, true);
|
|
2343
|
+
const all = await fetchItems(ctx, spec.kind);
|
|
2344
|
+
const state = flags.state ?? "all";
|
|
2345
|
+
const items = state === "all" ? all : all.filter((i) => i.status === state);
|
|
2346
|
+
if (flags.json) {
|
|
2347
|
+
io2.emitJson({
|
|
2348
|
+
command: spec.command,
|
|
2349
|
+
repoAddr: ctx.repoAddr,
|
|
2350
|
+
relays: ctx.relays,
|
|
2351
|
+
state,
|
|
2352
|
+
count: items.length,
|
|
2353
|
+
[spec.jsonKey]: items
|
|
2354
|
+
});
|
|
2355
|
+
return 0;
|
|
2356
|
+
}
|
|
2357
|
+
if (items.length === 0) {
|
|
2358
|
+
io2.out(
|
|
2359
|
+
`no ${state === "all" ? "" : `${state} `}${spec.jsonKey} found for ${repoATag(ctx.repoAddr)}`
|
|
2360
|
+
);
|
|
2361
|
+
return 0;
|
|
2362
|
+
}
|
|
2363
|
+
for (const item of items) {
|
|
2364
|
+
const labels = item.labels.length > 0 ? ` [${item.labels.join(", ")}]` : "";
|
|
2365
|
+
io2.out(
|
|
2366
|
+
`${item.status.padEnd(7)} ${item.eventId.slice(0, 8)} ${item.title} (${item.authorPubkey.slice(0, 8)}, ${isoDate(item.createdAt)})${labels}`
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
io2.out(
|
|
2370
|
+
`${items.length} ${spec.jsonKey}${state === "all" ? "" : ` (${state})`}`
|
|
2371
|
+
);
|
|
2372
|
+
return 0;
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
return emitCliError(io2, flags.json, spec.command, err);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async function runShow(args, deps, spec) {
|
|
2378
|
+
const { io: io2 } = deps;
|
|
2379
|
+
let flags;
|
|
2380
|
+
let eventId;
|
|
2381
|
+
try {
|
|
2382
|
+
const { values, positionals } = parseArgs6({
|
|
2383
|
+
args,
|
|
2384
|
+
options: TRACKER_OPTIONS,
|
|
2385
|
+
allowPositionals: true
|
|
2386
|
+
});
|
|
2387
|
+
flags = pickTrackerFlags(values);
|
|
2388
|
+
if (flags.help) {
|
|
2389
|
+
io2.out(spec.usage);
|
|
2390
|
+
return 0;
|
|
2391
|
+
}
|
|
2392
|
+
if (positionals.length !== 1) {
|
|
2393
|
+
throw new Error("expected exactly one <event-id>");
|
|
2394
|
+
}
|
|
2395
|
+
eventId = positionals[0];
|
|
2396
|
+
if (!HEX64_RE.test(eventId)) {
|
|
2397
|
+
throw new Error(
|
|
2398
|
+
`<event-id> must be a 64-char lowercase hex id (got ${JSON.stringify(eventId)})`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2403
|
+
io2.err(spec.usage);
|
|
2404
|
+
return 2;
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
const ctx = await resolveTrackerContext(flags, deps, false);
|
|
2408
|
+
const shown = await fetchItem(ctx, eventId, spec.kind);
|
|
2409
|
+
if (flags.json) {
|
|
2410
|
+
io2.emitJson({
|
|
2411
|
+
command: spec.command,
|
|
2412
|
+
relays: ctx.relays,
|
|
2413
|
+
repoATag: shown.repoATag,
|
|
2414
|
+
[spec.jsonKey]: shown.item,
|
|
2415
|
+
comments: shown.comments
|
|
2416
|
+
});
|
|
2417
|
+
return 0;
|
|
2418
|
+
}
|
|
2419
|
+
const { item } = shown;
|
|
2420
|
+
io2.out(`${spec.jsonKey} ${item.eventId}`);
|
|
2421
|
+
io2.out(`Title: ${item.title}`);
|
|
2422
|
+
io2.out(`Status: ${item.status}`);
|
|
2423
|
+
io2.out(`Author: ${item.authorPubkey}`);
|
|
2424
|
+
io2.out(`Date: ${isoDate(item.createdAt)}`);
|
|
2425
|
+
if (item.labels.length > 0) io2.out(`Labels: ${item.labels.join(", ")}`);
|
|
2426
|
+
if (item.branch !== void 0) io2.out(`Branch: ${item.branch}`);
|
|
2427
|
+
if (item.commitShas && item.commitShas.length > 0) {
|
|
2428
|
+
io2.out(`Commits: ${item.commitShas.join(", ")}`);
|
|
2429
|
+
}
|
|
2430
|
+
if (shown.repoATag !== null) io2.out(`Repo: ${shown.repoATag}`);
|
|
2431
|
+
if (item.description !== void 0) {
|
|
2432
|
+
io2.out("");
|
|
2433
|
+
io2.out("Body:");
|
|
2434
|
+
for (const line of item.description.split("\n")) io2.out(line);
|
|
2435
|
+
}
|
|
2436
|
+
io2.out("");
|
|
2437
|
+
io2.out(`${spec.bodyLabel}:`);
|
|
2438
|
+
for (const line of item.content.split("\n")) io2.out(line);
|
|
2439
|
+
io2.out("");
|
|
2440
|
+
io2.out(`Comments (${shown.comments.length}):`);
|
|
2441
|
+
for (const comment of shown.comments) {
|
|
2442
|
+
io2.out(
|
|
2443
|
+
`--- ${comment.eventId.slice(0, 8)} by ${comment.authorPubkey.slice(0, 8)} on ${isoDate(comment.createdAt)}`
|
|
2444
|
+
);
|
|
2445
|
+
for (const line of comment.content.split("\n")) io2.out(line);
|
|
2446
|
+
}
|
|
2447
|
+
return 0;
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
return emitCliError(io2, flags.json, spec.command, err);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
var ISSUE_STATES = ["open", "closed", "applied", "draft"];
|
|
2453
|
+
var PR_STATES = ["open", "applied", "closed", "draft"];
|
|
2454
|
+
function runIssueList(args, deps) {
|
|
2455
|
+
return runList(args, deps, {
|
|
2456
|
+
command: "issue list",
|
|
2457
|
+
kind: ISSUE_KIND,
|
|
2458
|
+
usage: ISSUE_LIST_USAGE,
|
|
2459
|
+
states: ISSUE_STATES,
|
|
2460
|
+
jsonKey: "issues"
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
function runIssueShow(args, deps) {
|
|
2464
|
+
return runShow(args, deps, {
|
|
2465
|
+
command: "issue show",
|
|
2466
|
+
kind: ISSUE_KIND,
|
|
2467
|
+
usage: ISSUE_SHOW_USAGE,
|
|
2468
|
+
jsonKey: "issue",
|
|
2469
|
+
bodyLabel: "Body"
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function runPrList(args, deps) {
|
|
2473
|
+
return runList(args, deps, {
|
|
2474
|
+
command: "pr list",
|
|
2475
|
+
kind: PATCH_KIND,
|
|
2476
|
+
usage: PR_LIST_USAGE,
|
|
2477
|
+
states: PR_STATES,
|
|
2478
|
+
jsonKey: "prs"
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
function runPrShow(args, deps) {
|
|
2482
|
+
return runShow(args, deps, {
|
|
2483
|
+
command: "pr show",
|
|
2484
|
+
kind: PATCH_KIND,
|
|
2485
|
+
usage: PR_SHOW_USAGE,
|
|
2486
|
+
jsonKey: "pr",
|
|
2487
|
+
bodyLabel: "Patch"
|
|
2488
|
+
});
|
|
900
2489
|
}
|
|
901
2490
|
|
|
902
2491
|
// src/cli/events.ts
|
|
@@ -932,7 +2521,11 @@ Options:
|
|
|
932
2521
|
--body <text> issue body (Markdown)
|
|
933
2522
|
--body-file <path> read the issue body from a file
|
|
934
2523
|
--label <label> label (t tag); repeatable
|
|
935
|
-
${COMMON_FLAGS_USAGE}
|
|
2524
|
+
${COMMON_FLAGS_USAGE}
|
|
2525
|
+
|
|
2526
|
+
Related (FREE reads \u2014 no payment):
|
|
2527
|
+
rig issue list [--state open|closed|all] list the repo's issues
|
|
2528
|
+
rig issue show <event-id> one issue + its comments`;
|
|
936
2529
|
var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
|
|
937
2530
|
|
|
938
2531
|
Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
|
|
@@ -956,11 +2549,17 @@ verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
|
|
|
956
2549
|
full series text \u2014 cover-letter threading (one event per commit) is out of
|
|
957
2550
|
scope in v1.
|
|
958
2551
|
|
|
2552
|
+
The PR body (--body/--body-file) is carried in a dedicated \`description\`
|
|
2553
|
+
tag, not in the event content \u2014 the content stays pure format-patch output so
|
|
2554
|
+
\`rig pr show \u2026 | git am\` keeps working.
|
|
2555
|
+
|
|
959
2556
|
Options:
|
|
960
2557
|
--title <title> patch/PR title (subject tag) [required]
|
|
961
2558
|
--range <range> revision range for format-patch: <rev>, <rev>..<rev>,
|
|
962
2559
|
or <rev>...<rev> (mutually exclusive with --patch-file)
|
|
963
2560
|
--patch-file <path> literal patch text to publish
|
|
2561
|
+
--body <text> PR description (Markdown; description tag)
|
|
2562
|
+
--body-file <path> read the PR description from a file
|
|
964
2563
|
--branch <name> branch name (t tag)
|
|
965
2564
|
${COMMON_FLAGS_USAGE}`;
|
|
966
2565
|
var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
|
|
@@ -976,10 +2575,14 @@ Options:
|
|
|
976
2575
|
${COMMON_FLAGS_USAGE}`;
|
|
977
2576
|
var PR_USAGE = `${PR_CREATE_USAGE}
|
|
978
2577
|
|
|
979
|
-
${PR_STATUS_USAGE}
|
|
980
|
-
|
|
2578
|
+
${PR_STATUS_USAGE}
|
|
2579
|
+
|
|
2580
|
+
${PR_LIST_USAGE}
|
|
2581
|
+
|
|
2582
|
+
${PR_SHOW_USAGE}`;
|
|
2583
|
+
var HEX64_RE2 = /^[0-9a-f]{64}$/;
|
|
981
2584
|
function assertHex64(value, what) {
|
|
982
|
-
if (!
|
|
2585
|
+
if (!HEX64_RE2.test(value)) {
|
|
983
2586
|
throw new Error(
|
|
984
2587
|
`${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
|
|
985
2588
|
);
|
|
@@ -1014,7 +2617,7 @@ function pickCommon(values) {
|
|
|
1014
2617
|
}
|
|
1015
2618
|
async function runEvent(opts) {
|
|
1016
2619
|
const { command, flags, deps, actionLabel } = opts;
|
|
1017
|
-
const { io
|
|
2620
|
+
const { io: io2 } = deps;
|
|
1018
2621
|
let standaloneCtx;
|
|
1019
2622
|
try {
|
|
1020
2623
|
let repoRoot;
|
|
@@ -1032,83 +2635,99 @@ async function runEvent(opts) {
|
|
|
1032
2635
|
repoRoot,
|
|
1033
2636
|
toonRelays: toonConfig.relays
|
|
1034
2637
|
});
|
|
1035
|
-
if (resolved.nudge !== void 0)
|
|
2638
|
+
if (resolved.nudge !== void 0) io2.err(resolved.nudge);
|
|
1036
2639
|
const relaysUsed = resolved.relays;
|
|
1037
2640
|
if (relaysUsed.length > 1) {
|
|
1038
|
-
|
|
2641
|
+
io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
|
|
1039
2642
|
return 1;
|
|
1040
2643
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
2644
|
+
const session = await loadPaidSession(deps, relaysUsed[0]);
|
|
2645
|
+
const path = session.path;
|
|
2646
|
+
let identity;
|
|
2647
|
+
let fee;
|
|
2648
|
+
if (session.path === "standalone") {
|
|
2649
|
+
standaloneCtx = session.ctx;
|
|
2650
|
+
identity = identityReport(standaloneCtx);
|
|
2651
|
+
fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
|
|
2652
|
+
} else {
|
|
2653
|
+
identity = session.identity;
|
|
2654
|
+
fee = session.feePerEvent;
|
|
2655
|
+
const resolvedRelay = relaysUsed[0];
|
|
2656
|
+
if (resolvedRelay !== void 0 && session.daemonRelayUrl !== void 0 && session.daemonRelayUrl !== resolvedRelay) {
|
|
2657
|
+
io2.err(
|
|
2658
|
+
`rig: the daemon publishes via its configured relay (${session.daemonRelayUrl}), not the resolved relay (${resolvedRelay}) \u2014 stop the daemon to force the standalone path if that matters`
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
1048
2662
|
const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
|
|
1049
2663
|
const addr = { ownerPubkey: owner, repoId };
|
|
1050
2664
|
const event = await opts.buildEvent(addr);
|
|
1051
2665
|
const action = `kind:${event.kind} ${actionLabel}`;
|
|
1052
2666
|
if (!flags.json) {
|
|
1053
|
-
for (const line of renderEventPlan({
|
|
1054
|
-
|
|
2667
|
+
for (const line of renderEventPlan({
|
|
2668
|
+
action,
|
|
2669
|
+
addr,
|
|
2670
|
+
identity,
|
|
2671
|
+
...fee !== void 0 ? { fee } : {}
|
|
2672
|
+
})) {
|
|
2673
|
+
io2.out(line);
|
|
1055
2674
|
}
|
|
1056
2675
|
}
|
|
1057
2676
|
if (!flags.yes) {
|
|
1058
2677
|
if (flags.json) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
);
|
|
2678
|
+
io2.emitJson({
|
|
2679
|
+
command,
|
|
2680
|
+
repoAddr: addr,
|
|
2681
|
+
path,
|
|
2682
|
+
identity,
|
|
2683
|
+
kind: event.kind,
|
|
2684
|
+
executed: false,
|
|
2685
|
+
feeEstimate: fee ?? null,
|
|
2686
|
+
hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
|
|
2687
|
+
});
|
|
1070
2688
|
return 0;
|
|
1071
2689
|
}
|
|
1072
|
-
if (!
|
|
1073
|
-
|
|
2690
|
+
if (!io2.isInteractive) {
|
|
2691
|
+
io2.err(
|
|
1074
2692
|
"refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
|
|
1075
2693
|
);
|
|
1076
2694
|
return 1;
|
|
1077
2695
|
}
|
|
1078
|
-
const proceed = await
|
|
2696
|
+
const proceed = await io2.confirm(
|
|
1079
2697
|
`Proceed with paid publish (${feeLabel(fee)})? [y/N] `
|
|
1080
2698
|
);
|
|
1081
2699
|
if (!proceed) {
|
|
1082
|
-
|
|
2700
|
+
io2.err("aborted \u2014 nothing was published.");
|
|
1083
2701
|
return 1;
|
|
1084
2702
|
}
|
|
1085
2703
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
command,
|
|
1092
|
-
repoAddr: addr,
|
|
1093
|
-
identity,
|
|
1094
|
-
kind: result.kind,
|
|
1095
|
-
executed: true,
|
|
1096
|
-
feeEstimate: fee,
|
|
1097
|
-
result
|
|
1098
|
-
})
|
|
2704
|
+
let result;
|
|
2705
|
+
if (session.path === "standalone") {
|
|
2706
|
+
const receipt = await session.ctx.publisher.publishEvent(
|
|
2707
|
+
event,
|
|
2708
|
+
relaysUsed
|
|
1099
2709
|
);
|
|
2710
|
+
result = serializeEventReceipt(event.kind, receipt);
|
|
1100
2711
|
} else {
|
|
1101
|
-
|
|
2712
|
+
result = await opts.sendDaemon(session.client, addr);
|
|
1102
2713
|
}
|
|
1103
|
-
return 0;
|
|
1104
|
-
} catch (err) {
|
|
1105
|
-
const described = describeError(err, command);
|
|
1106
2714
|
if (flags.json) {
|
|
1107
|
-
|
|
2715
|
+
io2.emitJson({
|
|
2716
|
+
command,
|
|
2717
|
+
repoAddr: addr,
|
|
2718
|
+
path,
|
|
2719
|
+
identity,
|
|
2720
|
+
kind: result.kind,
|
|
2721
|
+
executed: true,
|
|
2722
|
+
feeEstimate: fee ?? null,
|
|
2723
|
+
result
|
|
2724
|
+
});
|
|
1108
2725
|
} else {
|
|
1109
|
-
for (const line of
|
|
2726
|
+
for (const line of renderEventReceipt(action, result)) io2.out(line);
|
|
1110
2727
|
}
|
|
1111
|
-
return
|
|
2728
|
+
return 0;
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
return emitCliError(io2, flags.json, command, err);
|
|
1112
2731
|
} finally {
|
|
1113
2732
|
if (standaloneCtx) {
|
|
1114
2733
|
try {
|
|
@@ -1118,21 +2737,24 @@ async function runEvent(opts) {
|
|
|
1118
2737
|
}
|
|
1119
2738
|
}
|
|
1120
2739
|
}
|
|
1121
|
-
function jsonOut2(output) {
|
|
1122
|
-
return JSON.stringify(output, null, 2);
|
|
1123
|
-
}
|
|
1124
2740
|
async function runIssue(args, deps) {
|
|
1125
|
-
const { io } = deps;
|
|
2741
|
+
const { io: io2 } = deps;
|
|
1126
2742
|
const [sub, ...rest] = args;
|
|
1127
2743
|
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1128
|
-
|
|
2744
|
+
io2.out(ISSUE_USAGE);
|
|
2745
|
+
io2.out("");
|
|
2746
|
+
io2.out(ISSUE_LIST_USAGE);
|
|
2747
|
+
io2.out("");
|
|
2748
|
+
io2.out(ISSUE_SHOW_USAGE);
|
|
1129
2749
|
return 0;
|
|
1130
2750
|
}
|
|
2751
|
+
if (sub === "list") return runIssueList(rest, deps);
|
|
2752
|
+
if (sub === "show") return runIssueShow(rest, deps);
|
|
1131
2753
|
if (sub !== "create") {
|
|
1132
|
-
|
|
1133
|
-
sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
|
|
2754
|
+
io2.err(
|
|
2755
|
+
sub === void 0 ? "missing subcommand: rig issue <create|list|show>" : `unknown rig issue subcommand: ${sub}`
|
|
1134
2756
|
);
|
|
1135
|
-
|
|
2757
|
+
io2.err(ISSUE_USAGE);
|
|
1136
2758
|
return 2;
|
|
1137
2759
|
}
|
|
1138
2760
|
let flags;
|
|
@@ -1141,7 +2763,7 @@ async function runIssue(args, deps) {
|
|
|
1141
2763
|
let bodyFile;
|
|
1142
2764
|
let labels;
|
|
1143
2765
|
try {
|
|
1144
|
-
const { values } =
|
|
2766
|
+
const { values } = parseArgs7({
|
|
1145
2767
|
args: rest,
|
|
1146
2768
|
options: {
|
|
1147
2769
|
...COMMON_OPTIONS,
|
|
@@ -1164,12 +2786,12 @@ async function runIssue(args, deps) {
|
|
|
1164
2786
|
bodyFile = values["body-file"];
|
|
1165
2787
|
labels = values.label ?? [];
|
|
1166
2788
|
} catch (err) {
|
|
1167
|
-
|
|
1168
|
-
|
|
2789
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2790
|
+
io2.err(ISSUE_USAGE);
|
|
1169
2791
|
return 2;
|
|
1170
2792
|
}
|
|
1171
2793
|
if (flags.help) {
|
|
1172
|
-
|
|
2794
|
+
io2.out(ISSUE_USAGE);
|
|
1173
2795
|
return 0;
|
|
1174
2796
|
}
|
|
1175
2797
|
let body;
|
|
@@ -1179,20 +2801,20 @@ async function runIssue(args, deps) {
|
|
|
1179
2801
|
try {
|
|
1180
2802
|
body = await readFile(bodyFile, "utf-8");
|
|
1181
2803
|
} catch (err) {
|
|
1182
|
-
|
|
2804
|
+
io2.err(
|
|
1183
2805
|
`cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
|
|
1184
2806
|
);
|
|
1185
2807
|
return 2;
|
|
1186
2808
|
}
|
|
1187
|
-
} else if (!
|
|
2809
|
+
} else if (!io2.isInteractive) {
|
|
1188
2810
|
body = await (deps.readStdin ?? defaultReadStdin)();
|
|
1189
2811
|
} else {
|
|
1190
|
-
|
|
1191
|
-
|
|
2812
|
+
io2.err("an issue body is required: pass --body/--body-file or pipe stdin");
|
|
2813
|
+
io2.err(ISSUE_USAGE);
|
|
1192
2814
|
return 2;
|
|
1193
2815
|
}
|
|
1194
2816
|
if (body.trim() === "") {
|
|
1195
|
-
|
|
2817
|
+
io2.err("the issue body is empty \u2014 nothing to publish");
|
|
1196
2818
|
return 2;
|
|
1197
2819
|
}
|
|
1198
2820
|
return runEvent({
|
|
@@ -1200,18 +2822,24 @@ async function runIssue(args, deps) {
|
|
|
1200
2822
|
flags,
|
|
1201
2823
|
deps,
|
|
1202
2824
|
actionLabel: `issue ${JSON.stringify(title)}`,
|
|
1203
|
-
buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels)
|
|
2825
|
+
buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
|
|
2826
|
+
sendDaemon: (client, addr) => client.gitIssue({
|
|
2827
|
+
repoAddr: addr,
|
|
2828
|
+
title,
|
|
2829
|
+
body,
|
|
2830
|
+
...labels.length > 0 ? { labels } : {}
|
|
2831
|
+
})
|
|
1204
2832
|
});
|
|
1205
2833
|
}
|
|
1206
2834
|
async function runComment(args, deps) {
|
|
1207
|
-
const { io } = deps;
|
|
2835
|
+
const { io: io2 } = deps;
|
|
1208
2836
|
let flags;
|
|
1209
2837
|
let rootEventId;
|
|
1210
2838
|
let body;
|
|
1211
2839
|
let parentAuthor;
|
|
1212
2840
|
let marker;
|
|
1213
2841
|
try {
|
|
1214
|
-
const { values, positionals } =
|
|
2842
|
+
const { values, positionals } = parseArgs7({
|
|
1215
2843
|
args,
|
|
1216
2844
|
options: {
|
|
1217
2845
|
...COMMON_OPTIONS,
|
|
@@ -1223,7 +2851,7 @@ async function runComment(args, deps) {
|
|
|
1223
2851
|
});
|
|
1224
2852
|
flags = pickCommon(values);
|
|
1225
2853
|
if (flags.help) {
|
|
1226
|
-
|
|
2854
|
+
io2.out(COMMENT_USAGE);
|
|
1227
2855
|
return 0;
|
|
1228
2856
|
}
|
|
1229
2857
|
if (positionals.length !== 1) {
|
|
@@ -1245,8 +2873,8 @@ async function runComment(args, deps) {
|
|
|
1245
2873
|
}
|
|
1246
2874
|
marker = rawMarker;
|
|
1247
2875
|
} catch (err) {
|
|
1248
|
-
|
|
1249
|
-
|
|
2876
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2877
|
+
io2.err(COMMENT_USAGE);
|
|
1250
2878
|
return 2;
|
|
1251
2879
|
}
|
|
1252
2880
|
return runEvent({
|
|
@@ -1261,7 +2889,14 @@ async function runComment(args, deps) {
|
|
|
1261
2889
|
parentAuthor ?? addr.ownerPubkey,
|
|
1262
2890
|
body,
|
|
1263
2891
|
marker
|
|
1264
|
-
)
|
|
2892
|
+
),
|
|
2893
|
+
sendDaemon: (client, addr) => client.gitComment({
|
|
2894
|
+
repoAddr: addr,
|
|
2895
|
+
rootEventId,
|
|
2896
|
+
body,
|
|
2897
|
+
...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {},
|
|
2898
|
+
marker
|
|
2899
|
+
})
|
|
1265
2900
|
});
|
|
1266
2901
|
}
|
|
1267
2902
|
var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
|
|
@@ -1269,48 +2904,57 @@ function extractPatchShas(patchText) {
|
|
|
1269
2904
|
return [...patchText.matchAll(PATCH_FROM_RE)].map((m) => m[1]);
|
|
1270
2905
|
}
|
|
1271
2906
|
async function runPr(args, deps) {
|
|
1272
|
-
const { io } = deps;
|
|
2907
|
+
const { io: io2 } = deps;
|
|
1273
2908
|
const [sub, ...rest] = args;
|
|
1274
2909
|
switch (sub) {
|
|
1275
2910
|
case "create":
|
|
1276
2911
|
return runPrCreate(rest, deps);
|
|
1277
2912
|
case "status":
|
|
1278
2913
|
return runPrStatus(rest, deps);
|
|
2914
|
+
// FREE read subcommands (#278): relay queries only, no payment path.
|
|
2915
|
+
case "list":
|
|
2916
|
+
return runPrList(rest, deps);
|
|
2917
|
+
case "show":
|
|
2918
|
+
return runPrShow(rest, deps);
|
|
1279
2919
|
case "--help":
|
|
1280
2920
|
case "-h":
|
|
1281
2921
|
case "help":
|
|
1282
|
-
|
|
2922
|
+
io2.out(PR_USAGE);
|
|
1283
2923
|
return 0;
|
|
1284
2924
|
default:
|
|
1285
|
-
|
|
1286
|
-
sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
|
|
2925
|
+
io2.err(
|
|
2926
|
+
sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
|
|
1287
2927
|
);
|
|
1288
|
-
|
|
2928
|
+
io2.err(PR_USAGE);
|
|
1289
2929
|
return 2;
|
|
1290
2930
|
}
|
|
1291
2931
|
}
|
|
1292
2932
|
async function runPrCreate(rest, deps) {
|
|
1293
|
-
const { io } = deps;
|
|
2933
|
+
const { io: io2 } = deps;
|
|
1294
2934
|
let flags;
|
|
1295
2935
|
let title;
|
|
1296
2936
|
let range;
|
|
1297
2937
|
let patchFile;
|
|
1298
2938
|
let branch;
|
|
2939
|
+
let bodyFlag;
|
|
2940
|
+
let bodyFile;
|
|
1299
2941
|
try {
|
|
1300
|
-
const { values } =
|
|
2942
|
+
const { values } = parseArgs7({
|
|
1301
2943
|
args: rest,
|
|
1302
2944
|
options: {
|
|
1303
2945
|
...COMMON_OPTIONS,
|
|
1304
2946
|
title: { type: "string" },
|
|
1305
2947
|
range: { type: "string" },
|
|
1306
2948
|
"patch-file": { type: "string" },
|
|
2949
|
+
body: { type: "string" },
|
|
2950
|
+
"body-file": { type: "string" },
|
|
1307
2951
|
branch: { type: "string" }
|
|
1308
2952
|
},
|
|
1309
2953
|
allowPositionals: false
|
|
1310
2954
|
});
|
|
1311
2955
|
flags = pickCommon(values);
|
|
1312
2956
|
if (flags.help) {
|
|
1313
|
-
|
|
2957
|
+
io2.out(PR_CREATE_USAGE);
|
|
1314
2958
|
return 0;
|
|
1315
2959
|
}
|
|
1316
2960
|
if (values.title === void 0 || values.title === "") {
|
|
@@ -1319,15 +2963,39 @@ async function runPrCreate(rest, deps) {
|
|
|
1319
2963
|
if (values.range === void 0 === (values["patch-file"] === void 0)) {
|
|
1320
2964
|
throw new Error("exactly one of --range or --patch-file is required");
|
|
1321
2965
|
}
|
|
2966
|
+
if (values.body !== void 0 && values["body-file"] !== void 0) {
|
|
2967
|
+
throw new Error("--body and --body-file are mutually exclusive");
|
|
2968
|
+
}
|
|
1322
2969
|
title = values.title;
|
|
1323
2970
|
range = values.range;
|
|
1324
2971
|
patchFile = values["patch-file"];
|
|
1325
2972
|
branch = values.branch;
|
|
2973
|
+
bodyFlag = values.body;
|
|
2974
|
+
bodyFile = values["body-file"];
|
|
1326
2975
|
} catch (err) {
|
|
1327
|
-
|
|
1328
|
-
|
|
2976
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2977
|
+
io2.err(PR_CREATE_USAGE);
|
|
2978
|
+
return 2;
|
|
2979
|
+
}
|
|
2980
|
+
let body;
|
|
2981
|
+
if (bodyFlag !== void 0) {
|
|
2982
|
+
body = bodyFlag;
|
|
2983
|
+
} else if (bodyFile !== void 0) {
|
|
2984
|
+
try {
|
|
2985
|
+
body = await readFile(bodyFile, "utf-8");
|
|
2986
|
+
} catch (err) {
|
|
2987
|
+
io2.err(
|
|
2988
|
+
`cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
|
|
2989
|
+
);
|
|
2990
|
+
return 2;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (body !== void 0 && body.trim() === "") {
|
|
2994
|
+
io2.err("the PR body is empty \u2014 drop --body/--body-file or pass text");
|
|
1329
2995
|
return 2;
|
|
1330
2996
|
}
|
|
2997
|
+
let builtPatchText;
|
|
2998
|
+
let builtCommits = [];
|
|
1331
2999
|
return runEvent({
|
|
1332
3000
|
command: "pr",
|
|
1333
3001
|
flags,
|
|
@@ -1357,40 +3025,56 @@ async function runPrCreate(rest, deps) {
|
|
|
1357
3025
|
}
|
|
1358
3026
|
commits = [];
|
|
1359
3027
|
}
|
|
3028
|
+
builtPatchText = patchText;
|
|
3029
|
+
builtCommits = commits;
|
|
1360
3030
|
return buildPatch(
|
|
1361
3031
|
addr.ownerPubkey,
|
|
1362
3032
|
addr.repoId,
|
|
1363
3033
|
title,
|
|
1364
3034
|
commits,
|
|
1365
3035
|
branch,
|
|
1366
|
-
patchText
|
|
3036
|
+
patchText,
|
|
3037
|
+
body
|
|
1367
3038
|
);
|
|
3039
|
+
},
|
|
3040
|
+
sendDaemon: (client, addr) => {
|
|
3041
|
+
if (builtPatchText === void 0) {
|
|
3042
|
+
throw new Error("internal: patch text not built before delegation");
|
|
3043
|
+
}
|
|
3044
|
+
return client.gitPatch({
|
|
3045
|
+
repoAddr: addr,
|
|
3046
|
+
title,
|
|
3047
|
+
patchText: builtPatchText,
|
|
3048
|
+
...body !== void 0 ? { description: body } : {},
|
|
3049
|
+
...builtCommits.length > 0 ? { commits: builtCommits } : {},
|
|
3050
|
+
...branch !== void 0 ? { branch } : {}
|
|
3051
|
+
});
|
|
1368
3052
|
}
|
|
1369
3053
|
});
|
|
1370
3054
|
}
|
|
1371
3055
|
var STATUS_KIND_BY_VALUE = {
|
|
1372
|
-
open:
|
|
1373
|
-
applied:
|
|
1374
|
-
closed:
|
|
1375
|
-
draft:
|
|
3056
|
+
open: STATUS_OPEN_KIND2,
|
|
3057
|
+
applied: STATUS_APPLIED_KIND2,
|
|
3058
|
+
closed: STATUS_CLOSED_KIND2,
|
|
3059
|
+
draft: STATUS_DRAFT_KIND2
|
|
1376
3060
|
};
|
|
1377
3061
|
function isStatusValue(value) {
|
|
1378
3062
|
return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
|
|
1379
3063
|
}
|
|
1380
3064
|
async function runPrStatus(args, deps) {
|
|
1381
|
-
const { io } = deps;
|
|
3065
|
+
const { io: io2 } = deps;
|
|
1382
3066
|
let flags;
|
|
1383
3067
|
let targetEventId;
|
|
1384
3068
|
let status;
|
|
1385
3069
|
try {
|
|
1386
|
-
const { values, positionals } =
|
|
3070
|
+
const { values, positionals } = parseArgs7({
|
|
1387
3071
|
args,
|
|
1388
3072
|
options: COMMON_OPTIONS,
|
|
1389
3073
|
allowPositionals: true
|
|
1390
3074
|
});
|
|
1391
3075
|
flags = pickCommon(values);
|
|
1392
3076
|
if (flags.help) {
|
|
1393
|
-
|
|
3077
|
+
io2.out(PR_STATUS_USAGE);
|
|
1394
3078
|
return 0;
|
|
1395
3079
|
}
|
|
1396
3080
|
if (positionals.length !== 2) {
|
|
@@ -1408,8 +3092,8 @@ async function runPrStatus(args, deps) {
|
|
|
1408
3092
|
}
|
|
1409
3093
|
status = rawStatus;
|
|
1410
3094
|
} catch (err) {
|
|
1411
|
-
|
|
1412
|
-
|
|
3095
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3096
|
+
io2.err(PR_STATUS_USAGE);
|
|
1413
3097
|
return 2;
|
|
1414
3098
|
}
|
|
1415
3099
|
return runEvent({
|
|
@@ -1421,13 +3105,423 @@ async function runPrStatus(args, deps) {
|
|
|
1421
3105
|
const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
|
|
1422
3106
|
event.tags.push([
|
|
1423
3107
|
"a",
|
|
1424
|
-
`${
|
|
3108
|
+
`${REPOSITORY_ANNOUNCEMENT_KIND2}:${addr.ownerPubkey}:${addr.repoId}`
|
|
1425
3109
|
]);
|
|
1426
3110
|
return event;
|
|
1427
|
-
}
|
|
3111
|
+
},
|
|
3112
|
+
sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
|
|
1428
3113
|
});
|
|
1429
3114
|
}
|
|
1430
3115
|
|
|
3116
|
+
// src/cli/fetch.ts
|
|
3117
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
3118
|
+
var FETCH_USAGE = `Usage: rig fetch [remote] [options]
|
|
3119
|
+
|
|
3120
|
+
Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
|
|
3121
|
+
payments, no identity needed). Reads the kind:30618 repository state from the
|
|
3122
|
+
remote's relay, downloads only the git objects this repository is missing
|
|
3123
|
+
(SHA-1 verified), and updates the remote-tracking refs
|
|
3124
|
+
(refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
|
|
3125
|
+
|
|
3126
|
+
No merge happens: integrate with the git passthrough, e.g.
|
|
3127
|
+
\`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
|
|
3128
|
+
\`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
|
|
3129
|
+
|
|
3130
|
+
The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
|
|
3131
|
+
or \`rig clone\` writes; --repo-id/--owner override.
|
|
3132
|
+
|
|
3133
|
+
Options:
|
|
3134
|
+
--repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
3135
|
+
--owner <pubkey> repository owner (npub or 64-char hex; default: git config)
|
|
3136
|
+
--relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
|
|
3137
|
+
--concurrency <n> parallel gateway downloads (default 8)
|
|
3138
|
+
--json machine-readable result envelope
|
|
3139
|
+
-h, --help show this help`;
|
|
3140
|
+
var WS_URL_RE3 = /^wss?:\/\//i;
|
|
3141
|
+
function localRefFor(refname, remote) {
|
|
3142
|
+
if (refname.startsWith("refs/heads/")) {
|
|
3143
|
+
return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
|
|
3144
|
+
}
|
|
3145
|
+
if (refname.startsWith("refs/tags/")) return refname;
|
|
3146
|
+
return null;
|
|
3147
|
+
}
|
|
3148
|
+
async function resolveLocalRef(repoRoot, refname) {
|
|
3149
|
+
try {
|
|
3150
|
+
const out = await runGit(repoRoot, [
|
|
3151
|
+
"rev-parse",
|
|
3152
|
+
"--verify",
|
|
3153
|
+
"--quiet",
|
|
3154
|
+
refname
|
|
3155
|
+
]);
|
|
3156
|
+
const sha = out.trim();
|
|
3157
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
3158
|
+
} catch {
|
|
3159
|
+
return null;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
async function runFetch(args, deps) {
|
|
3163
|
+
const { io: io2 } = deps;
|
|
3164
|
+
let json = false;
|
|
3165
|
+
let remote = "origin";
|
|
3166
|
+
let relayFlag;
|
|
3167
|
+
let repoIdFlag;
|
|
3168
|
+
let ownerFlag;
|
|
3169
|
+
let concurrency;
|
|
3170
|
+
try {
|
|
3171
|
+
const { values, positionals } = parseArgs8({
|
|
3172
|
+
args,
|
|
3173
|
+
options: {
|
|
3174
|
+
json: { type: "boolean", default: false },
|
|
3175
|
+
relay: { type: "string" },
|
|
3176
|
+
"repo-id": { type: "string" },
|
|
3177
|
+
owner: { type: "string" },
|
|
3178
|
+
concurrency: { type: "string" },
|
|
3179
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3180
|
+
},
|
|
3181
|
+
allowPositionals: true
|
|
3182
|
+
});
|
|
3183
|
+
if (values.help) {
|
|
3184
|
+
io2.out(FETCH_USAGE);
|
|
3185
|
+
return 0;
|
|
3186
|
+
}
|
|
3187
|
+
json = values.json === true;
|
|
3188
|
+
if (positionals.length > 1) {
|
|
3189
|
+
throw new Error(
|
|
3190
|
+
`expected at most one [remote] argument, got ${positionals.length}`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
if (positionals[0] !== void 0) remote = positionals[0];
|
|
3194
|
+
relayFlag = values.relay;
|
|
3195
|
+
repoIdFlag = values["repo-id"];
|
|
3196
|
+
ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
|
|
3197
|
+
if (values.concurrency !== void 0) {
|
|
3198
|
+
concurrency = Number.parseInt(values.concurrency, 10);
|
|
3199
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
3200
|
+
throw new Error("--concurrency must be a positive integer");
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3205
|
+
io2.err(FETCH_USAGE);
|
|
3206
|
+
return 2;
|
|
3207
|
+
}
|
|
3208
|
+
try {
|
|
3209
|
+
const repoRoot = await resolveRepoRoot(deps.cwd);
|
|
3210
|
+
const toonConfig = await readToonConfig(repoRoot);
|
|
3211
|
+
const repoId = repoIdFlag ?? toonConfig.repoId;
|
|
3212
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
3213
|
+
const owner = ownerFlag ?? toonConfig.owner;
|
|
3214
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
3215
|
+
let relayUrl;
|
|
3216
|
+
if (relayFlag !== void 0) {
|
|
3217
|
+
relayUrl = relayFlag;
|
|
3218
|
+
} else {
|
|
3219
|
+
const urls = await getGitRemoteUrls(repoRoot, remote);
|
|
3220
|
+
if (urls.length === 0) throw new UnknownRemoteError(remote);
|
|
3221
|
+
if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
|
|
3222
|
+
relayUrl = urls[0];
|
|
3223
|
+
}
|
|
3224
|
+
if (!WS_URL_RE3.test(relayUrl)) {
|
|
3225
|
+
throw new InvalidRelayUrlError(
|
|
3226
|
+
relayUrl,
|
|
3227
|
+
`remote ${JSON.stringify(remote)}`
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
const remoteState = await fetchRemoteState({
|
|
3231
|
+
relayUrls: [relayUrl],
|
|
3232
|
+
ownerPubkey: owner,
|
|
3233
|
+
repoId,
|
|
3234
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
|
|
3235
|
+
...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
|
|
3236
|
+
});
|
|
3237
|
+
const planned = [];
|
|
3238
|
+
for (const [refname, sha] of remoteState.refs) {
|
|
3239
|
+
if (!isSafeRefname(refname)) {
|
|
3240
|
+
io2.err(
|
|
3241
|
+
`warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
|
|
3242
|
+
);
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
const localRef = localRefFor(refname, remote);
|
|
3246
|
+
if (localRef === null) {
|
|
3247
|
+
io2.err(
|
|
3248
|
+
`warning: skipping ref outside refs/heads and refs/tags: ${refname}`
|
|
3249
|
+
);
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
planned.push({ refname, localRef, newSha: sha });
|
|
3253
|
+
}
|
|
3254
|
+
const reader = new GitRepoReader(repoRoot);
|
|
3255
|
+
const tips = [...new Set(planned.map((p) => p.newSha))];
|
|
3256
|
+
const candidates = [.../* @__PURE__ */ new Set([...remoteState.shaToTxId.keys(), ...tips])];
|
|
3257
|
+
const { missing: absent } = await reader.statObjects(candidates);
|
|
3258
|
+
const absentSet = new Set(absent);
|
|
3259
|
+
const presentLocally = new Set(
|
|
3260
|
+
candidates.filter((sha) => !absentSet.has(sha))
|
|
3261
|
+
);
|
|
3262
|
+
const collected = await collectRepoObjects({
|
|
3263
|
+
tips,
|
|
3264
|
+
shaToTxId: remoteState.shaToTxId,
|
|
3265
|
+
resolveMissing: (shas) => remoteState.resolveMissing(shas),
|
|
3266
|
+
presentLocally,
|
|
3267
|
+
...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
|
|
3268
|
+
...concurrency !== void 0 ? { concurrency } : {}
|
|
3269
|
+
});
|
|
3270
|
+
if (collected.missing.length > 0) {
|
|
3271
|
+
throw new MissingRemoteObjectsError(
|
|
3272
|
+
collected.missing,
|
|
3273
|
+
`cannot fetch from ${relayUrl}`
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
const written = await writeGitObjects(repoRoot, collected.objects.values());
|
|
3277
|
+
const updates = [];
|
|
3278
|
+
for (const { refname, localRef, newSha } of planned) {
|
|
3279
|
+
const oldSha = await resolveLocalRef(repoRoot, localRef);
|
|
3280
|
+
if (oldSha === newSha) {
|
|
3281
|
+
updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
let kind;
|
|
3285
|
+
if (oldSha === null) {
|
|
3286
|
+
kind = "new";
|
|
3287
|
+
} else {
|
|
3288
|
+
kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
|
|
3289
|
+
}
|
|
3290
|
+
await updateRef(repoRoot, localRef, newSha);
|
|
3291
|
+
updates.push({ refname, localRef, oldSha, newSha, kind });
|
|
3292
|
+
}
|
|
3293
|
+
const moved = updates.filter((u) => u.kind !== "up-to-date");
|
|
3294
|
+
if (json) {
|
|
3295
|
+
io2.emitJson({
|
|
3296
|
+
command: "fetch",
|
|
3297
|
+
repoAddr: { ownerPubkey: owner, repoId },
|
|
3298
|
+
remote,
|
|
3299
|
+
relay: relayUrl,
|
|
3300
|
+
objectsDownloaded: written,
|
|
3301
|
+
updates,
|
|
3302
|
+
executed: true
|
|
3303
|
+
});
|
|
3304
|
+
return 0;
|
|
3305
|
+
}
|
|
3306
|
+
if (moved.length === 0) {
|
|
3307
|
+
io2.out("Already up to date.");
|
|
3308
|
+
return 0;
|
|
3309
|
+
}
|
|
3310
|
+
io2.out(`From ${relayUrl}`);
|
|
3311
|
+
for (const u of moved) {
|
|
3312
|
+
const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
|
|
3313
|
+
const src = short(u.refname);
|
|
3314
|
+
const dst = short(u.localRef);
|
|
3315
|
+
if (u.kind === "new") {
|
|
3316
|
+
const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
|
|
3317
|
+
io2.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
|
|
3318
|
+
} else if (u.kind === "forced") {
|
|
3319
|
+
io2.out(
|
|
3320
|
+
` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
|
|
3321
|
+
);
|
|
3322
|
+
} else {
|
|
3323
|
+
io2.out(
|
|
3324
|
+
` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
io2.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
|
|
3329
|
+
return 0;
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
return emitCliError(io2, json, "fetch", err);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// src/cli/fund.ts
|
|
3336
|
+
import { readFileSync } from "fs";
|
|
3337
|
+
import { homedir } from "os";
|
|
3338
|
+
import { join as join2 } from "path";
|
|
3339
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
3340
|
+
var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
|
|
3341
|
+
var CHAINS = ["evm", "solana", "mina"];
|
|
3342
|
+
var FUND_USAGE = `Usage: rig fund [options]
|
|
3343
|
+
|
|
3344
|
+
Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
|
|
3345
|
+
pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
|
|
3346
|
+
~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
|
|
3347
|
+
faucetUrl config field, or the deployed devnet faucet when the configured
|
|
3348
|
+
network is devnet. The faucet drips a FIXED amount per chain (there is no
|
|
3349
|
+
--amount). On a network without a faucet, prints the wallet address(es) to
|
|
3350
|
+
fund externally instead.
|
|
3351
|
+
|
|
3352
|
+
Options:
|
|
3353
|
+
--chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
|
|
3354
|
+
\`chain\` config field, else evm)
|
|
3355
|
+
--address <address> fund this address instead of the identity's own
|
|
3356
|
+
--json machine-readable envelope
|
|
3357
|
+
-h, --help show this help`;
|
|
3358
|
+
var SHARED_DEVNET_SUFFIX = ".devnet.toonprotocol.dev";
|
|
3359
|
+
function sharedDevnetOrigin(env, file) {
|
|
3360
|
+
const candidates = [
|
|
3361
|
+
env["TOON_CLIENT_RELAY_URL"] ?? file.relayUrl,
|
|
3362
|
+
env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl,
|
|
3363
|
+
env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl
|
|
3364
|
+
];
|
|
3365
|
+
for (const url of candidates) {
|
|
3366
|
+
if (!url) continue;
|
|
3367
|
+
try {
|
|
3368
|
+
const { hostname } = new URL(url);
|
|
3369
|
+
if (hostname.endsWith(SHARED_DEVNET_SUFFIX) || hostname === SHARED_DEVNET_SUFFIX.slice(1)) {
|
|
3370
|
+
return url;
|
|
3371
|
+
}
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
return void 0;
|
|
3376
|
+
}
|
|
3377
|
+
function noFaucetGuidance(network, devnetOrigin) {
|
|
3378
|
+
const head = `no faucet is configured for network ${JSON.stringify(network ?? "custom")}`;
|
|
3379
|
+
const external = "To fund the wallet externally instead, send the settlement token plus native gas to the address below for the chain your channels settle on.";
|
|
3380
|
+
if (devnetOrigin !== void 0) {
|
|
3381
|
+
return `${head} \u2014 but your configured origin (${devnetOrigin}) looks like the shared devnet. Set TOON_CLIENT_NETWORK=devnet (or "network": "devnet" in the client config) and re-run \`rig fund\` \u2014 no faucet URL is needed there. ${external}`;
|
|
3382
|
+
}
|
|
3383
|
+
return `${head}. If you meant the shared devnet, set TOON_CLIENT_NETWORK=devnet and re-run \`rig fund\` \u2014 no faucet URL is needed there. If this is a self-hosted network with its own faucet, set TOON_CLIENT_FAUCET_URL (or the faucetUrl config field). ${external}`;
|
|
3384
|
+
}
|
|
3385
|
+
function readFundConfig(env) {
|
|
3386
|
+
const dir = env["TOON_CLIENT_HOME"] ?? join2(homedir(), ".toon-client");
|
|
3387
|
+
const configPath = join2(dir, "config.json");
|
|
3388
|
+
try {
|
|
3389
|
+
return {
|
|
3390
|
+
file: JSON.parse(readFileSync(configPath, "utf8")),
|
|
3391
|
+
configPath
|
|
3392
|
+
};
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
if (err.code === "ENOENT") {
|
|
3395
|
+
return { file: {}, configPath };
|
|
3396
|
+
}
|
|
3397
|
+
throw new Error(
|
|
3398
|
+
`failed to read client config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
async function runFund(args, deps) {
|
|
3403
|
+
const { io: io2, env } = deps;
|
|
3404
|
+
let chainFlag;
|
|
3405
|
+
let addressFlag;
|
|
3406
|
+
let json = false;
|
|
3407
|
+
try {
|
|
3408
|
+
const { values } = parseArgs9({
|
|
3409
|
+
args,
|
|
3410
|
+
options: {
|
|
3411
|
+
chain: { type: "string" },
|
|
3412
|
+
address: { type: "string" },
|
|
3413
|
+
json: { type: "boolean", default: false },
|
|
3414
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
if (values.help) {
|
|
3418
|
+
io2.out(FUND_USAGE);
|
|
3419
|
+
return 0;
|
|
3420
|
+
}
|
|
3421
|
+
chainFlag = values.chain;
|
|
3422
|
+
addressFlag = values.address;
|
|
3423
|
+
json = values.json ?? false;
|
|
3424
|
+
if (chainFlag !== void 0 && !CHAINS.includes(chainFlag)) {
|
|
3425
|
+
throw new Error(
|
|
3426
|
+
`--chain must be one of ${CHAINS.join(" | ")}, got ${JSON.stringify(chainFlag)}`
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
} catch (err) {
|
|
3430
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3431
|
+
io2.err(FUND_USAGE);
|
|
3432
|
+
return 2;
|
|
3433
|
+
}
|
|
3434
|
+
try {
|
|
3435
|
+
const { file } = readFundConfig(env);
|
|
3436
|
+
const chain = chainFlag ?? env["TOON_CLIENT_CHAIN"] ?? file.chain ?? "evm";
|
|
3437
|
+
if (!CHAINS.includes(chain)) {
|
|
3438
|
+
throw new Error(
|
|
3439
|
+
`configured settlement chain ${JSON.stringify(chain)} has no faucet \u2014 pass --chain ${CHAINS.join(" | ")}`
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
|
|
3443
|
+
const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (network === "devnet" ? DEVNET_FAUCET_URL : void 0);
|
|
3444
|
+
const resolved = await resolveIdentity({
|
|
3445
|
+
env,
|
|
3446
|
+
cwd: deps.cwd,
|
|
3447
|
+
warn: (line) => io2.err(line)
|
|
3448
|
+
});
|
|
3449
|
+
const identity = {
|
|
3450
|
+
pubkey: resolved.pubkey,
|
|
3451
|
+
source: resolved.source,
|
|
3452
|
+
sourceLabel: resolved.sourceLabel
|
|
3453
|
+
};
|
|
3454
|
+
const client = await import("@toon-protocol/client");
|
|
3455
|
+
const derived = await client.deriveFullIdentity(
|
|
3456
|
+
resolved.mnemonic,
|
|
3457
|
+
resolved.accountIndex
|
|
3458
|
+
);
|
|
3459
|
+
const addresses = {
|
|
3460
|
+
evm: derived.evm.address || null,
|
|
3461
|
+
solana: derived.solana.publicKey || null,
|
|
3462
|
+
mina: derived.mina.publicKey || null
|
|
3463
|
+
};
|
|
3464
|
+
if (!faucetUrl) {
|
|
3465
|
+
const guidance = noFaucetGuidance(network, sharedDevnetOrigin(env, file));
|
|
3466
|
+
if (json) {
|
|
3467
|
+
io2.emitJson({
|
|
3468
|
+
command: "fund",
|
|
3469
|
+
identity,
|
|
3470
|
+
funded: false,
|
|
3471
|
+
network: network ?? null,
|
|
3472
|
+
chain,
|
|
3473
|
+
addresses,
|
|
3474
|
+
guidance
|
|
3475
|
+
});
|
|
3476
|
+
return 0;
|
|
3477
|
+
}
|
|
3478
|
+
io2.out(renderIdentityLine(identity));
|
|
3479
|
+
io2.out(guidance);
|
|
3480
|
+
io2.out("Wallet addresses:");
|
|
3481
|
+
io2.out(` evm ${addresses.evm ?? "(no key derived)"}`);
|
|
3482
|
+
io2.out(` solana ${addresses.solana ?? "(no key derived)"}`);
|
|
3483
|
+
io2.out(` mina ${addresses.mina ?? "(no key derived \u2014 optional mina-signer dependency missing)"}`);
|
|
3484
|
+
return 0;
|
|
3485
|
+
}
|
|
3486
|
+
const address = addressFlag ?? addresses[chain];
|
|
3487
|
+
if (!address) {
|
|
3488
|
+
throw new Error(
|
|
3489
|
+
`no ${chain} address could be derived for this identity \u2014 pass an explicit --address (for mina, install the optional mina-signer dependency)`
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
|
|
3493
|
+
const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
|
|
3494
|
+
if (!json) {
|
|
3495
|
+
io2.out(
|
|
3496
|
+
`Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
const { response } = await client.fundWallet(faucetUrl, address, chain, {
|
|
3500
|
+
timeout,
|
|
3501
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
3502
|
+
});
|
|
3503
|
+
if (json) {
|
|
3504
|
+
io2.emitJson({
|
|
3505
|
+
command: "fund",
|
|
3506
|
+
identity,
|
|
3507
|
+
funded: true,
|
|
3508
|
+
network: network ?? null,
|
|
3509
|
+
chain,
|
|
3510
|
+
address,
|
|
3511
|
+
faucetUrl,
|
|
3512
|
+
response
|
|
3513
|
+
});
|
|
3514
|
+
return 0;
|
|
3515
|
+
}
|
|
3516
|
+
io2.out(`Faucet drip succeeded: ${chain} \u2192 ${address}`);
|
|
3517
|
+
io2.out(renderIdentityLine(identity));
|
|
3518
|
+
io2.out("Re-check with `rig balance` (a drip can take a few blocks to land).");
|
|
3519
|
+
return 0;
|
|
3520
|
+
} catch (err) {
|
|
3521
|
+
return emitCliError(io2, json, "fund", err);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
1431
3525
|
// src/cli/git-passthrough.ts
|
|
1432
3526
|
import { spawn } from "child_process";
|
|
1433
3527
|
import { constants as osConstants } from "os";
|
|
@@ -1437,11 +3531,11 @@ function signalExitCode(signal) {
|
|
|
1437
3531
|
const num = osConstants.signals[signal];
|
|
1438
3532
|
return num === void 0 ? 1 : 128 + num;
|
|
1439
3533
|
}
|
|
1440
|
-
var runGitPassthrough = (
|
|
3534
|
+
var runGitPassthrough = (argv2, options = {}) => {
|
|
1441
3535
|
const err = options.err ?? ((line) => process.stderr.write(`${line}
|
|
1442
3536
|
`));
|
|
1443
|
-
return new Promise((
|
|
1444
|
-
const child = spawn("git",
|
|
3537
|
+
return new Promise((resolve2) => {
|
|
3538
|
+
const child = spawn("git", argv2, {
|
|
1445
3539
|
stdio: "inherit",
|
|
1446
3540
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1447
3541
|
env: options.env ?? process.env
|
|
@@ -1461,24 +3555,24 @@ var runGitPassthrough = (argv, options = {}) => {
|
|
|
1461
3555
|
restoreSignals();
|
|
1462
3556
|
if (spawnErr.code === "ENOENT") {
|
|
1463
3557
|
err(
|
|
1464
|
-
`rig: git not found \u2014 \`rig ${
|
|
3558
|
+
`rig: git not found \u2014 \`rig ${argv2[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
3559
|
);
|
|
1466
|
-
|
|
3560
|
+
resolve2(GIT_NOT_FOUND_EXIT);
|
|
1467
3561
|
return;
|
|
1468
3562
|
}
|
|
1469
3563
|
err(`rig: failed to run git: ${spawnErr.message}`);
|
|
1470
|
-
|
|
3564
|
+
resolve2(1);
|
|
1471
3565
|
});
|
|
1472
3566
|
child.on("close", (code, signal) => {
|
|
1473
3567
|
restoreSignals();
|
|
1474
|
-
|
|
3568
|
+
resolve2(signal !== null ? signalExitCode(signal) : code ?? 1);
|
|
1475
3569
|
});
|
|
1476
3570
|
});
|
|
1477
3571
|
};
|
|
1478
3572
|
|
|
1479
3573
|
// src/cli/init.ts
|
|
1480
|
-
import { basename } from "path";
|
|
1481
|
-
import { parseArgs as
|
|
3574
|
+
import { basename as basename2 } from "path";
|
|
3575
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
1482
3576
|
var INIT_USAGE = `Usage: rig init [options]
|
|
1483
3577
|
|
|
1484
3578
|
Set up the current git repository for rig (one-shot, idempotent, free):
|
|
@@ -1498,7 +3592,7 @@ Options:
|
|
|
1498
3592
|
--json machine-readable report
|
|
1499
3593
|
-h, --help show this help`;
|
|
1500
3594
|
function parseInitArgs(args) {
|
|
1501
|
-
const { values, positionals } =
|
|
3595
|
+
const { values, positionals } = parseArgs10({
|
|
1502
3596
|
args,
|
|
1503
3597
|
options: {
|
|
1504
3598
|
"repo-id": { type: "string" },
|
|
@@ -1522,17 +3616,17 @@ function parseInitArgs(args) {
|
|
|
1522
3616
|
return flags;
|
|
1523
3617
|
}
|
|
1524
3618
|
async function runInit(args, deps) {
|
|
1525
|
-
const { io, env } = deps;
|
|
3619
|
+
const { io: io2, env } = deps;
|
|
1526
3620
|
let flags;
|
|
1527
3621
|
try {
|
|
1528
3622
|
flags = parseInitArgs(args);
|
|
1529
3623
|
} catch (err) {
|
|
1530
|
-
|
|
1531
|
-
|
|
3624
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3625
|
+
io2.err(INIT_USAGE);
|
|
1532
3626
|
return 2;
|
|
1533
3627
|
}
|
|
1534
3628
|
if (flags.help) {
|
|
1535
|
-
|
|
3629
|
+
io2.out(INIT_USAGE);
|
|
1536
3630
|
return 0;
|
|
1537
3631
|
}
|
|
1538
3632
|
try {
|
|
@@ -1545,10 +3639,10 @@ async function runInit(args, deps) {
|
|
|
1545
3639
|
const identity = await (deps.resolveIdentityImpl ?? resolveIdentity)({
|
|
1546
3640
|
env,
|
|
1547
3641
|
cwd: deps.cwd,
|
|
1548
|
-
warn: (line) =>
|
|
3642
|
+
warn: (line) => io2.err(line)
|
|
1549
3643
|
});
|
|
1550
3644
|
const existing = await readToonConfig(repoRoot);
|
|
1551
|
-
const repoId = flags.repoId ?? existing.repoId ??
|
|
3645
|
+
const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
|
|
1552
3646
|
const changed = {
|
|
1553
3647
|
repoId: existing.repoId !== repoId,
|
|
1554
3648
|
owner: existing.owner !== identity.pubkey
|
|
@@ -1582,50 +3676,44 @@ async function runInit(args, deps) {
|
|
|
1582
3676
|
migratedToonRelay,
|
|
1583
3677
|
changed
|
|
1584
3678
|
};
|
|
1585
|
-
|
|
3679
|
+
io2.emitJson(output);
|
|
1586
3680
|
return 0;
|
|
1587
3681
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
3682
|
+
io2.out(`Initialized rig for ${repoRoot}`);
|
|
3683
|
+
io2.out(renderIdentityLine(identity));
|
|
3684
|
+
io2.out(
|
|
1591
3685
|
` toon.repoid = ${repoId}` + (changed.repoId ? "" : " (unchanged)") + (existing.repoId && changed.repoId ? ` (was ${existing.repoId})` : "")
|
|
1592
3686
|
);
|
|
1593
|
-
|
|
3687
|
+
io2.out(
|
|
1594
3688
|
` toon.owner = ${identity.pubkey}` + (changed.owner ? "" : " (unchanged)") + (existing.owner && changed.owner ? ` (was ${existing.owner})` : "")
|
|
1595
3689
|
);
|
|
1596
3690
|
if (originRelay !== void 0) {
|
|
1597
|
-
|
|
3691
|
+
io2.out(
|
|
1598
3692
|
` origin = ${originRelay}` + (migratedToonRelay ? " (migrated from git config toon.relay)" : "")
|
|
1599
3693
|
);
|
|
1600
3694
|
if (migratedToonRelay) {
|
|
1601
|
-
|
|
3695
|
+
io2.out(
|
|
1602
3696
|
"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
3697
|
);
|
|
1604
3698
|
}
|
|
1605
|
-
|
|
3699
|
+
io2.out('Ready: `rig push` publishes this repo via remote "origin".');
|
|
1606
3700
|
} else if (existing.relays.length > 0) {
|
|
1607
|
-
|
|
1608
|
-
|
|
3701
|
+
io2.out(` toon.relay = ${existing.relays.join(", ")} (deprecated)`);
|
|
3702
|
+
io2.out(
|
|
1609
3703
|
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
3704
|
);
|
|
1611
3705
|
} else if (origin !== void 0) {
|
|
1612
|
-
|
|
3706
|
+
io2.out(
|
|
1613
3707
|
`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
3708
|
);
|
|
1615
3709
|
} else {
|
|
1616
|
-
|
|
3710
|
+
io2.out(
|
|
1617
3711
|
"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
3712
|
);
|
|
1619
3713
|
}
|
|
1620
3714
|
return 0;
|
|
1621
3715
|
} catch (err) {
|
|
1622
|
-
|
|
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;
|
|
3716
|
+
return emitCliError(io2, flags.json, "init", err);
|
|
1629
3717
|
}
|
|
1630
3718
|
}
|
|
1631
3719
|
|
|
@@ -1640,26 +3728,58 @@ Commands rig owns:
|
|
|
1640
3728
|
remote add <name> <url> add a relay as a REAL git remote ("origin" is
|
|
1641
3729
|
remote remove <name> the default publish target); remove/list manage
|
|
1642
3730
|
remote list them \u2014 \`git remote -v\` shows the same data
|
|
3731
|
+
clone <relay-url> <owner>/<repo-id> [dir]
|
|
3732
|
+
clone a TOON repo (free): relay state + Arweave
|
|
3733
|
+
objects (SHA-1 verified) \u2192 a real git repository,
|
|
3734
|
+
push/pull-capable out of the box
|
|
3735
|
+
fetch [remote] fetch remote refs + missing objects (free) and
|
|
3736
|
+
update refs/remotes/<remote>/*; no merge. Shadows
|
|
3737
|
+
git fetch (plain \`git fetch\` stays available)
|
|
1643
3738
|
push [remote] [refspecs...] plan, price, confirm, and execute a paid push
|
|
1644
3739
|
to TOON (defaults to the "origin" remote). rig
|
|
1645
3740
|
push is the TOON transport and shadows git push;
|
|
1646
3741
|
plain-git pushes remain available by running
|
|
1647
3742
|
\`git push\` directly
|
|
1648
3743
|
issue create file an issue (kind:1621) against a repo
|
|
3744
|
+
issue list | show <id> read the repo's issues + comments (free)
|
|
3745
|
+
pr list | show <id> read the repo's patches; show prints the full
|
|
3746
|
+
patch text (free)
|
|
1649
3747
|
comment <root-event-id> comment (kind:1622) on an issue or patch
|
|
1650
3748
|
pr create publish a patch (kind:1617) with real
|
|
1651
3749
|
\`git format-patch\` content
|
|
1652
3750
|
pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
|
|
1653
3751
|
open | applied | closed | draft
|
|
3752
|
+
fund drip devnet faucet funds to this identity's
|
|
3753
|
+
wallet (free); on other networks prints the
|
|
3754
|
+
address(es) to fund externally
|
|
3755
|
+
balance wallet balances + payment-channel holdings
|
|
3756
|
+
(free \u2014 chain reads and local state only)
|
|
3757
|
+
channel list show the payment channels paid commands hold
|
|
3758
|
+
(free \u2014 reads local state)
|
|
3759
|
+
channel open explicitly open (or resume) the channel for a
|
|
3760
|
+
peer; --deposit adds collateral (on-chain)
|
|
3761
|
+
channel close <channelId> start the settlement challenge window (on-chain)
|
|
3762
|
+
channel settle <channelId> release collateral after the window (on-chain)
|
|
1654
3763
|
|
|
1655
3764
|
Any other command is passed through to git verbatim: \`rig status\` runs
|
|
1656
3765
|
\`git status\`, and \`rig add -p\`, \`rig commit\`, \`rig log --oneline\`,
|
|
1657
3766
|
\`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
|
|
1658
3767
|
|
|
1659
|
-
Run \`rig <command> --help\` for a rig command's flags. \`rig init
|
|
1660
|
-
\`rig remote\`
|
|
1661
|
-
|
|
1662
|
-
|
|
3768
|
+
Run \`rig <command> --help\` for a rig command's flags. \`rig init\`,
|
|
3769
|
+
\`rig remote\`, \`rig clone\`, \`rig fetch\`, \`rig issue list/show\`,
|
|
3770
|
+
\`rig pr list/show\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\`
|
|
3771
|
+
are free; push/issue create/comment/pr create/pr status are paid writes \u2014
|
|
3772
|
+
permanent and non-refundable \u2014
|
|
3773
|
+
and channel open/close/settle are on-chain wallet transactions; each states
|
|
3774
|
+
what it will spend and asks for confirmation before doing so (--yes skips,
|
|
3775
|
+
--json emits machine output).
|
|
3776
|
+
|
|
3777
|
+
With --json, stdout carries exactly ONE JSON document (everything human-facing
|
|
3778
|
+
goes to stderr), so \`rig <command> --json | jq\` always parses. --json is a
|
|
3779
|
+
per-subcommand flag on the commands rig owns, NOT a global rig flag: it does
|
|
3780
|
+
not apply to the git passthrough (\`rig status --json\` runs
|
|
3781
|
+
\`git status --json\`), and flags placed before the subcommand
|
|
3782
|
+
(\`rig --json status\`) pass through to git untouched.`;
|
|
1663
3783
|
function rigVersion() {
|
|
1664
3784
|
const require2 = createRequire(import.meta.url);
|
|
1665
3785
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
@@ -1671,14 +3791,19 @@ function rigVersion() {
|
|
|
1671
3791
|
}
|
|
1672
3792
|
return "unknown";
|
|
1673
3793
|
}
|
|
1674
|
-
async function dispatch(
|
|
1675
|
-
const [command, ...rest] =
|
|
1676
|
-
const { io } = deps;
|
|
3794
|
+
async function dispatch(argv2, deps) {
|
|
3795
|
+
const [command, ...rest] = argv2;
|
|
3796
|
+
const { io: io2 } = deps;
|
|
1677
3797
|
switch (command) {
|
|
1678
3798
|
case "init":
|
|
1679
3799
|
return runInit(rest, deps);
|
|
1680
3800
|
case "remote":
|
|
1681
3801
|
return runRemote(rest, deps);
|
|
3802
|
+
// The #278 read path — FREE (relay + Arweave gateway reads, no payment).
|
|
3803
|
+
case "clone":
|
|
3804
|
+
return runClone(rest, deps);
|
|
3805
|
+
case "fetch":
|
|
3806
|
+
return runFetch(rest, deps);
|
|
1682
3807
|
case "push":
|
|
1683
3808
|
return runPush(rest, deps);
|
|
1684
3809
|
case "issue":
|
|
@@ -1687,35 +3812,150 @@ async function dispatch(argv, deps) {
|
|
|
1687
3812
|
return runComment(rest, deps);
|
|
1688
3813
|
case "pr":
|
|
1689
3814
|
return runPr(rest, deps);
|
|
3815
|
+
case "channel":
|
|
3816
|
+
return runChannel(rest, deps);
|
|
3817
|
+
case "fund":
|
|
3818
|
+
return runFund(rest, deps);
|
|
3819
|
+
case "balance":
|
|
3820
|
+
return runBalance(rest, deps);
|
|
1690
3821
|
case "help":
|
|
1691
3822
|
case "--help":
|
|
1692
3823
|
case "-h":
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3824
|
+
io2.out(USAGE);
|
|
3825
|
+
io2.out("");
|
|
3826
|
+
io2.out(PUSH_USAGE);
|
|
1696
3827
|
return 0;
|
|
1697
3828
|
case "--version":
|
|
1698
|
-
|
|
3829
|
+
io2.out(`rig ${rigVersion()}`);
|
|
1699
3830
|
return 0;
|
|
1700
3831
|
case void 0:
|
|
1701
|
-
|
|
3832
|
+
io2.err(USAGE);
|
|
1702
3833
|
return 2;
|
|
1703
3834
|
default:
|
|
1704
|
-
return (deps.runGit ?? runGitPassthrough)(
|
|
3835
|
+
return (deps.runGit ?? runGitPassthrough)(argv2, {
|
|
1705
3836
|
cwd: deps.cwd,
|
|
1706
3837
|
env: deps.env,
|
|
1707
|
-
err: (line) =>
|
|
3838
|
+
err: (line) => io2.err(line)
|
|
1708
3839
|
});
|
|
1709
3840
|
}
|
|
1710
3841
|
}
|
|
1711
3842
|
|
|
1712
|
-
// src/cli/
|
|
1713
|
-
function
|
|
3843
|
+
// src/cli/output.ts
|
|
3844
|
+
function makeCliIo(options) {
|
|
3845
|
+
const { jsonMode, writeStdout, writeStderr } = options;
|
|
3846
|
+
let emittedJson = false;
|
|
3847
|
+
const humanLines = [];
|
|
3848
|
+
const toStderr = (line) => {
|
|
3849
|
+
if (jsonMode) humanLines.push(line);
|
|
3850
|
+
writeStderr(`${line}
|
|
3851
|
+
`);
|
|
3852
|
+
};
|
|
3853
|
+
return {
|
|
3854
|
+
jsonMode,
|
|
3855
|
+
get emittedJson() {
|
|
3856
|
+
return emittedJson;
|
|
3857
|
+
},
|
|
3858
|
+
out: (line) => {
|
|
3859
|
+
if (jsonMode) {
|
|
3860
|
+
toStderr(line);
|
|
3861
|
+
} else {
|
|
3862
|
+
writeStdout(`${line}
|
|
3863
|
+
`);
|
|
3864
|
+
}
|
|
3865
|
+
},
|
|
3866
|
+
err: toStderr,
|
|
3867
|
+
emitJson: (payload) => {
|
|
3868
|
+
emittedJson = true;
|
|
3869
|
+
writeStdout(`${JSON.stringify(payload, null, 2)}
|
|
3870
|
+
`);
|
|
3871
|
+
},
|
|
3872
|
+
isInteractive: options.isInteractive,
|
|
3873
|
+
confirm: options.confirm,
|
|
3874
|
+
ensureSingleJsonDoc(exitCode) {
|
|
3875
|
+
if (!jsonMode || emittedJson) return;
|
|
3876
|
+
const detail = humanLines.join("\n") || (exitCode === 0 ? "the command produced no machine output" : "the command failed before emitting machine output \u2014 see stderr");
|
|
3877
|
+
this.emitJson(
|
|
3878
|
+
exitCode === 0 ? { error: null, exitCode, detail } : { error: "error", exitCode, detail }
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
|
|
3884
|
+
"init",
|
|
3885
|
+
"remote",
|
|
3886
|
+
"clone",
|
|
3887
|
+
"fetch",
|
|
3888
|
+
"push",
|
|
3889
|
+
"issue",
|
|
3890
|
+
"comment",
|
|
3891
|
+
"pr",
|
|
3892
|
+
"channel",
|
|
3893
|
+
"fund",
|
|
3894
|
+
"balance"
|
|
3895
|
+
]);
|
|
3896
|
+
function isJsonInvocation(argv2) {
|
|
3897
|
+
const [verb, ...rest] = argv2;
|
|
3898
|
+
if (verb === void 0 || !RIG_OWNED_VERBS.has(verb)) return false;
|
|
3899
|
+
for (const arg of rest) {
|
|
3900
|
+
if (arg === "--") return false;
|
|
3901
|
+
if (arg === "--json") return true;
|
|
3902
|
+
}
|
|
3903
|
+
return false;
|
|
3904
|
+
}
|
|
3905
|
+
var ANNOUNCE_FAILED_RE = /\[Bootstrap\] Announce failed/;
|
|
3906
|
+
var ANNOUNCE_402_RE = /402|payment required/i;
|
|
3907
|
+
var ANNOUNCE_402_INFO = "rig: skipped the optional identity self-announce \u2014 the payment peer charges for announces and this identity has not paid for one (expected on a fresh or unfunded identity); harmless, the command continues.\n";
|
|
3908
|
+
function calmBootstrapNoise() {
|
|
3909
|
+
const original = process.stderr.write;
|
|
3910
|
+
let reframed = false;
|
|
3911
|
+
const patched = ((chunk, encodingOrCb, cb) => {
|
|
3912
|
+
let text = chunk;
|
|
3913
|
+
if (typeof chunk === "string" && ANNOUNCE_FAILED_RE.test(chunk)) {
|
|
3914
|
+
if (ANNOUNCE_402_RE.test(chunk)) {
|
|
3915
|
+
text = reframed ? "" : ANNOUNCE_402_INFO;
|
|
3916
|
+
reframed = true;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return typeof encodingOrCb === "function" ? original.call(process.stderr, text, encodingOrCb) : original.call(process.stderr, text, encodingOrCb, cb);
|
|
3920
|
+
});
|
|
3921
|
+
process.stderr.write = patched;
|
|
3922
|
+
return {
|
|
3923
|
+
restore: () => {
|
|
3924
|
+
if (process.stderr.write === patched) process.stderr.write = original;
|
|
3925
|
+
}
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
function redirectStdoutToStderr(realWrite) {
|
|
3929
|
+
const original = process.stdout.write;
|
|
3930
|
+
const real = realWrite ?? original.bind(process.stdout);
|
|
3931
|
+
const patched = ((chunk, encodingOrCb, cb) => typeof encodingOrCb === "function" ? process.stderr.write(chunk, encodingOrCb) : process.stderr.write(
|
|
3932
|
+
chunk,
|
|
3933
|
+
encodingOrCb,
|
|
3934
|
+
cb
|
|
3935
|
+
));
|
|
3936
|
+
process.stdout.write = patched;
|
|
1714
3937
|
return {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
3938
|
+
write: (text) => {
|
|
3939
|
+
real(text);
|
|
3940
|
+
},
|
|
3941
|
+
restore: () => {
|
|
3942
|
+
if (process.stdout.write === patched) process.stdout.write = original;
|
|
3943
|
+
}
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// src/cli/rig.ts
|
|
3948
|
+
function makeIo(jsonMode) {
|
|
3949
|
+
calmBootstrapNoise();
|
|
3950
|
+
const guard = jsonMode ? redirectStdoutToStderr() : void 0;
|
|
3951
|
+
return makeCliIo({
|
|
3952
|
+
jsonMode,
|
|
3953
|
+
writeStdout: guard ? guard.write : (text) => {
|
|
3954
|
+
process.stdout.write(text);
|
|
3955
|
+
},
|
|
3956
|
+
writeStderr: (text) => {
|
|
3957
|
+
process.stderr.write(text);
|
|
3958
|
+
},
|
|
1719
3959
|
isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1720
3960
|
confirm: async (question) => {
|
|
1721
3961
|
const rl = createInterface({
|
|
@@ -1729,22 +3969,36 @@ function makeIo() {
|
|
|
1729
3969
|
rl.close();
|
|
1730
3970
|
}
|
|
1731
3971
|
}
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
function exitWhenFlushed(code) {
|
|
3975
|
+
process.exitCode = code;
|
|
3976
|
+
let pending = 2;
|
|
3977
|
+
const done = () => {
|
|
3978
|
+
pending -= 1;
|
|
3979
|
+
if (pending === 0) process.exit(code);
|
|
1732
3980
|
};
|
|
3981
|
+
process.stdout.write("", done);
|
|
3982
|
+
process.stderr.write("", done);
|
|
1733
3983
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
3984
|
+
var argv = process.argv.slice(2);
|
|
3985
|
+
var io = makeIo(isJsonInvocation(argv));
|
|
3986
|
+
dispatch(argv, {
|
|
3987
|
+
io,
|
|
1736
3988
|
env: process.env,
|
|
1737
3989
|
cwd: process.cwd()
|
|
1738
3990
|
}).then(
|
|
1739
3991
|
(code) => {
|
|
1740
|
-
|
|
3992
|
+
io.ensureSingleJsonDoc(code);
|
|
3993
|
+
exitWhenFlushed(code);
|
|
1741
3994
|
},
|
|
1742
3995
|
(err) => {
|
|
1743
3996
|
process.stderr.write(
|
|
1744
3997
|
`rig: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
1745
3998
|
`
|
|
1746
3999
|
);
|
|
1747
|
-
|
|
4000
|
+
io.ensureSingleJsonDoc(1);
|
|
4001
|
+
exitWhenFlushed(1);
|
|
1748
4002
|
}
|
|
1749
4003
|
);
|
|
1750
4004
|
//# sourceMappingURL=rig.js.map
|