@toon-protocol/rig 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -13
- package/dist/{chunk-PLKZAUTG.js → chunk-3HRFDH7H.js} +160 -6
- package/dist/chunk-3HRFDH7H.js.map +1 -0
- package/dist/{chunk-3EKP7PMM.js → chunk-OIJNRACA.js} +5 -144
- package/dist/chunk-OIJNRACA.js.map +1 -0
- package/dist/{chunk-O6TXHKWG.js → chunk-SW7ZHMGS.js} +143 -2
- package/dist/chunk-SW7ZHMGS.js.map +1 -0
- package/dist/{chunk-LFGDLD6J.js → chunk-W2SDL2PE.js} +464 -4
- package/dist/chunk-W2SDL2PE.js.map +1 -0
- package/dist/cli/rig.js +1954 -163
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +277 -3
- package/dist/index.js +50 -8
- package/dist/{publisher-Cdr1H1hg.d.ts → publisher-BtVceNeI.d.ts} +39 -3
- package/dist/standalone/index.d.ts +7 -1
- package/dist/standalone/index.js +9 -9
- package/dist/{standalone-mode-FCKTQ33Y.js → standalone-mode-JLGAUMRF.js} +367 -97
- package/dist/standalone-mode-JLGAUMRF.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-3EKP7PMM.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-O6TXHKWG.js.map +0 -1
- package/dist/chunk-PLKZAUTG.js.map +0 -1
- package/dist/standalone-mode-FCKTQ33Y.js.map +0 -1
package/dist/cli/rig.js
CHANGED
|
@@ -4,27 +4,42 @@ 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-W2SDL2PE.js";
|
|
13
21
|
import {
|
|
14
22
|
MissingIdentityError,
|
|
15
23
|
resolveIdentity
|
|
16
24
|
} from "../chunk-CW4HJNMU.js";
|
|
17
25
|
import {
|
|
26
|
+
COMMENT_KIND,
|
|
27
|
+
authorizedStatusAuthors,
|
|
18
28
|
buildComment,
|
|
19
29
|
buildIssue,
|
|
20
30
|
buildPatch,
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
buildRepoAnnouncement,
|
|
32
|
+
buildStatus,
|
|
33
|
+
fetchRemoteState,
|
|
34
|
+
parseMaintainers,
|
|
35
|
+
queryRelay
|
|
36
|
+
} from "../chunk-3HRFDH7H.js";
|
|
23
37
|
import {
|
|
24
38
|
ChannelMapStore,
|
|
25
39
|
channelStatus,
|
|
40
|
+
defaultDaemonPort,
|
|
26
41
|
resolveChannelPaths
|
|
27
|
-
} from "../chunk-
|
|
42
|
+
} from "../chunk-SW7ZHMGS.js";
|
|
28
43
|
import {
|
|
29
44
|
MAX_OBJECT_SIZE
|
|
30
45
|
} from "../chunk-X2CZPPDM.js";
|
|
@@ -38,6 +53,155 @@ import { createRequire } from "module";
|
|
|
38
53
|
// src/cli/balance.ts
|
|
39
54
|
import { parseArgs as parseArgs3 } from "util";
|
|
40
55
|
|
|
56
|
+
// src/cli/daemon-session.ts
|
|
57
|
+
function daemonBaseUrl(env) {
|
|
58
|
+
const raw = env["TOON_CLIENT_HTTP_PORT"];
|
|
59
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
60
|
+
const port = Number.isFinite(parsed) && parsed > 0 ? parsed : defaultDaemonPort();
|
|
61
|
+
return `http://127.0.0.1:${port}`;
|
|
62
|
+
}
|
|
63
|
+
async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
|
|
64
|
+
const baseUrl = daemonBaseUrl(env);
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetchImpl(`${baseUrl}/status`, {
|
|
67
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) return { baseUrl, reachable: false };
|
|
70
|
+
const body = await res.json();
|
|
71
|
+
const probe = { baseUrl, reachable: true };
|
|
72
|
+
const pubkey = body?.identity?.nostrPubkey;
|
|
73
|
+
if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
|
|
74
|
+
if (typeof body?.ready === "boolean") probe.ready = body.ready;
|
|
75
|
+
if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
|
|
76
|
+
probe.relayUrl = body.relay.url;
|
|
77
|
+
}
|
|
78
|
+
if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
|
|
79
|
+
probe.feePerEvent = body.feePerEvent;
|
|
80
|
+
}
|
|
81
|
+
return probe;
|
|
82
|
+
} catch {
|
|
83
|
+
return { baseUrl, reachable: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var DaemonRouteError = class extends Error {
|
|
87
|
+
constructor(status, envelope) {
|
|
88
|
+
super(envelope.detail ?? envelope.error);
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.envelope = envelope;
|
|
91
|
+
this.name = "DaemonRouteError";
|
|
92
|
+
}
|
|
93
|
+
status;
|
|
94
|
+
envelope;
|
|
95
|
+
};
|
|
96
|
+
var DaemonUnreachableError = class extends Error {
|
|
97
|
+
constructor(baseUrl, cause) {
|
|
98
|
+
super(
|
|
99
|
+
`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})` : "")
|
|
100
|
+
);
|
|
101
|
+
this.baseUrl = baseUrl;
|
|
102
|
+
this.name = "DaemonUnreachableError";
|
|
103
|
+
}
|
|
104
|
+
baseUrl;
|
|
105
|
+
};
|
|
106
|
+
var DaemonGitClient = class {
|
|
107
|
+
constructor(baseUrl, fetchImpl) {
|
|
108
|
+
this.baseUrl = baseUrl;
|
|
109
|
+
this.fetchImpl = fetchImpl;
|
|
110
|
+
}
|
|
111
|
+
baseUrl;
|
|
112
|
+
fetchImpl;
|
|
113
|
+
gitEstimate(req) {
|
|
114
|
+
return this.post("/git/estimate", req);
|
|
115
|
+
}
|
|
116
|
+
gitPush(req) {
|
|
117
|
+
return this.post("/git/push", req);
|
|
118
|
+
}
|
|
119
|
+
gitIssue(req) {
|
|
120
|
+
return this.post("/git/issue", req);
|
|
121
|
+
}
|
|
122
|
+
gitComment(req) {
|
|
123
|
+
return this.post("/git/comment", req);
|
|
124
|
+
}
|
|
125
|
+
gitPatch(req) {
|
|
126
|
+
return this.post("/git/patch", req);
|
|
127
|
+
}
|
|
128
|
+
gitStatus(req) {
|
|
129
|
+
return this.post("/git/status", req);
|
|
130
|
+
}
|
|
131
|
+
async post(path, body) {
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/json" },
|
|
137
|
+
body: JSON.stringify(body)
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw new DaemonUnreachableError(this.baseUrl, err);
|
|
141
|
+
}
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = text === "" ? {} : JSON.parse(text);
|
|
146
|
+
} catch {
|
|
147
|
+
throw new DaemonRouteError(res.status, {
|
|
148
|
+
error: "invalid_response",
|
|
149
|
+
detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
|
|
154
|
+
throw new DaemonRouteError(res.status, envelope);
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
async function resolvePaidSession(options) {
|
|
160
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
161
|
+
const probe = await (options.probeDaemon ?? probeDaemon)(
|
|
162
|
+
options.env,
|
|
163
|
+
fetchImpl
|
|
164
|
+
);
|
|
165
|
+
if (probe.reachable && probe.identity !== void 0) {
|
|
166
|
+
const identity = await resolveIdentity({
|
|
167
|
+
env: options.env,
|
|
168
|
+
cwd: options.cwd,
|
|
169
|
+
warn: options.warn
|
|
170
|
+
});
|
|
171
|
+
if (identity.pubkey === probe.identity) {
|
|
172
|
+
options.warn(
|
|
173
|
+
`rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
path: "daemon",
|
|
177
|
+
client: new DaemonGitClient(probe.baseUrl, fetchImpl),
|
|
178
|
+
baseUrl: probe.baseUrl,
|
|
179
|
+
identity: {
|
|
180
|
+
pubkey: identity.pubkey,
|
|
181
|
+
source: identity.source,
|
|
182
|
+
sourceLabel: identity.sourceLabel
|
|
183
|
+
},
|
|
184
|
+
...probe.feePerEvent !== void 0 ? { feePerEvent: probe.feePerEvent } : {},
|
|
185
|
+
...probe.relayUrl !== void 0 ? { daemonRelayUrl: probe.relayUrl } : {}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
options.warn(
|
|
189
|
+
`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`
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
options.warn(
|
|
193
|
+
`rig: paid path: standalone (no toon-clientd at ${probe.baseUrl})`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const ctx = await options.loadStandalone({
|
|
197
|
+
env: options.env,
|
|
198
|
+
cwd: options.cwd,
|
|
199
|
+
warn: options.warn,
|
|
200
|
+
...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
|
|
201
|
+
});
|
|
202
|
+
return { path: "standalone", ctx };
|
|
203
|
+
}
|
|
204
|
+
|
|
41
205
|
// src/cli/errors.ts
|
|
42
206
|
var UnconfiguredRepoAddressError = class extends Error {
|
|
43
207
|
constructor(missing) {
|
|
@@ -199,6 +363,39 @@ function describeError(err, command = "push") {
|
|
|
199
363
|
json: { error: "git_error", detail: err.message }
|
|
200
364
|
};
|
|
201
365
|
}
|
|
366
|
+
if (err instanceof DaemonRouteError) {
|
|
367
|
+
const envelope = err.envelope;
|
|
368
|
+
if (envelope.error === "non_fast_forward" && Array.isArray(envelope["refs"])) {
|
|
369
|
+
return {
|
|
370
|
+
code: "non_fast_forward",
|
|
371
|
+
lines: nonFastForwardLines(envelope["refs"]),
|
|
372
|
+
json: { ...envelope }
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (envelope.error === "oversize_objects" && Array.isArray(envelope["objects"])) {
|
|
376
|
+
return {
|
|
377
|
+
code: "oversize_objects",
|
|
378
|
+
lines: oversizeLines(envelope["objects"]),
|
|
379
|
+
json: { ...envelope }
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const retryHint = envelope.retryable === true ? ["The daemon reports this as retryable \u2014 re-run shortly."] : [];
|
|
383
|
+
return {
|
|
384
|
+
code: envelope.error,
|
|
385
|
+
lines: [
|
|
386
|
+
`daemon rejected the operation (HTTP ${err.status}): ` + (envelope.detail ?? envelope.error),
|
|
387
|
+
...retryHint
|
|
388
|
+
],
|
|
389
|
+
json: { ...envelope }
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (err instanceof DaemonUnreachableError) {
|
|
393
|
+
return {
|
|
394
|
+
code: "daemon_unreachable",
|
|
395
|
+
lines: err.message.split("\n"),
|
|
396
|
+
json: { error: "daemon_unreachable", detail: err.message }
|
|
397
|
+
};
|
|
398
|
+
}
|
|
202
399
|
const name = err instanceof Error ? err.name : "";
|
|
203
400
|
const message = err instanceof Error ? err.message : String(err);
|
|
204
401
|
if (name === "DaemonIdentityConflictError") {
|
|
@@ -206,7 +403,7 @@ function describeError(err, command = "push") {
|
|
|
206
403
|
code: "daemon_identity_conflict",
|
|
207
404
|
lines: [
|
|
208
405
|
message,
|
|
209
|
-
"
|
|
406
|
+
"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."
|
|
210
407
|
],
|
|
211
408
|
json: { error: "daemon_identity_conflict", detail: message }
|
|
212
409
|
};
|
|
@@ -240,6 +437,32 @@ function describeError(err, command = "push") {
|
|
|
240
437
|
}
|
|
241
438
|
};
|
|
242
439
|
}
|
|
440
|
+
if (name === "RepoNotFoundError") {
|
|
441
|
+
return {
|
|
442
|
+
code: "repo_not_found",
|
|
443
|
+
lines: message.split("\n"),
|
|
444
|
+
json: { error: "repo_not_found", detail: message }
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (name === "MissingRemoteObjectsError") {
|
|
448
|
+
const missing = err.missing;
|
|
449
|
+
return {
|
|
450
|
+
code: "missing_remote_objects",
|
|
451
|
+
lines: message.split("\n"),
|
|
452
|
+
json: {
|
|
453
|
+
error: "missing_remote_objects",
|
|
454
|
+
detail: message,
|
|
455
|
+
...Array.isArray(missing) ? { missing } : {}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (name === "ObjectIntegrityError" || name === "ObjectWriteMismatchError") {
|
|
460
|
+
return {
|
|
461
|
+
code: "object_integrity",
|
|
462
|
+
lines: message.split("\n"),
|
|
463
|
+
json: { error: "object_integrity", detail: message }
|
|
464
|
+
};
|
|
465
|
+
}
|
|
243
466
|
return {
|
|
244
467
|
code: "error",
|
|
245
468
|
lines: [`rig ${command} failed: ${message}`],
|
|
@@ -665,8 +888,19 @@ async function runRemote(args, deps) {
|
|
|
665
888
|
}
|
|
666
889
|
|
|
667
890
|
// src/cli/push.ts
|
|
891
|
+
function loadPaidSession(deps, relayUrl) {
|
|
892
|
+
return resolvePaidSession({
|
|
893
|
+
env: deps.env,
|
|
894
|
+
cwd: deps.cwd,
|
|
895
|
+
warn: (line) => deps.io.err(line),
|
|
896
|
+
loadStandalone: deps.loadStandalone ?? defaultLoadStandalone,
|
|
897
|
+
...relayUrl !== void 0 ? { relayUrl } : {},
|
|
898
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
899
|
+
...deps.probeDaemon ? { probeDaemon: deps.probeDaemon } : {}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
668
902
|
var defaultLoadStandalone = async (options) => {
|
|
669
|
-
const mod = await import("../standalone-mode-
|
|
903
|
+
const mod = await import("../standalone-mode-JLGAUMRF.js");
|
|
670
904
|
return mod.createStandaloneContext(options);
|
|
671
905
|
};
|
|
672
906
|
function identityReport(ctx) {
|
|
@@ -777,7 +1011,7 @@ function parsePushArgs(args) {
|
|
|
777
1011
|
return flags;
|
|
778
1012
|
}
|
|
779
1013
|
async function runPush(args, deps) {
|
|
780
|
-
const { io: io2
|
|
1014
|
+
const { io: io2 } = deps;
|
|
781
1015
|
let flags;
|
|
782
1016
|
try {
|
|
783
1017
|
flags = parsePushArgs(args);
|
|
@@ -835,40 +1069,74 @@ async function runPush(args, deps) {
|
|
|
835
1069
|
io2.err(singleRelayRefusal(resolved, "Nothing was uploaded or paid."));
|
|
836
1070
|
return 1;
|
|
837
1071
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
1072
|
+
const session = await loadPaidSession(deps, relaysUsed[0]);
|
|
1073
|
+
const path = session.path;
|
|
1074
|
+
let identity;
|
|
1075
|
+
if (session.path === "standalone") {
|
|
1076
|
+
standaloneCtx = session.ctx;
|
|
1077
|
+
identity = identityReport(standaloneCtx);
|
|
1078
|
+
} else {
|
|
1079
|
+
identity = session.identity;
|
|
1080
|
+
}
|
|
846
1081
|
if (toonConfig.owner && toonConfig.owner !== identity.pubkey) {
|
|
847
1082
|
io2.err(
|
|
848
1083
|
`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.`
|
|
849
1084
|
);
|
|
850
1085
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
feeRates
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1086
|
+
let plan;
|
|
1087
|
+
let execute;
|
|
1088
|
+
if (session.path === "standalone") {
|
|
1089
|
+
const ctx = session.ctx;
|
|
1090
|
+
const remoteState = await ctx.fetchRemote({
|
|
1091
|
+
ownerPubkey: ctx.ownerPubkey,
|
|
1092
|
+
repoId,
|
|
1093
|
+
relayUrls: relaysUsed
|
|
1094
|
+
});
|
|
1095
|
+
const feeRates = await ctx.publisher.getFeeRates();
|
|
1096
|
+
const pushPlan = await planPush({
|
|
1097
|
+
repoReader: reader,
|
|
1098
|
+
remoteState,
|
|
1099
|
+
feeRates,
|
|
1100
|
+
repoId,
|
|
1101
|
+
refs: refspecs,
|
|
1102
|
+
force: flags.force
|
|
1103
|
+
});
|
|
1104
|
+
plan = serializePushPlan(pushPlan);
|
|
1105
|
+
execute = async () => serializePushResult(
|
|
1106
|
+
pushPlan,
|
|
1107
|
+
await executePush({
|
|
1108
|
+
plan: pushPlan,
|
|
1109
|
+
publisher: ctx.publisher,
|
|
1110
|
+
remoteState,
|
|
1111
|
+
repoReader: reader,
|
|
1112
|
+
relayUrls: relaysUsed
|
|
1113
|
+
})
|
|
1114
|
+
);
|
|
1115
|
+
} else {
|
|
1116
|
+
const client = session.client;
|
|
1117
|
+
plan = await client.gitEstimate({
|
|
1118
|
+
repoPath: repoRoot,
|
|
1119
|
+
repoId,
|
|
1120
|
+
refspecs,
|
|
1121
|
+
force: flags.force,
|
|
1122
|
+
relayUrls: relaysUsed
|
|
1123
|
+
});
|
|
1124
|
+
execute = () => client.gitPush({
|
|
1125
|
+
repoPath: repoRoot,
|
|
1126
|
+
repoId,
|
|
1127
|
+
refspecs,
|
|
1128
|
+
force: flags.force,
|
|
1129
|
+
relayUrls: relaysUsed,
|
|
1130
|
+
confirm: true
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
866
1133
|
const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
|
|
867
1134
|
if (upToDate) {
|
|
868
1135
|
if (flags.json) {
|
|
869
1136
|
io2.emitJson({
|
|
870
1137
|
command: "push",
|
|
871
1138
|
repoId,
|
|
1139
|
+
path,
|
|
872
1140
|
identity,
|
|
873
1141
|
executed: false,
|
|
874
1142
|
upToDate: true,
|
|
@@ -888,6 +1156,7 @@ async function runPush(args, deps) {
|
|
|
888
1156
|
io2.emitJson({
|
|
889
1157
|
command: "push",
|
|
890
1158
|
repoId,
|
|
1159
|
+
path,
|
|
891
1160
|
identity,
|
|
892
1161
|
executed: false,
|
|
893
1162
|
upToDate: false,
|
|
@@ -910,18 +1179,12 @@ async function runPush(args, deps) {
|
|
|
910
1179
|
return 1;
|
|
911
1180
|
}
|
|
912
1181
|
}
|
|
913
|
-
const
|
|
914
|
-
plan: pushPlan,
|
|
915
|
-
publisher: standaloneCtx.publisher,
|
|
916
|
-
remoteState,
|
|
917
|
-
repoReader: reader,
|
|
918
|
-
relayUrls: relaysUsed
|
|
919
|
-
});
|
|
920
|
-
const result = serializePushResult(pushPlan, pushResult);
|
|
1182
|
+
const result = await execute();
|
|
921
1183
|
if (flags.json) {
|
|
922
1184
|
io2.emitJson({
|
|
923
1185
|
command: "push",
|
|
924
1186
|
repoId,
|
|
1187
|
+
path,
|
|
925
1188
|
identity,
|
|
926
1189
|
executed: true,
|
|
927
1190
|
upToDate: false,
|
|
@@ -1210,7 +1473,7 @@ async function loadMoneyContext(deps, options) {
|
|
|
1210
1473
|
if (!money) {
|
|
1211
1474
|
await ctx.stop().catch(() => void 0);
|
|
1212
1475
|
throw new Error(
|
|
1213
|
-
"this standalone loader does not expose money operations \u2014 channel open/close/settle need the
|
|
1476
|
+
"this standalone loader does not expose money operations \u2014 channel open/close/settle need a loader with the money lifecycle"
|
|
1214
1477
|
);
|
|
1215
1478
|
}
|
|
1216
1479
|
return { ctx, money };
|
|
@@ -1519,78 +1782,838 @@ async function stopQuietly(ctx) {
|
|
|
1519
1782
|
}
|
|
1520
1783
|
}
|
|
1521
1784
|
|
|
1785
|
+
// src/cli/clone.ts
|
|
1786
|
+
import { mkdtemp, mkdir, readdir, rename, rm, rmdir } from "fs/promises";
|
|
1787
|
+
import { basename, dirname, join, resolve } from "path";
|
|
1788
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1789
|
+
var CLONE_USAGE = `Usage: rig clone <relay-url> <owner>/<repo-id> [dir] [options]
|
|
1790
|
+
|
|
1791
|
+
Clone a TOON repository \u2014 FREE (relay reads + Arweave gateway downloads; no
|
|
1792
|
+
payments, no channel, no identity needed). <relay-url> is a ws:// or wss://
|
|
1793
|
+
NIP-01 relay; <owner> is the repo owner's pubkey as npub1\u2026 or 64-char hex;
|
|
1794
|
+
[dir] defaults to <repo-id>.
|
|
1795
|
+
|
|
1796
|
+
Fetches the kind:30617/30618 repository state, downloads every git object
|
|
1797
|
+
the refs need from Arweave gateways (SHA-1 verified \u2014 corrupt content is
|
|
1798
|
+
rejected), and materializes a real git repository: objects via git plumbing,
|
|
1799
|
+
refs, HEAD, checked-out worktree, toon.* config, and the relay preconfigured
|
|
1800
|
+
as the "origin" remote \u2014 so \`rig fetch\`, \`rig push\`, and the issue/pr
|
|
1801
|
+
commands work immediately.
|
|
1802
|
+
|
|
1803
|
+
Note: recently pushed objects can take 10-20 minutes to become fetchable
|
|
1804
|
+
from Arweave gateways. A clone right after a push may report missing
|
|
1805
|
+
objects \u2014 retry after propagation.
|
|
1806
|
+
|
|
1807
|
+
Options:
|
|
1808
|
+
--concurrency <n> parallel gateway downloads (default 8)
|
|
1809
|
+
--json machine-readable result envelope
|
|
1810
|
+
-h, --help show this help`;
|
|
1811
|
+
var RepoNotFoundError = class extends Error {
|
|
1812
|
+
constructor(relay, owner, repoId) {
|
|
1813
|
+
super(
|
|
1814
|
+
`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.`
|
|
1815
|
+
);
|
|
1816
|
+
this.name = "RepoNotFoundError";
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
var MissingRemoteObjectsError = class extends Error {
|
|
1820
|
+
constructor(missing, context) {
|
|
1821
|
+
super(missingObjectsMessage(missing, context));
|
|
1822
|
+
this.missing = missing;
|
|
1823
|
+
this.name = "MissingRemoteObjectsError";
|
|
1824
|
+
}
|
|
1825
|
+
missing;
|
|
1826
|
+
};
|
|
1827
|
+
var WS_URL_RE = /^wss?:\/\//i;
|
|
1828
|
+
function parseCloneArgs(args) {
|
|
1829
|
+
const { values, positionals } = parseArgs5({
|
|
1830
|
+
args,
|
|
1831
|
+
options: {
|
|
1832
|
+
json: { type: "boolean", default: false },
|
|
1833
|
+
concurrency: { type: "string" },
|
|
1834
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1835
|
+
},
|
|
1836
|
+
allowPositionals: true
|
|
1837
|
+
});
|
|
1838
|
+
if (values.help) return { help: true };
|
|
1839
|
+
if (positionals.length < 2 || positionals.length > 3) {
|
|
1840
|
+
throw new Error("expected: rig clone <relay-url> <owner>/<repo-id> [dir]");
|
|
1841
|
+
}
|
|
1842
|
+
const [relayUrl, addr, dir] = positionals;
|
|
1843
|
+
if (!WS_URL_RE.test(relayUrl)) {
|
|
1844
|
+
throw new Error(
|
|
1845
|
+
`<relay-url> must be ws:// or wss:// (got ${JSON.stringify(relayUrl)})`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const slash = addr.indexOf("/");
|
|
1849
|
+
if (slash <= 0 || slash === addr.length - 1) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`expected <owner>/<repo-id> (npub or 64-char hex owner), got ${JSON.stringify(addr)}`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const owner = ownerToHex(addr.slice(0, slash));
|
|
1855
|
+
const repoId = addr.slice(slash + 1);
|
|
1856
|
+
let concurrency;
|
|
1857
|
+
if (values.concurrency !== void 0) {
|
|
1858
|
+
concurrency = Number.parseInt(values.concurrency, 10);
|
|
1859
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
1860
|
+
throw new Error(`--concurrency must be a positive integer`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
relayUrl,
|
|
1865
|
+
owner,
|
|
1866
|
+
repoId,
|
|
1867
|
+
dir,
|
|
1868
|
+
concurrency,
|
|
1869
|
+
json: values.json === true
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
async function isUsableDestination(dir) {
|
|
1873
|
+
try {
|
|
1874
|
+
const entries = await readdir(dir);
|
|
1875
|
+
return entries.length === 0;
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
return err.code === "ENOENT";
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function initialBranch(headSymref) {
|
|
1881
|
+
if (headSymref?.startsWith("refs/heads/")) {
|
|
1882
|
+
const name = headSymref.slice("refs/heads/".length);
|
|
1883
|
+
if (name && isSafeRefname(headSymref)) return name;
|
|
1884
|
+
}
|
|
1885
|
+
return "main";
|
|
1886
|
+
}
|
|
1887
|
+
async function runClone(args, deps) {
|
|
1888
|
+
const { io: io2 } = deps;
|
|
1889
|
+
let parsed;
|
|
1890
|
+
try {
|
|
1891
|
+
const result = parseCloneArgs(args);
|
|
1892
|
+
if ("help" in result) {
|
|
1893
|
+
io2.out(CLONE_USAGE);
|
|
1894
|
+
return 0;
|
|
1895
|
+
}
|
|
1896
|
+
parsed = result;
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
1899
|
+
io2.err(CLONE_USAGE);
|
|
1900
|
+
return 2;
|
|
1901
|
+
}
|
|
1902
|
+
const { relayUrl, owner, repoId, json } = parsed;
|
|
1903
|
+
const dest = resolve(deps.cwd, parsed.dir ?? repoId);
|
|
1904
|
+
let tempDir;
|
|
1905
|
+
try {
|
|
1906
|
+
if (!await isUsableDestination(dest)) {
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`destination path ${JSON.stringify(dest)} already exists and is not an empty directory`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
if (!json) io2.out(`Cloning into '${basename(dest)}'...`);
|
|
1912
|
+
const remoteState = await fetchRemoteState({
|
|
1913
|
+
relayUrls: [relayUrl],
|
|
1914
|
+
ownerPubkey: owner,
|
|
1915
|
+
repoId,
|
|
1916
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
|
|
1917
|
+
...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
|
|
1918
|
+
});
|
|
1919
|
+
if (!remoteState.announced && remoteState.refsEvent === null) {
|
|
1920
|
+
throw new RepoNotFoundError(relayUrl, owner, repoId);
|
|
1921
|
+
}
|
|
1922
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1923
|
+
for (const [refname, sha] of remoteState.refs) {
|
|
1924
|
+
if (!isSafeRefname(refname)) {
|
|
1925
|
+
io2.err(
|
|
1926
|
+
`warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
|
|
1927
|
+
);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
refs.set(refname, sha);
|
|
1931
|
+
}
|
|
1932
|
+
if (refs.size === 0) {
|
|
1933
|
+
io2.err(
|
|
1934
|
+
"warning: the repository has no refs yet (announced but never pushed) \u2014 cloning an empty repository"
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
const tips = [...new Set(refs.values())];
|
|
1938
|
+
const collected = await collectRepoObjects({
|
|
1939
|
+
tips,
|
|
1940
|
+
shaToTxId: remoteState.shaToTxId,
|
|
1941
|
+
resolveMissing: (shas) => remoteState.resolveMissing(shas),
|
|
1942
|
+
...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
|
|
1943
|
+
...parsed.concurrency !== void 0 ? { concurrency: parsed.concurrency } : {}
|
|
1944
|
+
});
|
|
1945
|
+
if (collected.missing.length > 0) {
|
|
1946
|
+
throw new MissingRemoteObjectsError(
|
|
1947
|
+
collected.missing,
|
|
1948
|
+
`cannot clone ${owner.slice(0, 8)}\u2026/${repoId}`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
for (const { sha, txId } of collected.skippedUnavailable) {
|
|
1952
|
+
io2.err(
|
|
1953
|
+
`warning: unreachable object ${sha} (tx ${txId}) could not be downloaded \u2014 not needed by any ref, continuing`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
const parent = dirname(dest);
|
|
1957
|
+
await mkdir(parent, { recursive: true });
|
|
1958
|
+
tempDir = await mkdtemp(join(parent, `.${basename(dest)}.rig-clone-`));
|
|
1959
|
+
await runGit(tempDir, [
|
|
1960
|
+
"init",
|
|
1961
|
+
"--quiet",
|
|
1962
|
+
`--initial-branch=${initialBranch(remoteState.headSymref)}`
|
|
1963
|
+
]);
|
|
1964
|
+
const written = await writeGitObjects(tempDir, collected.objects.values());
|
|
1965
|
+
for (const [refname, sha] of refs) {
|
|
1966
|
+
await updateRef(tempDir, refname, sha);
|
|
1967
|
+
if (refname.startsWith("refs/heads/")) {
|
|
1968
|
+
const branch = refname.slice("refs/heads/".length);
|
|
1969
|
+
await updateRef(tempDir, `refs/remotes/origin/${branch}`, sha);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const head = remoteState.headSymref !== null && isSafeRefname(remoteState.headSymref) && refs.has(remoteState.headSymref) ? remoteState.headSymref : [...refs.keys()].find((r) => r.startsWith("refs/heads/")) ?? null;
|
|
1973
|
+
if (head !== null) {
|
|
1974
|
+
await setHeadSymref(tempDir, head);
|
|
1975
|
+
await runGit(tempDir, ["reset", "--hard", "--quiet"]);
|
|
1976
|
+
}
|
|
1977
|
+
await runGit(tempDir, ["config", "toon.repoid", repoId]);
|
|
1978
|
+
await runGit(tempDir, ["config", "toon.owner", owner]);
|
|
1979
|
+
await runGit(tempDir, ["remote", "add", "origin", relayUrl]);
|
|
1980
|
+
if (head !== null) {
|
|
1981
|
+
const branch = head.slice("refs/heads/".length);
|
|
1982
|
+
await runGit(tempDir, ["config", `branch.${branch}.remote`, "origin"]);
|
|
1983
|
+
await runGit(tempDir, ["config", `branch.${branch}.merge`, head]);
|
|
1984
|
+
}
|
|
1985
|
+
try {
|
|
1986
|
+
await rmdir(dest);
|
|
1987
|
+
} catch {
|
|
1988
|
+
}
|
|
1989
|
+
await rename(tempDir, dest);
|
|
1990
|
+
tempDir = void 0;
|
|
1991
|
+
if (json) {
|
|
1992
|
+
io2.emitJson({
|
|
1993
|
+
command: "clone",
|
|
1994
|
+
repoAddr: { ownerPubkey: owner, repoId },
|
|
1995
|
+
relay: relayUrl,
|
|
1996
|
+
directory: dest,
|
|
1997
|
+
head,
|
|
1998
|
+
refs: Object.fromEntries(refs),
|
|
1999
|
+
name: remoteState.name,
|
|
2000
|
+
objectsDownloaded: written,
|
|
2001
|
+
executed: true
|
|
2002
|
+
});
|
|
2003
|
+
} else {
|
|
2004
|
+
io2.out(`Downloaded ${written} object(s) from Arweave (SHA-1 verified).`);
|
|
2005
|
+
for (const [refname, sha] of refs) {
|
|
2006
|
+
io2.out(` ${sha.slice(0, 7)} ${refname}`);
|
|
2007
|
+
}
|
|
2008
|
+
if (head !== null) io2.out(`HEAD is now at ${head}`);
|
|
2009
|
+
io2.out(
|
|
2010
|
+
`Configured toon.repoid=${repoId}, toon.owner=${owner.slice(0, 8)}\u2026, origin \u2192 ${relayUrl}`
|
|
2011
|
+
);
|
|
2012
|
+
io2.out("Done.");
|
|
2013
|
+
}
|
|
2014
|
+
return 0;
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
return emitCliError(io2, json, "clone", err);
|
|
2017
|
+
} finally {
|
|
2018
|
+
if (tempDir !== void 0) {
|
|
2019
|
+
await rm(tempDir, { recursive: true, force: true }).catch(
|
|
2020
|
+
() => void 0
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1522
2026
|
// src/cli/events.ts
|
|
1523
2027
|
import { readFile } from "fs/promises";
|
|
1524
|
-
import { parseArgs as
|
|
2028
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
2029
|
+
import {
|
|
2030
|
+
REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2,
|
|
2031
|
+
STATUS_APPLIED_KIND as STATUS_APPLIED_KIND2,
|
|
2032
|
+
STATUS_CLOSED_KIND as STATUS_CLOSED_KIND2,
|
|
2033
|
+
STATUS_DRAFT_KIND as STATUS_DRAFT_KIND2,
|
|
2034
|
+
STATUS_OPEN_KIND as STATUS_OPEN_KIND2
|
|
2035
|
+
} from "@toon-protocol/core/nip34";
|
|
2036
|
+
|
|
2037
|
+
// src/cli/tracker.ts
|
|
2038
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
1525
2039
|
import {
|
|
2040
|
+
ISSUE_KIND,
|
|
2041
|
+
PATCH_KIND,
|
|
1526
2042
|
REPOSITORY_ANNOUNCEMENT_KIND,
|
|
1527
2043
|
STATUS_APPLIED_KIND,
|
|
1528
2044
|
STATUS_CLOSED_KIND,
|
|
1529
2045
|
STATUS_DRAFT_KIND,
|
|
1530
2046
|
STATUS_OPEN_KIND
|
|
1531
2047
|
} from "@toon-protocol/core/nip34";
|
|
1532
|
-
var
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
};
|
|
1538
|
-
var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
|
|
1539
|
-
toon.repoid \u2014 run \`rig init\` to set it)
|
|
1540
|
-
--owner <pubkey> repository owner pubkey, 64-char hex (default: git config
|
|
1541
|
-
toon.owner, then the active identity)
|
|
1542
|
-
--remote <name> publish via this configured git remote (default: the
|
|
1543
|
-
"origin" remote \u2014 \`rig remote add origin <relay-url>\`)
|
|
1544
|
-
--relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
|
|
1545
|
-
configured remotes. The deprecated v0.1 \`git config
|
|
1546
|
-
toon.relay\` still works as a fallback, with a nudge
|
|
1547
|
-
--yes skip the fee confirmation (required when not a TTY)
|
|
1548
|
-
--json machine-readable receipt; without --yes it is a pure
|
|
1549
|
-
estimate (nothing published, exit 0)
|
|
2048
|
+
var READ_COMMON_FLAGS = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
2049
|
+
--owner <pubkey> repository owner (npub or 64-char hex; default: git config)
|
|
2050
|
+
--remote <name> read via this configured git remote (default: origin)
|
|
2051
|
+
--relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
|
|
2052
|
+
--json machine-readable envelope
|
|
1550
2053
|
-h, --help show this help`;
|
|
1551
|
-
var
|
|
2054
|
+
var ISSUE_LIST_USAGE = `Usage: rig issue list [--state open|closed|all] [options]
|
|
1552
2055
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
2056
|
+
List the repo's issues (kind:1621) with their derived state \u2014 FREE (relay
|
|
2057
|
+
reads only). State comes from kind:1630-1633 status events: the latest status
|
|
2058
|
+
wins; an issue with no status events is open.
|
|
1556
2059
|
|
|
1557
|
-
|
|
2060
|
+
Options:
|
|
2061
|
+
--state <state> open | closed | all (default: all)
|
|
2062
|
+
${READ_COMMON_FLAGS}`;
|
|
2063
|
+
var ISSUE_SHOW_USAGE = `Usage: rig issue show <event-id> [options]
|
|
2064
|
+
|
|
2065
|
+
Show one issue (kind:1621): metadata, derived state, body, and its
|
|
2066
|
+
kind:1622 comments \u2014 FREE (relay reads only). <event-id> is the 64-char hex
|
|
2067
|
+
id \`rig issue create\` printed (also visible in \`rig issue list\`).
|
|
1558
2068
|
|
|
1559
2069
|
Options:
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
--body-file <path> read the issue body from a file
|
|
1563
|
-
--label <label> label (t tag); repeatable
|
|
1564
|
-
${COMMON_FLAGS_USAGE}`;
|
|
1565
|
-
var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
|
|
2070
|
+
${READ_COMMON_FLAGS}`;
|
|
2071
|
+
var PR_LIST_USAGE = `Usage: rig pr list [--state open|applied|closed|draft|all] [options]
|
|
1566
2072
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2073
|
+
List the repo's patches/PRs (kind:1617) with their derived state \u2014 FREE
|
|
2074
|
+
(relay reads only). State comes from kind:1630-1633 status events: the
|
|
2075
|
+
latest status wins; a patch with no status events is open.
|
|
1570
2076
|
|
|
1571
2077
|
Options:
|
|
1572
|
-
--
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
--marker <root|reply> e-tag marker (default: root \u2014 commenting directly
|
|
1576
|
-
on the issue/patch)
|
|
1577
|
-
${COMMON_FLAGS_USAGE}`;
|
|
1578
|
-
var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
|
|
2078
|
+
--state <state> open | applied | closed | draft | all (default: all)
|
|
2079
|
+
${READ_COMMON_FLAGS}`;
|
|
2080
|
+
var PR_SHOW_USAGE = `Usage: rig pr show <event-id> [options]
|
|
1579
2081
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
commit/parent-commit tags; --patch-file publishes a pre-generated patch
|
|
1584
|
-
verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
|
|
1585
|
-
full series text \u2014 cover-letter threading (one event per commit) is out of
|
|
1586
|
-
scope in v1.
|
|
2082
|
+
Show one patch/PR (kind:1617): metadata, derived state, the FULL patch text
|
|
2083
|
+
(real \`git format-patch\` output \u2014 pipe it to \`git am\` to apply), and its
|
|
2084
|
+
kind:1622 comments \u2014 FREE (relay reads only).
|
|
1587
2085
|
|
|
1588
2086
|
Options:
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2087
|
+
${READ_COMMON_FLAGS}`;
|
|
2088
|
+
var STATUS_BY_KIND = {
|
|
2089
|
+
[STATUS_OPEN_KIND]: "open",
|
|
2090
|
+
[STATUS_APPLIED_KIND]: "applied",
|
|
2091
|
+
[STATUS_CLOSED_KIND]: "closed",
|
|
2092
|
+
[STATUS_DRAFT_KIND]: "draft"
|
|
2093
|
+
};
|
|
2094
|
+
var STATUS_KINDS = [
|
|
2095
|
+
STATUS_OPEN_KIND,
|
|
2096
|
+
STATUS_APPLIED_KIND,
|
|
2097
|
+
STATUS_CLOSED_KIND,
|
|
2098
|
+
STATUS_DRAFT_KIND
|
|
2099
|
+
];
|
|
2100
|
+
function tagValue(tags, name) {
|
|
2101
|
+
return tags.find((t) => t[0] === name)?.[1];
|
|
2102
|
+
}
|
|
2103
|
+
function tagValues(tags, name) {
|
|
2104
|
+
return tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]);
|
|
2105
|
+
}
|
|
2106
|
+
function deriveStatus(targetEventId, statusEvents, authorized) {
|
|
2107
|
+
let winner = null;
|
|
2108
|
+
for (const event of statusEvents) {
|
|
2109
|
+
if (STATUS_BY_KIND[event.kind] === void 0) continue;
|
|
2110
|
+
if (!authorized.has(event.pubkey.toLowerCase())) continue;
|
|
2111
|
+
if (!event.tags.some((t) => t[0] === "e" && t[1] === targetEventId))
|
|
2112
|
+
continue;
|
|
2113
|
+
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
2114
|
+
winner = event;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return winner === null ? "open" : STATUS_BY_KIND[winner.kind];
|
|
2118
|
+
}
|
|
2119
|
+
var RELAY_TIMEOUT_MS = 1e4;
|
|
2120
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
2121
|
+
var WS_URL_RE2 = /^wss?:\/\//i;
|
|
2122
|
+
function defaultWebSocketFactory(url) {
|
|
2123
|
+
const ctor = globalThis.WebSocket;
|
|
2124
|
+
if (!ctor) {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
return new ctor(url);
|
|
2130
|
+
}
|
|
2131
|
+
async function queryAll(relays, filters, webSocketFactory) {
|
|
2132
|
+
const jobs = relays.flatMap(
|
|
2133
|
+
(relay) => filters.map(
|
|
2134
|
+
(filter) => queryRelay(relay, filter, RELAY_TIMEOUT_MS, webSocketFactory)
|
|
2135
|
+
)
|
|
2136
|
+
);
|
|
2137
|
+
const results = await Promise.allSettled(jobs);
|
|
2138
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2139
|
+
let failures = 0;
|
|
2140
|
+
let firstError;
|
|
2141
|
+
for (const result of results) {
|
|
2142
|
+
if (result.status === "rejected") {
|
|
2143
|
+
failures += 1;
|
|
2144
|
+
firstError ??= result.reason;
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
for (const event of result.value) {
|
|
2148
|
+
if (typeof event.id === "string" && !byId.has(event.id)) {
|
|
2149
|
+
byId.set(event.id, event);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (failures === results.length && results.length > 0) {
|
|
2154
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
2155
|
+
}
|
|
2156
|
+
return byId;
|
|
2157
|
+
}
|
|
2158
|
+
var TRACKER_OPTIONS = {
|
|
2159
|
+
json: { type: "boolean", default: false },
|
|
2160
|
+
state: { type: "string" },
|
|
2161
|
+
relay: { type: "string", multiple: true },
|
|
2162
|
+
remote: { type: "string" },
|
|
2163
|
+
"repo-id": { type: "string" },
|
|
2164
|
+
owner: { type: "string" },
|
|
2165
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2166
|
+
};
|
|
2167
|
+
function pickTrackerFlags(values) {
|
|
2168
|
+
const flags = {
|
|
2169
|
+
json: values["json"] === true,
|
|
2170
|
+
help: values["help"] === true,
|
|
2171
|
+
relay: Array.isArray(values["relay"]) ? values["relay"] : []
|
|
2172
|
+
};
|
|
2173
|
+
if (typeof values["state"] === "string") flags.state = values["state"];
|
|
2174
|
+
if (typeof values["remote"] === "string") flags.remote = values["remote"];
|
|
2175
|
+
if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
|
|
2176
|
+
if (typeof values["owner"] === "string")
|
|
2177
|
+
flags.owner = ownerToHex(values["owner"]);
|
|
2178
|
+
return flags;
|
|
2179
|
+
}
|
|
2180
|
+
async function resolveTrackerContext(flags, deps, needRepoAddr) {
|
|
2181
|
+
let repoRoot;
|
|
2182
|
+
let toonConfig = {
|
|
2183
|
+
relays: []
|
|
2184
|
+
};
|
|
2185
|
+
try {
|
|
2186
|
+
repoRoot = await resolveRepoRoot(deps.cwd);
|
|
2187
|
+
toonConfig = await readToonConfig(repoRoot);
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
let repoAddr = null;
|
|
2191
|
+
const repoId = flags.repoId ?? toonConfig.repoId;
|
|
2192
|
+
const owner = flags.owner ?? toonConfig.owner;
|
|
2193
|
+
if (repoId && owner) {
|
|
2194
|
+
repoAddr = { ownerPubkey: owner, repoId };
|
|
2195
|
+
} else if (needRepoAddr) {
|
|
2196
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
2197
|
+
throw new UnconfiguredRepoAddressError("repository owner");
|
|
2198
|
+
}
|
|
2199
|
+
const resolved = await resolveRelays({
|
|
2200
|
+
relayFlags: flags.relay,
|
|
2201
|
+
remoteName: flags.remote,
|
|
2202
|
+
repoRoot,
|
|
2203
|
+
toonRelays: toonConfig.relays
|
|
2204
|
+
});
|
|
2205
|
+
const wsRelays = resolved.relays.filter((url) => WS_URL_RE2.test(url));
|
|
2206
|
+
if (wsRelays.length === 0) {
|
|
2207
|
+
throw new InvalidRelayUrlError(
|
|
2208
|
+
resolved.relays[0] ?? "",
|
|
2209
|
+
"reads need a ws:// or wss:// relay"
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
relays: wsRelays,
|
|
2214
|
+
repoAddr,
|
|
2215
|
+
webSocketFactory: deps.webSocketFactory ?? defaultWebSocketFactory
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function repoATag(addr) {
|
|
2219
|
+
return `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`;
|
|
2220
|
+
}
|
|
2221
|
+
async function fetchAuthorizedAuthors(ctx, ownerPubkey, repoId) {
|
|
2222
|
+
let announceTags = [];
|
|
2223
|
+
try {
|
|
2224
|
+
const events = await queryAll(
|
|
2225
|
+
ctx.relays,
|
|
2226
|
+
[
|
|
2227
|
+
{
|
|
2228
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND],
|
|
2229
|
+
authors: [ownerPubkey],
|
|
2230
|
+
"#d": [repoId]
|
|
2231
|
+
}
|
|
2232
|
+
],
|
|
2233
|
+
ctx.webSocketFactory
|
|
2234
|
+
);
|
|
2235
|
+
let latest = null;
|
|
2236
|
+
for (const event of events.values()) {
|
|
2237
|
+
if (event.kind !== REPOSITORY_ANNOUNCEMENT_KIND) continue;
|
|
2238
|
+
if (event.pubkey !== ownerPubkey) continue;
|
|
2239
|
+
if (!event.tags.some((t) => t[0] === "d" && t[1] === repoId)) continue;
|
|
2240
|
+
if (latest === null || event.created_at > latest.created_at || event.created_at === latest.created_at && event.id < latest.id) {
|
|
2241
|
+
latest = event;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (latest) announceTags = latest.tags;
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
return authorizedStatusAuthors(ownerPubkey, announceTags);
|
|
2248
|
+
}
|
|
2249
|
+
function parseTrackerItem(event, status) {
|
|
2250
|
+
const item = {
|
|
2251
|
+
eventId: event.id,
|
|
2252
|
+
kind: event.kind,
|
|
2253
|
+
title: tagValue(event.tags, "subject") ?? (event.content.split("\n")[0] ?? "").slice(0, 120),
|
|
2254
|
+
status,
|
|
2255
|
+
authorPubkey: event.pubkey,
|
|
2256
|
+
createdAt: event.created_at,
|
|
2257
|
+
labels: tagValues(event.tags, "t"),
|
|
2258
|
+
content: event.content
|
|
2259
|
+
};
|
|
2260
|
+
if (event.kind === PATCH_KIND) {
|
|
2261
|
+
item.commitShas = tagValues(event.tags, "commit");
|
|
2262
|
+
const branch = tagValue(event.tags, "branch");
|
|
2263
|
+
if (branch !== void 0) item.branch = branch;
|
|
2264
|
+
const description = tagValue(event.tags, "description");
|
|
2265
|
+
if (description !== void 0) item.description = description;
|
|
2266
|
+
}
|
|
2267
|
+
return item;
|
|
2268
|
+
}
|
|
2269
|
+
function isoDate(createdAt) {
|
|
2270
|
+
return new Date(createdAt * 1e3).toISOString().slice(0, 10);
|
|
2271
|
+
}
|
|
2272
|
+
async function fetchItems(ctx, kind) {
|
|
2273
|
+
const addr = ctx.repoAddr;
|
|
2274
|
+
const aTag = repoATag(addr);
|
|
2275
|
+
const authorizedPromise = fetchAuthorizedAuthors(
|
|
2276
|
+
ctx,
|
|
2277
|
+
addr.ownerPubkey,
|
|
2278
|
+
addr.repoId
|
|
2279
|
+
);
|
|
2280
|
+
const [itemEvents, aStatusEvents] = await Promise.all([
|
|
2281
|
+
queryAll(
|
|
2282
|
+
ctx.relays,
|
|
2283
|
+
[{ kinds: [kind], "#a": [aTag] }],
|
|
2284
|
+
ctx.webSocketFactory
|
|
2285
|
+
),
|
|
2286
|
+
queryAll(
|
|
2287
|
+
ctx.relays,
|
|
2288
|
+
[{ kinds: STATUS_KINDS, "#a": [aTag] }],
|
|
2289
|
+
ctx.webSocketFactory
|
|
2290
|
+
)
|
|
2291
|
+
]);
|
|
2292
|
+
const items = [...itemEvents.values()].filter(
|
|
2293
|
+
(e) => e.kind === kind && e.tags.some((t) => t[0] === "a" && t[1] === aTag)
|
|
2294
|
+
);
|
|
2295
|
+
const statuses = new Map(aStatusEvents);
|
|
2296
|
+
if (items.length > 0) {
|
|
2297
|
+
const byE = await queryAll(
|
|
2298
|
+
ctx.relays,
|
|
2299
|
+
[{ kinds: STATUS_KINDS, "#e": items.map((i) => i.id) }],
|
|
2300
|
+
ctx.webSocketFactory
|
|
2301
|
+
);
|
|
2302
|
+
for (const [id, event] of byE)
|
|
2303
|
+
if (!statuses.has(id)) statuses.set(id, event);
|
|
2304
|
+
}
|
|
2305
|
+
const authorized = await authorizedPromise;
|
|
2306
|
+
return items.map(
|
|
2307
|
+
(event) => parseTrackerItem(event, deriveStatus(event.id, statuses.values(), authorized))
|
|
2308
|
+
).sort((a, b) => b.createdAt - a.createdAt);
|
|
2309
|
+
}
|
|
2310
|
+
async function fetchItem(ctx, eventId, expectedKind) {
|
|
2311
|
+
const found = await queryAll(
|
|
2312
|
+
ctx.relays,
|
|
2313
|
+
[{ ids: [eventId] }],
|
|
2314
|
+
ctx.webSocketFactory
|
|
2315
|
+
);
|
|
2316
|
+
const event = found.get(eventId);
|
|
2317
|
+
if (!event) {
|
|
2318
|
+
throw new Error(`event ${eventId} not found on ${ctx.relays.join(", ")}`);
|
|
2319
|
+
}
|
|
2320
|
+
if (event.kind !== expectedKind) {
|
|
2321
|
+
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`)" : "";
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
`event ${eventId} has kind ${event.kind}, expected ${expectedKind}${hint}`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
const [statusEvents, commentEvents] = await Promise.all([
|
|
2327
|
+
queryAll(
|
|
2328
|
+
ctx.relays,
|
|
2329
|
+
[{ kinds: STATUS_KINDS, "#e": [eventId] }],
|
|
2330
|
+
ctx.webSocketFactory
|
|
2331
|
+
),
|
|
2332
|
+
queryAll(
|
|
2333
|
+
ctx.relays,
|
|
2334
|
+
[{ kinds: [COMMENT_KIND], "#e": [eventId] }],
|
|
2335
|
+
ctx.webSocketFactory
|
|
2336
|
+
)
|
|
2337
|
+
]);
|
|
2338
|
+
const comments = [...commentEvents.values()].filter(
|
|
2339
|
+
(e) => e.kind === COMMENT_KIND && e.tags.some((t) => t[0] === "e" && t[1] === eventId)
|
|
2340
|
+
).sort((a, b) => a.created_at - b.created_at).map((e) => ({
|
|
2341
|
+
eventId: e.id,
|
|
2342
|
+
authorPubkey: e.pubkey,
|
|
2343
|
+
createdAt: e.created_at,
|
|
2344
|
+
content: e.content
|
|
2345
|
+
}));
|
|
2346
|
+
const repoATag2 = tagValue(event.tags, "a") ?? null;
|
|
2347
|
+
const parsedAddr = parseRepoATag(repoATag2) ?? ctx.repoAddr;
|
|
2348
|
+
const authorized = parsedAddr ? await fetchAuthorizedAuthors(ctx, parsedAddr.ownerPubkey, parsedAddr.repoId) : /* @__PURE__ */ new Set();
|
|
2349
|
+
return {
|
|
2350
|
+
item: parseTrackerItem(
|
|
2351
|
+
event,
|
|
2352
|
+
deriveStatus(eventId, statusEvents.values(), authorized)
|
|
2353
|
+
),
|
|
2354
|
+
comments,
|
|
2355
|
+
repoATag: repoATag2
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function parseRepoATag(aTag) {
|
|
2359
|
+
if (!aTag) return null;
|
|
2360
|
+
const [kind, ownerPubkey, ...repoIdParts] = aTag.split(":");
|
|
2361
|
+
const repoId = repoIdParts.join(":");
|
|
2362
|
+
if (kind !== String(REPOSITORY_ANNOUNCEMENT_KIND) || !ownerPubkey || !repoId) {
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
return { ownerPubkey, repoId };
|
|
2366
|
+
}
|
|
2367
|
+
async function runList(args, deps, spec) {
|
|
2368
|
+
const { io: io2 } = deps;
|
|
2369
|
+
let flags;
|
|
2370
|
+
try {
|
|
2371
|
+
const { values, positionals } = parseArgs6({
|
|
2372
|
+
args,
|
|
2373
|
+
options: TRACKER_OPTIONS,
|
|
2374
|
+
allowPositionals: true
|
|
2375
|
+
});
|
|
2376
|
+
if (positionals.length > 0) {
|
|
2377
|
+
throw new Error(`rig ${spec.command} takes no positional arguments`);
|
|
2378
|
+
}
|
|
2379
|
+
flags = pickTrackerFlags(values);
|
|
2380
|
+
if (flags.state !== void 0 && flags.state !== "all" && !spec.states.includes(flags.state)) {
|
|
2381
|
+
throw new Error(
|
|
2382
|
+
`--state must be one of ${[...spec.states, "all"].join(" | ")} (got ${JSON.stringify(flags.state)})`
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2387
|
+
io2.err(spec.usage);
|
|
2388
|
+
return 2;
|
|
2389
|
+
}
|
|
2390
|
+
if (flags.help) {
|
|
2391
|
+
io2.out(spec.usage);
|
|
2392
|
+
return 0;
|
|
2393
|
+
}
|
|
2394
|
+
try {
|
|
2395
|
+
const ctx = await resolveTrackerContext(flags, deps, true);
|
|
2396
|
+
const all = await fetchItems(ctx, spec.kind);
|
|
2397
|
+
const state = flags.state ?? "all";
|
|
2398
|
+
const items = state === "all" ? all : all.filter((i) => i.status === state);
|
|
2399
|
+
if (flags.json) {
|
|
2400
|
+
io2.emitJson({
|
|
2401
|
+
command: spec.command,
|
|
2402
|
+
repoAddr: ctx.repoAddr,
|
|
2403
|
+
relays: ctx.relays,
|
|
2404
|
+
state,
|
|
2405
|
+
count: items.length,
|
|
2406
|
+
[spec.jsonKey]: items
|
|
2407
|
+
});
|
|
2408
|
+
return 0;
|
|
2409
|
+
}
|
|
2410
|
+
if (items.length === 0) {
|
|
2411
|
+
io2.out(
|
|
2412
|
+
`no ${state === "all" ? "" : `${state} `}${spec.jsonKey} found for ${repoATag(ctx.repoAddr)}`
|
|
2413
|
+
);
|
|
2414
|
+
return 0;
|
|
2415
|
+
}
|
|
2416
|
+
for (const item of items) {
|
|
2417
|
+
const labels = item.labels.length > 0 ? ` [${item.labels.join(", ")}]` : "";
|
|
2418
|
+
io2.out(
|
|
2419
|
+
`${item.status.padEnd(7)} ${item.eventId.slice(0, 8)} ${item.title} (${item.authorPubkey.slice(0, 8)}, ${isoDate(item.createdAt)})${labels}`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
io2.out(
|
|
2423
|
+
`${items.length} ${spec.jsonKey}${state === "all" ? "" : ` (${state})`}`
|
|
2424
|
+
);
|
|
2425
|
+
return 0;
|
|
2426
|
+
} catch (err) {
|
|
2427
|
+
return emitCliError(io2, flags.json, spec.command, err);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
async function runShow(args, deps, spec) {
|
|
2431
|
+
const { io: io2 } = deps;
|
|
2432
|
+
let flags;
|
|
2433
|
+
let eventId;
|
|
2434
|
+
try {
|
|
2435
|
+
const { values, positionals } = parseArgs6({
|
|
2436
|
+
args,
|
|
2437
|
+
options: TRACKER_OPTIONS,
|
|
2438
|
+
allowPositionals: true
|
|
2439
|
+
});
|
|
2440
|
+
flags = pickTrackerFlags(values);
|
|
2441
|
+
if (flags.help) {
|
|
2442
|
+
io2.out(spec.usage);
|
|
2443
|
+
return 0;
|
|
2444
|
+
}
|
|
2445
|
+
if (positionals.length !== 1) {
|
|
2446
|
+
throw new Error("expected exactly one <event-id>");
|
|
2447
|
+
}
|
|
2448
|
+
eventId = positionals[0];
|
|
2449
|
+
if (!HEX64_RE.test(eventId)) {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
`<event-id> must be a 64-char lowercase hex id (got ${JSON.stringify(eventId)})`
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
2456
|
+
io2.err(spec.usage);
|
|
2457
|
+
return 2;
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
const ctx = await resolveTrackerContext(flags, deps, false);
|
|
2461
|
+
const shown = await fetchItem(ctx, eventId, spec.kind);
|
|
2462
|
+
if (flags.json) {
|
|
2463
|
+
io2.emitJson({
|
|
2464
|
+
command: spec.command,
|
|
2465
|
+
relays: ctx.relays,
|
|
2466
|
+
repoATag: shown.repoATag,
|
|
2467
|
+
[spec.jsonKey]: shown.item,
|
|
2468
|
+
comments: shown.comments
|
|
2469
|
+
});
|
|
2470
|
+
return 0;
|
|
2471
|
+
}
|
|
2472
|
+
const { item } = shown;
|
|
2473
|
+
io2.out(`${spec.jsonKey} ${item.eventId}`);
|
|
2474
|
+
io2.out(`Title: ${item.title}`);
|
|
2475
|
+
io2.out(`Status: ${item.status}`);
|
|
2476
|
+
io2.out(`Author: ${item.authorPubkey}`);
|
|
2477
|
+
io2.out(`Date: ${isoDate(item.createdAt)}`);
|
|
2478
|
+
if (item.labels.length > 0) io2.out(`Labels: ${item.labels.join(", ")}`);
|
|
2479
|
+
if (item.branch !== void 0) io2.out(`Branch: ${item.branch}`);
|
|
2480
|
+
if (item.commitShas && item.commitShas.length > 0) {
|
|
2481
|
+
io2.out(`Commits: ${item.commitShas.join(", ")}`);
|
|
2482
|
+
}
|
|
2483
|
+
if (shown.repoATag !== null) io2.out(`Repo: ${shown.repoATag}`);
|
|
2484
|
+
if (item.description !== void 0) {
|
|
2485
|
+
io2.out("");
|
|
2486
|
+
io2.out("Body:");
|
|
2487
|
+
for (const line of item.description.split("\n")) io2.out(line);
|
|
2488
|
+
}
|
|
2489
|
+
io2.out("");
|
|
2490
|
+
io2.out(`${spec.bodyLabel}:`);
|
|
2491
|
+
for (const line of item.content.split("\n")) io2.out(line);
|
|
2492
|
+
io2.out("");
|
|
2493
|
+
io2.out(`Comments (${shown.comments.length}):`);
|
|
2494
|
+
for (const comment of shown.comments) {
|
|
2495
|
+
io2.out(
|
|
2496
|
+
`--- ${comment.eventId.slice(0, 8)} by ${comment.authorPubkey.slice(0, 8)} on ${isoDate(comment.createdAt)}`
|
|
2497
|
+
);
|
|
2498
|
+
for (const line of comment.content.split("\n")) io2.out(line);
|
|
2499
|
+
}
|
|
2500
|
+
return 0;
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
return emitCliError(io2, flags.json, spec.command, err);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
var ISSUE_STATES = ["open", "closed", "applied", "draft"];
|
|
2506
|
+
var PR_STATES = ["open", "applied", "closed", "draft"];
|
|
2507
|
+
function runIssueList(args, deps) {
|
|
2508
|
+
return runList(args, deps, {
|
|
2509
|
+
command: "issue list",
|
|
2510
|
+
kind: ISSUE_KIND,
|
|
2511
|
+
usage: ISSUE_LIST_USAGE,
|
|
2512
|
+
states: ISSUE_STATES,
|
|
2513
|
+
jsonKey: "issues"
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
function runIssueShow(args, deps) {
|
|
2517
|
+
return runShow(args, deps, {
|
|
2518
|
+
command: "issue show",
|
|
2519
|
+
kind: ISSUE_KIND,
|
|
2520
|
+
usage: ISSUE_SHOW_USAGE,
|
|
2521
|
+
jsonKey: "issue",
|
|
2522
|
+
bodyLabel: "Body"
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
function runPrList(args, deps) {
|
|
2526
|
+
return runList(args, deps, {
|
|
2527
|
+
command: "pr list",
|
|
2528
|
+
kind: PATCH_KIND,
|
|
2529
|
+
usage: PR_LIST_USAGE,
|
|
2530
|
+
states: PR_STATES,
|
|
2531
|
+
jsonKey: "prs"
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
function runPrShow(args, deps) {
|
|
2535
|
+
return runShow(args, deps, {
|
|
2536
|
+
command: "pr show",
|
|
2537
|
+
kind: PATCH_KIND,
|
|
2538
|
+
usage: PR_SHOW_USAGE,
|
|
2539
|
+
jsonKey: "pr",
|
|
2540
|
+
bodyLabel: "Patch"
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/cli/events.ts
|
|
2545
|
+
var defaultReadStdin = async () => {
|
|
2546
|
+
if (process.stdin.isTTY) return "";
|
|
2547
|
+
const chunks = [];
|
|
2548
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2549
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
2550
|
+
};
|
|
2551
|
+
var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config
|
|
2552
|
+
toon.repoid \u2014 run \`rig init\` to set it)
|
|
2553
|
+
--owner <pubkey> repository owner pubkey, 64-char hex (default: git config
|
|
2554
|
+
toon.owner, then the active identity)
|
|
2555
|
+
--remote <name> publish via this configured git remote (default: the
|
|
2556
|
+
"origin" remote \u2014 \`rig remote add origin <relay-url>\`)
|
|
2557
|
+
--relay <url> ad-hoc relay override (exactly one) \u2014 bypasses the
|
|
2558
|
+
configured remotes. The deprecated v0.1 \`git config
|
|
2559
|
+
toon.relay\` still works as a fallback, with a nudge
|
|
2560
|
+
--yes skip the fee confirmation (required when not a TTY)
|
|
2561
|
+
--json machine-readable receipt; without --yes it is a pure
|
|
2562
|
+
estimate (nothing published, exit 0)
|
|
2563
|
+
-h, --help show this help`;
|
|
2564
|
+
var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
|
|
2565
|
+
|
|
2566
|
+
File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
|
|
2567
|
+
permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
|
|
2568
|
+
from the toon.* git config keys \`rig init\` writes.
|
|
2569
|
+
|
|
2570
|
+
Body source (exactly one): --body, --body-file, or piped stdin.
|
|
2571
|
+
|
|
2572
|
+
Options:
|
|
2573
|
+
--title <title> issue title (subject tag) [required]
|
|
2574
|
+
--body <text> issue body (Markdown)
|
|
2575
|
+
--body-file <path> read the issue body from a file
|
|
2576
|
+
--label <label> label (t tag); repeatable
|
|
2577
|
+
${COMMON_FLAGS_USAGE}
|
|
2578
|
+
|
|
2579
|
+
Related (FREE reads \u2014 no payment):
|
|
2580
|
+
rig issue list [--state open|closed|all] list the repo's issues
|
|
2581
|
+
rig issue show <event-id> one issue + its comments`;
|
|
2582
|
+
var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
|
|
2583
|
+
|
|
2584
|
+
Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
|
|
2585
|
+
permanent and non-refundable. <root-event-id> is the 64-char hex id of the
|
|
2586
|
+
kind:1621 issue / kind:1617 patch being commented on.
|
|
2587
|
+
|
|
2588
|
+
Options:
|
|
2589
|
+
--body <text> comment body (Markdown) [required]
|
|
2590
|
+
--parent-author <pubkey> pubkey of the TARGET event's author (p threading
|
|
2591
|
+
tag; default: the repo owner)
|
|
2592
|
+
--marker <root|reply> e-tag marker (default: root \u2014 commenting directly
|
|
2593
|
+
on the issue/patch)
|
|
2594
|
+
${COMMON_FLAGS_USAGE}`;
|
|
2595
|
+
var PR_CREATE_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
|
|
2596
|
+
|
|
2597
|
+
Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
|
|
2598
|
+
a paid publish; writes are permanent and non-refundable. --range runs
|
|
2599
|
+
\`git format-patch --stdout <range>\` in the local repository and derives the
|
|
2600
|
+
commit/parent-commit tags; --patch-file publishes a pre-generated patch
|
|
2601
|
+
verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
|
|
2602
|
+
full series text \u2014 cover-letter threading (one event per commit) is out of
|
|
2603
|
+
scope in v1.
|
|
2604
|
+
|
|
2605
|
+
The PR body (--body/--body-file) is carried in a dedicated \`description\`
|
|
2606
|
+
tag, not in the event content \u2014 the content stays pure format-patch output so
|
|
2607
|
+
\`rig pr show \u2026 | git am\` keeps working.
|
|
2608
|
+
|
|
2609
|
+
Options:
|
|
2610
|
+
--title <title> patch/PR title (subject tag) [required]
|
|
2611
|
+
--range <range> revision range for format-patch: <rev>, <rev>..<rev>,
|
|
2612
|
+
or <rev>...<rev> (mutually exclusive with --patch-file)
|
|
2613
|
+
--patch-file <path> literal patch text to publish
|
|
2614
|
+
--body <text> PR description (Markdown; description tag)
|
|
2615
|
+
--body-file <path> read the PR description from a file
|
|
2616
|
+
--branch <name> branch name (t tag)
|
|
1594
2617
|
${COMMON_FLAGS_USAGE}`;
|
|
1595
2618
|
var PR_STATUS_USAGE = `Usage: rig pr status <target-event-id> <open|applied|closed|draft> [options]
|
|
1596
2619
|
|
|
@@ -1605,10 +2628,14 @@ Options:
|
|
|
1605
2628
|
${COMMON_FLAGS_USAGE}`;
|
|
1606
2629
|
var PR_USAGE = `${PR_CREATE_USAGE}
|
|
1607
2630
|
|
|
1608
|
-
${PR_STATUS_USAGE}
|
|
1609
|
-
|
|
2631
|
+
${PR_STATUS_USAGE}
|
|
2632
|
+
|
|
2633
|
+
${PR_LIST_USAGE}
|
|
2634
|
+
|
|
2635
|
+
${PR_SHOW_USAGE}`;
|
|
2636
|
+
var HEX64_RE2 = /^[0-9a-f]{64}$/;
|
|
1610
2637
|
function assertHex64(value, what) {
|
|
1611
|
-
if (!
|
|
2638
|
+
if (!HEX64_RE2.test(value)) {
|
|
1612
2639
|
throw new Error(
|
|
1613
2640
|
`${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
|
|
1614
2641
|
);
|
|
@@ -1643,7 +2670,7 @@ function pickCommon(values) {
|
|
|
1643
2670
|
}
|
|
1644
2671
|
async function runEvent(opts) {
|
|
1645
2672
|
const { command, flags, deps, actionLabel } = opts;
|
|
1646
|
-
const { io: io2
|
|
2673
|
+
const { io: io2 } = deps;
|
|
1647
2674
|
let standaloneCtx;
|
|
1648
2675
|
try {
|
|
1649
2676
|
let repoRoot;
|
|
@@ -1667,21 +2694,38 @@ async function runEvent(opts) {
|
|
|
1667
2694
|
io2.err(singleRelayRefusal(resolved, "Nothing was published or paid."));
|
|
1668
2695
|
return 1;
|
|
1669
2696
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2697
|
+
const session = await loadPaidSession(deps, relaysUsed[0]);
|
|
2698
|
+
const path = session.path;
|
|
2699
|
+
let identity;
|
|
2700
|
+
let fee;
|
|
2701
|
+
if (session.path === "standalone") {
|
|
2702
|
+
standaloneCtx = session.ctx;
|
|
2703
|
+
identity = identityReport(standaloneCtx);
|
|
2704
|
+
fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
|
|
2705
|
+
} else {
|
|
2706
|
+
identity = session.identity;
|
|
2707
|
+
fee = session.feePerEvent;
|
|
2708
|
+
const resolvedRelay = relaysUsed[0];
|
|
2709
|
+
if (resolvedRelay !== void 0 && session.daemonRelayUrl !== void 0 && session.daemonRelayUrl !== resolvedRelay) {
|
|
2710
|
+
io2.err(
|
|
2711
|
+
`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`
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
1679
2715
|
const owner = flags.owner ?? toonConfig.owner ?? identity.pubkey;
|
|
1680
2716
|
const addr = { ownerPubkey: owner, repoId };
|
|
1681
2717
|
const event = await opts.buildEvent(addr);
|
|
1682
2718
|
const action = `kind:${event.kind} ${actionLabel}`;
|
|
2719
|
+
if (opts.preConfirm) {
|
|
2720
|
+
await opts.preConfirm({ identity, addr, relayUrl: relaysUsed[0] });
|
|
2721
|
+
}
|
|
1683
2722
|
if (!flags.json) {
|
|
1684
|
-
for (const line of renderEventPlan({
|
|
2723
|
+
for (const line of renderEventPlan({
|
|
2724
|
+
action,
|
|
2725
|
+
addr,
|
|
2726
|
+
identity,
|
|
2727
|
+
...fee !== void 0 ? { fee } : {}
|
|
2728
|
+
})) {
|
|
1685
2729
|
io2.out(line);
|
|
1686
2730
|
}
|
|
1687
2731
|
}
|
|
@@ -1690,10 +2734,11 @@ async function runEvent(opts) {
|
|
|
1690
2734
|
io2.emitJson({
|
|
1691
2735
|
command,
|
|
1692
2736
|
repoAddr: addr,
|
|
2737
|
+
path,
|
|
1693
2738
|
identity,
|
|
1694
2739
|
kind: event.kind,
|
|
1695
2740
|
executed: false,
|
|
1696
|
-
feeEstimate: fee,
|
|
2741
|
+
feeEstimate: fee ?? null,
|
|
1697
2742
|
hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
|
|
1698
2743
|
});
|
|
1699
2744
|
return 0;
|
|
@@ -1712,16 +2757,25 @@ async function runEvent(opts) {
|
|
|
1712
2757
|
return 1;
|
|
1713
2758
|
}
|
|
1714
2759
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
2760
|
+
let result;
|
|
2761
|
+
if (session.path === "standalone") {
|
|
2762
|
+
const receipt = await session.ctx.publisher.publishEvent(
|
|
2763
|
+
event,
|
|
2764
|
+
relaysUsed
|
|
2765
|
+
);
|
|
2766
|
+
result = serializeEventReceipt(event.kind, receipt);
|
|
2767
|
+
} else {
|
|
2768
|
+
result = await opts.sendDaemon(session.client, addr);
|
|
2769
|
+
}
|
|
1717
2770
|
if (flags.json) {
|
|
1718
2771
|
io2.emitJson({
|
|
1719
2772
|
command,
|
|
1720
2773
|
repoAddr: addr,
|
|
2774
|
+
path,
|
|
1721
2775
|
identity,
|
|
1722
2776
|
kind: result.kind,
|
|
1723
2777
|
executed: true,
|
|
1724
|
-
feeEstimate: fee,
|
|
2778
|
+
feeEstimate: fee ?? null,
|
|
1725
2779
|
result
|
|
1726
2780
|
});
|
|
1727
2781
|
} else {
|
|
@@ -1744,11 +2798,17 @@ async function runIssue(args, deps) {
|
|
|
1744
2798
|
const [sub, ...rest] = args;
|
|
1745
2799
|
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1746
2800
|
io2.out(ISSUE_USAGE);
|
|
2801
|
+
io2.out("");
|
|
2802
|
+
io2.out(ISSUE_LIST_USAGE);
|
|
2803
|
+
io2.out("");
|
|
2804
|
+
io2.out(ISSUE_SHOW_USAGE);
|
|
1747
2805
|
return 0;
|
|
1748
2806
|
}
|
|
2807
|
+
if (sub === "list") return runIssueList(rest, deps);
|
|
2808
|
+
if (sub === "show") return runIssueShow(rest, deps);
|
|
1749
2809
|
if (sub !== "create") {
|
|
1750
2810
|
io2.err(
|
|
1751
|
-
sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
|
|
2811
|
+
sub === void 0 ? "missing subcommand: rig issue <create|list|show>" : `unknown rig issue subcommand: ${sub}`
|
|
1752
2812
|
);
|
|
1753
2813
|
io2.err(ISSUE_USAGE);
|
|
1754
2814
|
return 2;
|
|
@@ -1759,7 +2819,7 @@ async function runIssue(args, deps) {
|
|
|
1759
2819
|
let bodyFile;
|
|
1760
2820
|
let labels;
|
|
1761
2821
|
try {
|
|
1762
|
-
const { values } =
|
|
2822
|
+
const { values } = parseArgs7({
|
|
1763
2823
|
args: rest,
|
|
1764
2824
|
options: {
|
|
1765
2825
|
...COMMON_OPTIONS,
|
|
@@ -1818,7 +2878,13 @@ async function runIssue(args, deps) {
|
|
|
1818
2878
|
flags,
|
|
1819
2879
|
deps,
|
|
1820
2880
|
actionLabel: `issue ${JSON.stringify(title)}`,
|
|
1821
|
-
buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels)
|
|
2881
|
+
buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
|
|
2882
|
+
sendDaemon: (client, addr) => client.gitIssue({
|
|
2883
|
+
repoAddr: addr,
|
|
2884
|
+
title,
|
|
2885
|
+
body,
|
|
2886
|
+
...labels.length > 0 ? { labels } : {}
|
|
2887
|
+
})
|
|
1822
2888
|
});
|
|
1823
2889
|
}
|
|
1824
2890
|
async function runComment(args, deps) {
|
|
@@ -1829,7 +2895,7 @@ async function runComment(args, deps) {
|
|
|
1829
2895
|
let parentAuthor;
|
|
1830
2896
|
let marker;
|
|
1831
2897
|
try {
|
|
1832
|
-
const { values, positionals } =
|
|
2898
|
+
const { values, positionals } = parseArgs7({
|
|
1833
2899
|
args,
|
|
1834
2900
|
options: {
|
|
1835
2901
|
...COMMON_OPTIONS,
|
|
@@ -1879,7 +2945,14 @@ async function runComment(args, deps) {
|
|
|
1879
2945
|
parentAuthor ?? addr.ownerPubkey,
|
|
1880
2946
|
body,
|
|
1881
2947
|
marker
|
|
1882
|
-
)
|
|
2948
|
+
),
|
|
2949
|
+
sendDaemon: (client, addr) => client.gitComment({
|
|
2950
|
+
repoAddr: addr,
|
|
2951
|
+
rootEventId,
|
|
2952
|
+
body,
|
|
2953
|
+
...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {},
|
|
2954
|
+
marker
|
|
2955
|
+
})
|
|
1883
2956
|
});
|
|
1884
2957
|
}
|
|
1885
2958
|
var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
|
|
@@ -1894,6 +2967,11 @@ async function runPr(args, deps) {
|
|
|
1894
2967
|
return runPrCreate(rest, deps);
|
|
1895
2968
|
case "status":
|
|
1896
2969
|
return runPrStatus(rest, deps);
|
|
2970
|
+
// FREE read subcommands (#278): relay queries only, no payment path.
|
|
2971
|
+
case "list":
|
|
2972
|
+
return runPrList(rest, deps);
|
|
2973
|
+
case "show":
|
|
2974
|
+
return runPrShow(rest, deps);
|
|
1897
2975
|
case "--help":
|
|
1898
2976
|
case "-h":
|
|
1899
2977
|
case "help":
|
|
@@ -1901,7 +2979,7 @@ async function runPr(args, deps) {
|
|
|
1901
2979
|
return 0;
|
|
1902
2980
|
default:
|
|
1903
2981
|
io2.err(
|
|
1904
|
-
sub === void 0 ? "missing subcommand: rig pr <create|status>" : `unknown rig pr subcommand: ${sub}`
|
|
2982
|
+
sub === void 0 ? "missing subcommand: rig pr <create|status|list|show>" : `unknown rig pr subcommand: ${sub}`
|
|
1905
2983
|
);
|
|
1906
2984
|
io2.err(PR_USAGE);
|
|
1907
2985
|
return 2;
|
|
@@ -1914,14 +2992,18 @@ async function runPrCreate(rest, deps) {
|
|
|
1914
2992
|
let range;
|
|
1915
2993
|
let patchFile;
|
|
1916
2994
|
let branch;
|
|
2995
|
+
let bodyFlag;
|
|
2996
|
+
let bodyFile;
|
|
1917
2997
|
try {
|
|
1918
|
-
const { values } =
|
|
2998
|
+
const { values } = parseArgs7({
|
|
1919
2999
|
args: rest,
|
|
1920
3000
|
options: {
|
|
1921
3001
|
...COMMON_OPTIONS,
|
|
1922
3002
|
title: { type: "string" },
|
|
1923
3003
|
range: { type: "string" },
|
|
1924
3004
|
"patch-file": { type: "string" },
|
|
3005
|
+
body: { type: "string" },
|
|
3006
|
+
"body-file": { type: "string" },
|
|
1925
3007
|
branch: { type: "string" }
|
|
1926
3008
|
},
|
|
1927
3009
|
allowPositionals: false
|
|
@@ -1937,15 +3019,39 @@ async function runPrCreate(rest, deps) {
|
|
|
1937
3019
|
if (values.range === void 0 === (values["patch-file"] === void 0)) {
|
|
1938
3020
|
throw new Error("exactly one of --range or --patch-file is required");
|
|
1939
3021
|
}
|
|
3022
|
+
if (values.body !== void 0 && values["body-file"] !== void 0) {
|
|
3023
|
+
throw new Error("--body and --body-file are mutually exclusive");
|
|
3024
|
+
}
|
|
1940
3025
|
title = values.title;
|
|
1941
3026
|
range = values.range;
|
|
1942
3027
|
patchFile = values["patch-file"];
|
|
1943
3028
|
branch = values.branch;
|
|
3029
|
+
bodyFlag = values.body;
|
|
3030
|
+
bodyFile = values["body-file"];
|
|
1944
3031
|
} catch (err) {
|
|
1945
3032
|
io2.err(err instanceof Error ? err.message : String(err));
|
|
1946
3033
|
io2.err(PR_CREATE_USAGE);
|
|
1947
3034
|
return 2;
|
|
1948
3035
|
}
|
|
3036
|
+
let body;
|
|
3037
|
+
if (bodyFlag !== void 0) {
|
|
3038
|
+
body = bodyFlag;
|
|
3039
|
+
} else if (bodyFile !== void 0) {
|
|
3040
|
+
try {
|
|
3041
|
+
body = await readFile(bodyFile, "utf-8");
|
|
3042
|
+
} catch (err) {
|
|
3043
|
+
io2.err(
|
|
3044
|
+
`cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
|
|
3045
|
+
);
|
|
3046
|
+
return 2;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (body !== void 0 && body.trim() === "") {
|
|
3050
|
+
io2.err("the PR body is empty \u2014 drop --body/--body-file or pass text");
|
|
3051
|
+
return 2;
|
|
3052
|
+
}
|
|
3053
|
+
let builtPatchText;
|
|
3054
|
+
let builtCommits = [];
|
|
1949
3055
|
return runEvent({
|
|
1950
3056
|
command: "pr",
|
|
1951
3057
|
flags,
|
|
@@ -1975,22 +3081,38 @@ async function runPrCreate(rest, deps) {
|
|
|
1975
3081
|
}
|
|
1976
3082
|
commits = [];
|
|
1977
3083
|
}
|
|
3084
|
+
builtPatchText = patchText;
|
|
3085
|
+
builtCommits = commits;
|
|
1978
3086
|
return buildPatch(
|
|
1979
3087
|
addr.ownerPubkey,
|
|
1980
3088
|
addr.repoId,
|
|
1981
3089
|
title,
|
|
1982
3090
|
commits,
|
|
1983
3091
|
branch,
|
|
1984
|
-
patchText
|
|
3092
|
+
patchText,
|
|
3093
|
+
body
|
|
1985
3094
|
);
|
|
3095
|
+
},
|
|
3096
|
+
sendDaemon: (client, addr) => {
|
|
3097
|
+
if (builtPatchText === void 0) {
|
|
3098
|
+
throw new Error("internal: patch text not built before delegation");
|
|
3099
|
+
}
|
|
3100
|
+
return client.gitPatch({
|
|
3101
|
+
repoAddr: addr,
|
|
3102
|
+
title,
|
|
3103
|
+
patchText: builtPatchText,
|
|
3104
|
+
...body !== void 0 ? { description: body } : {},
|
|
3105
|
+
...builtCommits.length > 0 ? { commits: builtCommits } : {},
|
|
3106
|
+
...branch !== void 0 ? { branch } : {}
|
|
3107
|
+
});
|
|
1986
3108
|
}
|
|
1987
3109
|
});
|
|
1988
3110
|
}
|
|
1989
3111
|
var STATUS_KIND_BY_VALUE = {
|
|
1990
|
-
open:
|
|
1991
|
-
applied:
|
|
1992
|
-
closed:
|
|
1993
|
-
draft:
|
|
3112
|
+
open: STATUS_OPEN_KIND2,
|
|
3113
|
+
applied: STATUS_APPLIED_KIND2,
|
|
3114
|
+
closed: STATUS_CLOSED_KIND2,
|
|
3115
|
+
draft: STATUS_DRAFT_KIND2
|
|
1994
3116
|
};
|
|
1995
3117
|
function isStatusValue(value) {
|
|
1996
3118
|
return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
|
|
@@ -2001,7 +3123,7 @@ async function runPrStatus(args, deps) {
|
|
|
2001
3123
|
let targetEventId;
|
|
2002
3124
|
let status;
|
|
2003
3125
|
try {
|
|
2004
|
-
const { values, positionals } =
|
|
3126
|
+
const { values, positionals } = parseArgs7({
|
|
2005
3127
|
args,
|
|
2006
3128
|
options: COMMON_OPTIONS,
|
|
2007
3129
|
allowPositionals: true
|
|
@@ -2024,33 +3146,285 @@ async function runPrStatus(args, deps) {
|
|
|
2024
3146
|
`status must be one of open | applied | closed | draft (got ${JSON.stringify(rawStatus)})`
|
|
2025
3147
|
);
|
|
2026
3148
|
}
|
|
2027
|
-
status = rawStatus;
|
|
3149
|
+
status = rawStatus;
|
|
3150
|
+
} catch (err) {
|
|
3151
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3152
|
+
io2.err(PR_STATUS_USAGE);
|
|
3153
|
+
return 2;
|
|
3154
|
+
}
|
|
3155
|
+
return runEvent({
|
|
3156
|
+
command: "pr status",
|
|
3157
|
+
flags,
|
|
3158
|
+
deps,
|
|
3159
|
+
actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
|
|
3160
|
+
buildEvent: async (addr) => {
|
|
3161
|
+
const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
|
|
3162
|
+
event.tags.push([
|
|
3163
|
+
"a",
|
|
3164
|
+
`${REPOSITORY_ANNOUNCEMENT_KIND2}:${addr.ownerPubkey}:${addr.repoId}`
|
|
3165
|
+
]);
|
|
3166
|
+
return event;
|
|
3167
|
+
},
|
|
3168
|
+
// Authority warning (#287): a status event only moves an issue/PR's
|
|
3169
|
+
// resolved state for repo-authority-honoring clients if its author is the
|
|
3170
|
+
// owner or a declared maintainer. Warn clearly when the active identity is
|
|
3171
|
+
// neither — the publish still happens (permissionless relay; the caller
|
|
3172
|
+
// may be acting on their own repo/fork), but the futility is made obvious.
|
|
3173
|
+
preConfirm: (args2) => warnIfNotMaintainer(deps, args2),
|
|
3174
|
+
sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
async function warnIfNotMaintainer(deps, args) {
|
|
3178
|
+
const { io: io2 } = deps;
|
|
3179
|
+
const { identity, addr, relayUrl } = args;
|
|
3180
|
+
if (identity.pubkey.toLowerCase() === addr.ownerPubkey.toLowerCase()) return;
|
|
3181
|
+
if (relayUrl === void 0) return;
|
|
3182
|
+
try {
|
|
3183
|
+
const remote = await fetchRemoteState({
|
|
3184
|
+
relayUrls: [relayUrl],
|
|
3185
|
+
ownerPubkey: addr.ownerPubkey,
|
|
3186
|
+
repoId: addr.repoId,
|
|
3187
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
|
|
3188
|
+
});
|
|
3189
|
+
const authorized = authorizedStatusAuthors(
|
|
3190
|
+
addr.ownerPubkey,
|
|
3191
|
+
remote.announceEvent?.tags ?? []
|
|
3192
|
+
);
|
|
3193
|
+
if (authorized.has(identity.pubkey.toLowerCase())) return;
|
|
3194
|
+
io2.err(
|
|
3195
|
+
`warning: you (${identity.pubkey.slice(0, 8)}\u2026) are not a maintainer of 30617:${addr.ownerPubkey.slice(0, 8)}\u2026:${addr.repoId} \u2014 clients honoring repo authority (rig, rig-web) will IGNORE this status when resolving the issue/PR's state. Publishing anyway (the relay is permissionless); ask the owner to \`rig maintainers add\` you if this should stick.`
|
|
3196
|
+
);
|
|
3197
|
+
} catch {
|
|
3198
|
+
io2.err(
|
|
3199
|
+
"warning: could not read the repo announcement to verify your maintainer status \u2014 if you are not the owner or a declared maintainer, authority-honoring clients will ignore this status."
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// src/cli/fetch.ts
|
|
3205
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
3206
|
+
var FETCH_USAGE = `Usage: rig fetch [remote] [options]
|
|
3207
|
+
|
|
3208
|
+
Fetch from a TOON remote \u2014 FREE (relay reads + Arweave gateway downloads; no
|
|
3209
|
+
payments, no identity needed). Reads the kind:30618 repository state from the
|
|
3210
|
+
remote's relay, downloads only the git objects this repository is missing
|
|
3211
|
+
(SHA-1 verified), and updates the remote-tracking refs
|
|
3212
|
+
(refs/remotes/<remote>/*; tags land at refs/tags/*), reporting what moved.
|
|
3213
|
+
|
|
3214
|
+
No merge happens: integrate with the git passthrough, e.g.
|
|
3215
|
+
\`rig merge origin/main\`. \`rig fetch\` is the TOON transport and shadows
|
|
3216
|
+
\`git fetch\` \u2014 plain-git fetches remain available via \`git fetch\` directly.
|
|
3217
|
+
|
|
3218
|
+
The repo address (toon.repoid/toon.owner) comes from the config \`rig init\`
|
|
3219
|
+
or \`rig clone\` writes; --repo-id/--owner override.
|
|
3220
|
+
|
|
3221
|
+
Options:
|
|
3222
|
+
--repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
3223
|
+
--owner <pubkey> repository owner (npub or 64-char hex; default: git config)
|
|
3224
|
+
--relay <url> ad-hoc relay override \u2014 bypasses the remote's URL
|
|
3225
|
+
--concurrency <n> parallel gateway downloads (default 8)
|
|
3226
|
+
--json machine-readable result envelope
|
|
3227
|
+
-h, --help show this help`;
|
|
3228
|
+
var WS_URL_RE3 = /^wss?:\/\//i;
|
|
3229
|
+
function localRefFor(refname, remote) {
|
|
3230
|
+
if (refname.startsWith("refs/heads/")) {
|
|
3231
|
+
return `refs/remotes/${remote}/${refname.slice("refs/heads/".length)}`;
|
|
3232
|
+
}
|
|
3233
|
+
if (refname.startsWith("refs/tags/")) return refname;
|
|
3234
|
+
return null;
|
|
3235
|
+
}
|
|
3236
|
+
async function resolveLocalRef(repoRoot, refname) {
|
|
3237
|
+
try {
|
|
3238
|
+
const out = await runGit(repoRoot, [
|
|
3239
|
+
"rev-parse",
|
|
3240
|
+
"--verify",
|
|
3241
|
+
"--quiet",
|
|
3242
|
+
refname
|
|
3243
|
+
]);
|
|
3244
|
+
const sha = out.trim();
|
|
3245
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
3246
|
+
} catch {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async function runFetch(args, deps) {
|
|
3251
|
+
const { io: io2 } = deps;
|
|
3252
|
+
let json = false;
|
|
3253
|
+
let remote = "origin";
|
|
3254
|
+
let relayFlag;
|
|
3255
|
+
let repoIdFlag;
|
|
3256
|
+
let ownerFlag;
|
|
3257
|
+
let concurrency;
|
|
3258
|
+
try {
|
|
3259
|
+
const { values, positionals } = parseArgs8({
|
|
3260
|
+
args,
|
|
3261
|
+
options: {
|
|
3262
|
+
json: { type: "boolean", default: false },
|
|
3263
|
+
relay: { type: "string" },
|
|
3264
|
+
"repo-id": { type: "string" },
|
|
3265
|
+
owner: { type: "string" },
|
|
3266
|
+
concurrency: { type: "string" },
|
|
3267
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3268
|
+
},
|
|
3269
|
+
allowPositionals: true
|
|
3270
|
+
});
|
|
3271
|
+
if (values.help) {
|
|
3272
|
+
io2.out(FETCH_USAGE);
|
|
3273
|
+
return 0;
|
|
3274
|
+
}
|
|
3275
|
+
json = values.json === true;
|
|
3276
|
+
if (positionals.length > 1) {
|
|
3277
|
+
throw new Error(
|
|
3278
|
+
`expected at most one [remote] argument, got ${positionals.length}`
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
if (positionals[0] !== void 0) remote = positionals[0];
|
|
3282
|
+
relayFlag = values.relay;
|
|
3283
|
+
repoIdFlag = values["repo-id"];
|
|
3284
|
+
ownerFlag = values.owner !== void 0 ? ownerToHex(values.owner) : void 0;
|
|
3285
|
+
if (values.concurrency !== void 0) {
|
|
3286
|
+
concurrency = Number.parseInt(values.concurrency, 10);
|
|
3287
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
3288
|
+
throw new Error("--concurrency must be a positive integer");
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
2028
3291
|
} catch (err) {
|
|
2029
3292
|
io2.err(err instanceof Error ? err.message : String(err));
|
|
2030
|
-
io2.err(
|
|
3293
|
+
io2.err(FETCH_USAGE);
|
|
2031
3294
|
return 2;
|
|
2032
3295
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
3296
|
+
try {
|
|
3297
|
+
const repoRoot = await resolveRepoRoot(deps.cwd);
|
|
3298
|
+
const toonConfig = await readToonConfig(repoRoot);
|
|
3299
|
+
const repoId = repoIdFlag ?? toonConfig.repoId;
|
|
3300
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
3301
|
+
const owner = ownerFlag ?? toonConfig.owner;
|
|
3302
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
3303
|
+
let relayUrl;
|
|
3304
|
+
if (relayFlag !== void 0) {
|
|
3305
|
+
relayUrl = relayFlag;
|
|
3306
|
+
} else {
|
|
3307
|
+
const urls = await getGitRemoteUrls(repoRoot, remote);
|
|
3308
|
+
if (urls.length === 0) throw new UnknownRemoteError(remote);
|
|
3309
|
+
if (urls.length > 1) throw new MultiUrlRemoteError(remote, urls);
|
|
3310
|
+
relayUrl = urls[0];
|
|
2045
3311
|
}
|
|
2046
|
-
|
|
3312
|
+
if (!WS_URL_RE3.test(relayUrl)) {
|
|
3313
|
+
throw new InvalidRelayUrlError(
|
|
3314
|
+
relayUrl,
|
|
3315
|
+
`remote ${JSON.stringify(remote)}`
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
const remoteState = await fetchRemoteState({
|
|
3319
|
+
relayUrls: [relayUrl],
|
|
3320
|
+
ownerPubkey: owner,
|
|
3321
|
+
repoId,
|
|
3322
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {},
|
|
3323
|
+
...deps.resolveSha ? { resolveSha: deps.resolveSha } : {}
|
|
3324
|
+
});
|
|
3325
|
+
const planned = [];
|
|
3326
|
+
for (const [refname, sha] of remoteState.refs) {
|
|
3327
|
+
if (!isSafeRefname(refname)) {
|
|
3328
|
+
io2.err(
|
|
3329
|
+
`warning: skipping unsafe ref name from relay: ${JSON.stringify(refname)}`
|
|
3330
|
+
);
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
const localRef = localRefFor(refname, remote);
|
|
3334
|
+
if (localRef === null) {
|
|
3335
|
+
io2.err(
|
|
3336
|
+
`warning: skipping ref outside refs/heads and refs/tags: ${refname}`
|
|
3337
|
+
);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
planned.push({ refname, localRef, newSha: sha });
|
|
3341
|
+
}
|
|
3342
|
+
const reader = new GitRepoReader(repoRoot);
|
|
3343
|
+
const tips = [...new Set(planned.map((p) => p.newSha))];
|
|
3344
|
+
const candidates = [.../* @__PURE__ */ new Set([...remoteState.shaToTxId.keys(), ...tips])];
|
|
3345
|
+
const { missing: absent } = await reader.statObjects(candidates);
|
|
3346
|
+
const absentSet = new Set(absent);
|
|
3347
|
+
const presentLocally = new Set(
|
|
3348
|
+
candidates.filter((sha) => !absentSet.has(sha))
|
|
3349
|
+
);
|
|
3350
|
+
const collected = await collectRepoObjects({
|
|
3351
|
+
tips,
|
|
3352
|
+
shaToTxId: remoteState.shaToTxId,
|
|
3353
|
+
resolveMissing: (shas) => remoteState.resolveMissing(shas),
|
|
3354
|
+
presentLocally,
|
|
3355
|
+
...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
|
|
3356
|
+
...concurrency !== void 0 ? { concurrency } : {}
|
|
3357
|
+
});
|
|
3358
|
+
if (collected.missing.length > 0) {
|
|
3359
|
+
throw new MissingRemoteObjectsError(
|
|
3360
|
+
collected.missing,
|
|
3361
|
+
`cannot fetch from ${relayUrl}`
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
const written = await writeGitObjects(repoRoot, collected.objects.values());
|
|
3365
|
+
const updates = [];
|
|
3366
|
+
for (const { refname, localRef, newSha } of planned) {
|
|
3367
|
+
const oldSha = await resolveLocalRef(repoRoot, localRef);
|
|
3368
|
+
if (oldSha === newSha) {
|
|
3369
|
+
updates.push({ refname, localRef, oldSha, newSha, kind: "up-to-date" });
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
let kind;
|
|
3373
|
+
if (oldSha === null) {
|
|
3374
|
+
kind = "new";
|
|
3375
|
+
} else {
|
|
3376
|
+
kind = await reader.isAncestor(oldSha, newSha) ? "fast-forward" : "forced";
|
|
3377
|
+
}
|
|
3378
|
+
await updateRef(repoRoot, localRef, newSha);
|
|
3379
|
+
updates.push({ refname, localRef, oldSha, newSha, kind });
|
|
3380
|
+
}
|
|
3381
|
+
const moved = updates.filter((u) => u.kind !== "up-to-date");
|
|
3382
|
+
if (json) {
|
|
3383
|
+
io2.emitJson({
|
|
3384
|
+
command: "fetch",
|
|
3385
|
+
repoAddr: { ownerPubkey: owner, repoId },
|
|
3386
|
+
remote,
|
|
3387
|
+
relay: relayUrl,
|
|
3388
|
+
objectsDownloaded: written,
|
|
3389
|
+
updates,
|
|
3390
|
+
executed: true
|
|
3391
|
+
});
|
|
3392
|
+
return 0;
|
|
3393
|
+
}
|
|
3394
|
+
if (moved.length === 0) {
|
|
3395
|
+
io2.out("Already up to date.");
|
|
3396
|
+
return 0;
|
|
3397
|
+
}
|
|
3398
|
+
io2.out(`From ${relayUrl}`);
|
|
3399
|
+
for (const u of moved) {
|
|
3400
|
+
const short = (refname) => refname.replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/^refs\/remotes\//, "");
|
|
3401
|
+
const src = short(u.refname);
|
|
3402
|
+
const dst = short(u.localRef);
|
|
3403
|
+
if (u.kind === "new") {
|
|
3404
|
+
const label = u.refname.startsWith("refs/tags/") ? "[new tag]" : "[new branch]";
|
|
3405
|
+
io2.out(` * ${label.padEnd(17)} ${src.padEnd(14)} -> ${dst}`);
|
|
3406
|
+
} else if (u.kind === "forced") {
|
|
3407
|
+
io2.out(
|
|
3408
|
+
` + ${u.oldSha.slice(0, 7)}...${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst} (forced update)`
|
|
3409
|
+
);
|
|
3410
|
+
} else {
|
|
3411
|
+
io2.out(
|
|
3412
|
+
` ${u.oldSha.slice(0, 7)}..${u.newSha.slice(0, 7)} ${src.padEnd(14)} -> ${dst}`
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
io2.out(`Downloaded ${written} object(s) (SHA-1 verified).`);
|
|
3417
|
+
return 0;
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
return emitCliError(io2, json, "fetch", err);
|
|
3420
|
+
}
|
|
2047
3421
|
}
|
|
2048
3422
|
|
|
2049
3423
|
// src/cli/fund.ts
|
|
2050
3424
|
import { readFileSync } from "fs";
|
|
2051
3425
|
import { homedir } from "os";
|
|
2052
|
-
import { join } from "path";
|
|
2053
|
-
import { parseArgs as
|
|
3426
|
+
import { join as join2 } from "path";
|
|
3427
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
2054
3428
|
var DEVNET_FAUCET_URL = "https://faucet.devnet.toonprotocol.dev";
|
|
2055
3429
|
var CHAINS = ["evm", "solana", "mina"];
|
|
2056
3430
|
var FUND_USAGE = `Usage: rig fund [options]
|
|
@@ -2058,10 +3432,11 @@ var FUND_USAGE = `Usage: rig fund [options]
|
|
|
2058
3432
|
Drip devnet test funds to the active identity's wallet \u2014 free (the faucet
|
|
2059
3433
|
pays). The identity comes from RIG_MNEMONIC (env or a project .env) or the
|
|
2060
3434
|
~/.toon-client keystore/config; the faucet from TOON_CLIENT_FAUCET_URL, the
|
|
2061
|
-
faucetUrl config field, or the deployed devnet faucet when the
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
fund externally
|
|
3435
|
+
faucetUrl config field, or the deployed devnet faucet when the network is
|
|
3436
|
+
devnet \u2014 including when a configured *.devnet.toonprotocol.dev origin infers
|
|
3437
|
+
it. The faucet drips a FIXED amount per chain (there is no --amount). On a
|
|
3438
|
+
network without a faucet, prints the wallet address(es) to fund externally
|
|
3439
|
+
instead.
|
|
2065
3440
|
|
|
2066
3441
|
Options:
|
|
2067
3442
|
--chain <chain> evm | solana | mina (default: TOON_CLIENT_CHAIN, the
|
|
@@ -2069,9 +3444,36 @@ Options:
|
|
|
2069
3444
|
--address <address> fund this address instead of the identity's own
|
|
2070
3445
|
--json machine-readable envelope
|
|
2071
3446
|
-h, --help show this help`;
|
|
3447
|
+
var SHARED_DEVNET_SUFFIX = ".devnet.toonprotocol.dev";
|
|
3448
|
+
function sharedDevnetOrigin(env, file) {
|
|
3449
|
+
const candidates = [
|
|
3450
|
+
env["TOON_CLIENT_RELAY_URL"] ?? file.relayUrl,
|
|
3451
|
+
env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl,
|
|
3452
|
+
env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl
|
|
3453
|
+
];
|
|
3454
|
+
for (const url of candidates) {
|
|
3455
|
+
if (!url) continue;
|
|
3456
|
+
try {
|
|
3457
|
+
const { hostname } = new URL(url);
|
|
3458
|
+
if (hostname.endsWith(SHARED_DEVNET_SUFFIX) || hostname === SHARED_DEVNET_SUFFIX.slice(1)) {
|
|
3459
|
+
return url;
|
|
3460
|
+
}
|
|
3461
|
+
} catch {
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return void 0;
|
|
3465
|
+
}
|
|
3466
|
+
function noFaucetGuidance(network, devnetOrigin) {
|
|
3467
|
+
const head = `no faucet is configured for network ${JSON.stringify(network ?? "custom")}`;
|
|
3468
|
+
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.";
|
|
3469
|
+
if (devnetOrigin !== void 0) {
|
|
3470
|
+
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}`;
|
|
3471
|
+
}
|
|
3472
|
+
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}`;
|
|
3473
|
+
}
|
|
2072
3474
|
function readFundConfig(env) {
|
|
2073
|
-
const dir = env["TOON_CLIENT_HOME"] ??
|
|
2074
|
-
const configPath =
|
|
3475
|
+
const dir = env["TOON_CLIENT_HOME"] ?? join2(homedir(), ".toon-client");
|
|
3476
|
+
const configPath = join2(dir, "config.json");
|
|
2075
3477
|
try {
|
|
2076
3478
|
return {
|
|
2077
3479
|
file: JSON.parse(readFileSync(configPath, "utf8")),
|
|
@@ -2092,7 +3494,7 @@ async function runFund(args, deps) {
|
|
|
2092
3494
|
let addressFlag;
|
|
2093
3495
|
let json = false;
|
|
2094
3496
|
try {
|
|
2095
|
-
const { values } =
|
|
3497
|
+
const { values } = parseArgs9({
|
|
2096
3498
|
args,
|
|
2097
3499
|
options: {
|
|
2098
3500
|
chain: { type: "string" },
|
|
@@ -2127,7 +3529,10 @@ async function runFund(args, deps) {
|
|
|
2127
3529
|
);
|
|
2128
3530
|
}
|
|
2129
3531
|
const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
|
|
2130
|
-
const
|
|
3532
|
+
const devnetOrigin = sharedDevnetOrigin(env, file);
|
|
3533
|
+
const inferredDevnet = devnetOrigin !== void 0 && (network === void 0 || network === "custom");
|
|
3534
|
+
const effectiveNetwork = inferredDevnet ? "devnet" : network;
|
|
3535
|
+
const faucetUrl = env["TOON_CLIENT_FAUCET_URL"] ?? file.faucetUrl ?? (effectiveNetwork === "devnet" ? DEVNET_FAUCET_URL : void 0);
|
|
2131
3536
|
const resolved = await resolveIdentity({
|
|
2132
3537
|
env,
|
|
2133
3538
|
cwd: deps.cwd,
|
|
@@ -2149,7 +3554,7 @@ async function runFund(args, deps) {
|
|
|
2149
3554
|
mina: derived.mina.publicKey || null
|
|
2150
3555
|
};
|
|
2151
3556
|
if (!faucetUrl) {
|
|
2152
|
-
const guidance =
|
|
3557
|
+
const guidance = noFaucetGuidance(network, devnetOrigin);
|
|
2153
3558
|
if (json) {
|
|
2154
3559
|
io2.emitJson({
|
|
2155
3560
|
command: "fund",
|
|
@@ -2178,6 +3583,11 @@ async function runFund(args, deps) {
|
|
|
2178
3583
|
}
|
|
2179
3584
|
const timeoutEnv = env["TOON_CLIENT_FAUCET_TIMEOUT_MS"];
|
|
2180
3585
|
const timeout = timeoutEnv && Number.isFinite(Number(timeoutEnv)) ? Number(timeoutEnv) : file.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
|
|
3586
|
+
if (!json && inferredDevnet) {
|
|
3587
|
+
io2.out(
|
|
3588
|
+
`Inferred network 'devnet' from the configured origin ${devnetOrigin} (network was ${JSON.stringify(network ?? "custom")}). Set TOON_CLIENT_NETWORK explicitly to override.`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
2181
3591
|
if (!json) {
|
|
2182
3592
|
io2.out(
|
|
2183
3593
|
`Requesting ${chain} drip from ${faucetUrl} for ${address} \u2026` + (chain === "mina" ? " (mina settles slowly; this can take ~2 minutes)" : "")
|
|
@@ -2192,11 +3602,12 @@ async function runFund(args, deps) {
|
|
|
2192
3602
|
command: "fund",
|
|
2193
3603
|
identity,
|
|
2194
3604
|
funded: true,
|
|
2195
|
-
network:
|
|
3605
|
+
network: effectiveNetwork ?? null,
|
|
2196
3606
|
chain,
|
|
2197
3607
|
address,
|
|
2198
3608
|
faucetUrl,
|
|
2199
|
-
response
|
|
3609
|
+
response,
|
|
3610
|
+
...inferredDevnet ? { inferredDevnetFrom: devnetOrigin } : {}
|
|
2200
3611
|
});
|
|
2201
3612
|
return 0;
|
|
2202
3613
|
}
|
|
@@ -2221,7 +3632,7 @@ function signalExitCode(signal) {
|
|
|
2221
3632
|
var runGitPassthrough = (argv2, options = {}) => {
|
|
2222
3633
|
const err = options.err ?? ((line) => process.stderr.write(`${line}
|
|
2223
3634
|
`));
|
|
2224
|
-
return new Promise((
|
|
3635
|
+
return new Promise((resolve2) => {
|
|
2225
3636
|
const child = spawn("git", argv2, {
|
|
2226
3637
|
stdio: "inherit",
|
|
2227
3638
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
@@ -2244,22 +3655,22 @@ var runGitPassthrough = (argv2, options = {}) => {
|
|
|
2244
3655
|
err(
|
|
2245
3656
|
`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.`
|
|
2246
3657
|
);
|
|
2247
|
-
|
|
3658
|
+
resolve2(GIT_NOT_FOUND_EXIT);
|
|
2248
3659
|
return;
|
|
2249
3660
|
}
|
|
2250
3661
|
err(`rig: failed to run git: ${spawnErr.message}`);
|
|
2251
|
-
|
|
3662
|
+
resolve2(1);
|
|
2252
3663
|
});
|
|
2253
3664
|
child.on("close", (code, signal) => {
|
|
2254
3665
|
restoreSignals();
|
|
2255
|
-
|
|
3666
|
+
resolve2(signal !== null ? signalExitCode(signal) : code ?? 1);
|
|
2256
3667
|
});
|
|
2257
3668
|
});
|
|
2258
3669
|
};
|
|
2259
3670
|
|
|
2260
3671
|
// src/cli/init.ts
|
|
2261
|
-
import { basename } from "path";
|
|
2262
|
-
import { parseArgs as
|
|
3672
|
+
import { basename as basename2 } from "path";
|
|
3673
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
2263
3674
|
var INIT_USAGE = `Usage: rig init [options]
|
|
2264
3675
|
|
|
2265
3676
|
Set up the current git repository for rig (one-shot, idempotent, free):
|
|
@@ -2279,7 +3690,7 @@ Options:
|
|
|
2279
3690
|
--json machine-readable report
|
|
2280
3691
|
-h, --help show this help`;
|
|
2281
3692
|
function parseInitArgs(args) {
|
|
2282
|
-
const { values, positionals } =
|
|
3693
|
+
const { values, positionals } = parseArgs10({
|
|
2283
3694
|
args,
|
|
2284
3695
|
options: {
|
|
2285
3696
|
"repo-id": { type: "string" },
|
|
@@ -2329,7 +3740,7 @@ async function runInit(args, deps) {
|
|
|
2329
3740
|
warn: (line) => io2.err(line)
|
|
2330
3741
|
});
|
|
2331
3742
|
const existing = await readToonConfig(repoRoot);
|
|
2332
|
-
const repoId = flags.repoId ?? existing.repoId ??
|
|
3743
|
+
const repoId = flags.repoId ?? existing.repoId ?? basename2(repoRoot);
|
|
2333
3744
|
const changed = {
|
|
2334
3745
|
repoId: existing.repoId !== repoId,
|
|
2335
3746
|
owner: existing.owner !== identity.pubkey
|
|
@@ -2404,6 +3815,326 @@ async function runInit(args, deps) {
|
|
|
2404
3815
|
}
|
|
2405
3816
|
}
|
|
2406
3817
|
|
|
3818
|
+
// src/cli/maintainers.ts
|
|
3819
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
3820
|
+
var HEX64_RE3 = /^[0-9a-f]{64}$/;
|
|
3821
|
+
var WS_URL_RE4 = /^wss?:\/\//i;
|
|
3822
|
+
var MAINTAINERS_USAGE = `Usage: rig maintainers <list|add|remove> [<pubkey>] [options]
|
|
3823
|
+
|
|
3824
|
+
Manage a repo's declared maintainers (#287). Status authority is consumer-side:
|
|
3825
|
+
rig and rig-web honor an issue/PR status (kind:1630-1633) ONLY when its author
|
|
3826
|
+
is the repo owner or a declared maintainer. The owner is always an implicit
|
|
3827
|
+
maintainer; this command edits the explicit set on the kind:30617 announcement.
|
|
3828
|
+
|
|
3829
|
+
Subcommands:
|
|
3830
|
+
list show the owner + declared maintainers \u2014 FREE (relay read)
|
|
3831
|
+
add <pubkey> add a maintainer (npub or 64-char hex) \u2014 PAID: republishes
|
|
3832
|
+
the kind:30617 (permanent, non-refundable)
|
|
3833
|
+
remove <pubkey> remove a maintainer \u2014 PAID: republishes the kind:30617
|
|
3834
|
+
|
|
3835
|
+
add/remove must run under the repo OWNER's identity (only the owner's
|
|
3836
|
+
announcement is authoritative).
|
|
3837
|
+
|
|
3838
|
+
Options:
|
|
3839
|
+
--repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
3840
|
+
--owner <pubkey> repository owner (npub or hex; default: git config)
|
|
3841
|
+
--remote <name> publish/read via this configured git remote (default: origin)
|
|
3842
|
+
--relay <url> ad-hoc relay override (exactly one for add/remove)
|
|
3843
|
+
--yes skip the fee confirmation (required when not a TTY)
|
|
3844
|
+
--json machine-readable envelope
|
|
3845
|
+
-h, --help show this help`;
|
|
3846
|
+
var MAINT_OPTIONS = {
|
|
3847
|
+
json: { type: "boolean", default: false },
|
|
3848
|
+
yes: { type: "boolean", default: false },
|
|
3849
|
+
relay: { type: "string", multiple: true },
|
|
3850
|
+
remote: { type: "string" },
|
|
3851
|
+
"repo-id": { type: "string" },
|
|
3852
|
+
owner: { type: "string" },
|
|
3853
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3854
|
+
};
|
|
3855
|
+
function pickFlags(values) {
|
|
3856
|
+
const flags = {
|
|
3857
|
+
json: values["json"] === true,
|
|
3858
|
+
yes: values["yes"] === true,
|
|
3859
|
+
help: values["help"] === true,
|
|
3860
|
+
relay: Array.isArray(values["relay"]) ? values["relay"] : []
|
|
3861
|
+
};
|
|
3862
|
+
if (typeof values["remote"] === "string") flags.remote = values["remote"];
|
|
3863
|
+
if (typeof values["repo-id"] === "string") flags.repoId = values["repo-id"];
|
|
3864
|
+
if (typeof values["owner"] === "string")
|
|
3865
|
+
flags.owner = ownerToHex(values["owner"]);
|
|
3866
|
+
return flags;
|
|
3867
|
+
}
|
|
3868
|
+
async function resolveContext(flags, deps) {
|
|
3869
|
+
let repoRoot;
|
|
3870
|
+
let toonConfig = {
|
|
3871
|
+
relays: []
|
|
3872
|
+
};
|
|
3873
|
+
try {
|
|
3874
|
+
repoRoot = await resolveRepoRoot(deps.cwd);
|
|
3875
|
+
toonConfig = await readToonConfig(repoRoot);
|
|
3876
|
+
} catch {
|
|
3877
|
+
}
|
|
3878
|
+
const repoId = flags.repoId ?? toonConfig.repoId;
|
|
3879
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
3880
|
+
const owner = flags.owner ?? toonConfig.owner;
|
|
3881
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
3882
|
+
const resolved = await resolveRelays({
|
|
3883
|
+
relayFlags: flags.relay,
|
|
3884
|
+
remoteName: flags.remote,
|
|
3885
|
+
repoRoot,
|
|
3886
|
+
toonRelays: toonConfig.relays
|
|
3887
|
+
});
|
|
3888
|
+
if (resolved.nudge !== void 0) deps.io.err(resolved.nudge);
|
|
3889
|
+
return {
|
|
3890
|
+
repoId,
|
|
3891
|
+
owner,
|
|
3892
|
+
relays: resolved.relays,
|
|
3893
|
+
resolved,
|
|
3894
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
async function runMaintainers(args, deps) {
|
|
3898
|
+
const { io: io2 } = deps;
|
|
3899
|
+
const [sub, ...rest] = args;
|
|
3900
|
+
switch (sub) {
|
|
3901
|
+
case "list":
|
|
3902
|
+
return runList2(rest, deps);
|
|
3903
|
+
case "add":
|
|
3904
|
+
return runMutate("add", rest, deps);
|
|
3905
|
+
case "remove":
|
|
3906
|
+
return runMutate("remove", rest, deps);
|
|
3907
|
+
case "--help":
|
|
3908
|
+
case "-h":
|
|
3909
|
+
case "help":
|
|
3910
|
+
io2.out(MAINTAINERS_USAGE);
|
|
3911
|
+
return 0;
|
|
3912
|
+
default:
|
|
3913
|
+
io2.err(
|
|
3914
|
+
sub === void 0 ? "missing subcommand: rig maintainers <list|add|remove>" : `unknown rig maintainers subcommand: ${sub}`
|
|
3915
|
+
);
|
|
3916
|
+
io2.err(MAINTAINERS_USAGE);
|
|
3917
|
+
return 2;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
async function runList2(args, deps) {
|
|
3921
|
+
const { io: io2 } = deps;
|
|
3922
|
+
let flags;
|
|
3923
|
+
try {
|
|
3924
|
+
const { values, positionals } = parseArgs11({
|
|
3925
|
+
args,
|
|
3926
|
+
options: MAINT_OPTIONS,
|
|
3927
|
+
allowPositionals: true
|
|
3928
|
+
});
|
|
3929
|
+
if (positionals.length > 0) {
|
|
3930
|
+
throw new Error("rig maintainers list takes no positional arguments");
|
|
3931
|
+
}
|
|
3932
|
+
flags = pickFlags(values);
|
|
3933
|
+
} catch (err) {
|
|
3934
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
3935
|
+
io2.err(MAINTAINERS_USAGE);
|
|
3936
|
+
return 2;
|
|
3937
|
+
}
|
|
3938
|
+
if (flags.help) {
|
|
3939
|
+
io2.out(MAINTAINERS_USAGE);
|
|
3940
|
+
return 0;
|
|
3941
|
+
}
|
|
3942
|
+
try {
|
|
3943
|
+
const ctx = await resolveContext(flags, deps);
|
|
3944
|
+
const wsRelays = ctx.relays.filter((url) => WS_URL_RE4.test(url));
|
|
3945
|
+
if (wsRelays.length === 0) {
|
|
3946
|
+
throw new InvalidRelayUrlError(
|
|
3947
|
+
ctx.relays[0] ?? "",
|
|
3948
|
+
"reads need a ws:// or wss:// relay"
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
const remote = await fetchRemoteState({
|
|
3952
|
+
relayUrls: wsRelays,
|
|
3953
|
+
ownerPubkey: ctx.owner,
|
|
3954
|
+
repoId: ctx.repoId,
|
|
3955
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
|
|
3956
|
+
});
|
|
3957
|
+
const maintainers = remote.maintainers;
|
|
3958
|
+
if (flags.json) {
|
|
3959
|
+
io2.emitJson({
|
|
3960
|
+
command: "maintainers list",
|
|
3961
|
+
repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
|
|
3962
|
+
announced: remote.announced,
|
|
3963
|
+
owner: ctx.owner,
|
|
3964
|
+
maintainers
|
|
3965
|
+
});
|
|
3966
|
+
return 0;
|
|
3967
|
+
}
|
|
3968
|
+
io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
|
|
3969
|
+
io2.out(`Owner: ${ctx.owner} (implicit maintainer)`);
|
|
3970
|
+
if (!remote.announced) {
|
|
3971
|
+
io2.out("No kind:30617 announcement found \u2014 owner-only authority.");
|
|
3972
|
+
return 0;
|
|
3973
|
+
}
|
|
3974
|
+
if (maintainers.length === 0) {
|
|
3975
|
+
io2.out("Maintainers: (none declared \u2014 owner-only authority)");
|
|
3976
|
+
} else {
|
|
3977
|
+
io2.out(`Maintainers (${maintainers.length}):`);
|
|
3978
|
+
for (const m of maintainers) io2.out(` ${m}`);
|
|
3979
|
+
}
|
|
3980
|
+
return 0;
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
return emitCliError(io2, flags.json, "maintainers list", err);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
async function runMutate(op, args, deps) {
|
|
3986
|
+
const { io: io2 } = deps;
|
|
3987
|
+
const command = `maintainers ${op}`;
|
|
3988
|
+
let flags;
|
|
3989
|
+
let pubkey;
|
|
3990
|
+
try {
|
|
3991
|
+
const { values, positionals } = parseArgs11({
|
|
3992
|
+
args,
|
|
3993
|
+
options: MAINT_OPTIONS,
|
|
3994
|
+
allowPositionals: true
|
|
3995
|
+
});
|
|
3996
|
+
flags = pickFlags(values);
|
|
3997
|
+
if (flags.help) {
|
|
3998
|
+
io2.out(MAINTAINERS_USAGE);
|
|
3999
|
+
return 0;
|
|
4000
|
+
}
|
|
4001
|
+
if (positionals.length !== 1) {
|
|
4002
|
+
throw new Error(`expected exactly one <pubkey> to ${op}`);
|
|
4003
|
+
}
|
|
4004
|
+
pubkey = ownerToHex(positionals[0]).toLowerCase();
|
|
4005
|
+
if (!HEX64_RE3.test(pubkey)) {
|
|
4006
|
+
throw new Error(`<pubkey> must resolve to 64-char hex (got ${JSON.stringify(positionals[0])})`);
|
|
4007
|
+
}
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
4010
|
+
io2.err(MAINTAINERS_USAGE);
|
|
4011
|
+
return 2;
|
|
4012
|
+
}
|
|
4013
|
+
let standaloneCtx;
|
|
4014
|
+
try {
|
|
4015
|
+
const ctx = await resolveContext(flags, deps);
|
|
4016
|
+
if (ctx.relays.length > 1) {
|
|
4017
|
+
io2.err(singleRelayRefusal(ctx.resolved, "Nothing was published or paid."));
|
|
4018
|
+
return 1;
|
|
4019
|
+
}
|
|
4020
|
+
const relayUrl = ctx.relays[0];
|
|
4021
|
+
if (relayUrl === void 0 || !WS_URL_RE4.test(relayUrl)) {
|
|
4022
|
+
throw new InvalidRelayUrlError(
|
|
4023
|
+
relayUrl ?? "",
|
|
4024
|
+
"a paid publish needs a ws:// or wss:// relay"
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
const load = deps.loadStandalone ?? defaultLoadStandalone;
|
|
4028
|
+
standaloneCtx = await load({
|
|
4029
|
+
env: deps.env,
|
|
4030
|
+
cwd: deps.cwd,
|
|
4031
|
+
warn: (line) => io2.err(line),
|
|
4032
|
+
relayUrl
|
|
4033
|
+
});
|
|
4034
|
+
const identity = identityReport(standaloneCtx);
|
|
4035
|
+
if (identity.pubkey.toLowerCase() !== ctx.owner.toLowerCase()) {
|
|
4036
|
+
io2.err(
|
|
4037
|
+
`rig: only the repo owner (${ctx.owner.slice(0, 8)}\u2026) can change the maintainer set \u2014 the active identity is ${identity.pubkey.slice(0, 8)}\u2026. A non-owner republish would write your own (ignored) announcement. Nothing was published or paid.`
|
|
4038
|
+
);
|
|
4039
|
+
return 1;
|
|
4040
|
+
}
|
|
4041
|
+
const remote = await fetchRemoteState({
|
|
4042
|
+
relayUrls: [relayUrl],
|
|
4043
|
+
ownerPubkey: ctx.owner,
|
|
4044
|
+
repoId: ctx.repoId,
|
|
4045
|
+
...deps.webSocketFactory ? { webSocketFactory: deps.webSocketFactory } : {}
|
|
4046
|
+
});
|
|
4047
|
+
if (!remote.announced) {
|
|
4048
|
+
io2.err(
|
|
4049
|
+
`rig: 30617:${ctx.owner.slice(0, 8)}\u2026:${ctx.repoId} has no announcement yet \u2014 run \`rig push\` to publish the repo (with its real name/description) before managing maintainers. Nothing was published or paid.`
|
|
4050
|
+
);
|
|
4051
|
+
return 1;
|
|
4052
|
+
}
|
|
4053
|
+
const current = remote.announceEvent ? parseMaintainers(remote.announceEvent.tags) : [];
|
|
4054
|
+
const currentSet = new Set(current);
|
|
4055
|
+
if (op === "add" && currentSet.has(pubkey)) {
|
|
4056
|
+
io2.err(
|
|
4057
|
+
`rig: ${pubkey.slice(0, 8)}\u2026 is already a maintainer \u2014 nothing to do (not published).`
|
|
4058
|
+
);
|
|
4059
|
+
return 0;
|
|
4060
|
+
}
|
|
4061
|
+
if (op === "remove" && !currentSet.has(pubkey)) {
|
|
4062
|
+
io2.err(
|
|
4063
|
+
`rig: ${pubkey.slice(0, 8)}\u2026 is not a declared maintainer \u2014 nothing to do (not published).`
|
|
4064
|
+
);
|
|
4065
|
+
return 0;
|
|
4066
|
+
}
|
|
4067
|
+
const next = op === "add" ? [...current, pubkey] : current.filter((m) => m !== pubkey);
|
|
4068
|
+
const name = remote.name ?? ctx.repoId;
|
|
4069
|
+
const description = remote.description ?? "";
|
|
4070
|
+
const event = buildRepoAnnouncement(ctx.repoId, name, description, next);
|
|
4071
|
+
const fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
|
|
4072
|
+
const action = `kind:30617 maintainers ${op} ${pubkey.slice(0, 8)}\u2026`;
|
|
4073
|
+
if (!flags.json) {
|
|
4074
|
+
io2.out(`Republish ${action}`);
|
|
4075
|
+
io2.out(`Repo: 30617:${ctx.owner}:${ctx.repoId}`);
|
|
4076
|
+
io2.out(`Maintainers after: ${next.length === 0 ? "(none)" : next.join(", ")}`);
|
|
4077
|
+
io2.out(`Fee: ${feeLabel(fee)}. Writes are permanent and non-refundable.`);
|
|
4078
|
+
}
|
|
4079
|
+
if (!flags.yes) {
|
|
4080
|
+
if (flags.json) {
|
|
4081
|
+
io2.emitJson({
|
|
4082
|
+
command,
|
|
4083
|
+
repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
|
|
4084
|
+
identity,
|
|
4085
|
+
executed: false,
|
|
4086
|
+
feeEstimate: fee,
|
|
4087
|
+
maintainers: next,
|
|
4088
|
+
hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
|
|
4089
|
+
});
|
|
4090
|
+
return 0;
|
|
4091
|
+
}
|
|
4092
|
+
if (!io2.isInteractive) {
|
|
4093
|
+
io2.err(
|
|
4094
|
+
"refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
|
|
4095
|
+
);
|
|
4096
|
+
return 1;
|
|
4097
|
+
}
|
|
4098
|
+
const proceed = await io2.confirm(
|
|
4099
|
+
`Proceed with paid republish (${feeLabel(fee)})? [y/N] `
|
|
4100
|
+
);
|
|
4101
|
+
if (!proceed) {
|
|
4102
|
+
io2.err("aborted \u2014 nothing was published.");
|
|
4103
|
+
return 1;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
const receipt = await standaloneCtx.publisher.publishEvent(event, [
|
|
4107
|
+
relayUrl
|
|
4108
|
+
]);
|
|
4109
|
+
const result = serializeEventReceipt(event.kind, receipt);
|
|
4110
|
+
if (flags.json) {
|
|
4111
|
+
io2.emitJson({
|
|
4112
|
+
command,
|
|
4113
|
+
repoAddr: { ownerPubkey: ctx.owner, repoId: ctx.repoId },
|
|
4114
|
+
identity,
|
|
4115
|
+
executed: true,
|
|
4116
|
+
feeEstimate: fee,
|
|
4117
|
+
maintainers: next,
|
|
4118
|
+
result
|
|
4119
|
+
});
|
|
4120
|
+
} else {
|
|
4121
|
+
io2.out(
|
|
4122
|
+
`Published ${action}: ${result.eventId} paid ${result.feePaid} base units`
|
|
4123
|
+
);
|
|
4124
|
+
}
|
|
4125
|
+
return 0;
|
|
4126
|
+
} catch (err) {
|
|
4127
|
+
return emitCliError(io2, flags.json, command, err);
|
|
4128
|
+
} finally {
|
|
4129
|
+
if (standaloneCtx) {
|
|
4130
|
+
try {
|
|
4131
|
+
await standaloneCtx.stop();
|
|
4132
|
+
} catch {
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
|
|
2407
4138
|
// src/cli/dispatch.ts
|
|
2408
4139
|
var USAGE = `rig \u2014 git with a TOON remote (pay-to-write Nostr + Arweave)
|
|
2409
4140
|
|
|
@@ -2415,17 +4146,31 @@ Commands rig owns:
|
|
|
2415
4146
|
remote add <name> <url> add a relay as a REAL git remote ("origin" is
|
|
2416
4147
|
remote remove <name> the default publish target); remove/list manage
|
|
2417
4148
|
remote list them \u2014 \`git remote -v\` shows the same data
|
|
4149
|
+
clone <relay-url> <owner>/<repo-id> [dir]
|
|
4150
|
+
clone a TOON repo (free): relay state + Arweave
|
|
4151
|
+
objects (SHA-1 verified) \u2192 a real git repository,
|
|
4152
|
+
push/pull-capable out of the box
|
|
4153
|
+
fetch [remote] fetch remote refs + missing objects (free) and
|
|
4154
|
+
update refs/remotes/<remote>/*; no merge. Shadows
|
|
4155
|
+
git fetch (plain \`git fetch\` stays available)
|
|
2418
4156
|
push [remote] [refspecs...] plan, price, confirm, and execute a paid push
|
|
2419
4157
|
to TOON (defaults to the "origin" remote). rig
|
|
2420
4158
|
push is the TOON transport and shadows git push;
|
|
2421
4159
|
plain-git pushes remain available by running
|
|
2422
4160
|
\`git push\` directly
|
|
2423
4161
|
issue create file an issue (kind:1621) against a repo
|
|
4162
|
+
issue list | show <id> read the repo's issues + comments (free)
|
|
4163
|
+
pr list | show <id> read the repo's patches; show prints the full
|
|
4164
|
+
patch text (free)
|
|
2424
4165
|
comment <root-event-id> comment (kind:1622) on an issue or patch
|
|
2425
4166
|
pr create publish a patch (kind:1617) with real
|
|
2426
4167
|
\`git format-patch\` content
|
|
2427
4168
|
pr status <event-id> <state> set an issue/patch status (kind:1630-1633):
|
|
2428
4169
|
open | applied | closed | draft
|
|
4170
|
+
maintainers list show the repo's declared maintainers (free); add/
|
|
4171
|
+
maintainers add <pubkey> remove republish the kind:30617 to change who may
|
|
4172
|
+
maintainers remove <pubkey> author authoritative issue/PR status (owner is
|
|
4173
|
+
always an implicit maintainer)
|
|
2429
4174
|
fund drip devnet faucet funds to this identity's
|
|
2430
4175
|
wallet (free); on other networks prints the
|
|
2431
4176
|
address(es) to fund externally
|
|
@@ -2443,8 +4188,10 @@ Any other command is passed through to git verbatim: \`rig status\` runs
|
|
|
2443
4188
|
\`rig rebase -i\`, \u2026 behave exactly like git (same output, same exit code).
|
|
2444
4189
|
|
|
2445
4190
|
Run \`rig <command> --help\` for a rig command's flags. \`rig init\`,
|
|
2446
|
-
\`rig remote\`, \`rig
|
|
2447
|
-
|
|
4191
|
+
\`rig remote\`, \`rig clone\`, \`rig fetch\`, \`rig issue list/show\`,
|
|
4192
|
+
\`rig pr list/show\`, \`rig fund\`, \`rig balance\`, and \`rig channel list\`
|
|
4193
|
+
are free; push/issue create/comment/pr create/pr status are paid writes \u2014
|
|
4194
|
+
permanent and non-refundable \u2014
|
|
2448
4195
|
and channel open/close/settle are on-chain wallet transactions; each states
|
|
2449
4196
|
what it will spend and asks for confirmation before doing so (--yes skips,
|
|
2450
4197
|
--json emits machine output).
|
|
@@ -2474,6 +4221,11 @@ async function dispatch(argv2, deps) {
|
|
|
2474
4221
|
return runInit(rest, deps);
|
|
2475
4222
|
case "remote":
|
|
2476
4223
|
return runRemote(rest, deps);
|
|
4224
|
+
// The #278 read path — FREE (relay + Arweave gateway reads, no payment).
|
|
4225
|
+
case "clone":
|
|
4226
|
+
return runClone(rest, deps);
|
|
4227
|
+
case "fetch":
|
|
4228
|
+
return runFetch(rest, deps);
|
|
2477
4229
|
case "push":
|
|
2478
4230
|
return runPush(rest, deps);
|
|
2479
4231
|
case "issue":
|
|
@@ -2482,6 +4234,8 @@ async function dispatch(argv2, deps) {
|
|
|
2482
4234
|
return runComment(rest, deps);
|
|
2483
4235
|
case "pr":
|
|
2484
4236
|
return runPr(rest, deps);
|
|
4237
|
+
case "maintainers":
|
|
4238
|
+
return runMaintainers(rest, deps);
|
|
2485
4239
|
case "channel":
|
|
2486
4240
|
return runChannel(rest, deps);
|
|
2487
4241
|
case "fund":
|
|
@@ -2553,10 +4307,13 @@ function makeCliIo(options) {
|
|
|
2553
4307
|
var RIG_OWNED_VERBS = /* @__PURE__ */ new Set([
|
|
2554
4308
|
"init",
|
|
2555
4309
|
"remote",
|
|
4310
|
+
"clone",
|
|
4311
|
+
"fetch",
|
|
2556
4312
|
"push",
|
|
2557
4313
|
"issue",
|
|
2558
4314
|
"comment",
|
|
2559
4315
|
"pr",
|
|
4316
|
+
"maintainers",
|
|
2560
4317
|
"channel",
|
|
2561
4318
|
"fund",
|
|
2562
4319
|
"balance"
|
|
@@ -2570,6 +4327,29 @@ function isJsonInvocation(argv2) {
|
|
|
2570
4327
|
}
|
|
2571
4328
|
return false;
|
|
2572
4329
|
}
|
|
4330
|
+
var ANNOUNCE_FAILED_RE = /\[Bootstrap\] Announce failed/;
|
|
4331
|
+
var ANNOUNCE_402_RE = /402|payment required/i;
|
|
4332
|
+
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";
|
|
4333
|
+
function calmBootstrapNoise() {
|
|
4334
|
+
const original = process.stderr.write;
|
|
4335
|
+
let reframed = false;
|
|
4336
|
+
const patched = ((chunk, encodingOrCb, cb) => {
|
|
4337
|
+
let text = chunk;
|
|
4338
|
+
if (typeof chunk === "string" && ANNOUNCE_FAILED_RE.test(chunk)) {
|
|
4339
|
+
if (ANNOUNCE_402_RE.test(chunk)) {
|
|
4340
|
+
text = reframed ? "" : ANNOUNCE_402_INFO;
|
|
4341
|
+
reframed = true;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
return typeof encodingOrCb === "function" ? original.call(process.stderr, text, encodingOrCb) : original.call(process.stderr, text, encodingOrCb, cb);
|
|
4345
|
+
});
|
|
4346
|
+
process.stderr.write = patched;
|
|
4347
|
+
return {
|
|
4348
|
+
restore: () => {
|
|
4349
|
+
if (process.stderr.write === patched) process.stderr.write = original;
|
|
4350
|
+
}
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
2573
4353
|
function redirectStdoutToStderr(realWrite) {
|
|
2574
4354
|
const original = process.stdout.write;
|
|
2575
4355
|
const real = realWrite ?? original.bind(process.stdout);
|
|
@@ -2591,6 +4371,7 @@ function redirectStdoutToStderr(realWrite) {
|
|
|
2591
4371
|
|
|
2592
4372
|
// src/cli/rig.ts
|
|
2593
4373
|
function makeIo(jsonMode) {
|
|
4374
|
+
calmBootstrapNoise();
|
|
2594
4375
|
const guard = jsonMode ? redirectStdoutToStderr() : void 0;
|
|
2595
4376
|
return makeCliIo({
|
|
2596
4377
|
jsonMode,
|
|
@@ -2615,6 +4396,16 @@ function makeIo(jsonMode) {
|
|
|
2615
4396
|
}
|
|
2616
4397
|
});
|
|
2617
4398
|
}
|
|
4399
|
+
function exitWhenFlushed(code) {
|
|
4400
|
+
process.exitCode = code;
|
|
4401
|
+
let pending = 2;
|
|
4402
|
+
const done = () => {
|
|
4403
|
+
pending -= 1;
|
|
4404
|
+
if (pending === 0) process.exit(code);
|
|
4405
|
+
};
|
|
4406
|
+
process.stdout.write("", done);
|
|
4407
|
+
process.stderr.write("", done);
|
|
4408
|
+
}
|
|
2618
4409
|
var argv = process.argv.slice(2);
|
|
2619
4410
|
var io = makeIo(isJsonInvocation(argv));
|
|
2620
4411
|
dispatch(argv, {
|
|
@@ -2624,7 +4415,7 @@ dispatch(argv, {
|
|
|
2624
4415
|
}).then(
|
|
2625
4416
|
(code) => {
|
|
2626
4417
|
io.ensureSingleJsonDoc(code);
|
|
2627
|
-
|
|
4418
|
+
exitWhenFlushed(code);
|
|
2628
4419
|
},
|
|
2629
4420
|
(err) => {
|
|
2630
4421
|
process.stderr.write(
|
|
@@ -2632,7 +4423,7 @@ dispatch(argv, {
|
|
|
2632
4423
|
`
|
|
2633
4424
|
);
|
|
2634
4425
|
io.ensureSingleJsonDoc(1);
|
|
2635
|
-
|
|
4426
|
+
exitWhenFlushed(1);
|
|
2636
4427
|
}
|
|
2637
4428
|
);
|
|
2638
4429
|
//# sourceMappingURL=rig.js.map
|