@toon-protocol/git 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/chunk-4WFGAICZ.js +707 -0
- package/dist/chunk-4WFGAICZ.js.map +1 -0
- package/dist/chunk-KXXHAUXL.js +109 -0
- package/dist/chunk-KXXHAUXL.js.map +1 -0
- package/dist/chunk-LJA7PPZI.js +144 -0
- package/dist/chunk-LJA7PPZI.js.map +1 -0
- package/dist/chunk-M7O4SEVW.js +56 -0
- package/dist/chunk-M7O4SEVW.js.map +1 -0
- package/dist/chunk-R3JVS6SX.js +345 -0
- package/dist/chunk-R3JVS6SX.js.map +1 -0
- package/dist/chunk-SBMFWVCP.js +265 -0
- package/dist/chunk-SBMFWVCP.js.map +1 -0
- package/dist/cli/rig.d.ts +1 -0
- package/dist/cli/rig.js +1430 -0
- package/dist/cli/rig.js.map +1 -0
- package/dist/index.d.ts +742 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher-VEIEQHl6.d.ts +254 -0
- package/dist/standalone/index.d.ts +272 -0
- package/dist/standalone/index.js +30 -0
- package/dist/standalone/index.js.map +1 -0
- package/dist/standalone-mode-UFMHGUOM.js +132 -0
- package/dist/standalone-mode-UFMHGUOM.js.map +1 -0
- package/package.json +71 -0
package/dist/cli/rig.js
ADDED
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
GitError,
|
|
4
|
+
GitRepoReader,
|
|
5
|
+
NonFastForwardError,
|
|
6
|
+
OversizeObjectsError,
|
|
7
|
+
executePush,
|
|
8
|
+
planPush,
|
|
9
|
+
serializeEventReceipt,
|
|
10
|
+
serializePushPlan,
|
|
11
|
+
serializePushResult
|
|
12
|
+
} from "../chunk-4WFGAICZ.js";
|
|
13
|
+
import {
|
|
14
|
+
buildComment,
|
|
15
|
+
buildIssue,
|
|
16
|
+
buildPatch,
|
|
17
|
+
buildStatus
|
|
18
|
+
} from "../chunk-KXXHAUXL.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_DAEMON_PORT
|
|
21
|
+
} from "../chunk-LJA7PPZI.js";
|
|
22
|
+
import {
|
|
23
|
+
MAX_OBJECT_SIZE
|
|
24
|
+
} from "../chunk-M7O4SEVW.js";
|
|
25
|
+
|
|
26
|
+
// src/cli/rig.ts
|
|
27
|
+
import { createInterface } from "readline/promises";
|
|
28
|
+
|
|
29
|
+
// src/cli/events.ts
|
|
30
|
+
import { readFile } from "fs/promises";
|
|
31
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
32
|
+
import {
|
|
33
|
+
REPOSITORY_ANNOUNCEMENT_KIND,
|
|
34
|
+
STATUS_APPLIED_KIND,
|
|
35
|
+
STATUS_CLOSED_KIND,
|
|
36
|
+
STATUS_DRAFT_KIND,
|
|
37
|
+
STATUS_OPEN_KIND
|
|
38
|
+
} from "@toon-protocol/core/nip34";
|
|
39
|
+
|
|
40
|
+
// src/cli/daemon.ts
|
|
41
|
+
var DaemonRouteError = class extends Error {
|
|
42
|
+
constructor(status, envelope) {
|
|
43
|
+
super(envelope.detail ?? envelope.error);
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.envelope = envelope;
|
|
46
|
+
this.name = "DaemonRouteError";
|
|
47
|
+
}
|
|
48
|
+
status;
|
|
49
|
+
envelope;
|
|
50
|
+
};
|
|
51
|
+
var DaemonUnreachableError = class extends Error {
|
|
52
|
+
constructor(baseUrl, cause) {
|
|
53
|
+
super(
|
|
54
|
+
`toon-clientd control API is not reachable at ${baseUrl} \u2014 start the daemon (\`toon-clientd\`, shipped by @toon-protocol/client-mcp) and re-run, or use --standalone with TOON_CLIENT_MNEMONIC set` + (cause instanceof Error ? ` (${cause.message})` : "")
|
|
55
|
+
);
|
|
56
|
+
this.baseUrl = baseUrl;
|
|
57
|
+
this.name = "DaemonUnreachableError";
|
|
58
|
+
}
|
|
59
|
+
baseUrl;
|
|
60
|
+
};
|
|
61
|
+
var DaemonGitClient = class {
|
|
62
|
+
constructor(baseUrl, fetchImpl) {
|
|
63
|
+
this.baseUrl = baseUrl;
|
|
64
|
+
this.fetchImpl = fetchImpl;
|
|
65
|
+
}
|
|
66
|
+
baseUrl;
|
|
67
|
+
fetchImpl;
|
|
68
|
+
gitEstimate(req) {
|
|
69
|
+
return this.post("/git/estimate", req);
|
|
70
|
+
}
|
|
71
|
+
gitPush(req) {
|
|
72
|
+
return this.post("/git/push", req);
|
|
73
|
+
}
|
|
74
|
+
gitIssue(req) {
|
|
75
|
+
return this.post("/git/issue", req);
|
|
76
|
+
}
|
|
77
|
+
gitComment(req) {
|
|
78
|
+
return this.post("/git/comment", req);
|
|
79
|
+
}
|
|
80
|
+
gitPatch(req) {
|
|
81
|
+
return this.post("/git/patch", req);
|
|
82
|
+
}
|
|
83
|
+
gitStatus(req) {
|
|
84
|
+
return this.post("/git/status", req);
|
|
85
|
+
}
|
|
86
|
+
async post(path, body) {
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify(body)
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new DaemonUnreachableError(this.baseUrl, err);
|
|
96
|
+
}
|
|
97
|
+
const text = await res.text();
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = text === "" ? {} : JSON.parse(text);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new DaemonRouteError(res.status, {
|
|
103
|
+
error: "invalid_response",
|
|
104
|
+
detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
|
|
109
|
+
throw new DaemonRouteError(res.status, envelope);
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/cli/mode.ts
|
|
116
|
+
import { existsSync, readFileSync } from "fs";
|
|
117
|
+
import { homedir } from "os";
|
|
118
|
+
import { join } from "path";
|
|
119
|
+
function daemonBaseUrl(env) {
|
|
120
|
+
const raw = env["TOON_CLIENT_HTTP_PORT"];
|
|
121
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
122
|
+
const port = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
|
|
123
|
+
return `http://127.0.0.1:${port}`;
|
|
124
|
+
}
|
|
125
|
+
async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
|
|
126
|
+
const baseUrl = daemonBaseUrl(env);
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetchImpl(`${baseUrl}/status`, {
|
|
129
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) return { baseUrl, reachable: false };
|
|
132
|
+
const body = await res.json();
|
|
133
|
+
const probe = { baseUrl, reachable: true };
|
|
134
|
+
const pubkey = body?.identity?.nostrPubkey;
|
|
135
|
+
if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
|
|
136
|
+
if (typeof body?.ready === "boolean") probe.ready = body.ready;
|
|
137
|
+
if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
|
|
138
|
+
probe.relayUrl = body.relay.url;
|
|
139
|
+
}
|
|
140
|
+
if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
|
|
141
|
+
probe.feePerEvent = body.feePerEvent;
|
|
142
|
+
}
|
|
143
|
+
return probe;
|
|
144
|
+
} catch {
|
|
145
|
+
return { baseUrl, reachable: false };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function clientConfigPath(env) {
|
|
149
|
+
const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
150
|
+
return join(dir, "config.json");
|
|
151
|
+
}
|
|
152
|
+
function standaloneAvailability(env) {
|
|
153
|
+
const configPath = clientConfigPath(env);
|
|
154
|
+
if (env["TOON_CLIENT_MNEMONIC"]) {
|
|
155
|
+
return { available: true, source: "env", configPath };
|
|
156
|
+
}
|
|
157
|
+
if (existsSync(configPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const file = JSON.parse(readFileSync(configPath, "utf8"));
|
|
160
|
+
if (typeof file.mnemonic === "string" && file.mnemonic !== "" || typeof file.keystorePath === "string" && file.keystorePath !== "") {
|
|
161
|
+
return { available: true, source: "config", configPath };
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { available: false, configPath };
|
|
167
|
+
}
|
|
168
|
+
var NoPublisherError = class extends Error {
|
|
169
|
+
constructor(probe, standalone) {
|
|
170
|
+
super(
|
|
171
|
+
`no way to pay for this push \u2014 neither publisher mode is available:
|
|
172
|
+
\u2022 daemon: no toon-clientd control API at ${probe.baseUrl}/status \u2014 start it (\`toon-clientd\`, shipped by @toon-protocol/client-mcp) and re-run, or pass --daemon once it is up
|
|
173
|
+
\u2022 standalone: no identity found \u2014 set TOON_CLIENT_MNEMONIC (BIP-39 seed phrase) or configure ${standalone.configPath} (mnemonic / keystorePath), then re-run (or pass --standalone)`
|
|
174
|
+
);
|
|
175
|
+
this.name = "NoPublisherError";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
async function selectMode(options) {
|
|
179
|
+
const { env, fetchImpl } = options;
|
|
180
|
+
if (options.daemon && options.standalone) {
|
|
181
|
+
throw new Error("--daemon and --standalone are mutually exclusive");
|
|
182
|
+
}
|
|
183
|
+
if (options.standalone) {
|
|
184
|
+
return {
|
|
185
|
+
mode: "standalone",
|
|
186
|
+
probe: { baseUrl: daemonBaseUrl(env), reachable: false }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const probe = await probeDaemon(env, fetchImpl);
|
|
190
|
+
if (options.daemon) return { mode: "daemon", probe };
|
|
191
|
+
if (probe.reachable && probe.identity) return { mode: "daemon", probe };
|
|
192
|
+
const standalone = standaloneAvailability(env);
|
|
193
|
+
if (standalone.available) return { mode: "standalone", probe };
|
|
194
|
+
throw new NoPublisherError(probe, standalone);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/cli/errors.ts
|
|
198
|
+
var FUNDING_REMEDIATION = (command) => [
|
|
199
|
+
"Remediation:",
|
|
200
|
+
" \u2022 fund the settlement wallet: run the toon_fund_wallet MCP tool (devnet faucet), or send gas/tokens to the wallet yourself",
|
|
201
|
+
" \u2022 open (or top up) a payment channel: toon_open_channel / toon_channel_deposit",
|
|
202
|
+
` \u2022 then re-run rig ${command}`
|
|
203
|
+
];
|
|
204
|
+
var UnconfiguredRepoAddressError = class extends Error {
|
|
205
|
+
constructor(missing) {
|
|
206
|
+
super(
|
|
207
|
+
`no ${missing} configured \u2014 this command addresses the repo as 30617:<ownerPubkey>:<repoId>. Run \`rig push\` once inside the repo (it persists toon.repoid/toon.owner/toon.relay to git config), or pass ${missing === "repository id" ? "--repo-id <id>" : "--owner <pubkey>"} explicitly (use --owner for repos you don't own).`
|
|
208
|
+
);
|
|
209
|
+
this.missing = missing;
|
|
210
|
+
this.name = "UnconfiguredRepoAddressError";
|
|
211
|
+
}
|
|
212
|
+
missing;
|
|
213
|
+
};
|
|
214
|
+
function nonFastForwardLines(refs) {
|
|
215
|
+
return [
|
|
216
|
+
"Push rejected: non-fast-forward update for:",
|
|
217
|
+
...refs.map(
|
|
218
|
+
(r) => ` ${r.refname} remote ${r.remoteSha.slice(0, 7)} is not an ancestor of local ${r.localSha.slice(0, 7)}`
|
|
219
|
+
),
|
|
220
|
+
"The remote moved since your last push. Re-run with --force to overwrite it",
|
|
221
|
+
"WARNING: --force rewrites the published ref history for every reader of this repo."
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
function oversizeLines(objects) {
|
|
225
|
+
return [
|
|
226
|
+
`Push rejected: ${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE}-byte (95KB) upload limit:`,
|
|
227
|
+
...objects.map(
|
|
228
|
+
(o) => ` ${o.path ?? o.sha} ${o.type}, ${o.size} bytes`
|
|
229
|
+
),
|
|
230
|
+
"Objects over 95KB are a hard error in v1 \u2014 split or remove the file(s) from history to push.",
|
|
231
|
+
"Large-object support is tracked in toon-client#235."
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
function fromDaemonEnvelope(err, command) {
|
|
235
|
+
const { envelope, status } = err;
|
|
236
|
+
const json = { ...envelope, status };
|
|
237
|
+
switch (envelope.error) {
|
|
238
|
+
case "non_fast_forward": {
|
|
239
|
+
const refs = Array.isArray(envelope["refs"]) ? envelope["refs"] : [];
|
|
240
|
+
return { code: "non_fast_forward", lines: nonFastForwardLines(refs), json };
|
|
241
|
+
}
|
|
242
|
+
case "oversize_objects": {
|
|
243
|
+
const objects = Array.isArray(envelope["objects"]) ? envelope["objects"] : [];
|
|
244
|
+
return { code: "oversize_objects", lines: oversizeLines(objects), json };
|
|
245
|
+
}
|
|
246
|
+
case "bootstrapping":
|
|
247
|
+
return {
|
|
248
|
+
code: "bootstrapping",
|
|
249
|
+
lines: [
|
|
250
|
+
`toon-clientd is still bootstrapping: ${envelope.detail ?? "transport/channel coming up"}`,
|
|
251
|
+
"Retry in a few seconds. If it never becomes ready, check the toon-clientd logs."
|
|
252
|
+
],
|
|
253
|
+
json
|
|
254
|
+
};
|
|
255
|
+
case "insufficient_gas":
|
|
256
|
+
return {
|
|
257
|
+
code: "insufficient_gas",
|
|
258
|
+
lines: [
|
|
259
|
+
`Payment failed: ${envelope.detail ?? "the settlement wallet cannot fund the channel"}`,
|
|
260
|
+
...FUNDING_REMEDIATION(command)
|
|
261
|
+
],
|
|
262
|
+
json
|
|
263
|
+
};
|
|
264
|
+
default:
|
|
265
|
+
return {
|
|
266
|
+
code: envelope.error,
|
|
267
|
+
lines: [
|
|
268
|
+
`rig ${command} failed (${envelope.error}, HTTP ${status})` + (envelope.detail ? `: ${envelope.detail}` : ""),
|
|
269
|
+
...envelope.retryable ? ["This error is retryable \u2014 try again shortly."] : []
|
|
270
|
+
],
|
|
271
|
+
json
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function describeError(err, command = "push") {
|
|
276
|
+
if (err instanceof DaemonRouteError) return fromDaemonEnvelope(err, command);
|
|
277
|
+
if (err instanceof UnconfiguredRepoAddressError) {
|
|
278
|
+
return {
|
|
279
|
+
code: "unconfigured_repo_address",
|
|
280
|
+
lines: err.message.split("\n"),
|
|
281
|
+
json: { error: "unconfigured_repo_address", detail: err.message }
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (err instanceof DaemonUnreachableError) {
|
|
285
|
+
return {
|
|
286
|
+
code: "daemon_unreachable",
|
|
287
|
+
lines: [err.message],
|
|
288
|
+
json: { error: "daemon_unreachable", detail: err.message }
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (err instanceof NoPublisherError) {
|
|
292
|
+
return {
|
|
293
|
+
code: "no_publisher",
|
|
294
|
+
lines: err.message.split("\n"),
|
|
295
|
+
json: { error: "no_publisher", detail: err.message }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (err instanceof NonFastForwardError) {
|
|
299
|
+
return {
|
|
300
|
+
code: "non_fast_forward",
|
|
301
|
+
lines: nonFastForwardLines(err.refs),
|
|
302
|
+
json: { error: "non_fast_forward", detail: err.message, refs: err.refs }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (err instanceof OversizeObjectsError) {
|
|
306
|
+
return {
|
|
307
|
+
code: "oversize_objects",
|
|
308
|
+
lines: oversizeLines(err.objects),
|
|
309
|
+
json: {
|
|
310
|
+
error: "oversize_objects",
|
|
311
|
+
detail: err.message,
|
|
312
|
+
objects: err.objects
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (err instanceof GitError) {
|
|
317
|
+
return {
|
|
318
|
+
code: "git_error",
|
|
319
|
+
lines: [`git failed: ${err.message}`],
|
|
320
|
+
json: { error: "git_error", detail: err.message }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const name = err instanceof Error ? err.name : "";
|
|
324
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
325
|
+
if (name === "DaemonIdentityConflictError") {
|
|
326
|
+
return {
|
|
327
|
+
code: "daemon_identity_conflict",
|
|
328
|
+
lines: [message, "Re-run without --standalone (or with --daemon) to push through the running daemon."],
|
|
329
|
+
json: { error: "daemon_identity_conflict", detail: message }
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (name === "MissingMnemonicError" || name === "MissingUplinkError") {
|
|
333
|
+
return {
|
|
334
|
+
code: name === "MissingMnemonicError" ? "missing_mnemonic" : "missing_uplink",
|
|
335
|
+
lines: [message],
|
|
336
|
+
json: { error: "standalone_unavailable", detail: message }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
code: "error",
|
|
341
|
+
lines: [`rig ${command} failed: ${message}`],
|
|
342
|
+
json: { error: "error", detail: message }
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/cli/git-config.ts
|
|
347
|
+
import { execFile } from "child_process";
|
|
348
|
+
import { promisify } from "util";
|
|
349
|
+
var execFileAsync = promisify(execFile);
|
|
350
|
+
async function git(repoPath, args, allowExitCodes = []) {
|
|
351
|
+
try {
|
|
352
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
353
|
+
cwd: repoPath,
|
|
354
|
+
encoding: "utf-8"
|
|
355
|
+
});
|
|
356
|
+
return { stdout, exitCode: 0 };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const e = err;
|
|
359
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
360
|
+
if (exitCode !== void 0 && allowExitCodes.includes(exitCode)) {
|
|
361
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
362
|
+
}
|
|
363
|
+
throw new Error(
|
|
364
|
+
`git ${args.join(" ")} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function resolveRepoRoot(cwd) {
|
|
369
|
+
try {
|
|
370
|
+
const { stdout } = await execFileAsync(
|
|
371
|
+
"git",
|
|
372
|
+
["rev-parse", "--show-toplevel"],
|
|
373
|
+
{ cwd, encoding: "utf-8" }
|
|
374
|
+
);
|
|
375
|
+
const root = stdout.trim();
|
|
376
|
+
if (!root) throw new Error("empty rev-parse output");
|
|
377
|
+
return root;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const e = err;
|
|
380
|
+
throw new Error(
|
|
381
|
+
`not a git repository (run rig inside a repo): ${(e.stderr ?? e.message ?? "").trim()}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function readToonConfig(repoPath) {
|
|
386
|
+
const [repoId, owner, relays] = await Promise.all([
|
|
387
|
+
git(repoPath, ["config", "--get", "toon.repoid"], [1]),
|
|
388
|
+
git(repoPath, ["config", "--get", "toon.owner"], [1]),
|
|
389
|
+
git(repoPath, ["config", "--get-all", "toon.relay"], [1])
|
|
390
|
+
]);
|
|
391
|
+
const config = {
|
|
392
|
+
relays: relays.stdout.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
393
|
+
};
|
|
394
|
+
const id = repoId.stdout.trim();
|
|
395
|
+
if (repoId.exitCode === 0 && id) config.repoId = id;
|
|
396
|
+
const own = owner.stdout.trim();
|
|
397
|
+
if (owner.exitCode === 0 && own) config.owner = own;
|
|
398
|
+
return config;
|
|
399
|
+
}
|
|
400
|
+
async function writeToonConfig(repoPath, config) {
|
|
401
|
+
if (config.repoId !== void 0) {
|
|
402
|
+
await git(repoPath, ["config", "toon.repoid", config.repoId]);
|
|
403
|
+
}
|
|
404
|
+
if (config.owner !== void 0) {
|
|
405
|
+
await git(repoPath, ["config", "toon.owner", config.owner]);
|
|
406
|
+
}
|
|
407
|
+
if (config.relays !== void 0 && config.relays.length > 0) {
|
|
408
|
+
await git(repoPath, ["config", "--unset-all", "toon.relay"], [5]);
|
|
409
|
+
for (const relay of config.relays) {
|
|
410
|
+
await git(repoPath, ["config", "--add", "toon.relay", relay]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/cli/push.ts
|
|
416
|
+
import { basename } from "path";
|
|
417
|
+
import { parseArgs } from "util";
|
|
418
|
+
|
|
419
|
+
// src/cli/render.ts
|
|
420
|
+
function formatNumber(value) {
|
|
421
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
422
|
+
}
|
|
423
|
+
function shortSha(sha) {
|
|
424
|
+
return sha ? sha.slice(0, 7) : "(none)";
|
|
425
|
+
}
|
|
426
|
+
function refLine(update) {
|
|
427
|
+
const arrow = `${shortSha(update.remoteSha)} \u2192 ${shortSha(update.localSha)}`;
|
|
428
|
+
return ` ${update.refname} ${arrow} (${update.kind})`;
|
|
429
|
+
}
|
|
430
|
+
function renderPlan(plan, mode) {
|
|
431
|
+
const lines = [];
|
|
432
|
+
lines.push(
|
|
433
|
+
`Push plan for repo "${plan.repoId}" (${mode} mode)` + (plan.announceNeeded ? " \u2014 first push, will announce (kind:30617)" : "")
|
|
434
|
+
);
|
|
435
|
+
lines.push("Refs:");
|
|
436
|
+
for (const update of plan.refUpdates) lines.push(refLine(update));
|
|
437
|
+
const est = plan.estimate;
|
|
438
|
+
const skipped = Object.keys(plan.knownShaToTxId).length;
|
|
439
|
+
lines.push(
|
|
440
|
+
`Objects: ${est.objectCount} to upload (${formatNumber(est.totalObjectBytes)} bytes)` + (skipped > 0 ? `; ${skipped} already on Arweave (free)` : "")
|
|
441
|
+
);
|
|
442
|
+
lines.push("Fees (base units):");
|
|
443
|
+
lines.push(
|
|
444
|
+
` upload ${est.objectCount} object(s), ${formatNumber(est.totalObjectBytes)} bytes ${formatNumber(est.uploadFee)}`
|
|
445
|
+
);
|
|
446
|
+
lines.push(` events ${est.eventCount} event(s) ${formatNumber(est.eventFees)}`);
|
|
447
|
+
lines.push(` total ${formatNumber(est.totalFee)}`);
|
|
448
|
+
lines.push("Writes are permanent and non-refundable.");
|
|
449
|
+
return lines;
|
|
450
|
+
}
|
|
451
|
+
function feeLabel(fee) {
|
|
452
|
+
return fee !== void 0 ? `${formatNumber(fee)} base units` : "the publisher's configured per-event fee";
|
|
453
|
+
}
|
|
454
|
+
function renderEventPlan(opts) {
|
|
455
|
+
return [
|
|
456
|
+
`Publish ${opts.action} (${opts.mode} mode)`,
|
|
457
|
+
`Repo: 30617:${opts.addr.ownerPubkey}:${opts.addr.repoId}`,
|
|
458
|
+
`Fee: ${feeLabel(opts.fee)}. Writes are permanent and non-refundable.`
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
function renderEventReceipt(action, result) {
|
|
462
|
+
const lines = [
|
|
463
|
+
`Published ${action}: ${result.eventId} paid ${formatNumber(result.feePaid)} base units`
|
|
464
|
+
];
|
|
465
|
+
if (result.channelBalanceAfter !== void 0) {
|
|
466
|
+
lines.push(
|
|
467
|
+
`Channel balance after: ${formatNumber(result.channelBalanceAfter)} base units`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return lines;
|
|
471
|
+
}
|
|
472
|
+
function renderResult(result) {
|
|
473
|
+
const lines = [];
|
|
474
|
+
lines.push(`Pushed "${result.repoId}":`);
|
|
475
|
+
const paid = result.uploads.filter((u) => !u.skipped);
|
|
476
|
+
const skipped = result.uploads.filter((u) => u.skipped);
|
|
477
|
+
for (const upload of result.uploads) {
|
|
478
|
+
lines.push(
|
|
479
|
+
` object ${upload.sha.slice(0, 12)} ${upload.skipped ? "skipped (already stored)" : `paid ${formatNumber(upload.feePaid)}`} ar:${upload.txId}`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
lines.push(
|
|
483
|
+
`Uploads: ${paid.length} paid, ${skipped.length} skipped (content-addressed).`
|
|
484
|
+
);
|
|
485
|
+
if (result.announceReceipt) {
|
|
486
|
+
lines.push(
|
|
487
|
+
`Announcement (kind:30617): ${result.announceReceipt.eventId} paid ${formatNumber(result.announceReceipt.feePaid)}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
lines.push(
|
|
491
|
+
`Refs event (kind:30618): ${result.refsReceipt.eventId} paid ${formatNumber(result.refsReceipt.feePaid)}`
|
|
492
|
+
);
|
|
493
|
+
lines.push(
|
|
494
|
+
`Total paid: ${formatNumber(result.totalFeePaid)} base units (estimate was ${formatNumber(result.estimate.totalFee)})`
|
|
495
|
+
);
|
|
496
|
+
return lines;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/cli/push.ts
|
|
500
|
+
var defaultLoadStandalone = async (env) => {
|
|
501
|
+
const mod = await import("../standalone-mode-UFMHGUOM.js");
|
|
502
|
+
return mod.createStandaloneContext(env);
|
|
503
|
+
};
|
|
504
|
+
var PUSH_USAGE = `Usage: rig push [refspecs...] [options]
|
|
505
|
+
|
|
506
|
+
Push local git refs to TOON: uploads the object delta to Arweave (paid) and
|
|
507
|
+
publishes the NIP-34 refs event (kind:30618; kind:30617 announce on first
|
|
508
|
+
push). Writes are permanent and non-refundable.
|
|
509
|
+
|
|
510
|
+
Refspecs are branch/tag names or full refnames; default is the current branch.
|
|
511
|
+
|
|
512
|
+
Options:
|
|
513
|
+
--force allow non-fast-forward ref updates (overwrites remote history)
|
|
514
|
+
--all push all local branches
|
|
515
|
+
--tags push all local tags
|
|
516
|
+
--yes skip the fee confirmation (required when not a TTY)
|
|
517
|
+
--json machine-readable plan/receipts; without --yes it is a pure
|
|
518
|
+
estimate (nothing executed)
|
|
519
|
+
--relay <url> relay URL (repeatable in daemon mode; standalone mode
|
|
520
|
+
supports exactly one; default: git config toon.relay,
|
|
521
|
+
then the mode's default relay)
|
|
522
|
+
--repo-id <id> repository id / NIP-34 d-tag (default: git config
|
|
523
|
+
toon.repoid, then the repo directory name)
|
|
524
|
+
--daemon force daemon mode (toon-clientd control API)
|
|
525
|
+
--standalone force standalone mode (embedded client from TOON_CLIENT_MNEMONIC)
|
|
526
|
+
-h, --help show this help`;
|
|
527
|
+
async function selectRefspecs(reader, positionals, all, tags) {
|
|
528
|
+
const { head, refs } = await reader.listRefs();
|
|
529
|
+
const byName = new Set(refs.map((r) => r.refname));
|
|
530
|
+
const selected = [];
|
|
531
|
+
const add = (refname) => {
|
|
532
|
+
if (!selected.includes(refname)) selected.push(refname);
|
|
533
|
+
};
|
|
534
|
+
for (const spec of positionals) {
|
|
535
|
+
if (byName.has(spec)) {
|
|
536
|
+
add(spec);
|
|
537
|
+
} else if (byName.has(`refs/heads/${spec}`)) {
|
|
538
|
+
add(`refs/heads/${spec}`);
|
|
539
|
+
} else if (byName.has(`refs/tags/${spec}`)) {
|
|
540
|
+
add(`refs/tags/${spec}`);
|
|
541
|
+
} else {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`refspec ${JSON.stringify(spec)} matches no local branch or tag (ref deletion is out of scope in v1)`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (all) {
|
|
548
|
+
for (const ref of refs) {
|
|
549
|
+
if (ref.refname.startsWith("refs/heads/")) add(ref.refname);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (tags) {
|
|
553
|
+
for (const ref of refs) {
|
|
554
|
+
if (ref.refname.startsWith("refs/tags/")) add(ref.refname);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (selected.length === 0) {
|
|
558
|
+
if (!head) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
"HEAD is detached and no refspec was given \u2014 pass a branch/tag name, --all, or --tags"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
add(head);
|
|
564
|
+
}
|
|
565
|
+
return selected;
|
|
566
|
+
}
|
|
567
|
+
function parsePushArgs(args) {
|
|
568
|
+
const { values, positionals } = parseArgs({
|
|
569
|
+
args,
|
|
570
|
+
options: {
|
|
571
|
+
force: { type: "boolean", default: false },
|
|
572
|
+
all: { type: "boolean", default: false },
|
|
573
|
+
tags: { type: "boolean", default: false },
|
|
574
|
+
yes: { type: "boolean", default: false },
|
|
575
|
+
json: { type: "boolean", default: false },
|
|
576
|
+
daemon: { type: "boolean", default: false },
|
|
577
|
+
standalone: { type: "boolean", default: false },
|
|
578
|
+
relay: { type: "string", multiple: true },
|
|
579
|
+
"repo-id": { type: "string" },
|
|
580
|
+
help: { type: "boolean", short: "h", default: false }
|
|
581
|
+
},
|
|
582
|
+
allowPositionals: true
|
|
583
|
+
});
|
|
584
|
+
const flags = {
|
|
585
|
+
force: values.force ?? false,
|
|
586
|
+
all: values.all ?? false,
|
|
587
|
+
tags: values.tags ?? false,
|
|
588
|
+
yes: values.yes ?? false,
|
|
589
|
+
json: values.json ?? false,
|
|
590
|
+
daemon: values.daemon ?? false,
|
|
591
|
+
standalone: values.standalone ?? false,
|
|
592
|
+
relay: values.relay ?? [],
|
|
593
|
+
help: values.help ?? false,
|
|
594
|
+
positionals
|
|
595
|
+
};
|
|
596
|
+
const repoId = values["repo-id"];
|
|
597
|
+
if (repoId !== void 0) flags.repoId = repoId;
|
|
598
|
+
return flags;
|
|
599
|
+
}
|
|
600
|
+
async function runPush(args, deps) {
|
|
601
|
+
const { io, env, fetchImpl } = deps;
|
|
602
|
+
let flags;
|
|
603
|
+
try {
|
|
604
|
+
flags = parsePushArgs(args);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
607
|
+
io.err(PUSH_USAGE);
|
|
608
|
+
return 2;
|
|
609
|
+
}
|
|
610
|
+
if (flags.help) {
|
|
611
|
+
io.out(PUSH_USAGE);
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
let standaloneCtx;
|
|
615
|
+
let standalonePlan;
|
|
616
|
+
try {
|
|
617
|
+
const repoRoot = await resolveRepoRoot(deps.cwd);
|
|
618
|
+
const toonConfig = await readToonConfig(repoRoot);
|
|
619
|
+
const repoId = flags.repoId ?? toonConfig.repoId ?? basename(repoRoot);
|
|
620
|
+
const reader = new GitRepoReader(repoRoot);
|
|
621
|
+
const refspecs = await selectRefspecs(
|
|
622
|
+
reader,
|
|
623
|
+
flags.positionals,
|
|
624
|
+
flags.all,
|
|
625
|
+
flags.tags
|
|
626
|
+
);
|
|
627
|
+
const explicitRelays = flags.relay.length > 0 ? flags.relay : toonConfig.relays.length > 0 ? toonConfig.relays : void 0;
|
|
628
|
+
const { mode, probe } = await selectMode({
|
|
629
|
+
daemon: flags.daemon,
|
|
630
|
+
standalone: flags.standalone,
|
|
631
|
+
env,
|
|
632
|
+
fetchImpl
|
|
633
|
+
});
|
|
634
|
+
let plan;
|
|
635
|
+
let identity;
|
|
636
|
+
let relaysUsed = explicitRelays;
|
|
637
|
+
const daemonClient = new DaemonGitClient(probe.baseUrl, fetchImpl);
|
|
638
|
+
const daemonRequest = {
|
|
639
|
+
repoPath: repoRoot,
|
|
640
|
+
repoId,
|
|
641
|
+
refspecs,
|
|
642
|
+
force: flags.force,
|
|
643
|
+
...explicitRelays ? { relayUrls: explicitRelays } : {}
|
|
644
|
+
};
|
|
645
|
+
if (mode === "daemon") {
|
|
646
|
+
identity = probe.identity;
|
|
647
|
+
relaysUsed ??= probe.relayUrl ? [probe.relayUrl] : void 0;
|
|
648
|
+
plan = await daemonClient.gitEstimate(daemonRequest);
|
|
649
|
+
} else {
|
|
650
|
+
standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)(env);
|
|
651
|
+
identity = standaloneCtx.ownerPubkey;
|
|
652
|
+
relaysUsed ??= standaloneCtx.defaultRelayUrls;
|
|
653
|
+
if (relaysUsed && relaysUsed.length > 1) {
|
|
654
|
+
io.err(
|
|
655
|
+
`standalone mode publishes to a single relay, but ${relaysUsed.length} are configured (${relaysUsed.join(", ")}) \u2014 re-run with exactly one --relay <url> (or trim git config toon.relay). Nothing was uploaded or paid.`
|
|
656
|
+
);
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
const remoteState = await standaloneCtx.fetchRemote({
|
|
660
|
+
ownerPubkey: standaloneCtx.ownerPubkey,
|
|
661
|
+
repoId,
|
|
662
|
+
relayUrls: relaysUsed
|
|
663
|
+
});
|
|
664
|
+
const feeRates = await standaloneCtx.publisher.getFeeRates();
|
|
665
|
+
const pushPlan = await planPush({
|
|
666
|
+
repoReader: reader,
|
|
667
|
+
remoteState,
|
|
668
|
+
feeRates,
|
|
669
|
+
repoId,
|
|
670
|
+
refs: refspecs,
|
|
671
|
+
force: flags.force
|
|
672
|
+
});
|
|
673
|
+
plan = serializePushPlan(pushPlan);
|
|
674
|
+
standalonePlan = { pushPlan, remoteState, reader, relaysUsed };
|
|
675
|
+
}
|
|
676
|
+
if (toonConfig.owner && identity && toonConfig.owner !== identity) {
|
|
677
|
+
io.err(
|
|
678
|
+
`warning: git config toon.owner (${toonConfig.owner.slice(0, 8)}\u2026) differs from the active ${mode} identity (${identity.slice(0, 8)}\u2026) \u2014 this push publishes under the ACTIVE identity's repo namespace, not the configured owner's`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const upToDate = plan.refUpdates.every((u) => u.kind === "up-to-date");
|
|
682
|
+
if (upToDate) {
|
|
683
|
+
if (flags.json) {
|
|
684
|
+
io.out(jsonOut({ command: "push", mode, repoId, executed: false, upToDate: true, plan }));
|
|
685
|
+
} else {
|
|
686
|
+
io.out("Everything up-to-date \u2014 nothing to push (and nothing paid).");
|
|
687
|
+
}
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
if (!flags.json) {
|
|
691
|
+
for (const line of renderPlan(plan, mode)) io.out(line);
|
|
692
|
+
}
|
|
693
|
+
if (!flags.yes) {
|
|
694
|
+
if (flags.json) {
|
|
695
|
+
io.out(
|
|
696
|
+
jsonOut({
|
|
697
|
+
command: "push",
|
|
698
|
+
mode,
|
|
699
|
+
repoId,
|
|
700
|
+
executed: false,
|
|
701
|
+
upToDate: false,
|
|
702
|
+
plan,
|
|
703
|
+
hint: "estimate only \u2014 re-run with --yes to upload and publish (permanent, non-refundable)"
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
if (!io.isInteractive) {
|
|
709
|
+
io.err(
|
|
710
|
+
"refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
|
|
711
|
+
);
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
714
|
+
const proceed = await io.confirm(
|
|
715
|
+
`Proceed with paid push (total ${plan.estimate.totalFee} base units)? [y/N] `
|
|
716
|
+
);
|
|
717
|
+
if (!proceed) {
|
|
718
|
+
io.err("aborted \u2014 nothing was uploaded or published.");
|
|
719
|
+
return 1;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
let result;
|
|
723
|
+
if (mode === "daemon") {
|
|
724
|
+
result = await daemonClient.gitPush({ ...daemonRequest, confirm: true });
|
|
725
|
+
} else {
|
|
726
|
+
const cached = standalonePlan;
|
|
727
|
+
if (!cached || !standaloneCtx) {
|
|
728
|
+
throw new Error("internal: standalone plan cache missing");
|
|
729
|
+
}
|
|
730
|
+
const pushResult = await executePush({
|
|
731
|
+
plan: cached.pushPlan,
|
|
732
|
+
publisher: standaloneCtx.publisher,
|
|
733
|
+
remoteState: cached.remoteState,
|
|
734
|
+
repoReader: cached.reader,
|
|
735
|
+
relayUrls: cached.relaysUsed
|
|
736
|
+
});
|
|
737
|
+
result = serializePushResult(cached.pushPlan, pushResult);
|
|
738
|
+
}
|
|
739
|
+
if (flags.json) {
|
|
740
|
+
io.out(
|
|
741
|
+
jsonOut({ command: "push", mode, repoId, executed: true, upToDate: false, plan, result })
|
|
742
|
+
);
|
|
743
|
+
} else {
|
|
744
|
+
for (const line of renderResult(result)) io.out(line);
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
await writeToonConfig(repoRoot, {
|
|
748
|
+
repoId,
|
|
749
|
+
...identity ? { owner: identity } : {},
|
|
750
|
+
...relaysUsed ? { relays: relaysUsed } : {}
|
|
751
|
+
});
|
|
752
|
+
} catch (err) {
|
|
753
|
+
io.err(
|
|
754
|
+
`warning: push succeeded but persisting git config failed: ${err instanceof Error ? err.message : String(err)}`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return 0;
|
|
758
|
+
} catch (err) {
|
|
759
|
+
const described = describeError(err);
|
|
760
|
+
if (flags.json) {
|
|
761
|
+
io.out(JSON.stringify({ command: "push", ...described.json }, null, 2));
|
|
762
|
+
} else {
|
|
763
|
+
for (const line of described.lines) io.err(line);
|
|
764
|
+
}
|
|
765
|
+
return 1;
|
|
766
|
+
} finally {
|
|
767
|
+
if (standaloneCtx) {
|
|
768
|
+
try {
|
|
769
|
+
await standaloneCtx.stop();
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function jsonOut(output) {
|
|
776
|
+
return JSON.stringify(output, null, 2);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/cli/events.ts
|
|
780
|
+
var defaultReadStdin = async () => {
|
|
781
|
+
if (process.stdin.isTTY) return "";
|
|
782
|
+
const chunks = [];
|
|
783
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
784
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
785
|
+
};
|
|
786
|
+
var COMMON_FLAGS_USAGE = ` --repo-id <id> repository id / NIP-34 d-tag (default: git config toon.repoid)
|
|
787
|
+
--owner <pubkey> repository owner pubkey, 64-char hex (default: git config
|
|
788
|
+
toon.owner, then the active publisher identity)
|
|
789
|
+
--relay <url> relay to publish to in standalone mode (default: git config
|
|
790
|
+
toon.relay, then the mode's default; standalone supports
|
|
791
|
+
exactly one relay \u2014 daemon mode publishes via its apex)
|
|
792
|
+
--yes skip the fee confirmation (required when not a TTY)
|
|
793
|
+
--json machine-readable receipt; without --yes it is a pure
|
|
794
|
+
estimate (nothing published, exit 0)
|
|
795
|
+
--daemon force daemon mode (toon-clientd control API)
|
|
796
|
+
--standalone force standalone mode (embedded client from TOON_CLIENT_MNEMONIC)
|
|
797
|
+
-h, --help show this help`;
|
|
798
|
+
var ISSUE_USAGE = `Usage: rig issue create --title <title> [options]
|
|
799
|
+
|
|
800
|
+
File an issue (kind:1621) against a TOON repo \u2014 a paid publish; writes are
|
|
801
|
+
permanent and non-refundable. The repo address (30617:<owner>:<repoId>) comes
|
|
802
|
+
from the toon.* git config keys \`rig push\` persists.
|
|
803
|
+
|
|
804
|
+
Body source (exactly one): --body, --body-file, or piped stdin.
|
|
805
|
+
|
|
806
|
+
Options:
|
|
807
|
+
--title <title> issue title (subject tag) [required]
|
|
808
|
+
--body <text> issue body (Markdown)
|
|
809
|
+
--body-file <path> read the issue body from a file
|
|
810
|
+
--label <label> label (t tag); repeatable
|
|
811
|
+
${COMMON_FLAGS_USAGE}`;
|
|
812
|
+
var COMMENT_USAGE = `Usage: rig comment <root-event-id> --body <text> [options]
|
|
813
|
+
|
|
814
|
+
Comment (kind:1622) on an issue or patch \u2014 a paid publish; writes are
|
|
815
|
+
permanent and non-refundable. <root-event-id> is the 64-char hex id of the
|
|
816
|
+
kind:1621 issue / kind:1617 patch being commented on.
|
|
817
|
+
|
|
818
|
+
Options:
|
|
819
|
+
--body <text> comment body (Markdown) [required]
|
|
820
|
+
--parent-author <pubkey> pubkey of the TARGET event's author (p threading
|
|
821
|
+
tag; default: the repo owner)
|
|
822
|
+
--marker <root|reply> e-tag marker (default: root \u2014 commenting directly
|
|
823
|
+
on the issue/patch)
|
|
824
|
+
${COMMON_FLAGS_USAGE}`;
|
|
825
|
+
var PR_USAGE = `Usage: rig pr create --title <title> (--range <range> | --patch-file <path>) [options]
|
|
826
|
+
|
|
827
|
+
Publish a patch (kind:1617) whose content is REAL \`git format-patch\` output \u2014
|
|
828
|
+
a paid publish; writes are permanent and non-refundable. --range runs
|
|
829
|
+
\`git format-patch --stdout <range>\` in the local repository and derives the
|
|
830
|
+
commit/parent-commit tags; --patch-file publishes a pre-generated patch
|
|
831
|
+
verbatim. A multi-commit range publishes ONE kind:1617 event carrying the
|
|
832
|
+
full series text \u2014 cover-letter threading (one event per commit) is out of
|
|
833
|
+
scope in v1.
|
|
834
|
+
|
|
835
|
+
Options:
|
|
836
|
+
--title <title> patch/PR title (subject tag) [required]
|
|
837
|
+
--range <range> revision range for format-patch: <rev>, <rev>..<rev>,
|
|
838
|
+
or <rev>...<rev> (mutually exclusive with --patch-file)
|
|
839
|
+
--patch-file <path> literal patch text to publish
|
|
840
|
+
--branch <name> branch name (t tag)
|
|
841
|
+
${COMMON_FLAGS_USAGE}`;
|
|
842
|
+
var STATUS_USAGE = `Usage: rig status <target-event-id> <open|applied|closed|draft> [options]
|
|
843
|
+
|
|
844
|
+
Set the status of an issue or patch \u2014 a paid publish; writes are permanent
|
|
845
|
+
and non-refundable. Publishes kind:1630 (open), 1631 (applied), 1632
|
|
846
|
+
(closed), or 1633 (draft) against the 64-char hex id of the target event,
|
|
847
|
+
with the repo a-tag attached so readers can scope a status stream to the
|
|
848
|
+
repository.
|
|
849
|
+
|
|
850
|
+
Options:
|
|
851
|
+
${COMMON_FLAGS_USAGE}`;
|
|
852
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
853
|
+
function assertHex64(value, what) {
|
|
854
|
+
if (!HEX64_RE.test(value)) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`${what} must be a 64-char lowercase hex id (got ${JSON.stringify(value)})`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
var COMMON_OPTIONS = {
|
|
861
|
+
yes: { type: "boolean", default: false },
|
|
862
|
+
json: { type: "boolean", default: false },
|
|
863
|
+
daemon: { type: "boolean", default: false },
|
|
864
|
+
standalone: { type: "boolean", default: false },
|
|
865
|
+
relay: { type: "string", multiple: true },
|
|
866
|
+
"repo-id": { type: "string" },
|
|
867
|
+
owner: { type: "string" },
|
|
868
|
+
help: { type: "boolean", short: "h", default: false }
|
|
869
|
+
};
|
|
870
|
+
function pickCommon(values) {
|
|
871
|
+
const flags = {
|
|
872
|
+
yes: values["yes"] === true,
|
|
873
|
+
json: values["json"] === true,
|
|
874
|
+
daemon: values["daemon"] === true,
|
|
875
|
+
standalone: values["standalone"] === true,
|
|
876
|
+
relay: Array.isArray(values["relay"]) ? values["relay"] : [],
|
|
877
|
+
help: values["help"] === true
|
|
878
|
+
};
|
|
879
|
+
const repoId = values["repo-id"];
|
|
880
|
+
if (typeof repoId === "string") flags.repoId = repoId;
|
|
881
|
+
const owner = values["owner"];
|
|
882
|
+
if (typeof owner === "string") {
|
|
883
|
+
assertHex64(owner, "--owner");
|
|
884
|
+
flags.owner = owner;
|
|
885
|
+
}
|
|
886
|
+
return flags;
|
|
887
|
+
}
|
|
888
|
+
async function runEvent(opts) {
|
|
889
|
+
const { command, flags, deps, actionLabel } = opts;
|
|
890
|
+
const { io, env, fetchImpl } = deps;
|
|
891
|
+
let standaloneCtx;
|
|
892
|
+
try {
|
|
893
|
+
let toonConfig = { relays: [] };
|
|
894
|
+
try {
|
|
895
|
+
toonConfig = await readToonConfig(await resolveRepoRoot(deps.cwd));
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
const repoId = flags.repoId ?? toonConfig.repoId;
|
|
899
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
900
|
+
const explicitRelays = flags.relay.length > 0 ? flags.relay : toonConfig.relays.length > 0 ? toonConfig.relays : void 0;
|
|
901
|
+
const { mode, probe } = await selectMode({
|
|
902
|
+
daemon: flags.daemon,
|
|
903
|
+
standalone: flags.standalone,
|
|
904
|
+
env,
|
|
905
|
+
fetchImpl
|
|
906
|
+
});
|
|
907
|
+
const daemonClient = new DaemonGitClient(probe.baseUrl, fetchImpl);
|
|
908
|
+
let identity;
|
|
909
|
+
let fee;
|
|
910
|
+
let relaysUsed = explicitRelays;
|
|
911
|
+
if (mode === "daemon") {
|
|
912
|
+
identity = probe.identity;
|
|
913
|
+
fee = probe.feePerEvent;
|
|
914
|
+
} else {
|
|
915
|
+
standaloneCtx = await (deps.loadStandalone ?? defaultLoadStandalone)(env);
|
|
916
|
+
identity = standaloneCtx.ownerPubkey;
|
|
917
|
+
relaysUsed ??= standaloneCtx.defaultRelayUrls;
|
|
918
|
+
if (relaysUsed.length > 1) {
|
|
919
|
+
io.err(
|
|
920
|
+
`standalone mode publishes to a single relay, but ${relaysUsed.length} are configured (${relaysUsed.join(", ")}) \u2014 re-run with exactly one --relay <url> (or trim git config toon.relay). Nothing was published or paid.`
|
|
921
|
+
);
|
|
922
|
+
return 1;
|
|
923
|
+
}
|
|
924
|
+
fee = (await standaloneCtx.publisher.getFeeRates()).eventFee.toString();
|
|
925
|
+
}
|
|
926
|
+
const owner = flags.owner ?? toonConfig.owner ?? identity;
|
|
927
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
928
|
+
const addr = { ownerPubkey: owner, repoId };
|
|
929
|
+
const event = await opts.buildEvent(addr);
|
|
930
|
+
const action = `kind:${event.kind} ${actionLabel}`;
|
|
931
|
+
if (!flags.json) {
|
|
932
|
+
for (const line of renderEventPlan({
|
|
933
|
+
action,
|
|
934
|
+
addr,
|
|
935
|
+
mode,
|
|
936
|
+
...fee !== void 0 ? { fee } : {}
|
|
937
|
+
})) {
|
|
938
|
+
io.out(line);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (!flags.yes) {
|
|
942
|
+
if (flags.json) {
|
|
943
|
+
io.out(
|
|
944
|
+
jsonOut2({
|
|
945
|
+
command,
|
|
946
|
+
mode,
|
|
947
|
+
repoAddr: addr,
|
|
948
|
+
kind: event.kind,
|
|
949
|
+
executed: false,
|
|
950
|
+
feeEstimate: fee ?? null,
|
|
951
|
+
hint: "estimate only \u2014 re-run with --yes to publish (permanent, non-refundable)"
|
|
952
|
+
})
|
|
953
|
+
);
|
|
954
|
+
return 0;
|
|
955
|
+
}
|
|
956
|
+
if (!io.isInteractive) {
|
|
957
|
+
io.err(
|
|
958
|
+
"refusing to spend channel funds without confirmation in a non-interactive session \u2014 re-run with --yes (or use --json for an estimate)"
|
|
959
|
+
);
|
|
960
|
+
return 1;
|
|
961
|
+
}
|
|
962
|
+
const proceed = await io.confirm(
|
|
963
|
+
`Proceed with paid publish (${feeLabel(fee)})? [y/N] `
|
|
964
|
+
);
|
|
965
|
+
if (!proceed) {
|
|
966
|
+
io.err("aborted \u2014 nothing was published.");
|
|
967
|
+
return 1;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
let result;
|
|
971
|
+
if (mode === "daemon") {
|
|
972
|
+
result = await opts.sendDaemon(daemonClient, addr);
|
|
973
|
+
} else {
|
|
974
|
+
if (!standaloneCtx) throw new Error("internal: standalone context missing");
|
|
975
|
+
const receipt = await standaloneCtx.publisher.publishEvent(
|
|
976
|
+
event,
|
|
977
|
+
relaysUsed ?? []
|
|
978
|
+
);
|
|
979
|
+
result = serializeEventReceipt(event.kind, receipt);
|
|
980
|
+
}
|
|
981
|
+
if (flags.json) {
|
|
982
|
+
io.out(
|
|
983
|
+
jsonOut2({
|
|
984
|
+
command,
|
|
985
|
+
mode,
|
|
986
|
+
repoAddr: addr,
|
|
987
|
+
kind: result.kind,
|
|
988
|
+
executed: true,
|
|
989
|
+
feeEstimate: fee ?? null,
|
|
990
|
+
result
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
} else {
|
|
994
|
+
for (const line of renderEventReceipt(action, result)) io.out(line);
|
|
995
|
+
}
|
|
996
|
+
return 0;
|
|
997
|
+
} catch (err) {
|
|
998
|
+
const described = describeError(err, command);
|
|
999
|
+
if (flags.json) {
|
|
1000
|
+
io.out(JSON.stringify({ command, ...described.json }, null, 2));
|
|
1001
|
+
} else {
|
|
1002
|
+
for (const line of described.lines) io.err(line);
|
|
1003
|
+
}
|
|
1004
|
+
return 1;
|
|
1005
|
+
} finally {
|
|
1006
|
+
if (standaloneCtx) {
|
|
1007
|
+
try {
|
|
1008
|
+
await standaloneCtx.stop();
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function jsonOut2(output) {
|
|
1015
|
+
return JSON.stringify(output, null, 2);
|
|
1016
|
+
}
|
|
1017
|
+
async function runIssue(args, deps) {
|
|
1018
|
+
const { io } = deps;
|
|
1019
|
+
const [sub, ...rest] = args;
|
|
1020
|
+
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1021
|
+
io.out(ISSUE_USAGE);
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
if (sub !== "create") {
|
|
1025
|
+
io.err(
|
|
1026
|
+
sub === void 0 ? "missing subcommand: rig issue create" : `unknown rig issue subcommand: ${sub}`
|
|
1027
|
+
);
|
|
1028
|
+
io.err(ISSUE_USAGE);
|
|
1029
|
+
return 2;
|
|
1030
|
+
}
|
|
1031
|
+
let flags;
|
|
1032
|
+
let title;
|
|
1033
|
+
let bodyFlag;
|
|
1034
|
+
let bodyFile;
|
|
1035
|
+
let labels;
|
|
1036
|
+
try {
|
|
1037
|
+
const { values } = parseArgs2({
|
|
1038
|
+
args: rest,
|
|
1039
|
+
options: {
|
|
1040
|
+
...COMMON_OPTIONS,
|
|
1041
|
+
title: { type: "string" },
|
|
1042
|
+
body: { type: "string" },
|
|
1043
|
+
"body-file": { type: "string" },
|
|
1044
|
+
label: { type: "string", multiple: true }
|
|
1045
|
+
},
|
|
1046
|
+
allowPositionals: false
|
|
1047
|
+
});
|
|
1048
|
+
flags = pickCommon(values);
|
|
1049
|
+
if (!flags.help && (values.title === void 0 || values.title === "")) {
|
|
1050
|
+
throw new Error("--title is required");
|
|
1051
|
+
}
|
|
1052
|
+
if (values.body !== void 0 && values["body-file"] !== void 0) {
|
|
1053
|
+
throw new Error("--body and --body-file are mutually exclusive");
|
|
1054
|
+
}
|
|
1055
|
+
title = values.title ?? "";
|
|
1056
|
+
bodyFlag = values.body;
|
|
1057
|
+
bodyFile = values["body-file"];
|
|
1058
|
+
labels = values.label ?? [];
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
1061
|
+
io.err(ISSUE_USAGE);
|
|
1062
|
+
return 2;
|
|
1063
|
+
}
|
|
1064
|
+
if (flags.help) {
|
|
1065
|
+
io.out(ISSUE_USAGE);
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1068
|
+
let body;
|
|
1069
|
+
if (bodyFlag !== void 0) {
|
|
1070
|
+
body = bodyFlag;
|
|
1071
|
+
} else if (bodyFile !== void 0) {
|
|
1072
|
+
try {
|
|
1073
|
+
body = await readFile(bodyFile, "utf-8");
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
io.err(
|
|
1076
|
+
`cannot read --body-file ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`
|
|
1077
|
+
);
|
|
1078
|
+
return 2;
|
|
1079
|
+
}
|
|
1080
|
+
} else if (!io.isInteractive) {
|
|
1081
|
+
body = await (deps.readStdin ?? defaultReadStdin)();
|
|
1082
|
+
} else {
|
|
1083
|
+
io.err("an issue body is required: pass --body/--body-file or pipe stdin");
|
|
1084
|
+
io.err(ISSUE_USAGE);
|
|
1085
|
+
return 2;
|
|
1086
|
+
}
|
|
1087
|
+
if (body.trim() === "") {
|
|
1088
|
+
io.err("the issue body is empty \u2014 nothing to publish");
|
|
1089
|
+
return 2;
|
|
1090
|
+
}
|
|
1091
|
+
return runEvent({
|
|
1092
|
+
command: "issue",
|
|
1093
|
+
flags,
|
|
1094
|
+
deps,
|
|
1095
|
+
actionLabel: `issue ${JSON.stringify(title)}`,
|
|
1096
|
+
buildEvent: async (addr) => buildIssue(addr.ownerPubkey, addr.repoId, title, body, labels),
|
|
1097
|
+
sendDaemon: (client, addr) => client.gitIssue({
|
|
1098
|
+
repoAddr: addr,
|
|
1099
|
+
title,
|
|
1100
|
+
body,
|
|
1101
|
+
...labels.length > 0 ? { labels } : {}
|
|
1102
|
+
})
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
async function runComment(args, deps) {
|
|
1106
|
+
const { io } = deps;
|
|
1107
|
+
let flags;
|
|
1108
|
+
let rootEventId;
|
|
1109
|
+
let body;
|
|
1110
|
+
let parentAuthor;
|
|
1111
|
+
let marker;
|
|
1112
|
+
try {
|
|
1113
|
+
const { values, positionals } = parseArgs2({
|
|
1114
|
+
args,
|
|
1115
|
+
options: {
|
|
1116
|
+
...COMMON_OPTIONS,
|
|
1117
|
+
body: { type: "string" },
|
|
1118
|
+
"parent-author": { type: "string" },
|
|
1119
|
+
marker: { type: "string" }
|
|
1120
|
+
},
|
|
1121
|
+
allowPositionals: true
|
|
1122
|
+
});
|
|
1123
|
+
flags = pickCommon(values);
|
|
1124
|
+
if (flags.help) {
|
|
1125
|
+
io.out(COMMENT_USAGE);
|
|
1126
|
+
return 0;
|
|
1127
|
+
}
|
|
1128
|
+
if (positionals.length !== 1) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
positionals.length === 0 ? "<root-event-id> is required" : `expected exactly one <root-event-id>, got ${positionals.length} positionals`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
rootEventId = positionals[0];
|
|
1134
|
+
assertHex64(rootEventId, "<root-event-id>");
|
|
1135
|
+
if (values.body === void 0 || values.body === "") {
|
|
1136
|
+
throw new Error("--body is required");
|
|
1137
|
+
}
|
|
1138
|
+
body = values.body;
|
|
1139
|
+
parentAuthor = values["parent-author"];
|
|
1140
|
+
if (parentAuthor !== void 0) assertHex64(parentAuthor, "--parent-author");
|
|
1141
|
+
const rawMarker = values.marker ?? "root";
|
|
1142
|
+
if (rawMarker !== "root" && rawMarker !== "reply") {
|
|
1143
|
+
throw new Error(`--marker must be root or reply (got ${JSON.stringify(rawMarker)})`);
|
|
1144
|
+
}
|
|
1145
|
+
marker = rawMarker;
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
1148
|
+
io.err(COMMENT_USAGE);
|
|
1149
|
+
return 2;
|
|
1150
|
+
}
|
|
1151
|
+
return runEvent({
|
|
1152
|
+
command: "comment",
|
|
1153
|
+
flags,
|
|
1154
|
+
deps,
|
|
1155
|
+
actionLabel: `comment on ${rootEventId.slice(0, 8)}\u2026`,
|
|
1156
|
+
buildEvent: async (addr) => buildComment(
|
|
1157
|
+
addr.ownerPubkey,
|
|
1158
|
+
addr.repoId,
|
|
1159
|
+
rootEventId,
|
|
1160
|
+
parentAuthor ?? addr.ownerPubkey,
|
|
1161
|
+
body,
|
|
1162
|
+
marker
|
|
1163
|
+
),
|
|
1164
|
+
sendDaemon: (client, addr) => client.gitComment({
|
|
1165
|
+
repoAddr: addr,
|
|
1166
|
+
rootEventId,
|
|
1167
|
+
body,
|
|
1168
|
+
marker,
|
|
1169
|
+
...parentAuthor !== void 0 ? { parentAuthorPubkey: parentAuthor } : {}
|
|
1170
|
+
})
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
var PATCH_FROM_RE = /^From ([0-9a-f]{40}) /gm;
|
|
1174
|
+
function extractPatchShas(patchText) {
|
|
1175
|
+
return [...patchText.matchAll(PATCH_FROM_RE)].map((m) => m[1]);
|
|
1176
|
+
}
|
|
1177
|
+
async function runPr(args, deps) {
|
|
1178
|
+
const { io } = deps;
|
|
1179
|
+
const [sub, ...rest] = args;
|
|
1180
|
+
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1181
|
+
io.out(PR_USAGE);
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
if (sub !== "create") {
|
|
1185
|
+
io.err(
|
|
1186
|
+
sub === void 0 ? "missing subcommand: rig pr create" : `unknown rig pr subcommand: ${sub}`
|
|
1187
|
+
);
|
|
1188
|
+
io.err(PR_USAGE);
|
|
1189
|
+
return 2;
|
|
1190
|
+
}
|
|
1191
|
+
let flags;
|
|
1192
|
+
let title;
|
|
1193
|
+
let range;
|
|
1194
|
+
let patchFile;
|
|
1195
|
+
let branch;
|
|
1196
|
+
try {
|
|
1197
|
+
const { values } = parseArgs2({
|
|
1198
|
+
args: rest,
|
|
1199
|
+
options: {
|
|
1200
|
+
...COMMON_OPTIONS,
|
|
1201
|
+
title: { type: "string" },
|
|
1202
|
+
range: { type: "string" },
|
|
1203
|
+
"patch-file": { type: "string" },
|
|
1204
|
+
branch: { type: "string" }
|
|
1205
|
+
},
|
|
1206
|
+
allowPositionals: false
|
|
1207
|
+
});
|
|
1208
|
+
flags = pickCommon(values);
|
|
1209
|
+
if (flags.help) {
|
|
1210
|
+
io.out(PR_USAGE);
|
|
1211
|
+
return 0;
|
|
1212
|
+
}
|
|
1213
|
+
if (values.title === void 0 || values.title === "") {
|
|
1214
|
+
throw new Error("--title is required");
|
|
1215
|
+
}
|
|
1216
|
+
if (values.range === void 0 === (values["patch-file"] === void 0)) {
|
|
1217
|
+
throw new Error("exactly one of --range or --patch-file is required");
|
|
1218
|
+
}
|
|
1219
|
+
title = values.title;
|
|
1220
|
+
range = values.range;
|
|
1221
|
+
patchFile = values["patch-file"];
|
|
1222
|
+
branch = values.branch;
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
1225
|
+
io.err(PR_USAGE);
|
|
1226
|
+
return 2;
|
|
1227
|
+
}
|
|
1228
|
+
let prepared;
|
|
1229
|
+
const prepare = async () => {
|
|
1230
|
+
if (prepared) return prepared;
|
|
1231
|
+
if (range !== void 0) {
|
|
1232
|
+
const reader = new GitRepoReader(await resolveRepoRoot(deps.cwd));
|
|
1233
|
+
const patchText = await reader.formatPatch(range);
|
|
1234
|
+
if (patchText === "") {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`range ${JSON.stringify(range)} selects no commits \u2014 nothing to publish`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const shas = extractPatchShas(patchText);
|
|
1240
|
+
const parents = await reader.commitParents(shas);
|
|
1241
|
+
const commits = shas.flatMap((sha) => {
|
|
1242
|
+
const parentSha = parents.get(sha)?.[0];
|
|
1243
|
+
return parentSha ? [{ sha, parentSha }] : [];
|
|
1244
|
+
});
|
|
1245
|
+
prepared = { patchText, commits };
|
|
1246
|
+
} else {
|
|
1247
|
+
const patchText = await readFile(patchFile, "utf-8");
|
|
1248
|
+
if (patchText.trim() === "") {
|
|
1249
|
+
throw new Error(`--patch-file ${patchFile} is empty \u2014 nothing to publish`);
|
|
1250
|
+
}
|
|
1251
|
+
prepared = { patchText, commits: [] };
|
|
1252
|
+
}
|
|
1253
|
+
return prepared;
|
|
1254
|
+
};
|
|
1255
|
+
return runEvent({
|
|
1256
|
+
command: "pr",
|
|
1257
|
+
flags,
|
|
1258
|
+
deps,
|
|
1259
|
+
actionLabel: `patch ${JSON.stringify(title)}`,
|
|
1260
|
+
buildEvent: async (addr) => {
|
|
1261
|
+
const { patchText, commits } = await prepare();
|
|
1262
|
+
return buildPatch(
|
|
1263
|
+
addr.ownerPubkey,
|
|
1264
|
+
addr.repoId,
|
|
1265
|
+
title,
|
|
1266
|
+
commits,
|
|
1267
|
+
branch,
|
|
1268
|
+
patchText
|
|
1269
|
+
);
|
|
1270
|
+
},
|
|
1271
|
+
sendDaemon: async (client, addr) => {
|
|
1272
|
+
const { patchText, commits } = await prepare();
|
|
1273
|
+
return client.gitPatch({
|
|
1274
|
+
repoAddr: addr,
|
|
1275
|
+
title,
|
|
1276
|
+
patchText,
|
|
1277
|
+
...commits.length > 0 ? { commits } : {},
|
|
1278
|
+
...branch !== void 0 ? { branch } : {}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
var STATUS_KIND_BY_VALUE = {
|
|
1284
|
+
open: STATUS_OPEN_KIND,
|
|
1285
|
+
applied: STATUS_APPLIED_KIND,
|
|
1286
|
+
closed: STATUS_CLOSED_KIND,
|
|
1287
|
+
draft: STATUS_DRAFT_KIND
|
|
1288
|
+
};
|
|
1289
|
+
function isStatusValue(value) {
|
|
1290
|
+
return Object.hasOwn(STATUS_KIND_BY_VALUE, value);
|
|
1291
|
+
}
|
|
1292
|
+
async function runStatus(args, deps) {
|
|
1293
|
+
const { io } = deps;
|
|
1294
|
+
let flags;
|
|
1295
|
+
let targetEventId;
|
|
1296
|
+
let status;
|
|
1297
|
+
try {
|
|
1298
|
+
const { values, positionals } = parseArgs2({
|
|
1299
|
+
args,
|
|
1300
|
+
options: COMMON_OPTIONS,
|
|
1301
|
+
allowPositionals: true
|
|
1302
|
+
});
|
|
1303
|
+
flags = pickCommon(values);
|
|
1304
|
+
if (flags.help) {
|
|
1305
|
+
io.out(STATUS_USAGE);
|
|
1306
|
+
return 0;
|
|
1307
|
+
}
|
|
1308
|
+
if (positionals.length !== 2) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
"expected exactly two arguments: <target-event-id> <open|applied|closed|draft>"
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
targetEventId = positionals[0];
|
|
1314
|
+
assertHex64(targetEventId, "<target-event-id>");
|
|
1315
|
+
const rawStatus = positionals[1];
|
|
1316
|
+
if (!isStatusValue(rawStatus)) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
`status must be one of open | applied | closed | draft (got ${JSON.stringify(rawStatus)})`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
status = rawStatus;
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
1324
|
+
io.err(STATUS_USAGE);
|
|
1325
|
+
return 2;
|
|
1326
|
+
}
|
|
1327
|
+
return runEvent({
|
|
1328
|
+
command: "status",
|
|
1329
|
+
flags,
|
|
1330
|
+
deps,
|
|
1331
|
+
actionLabel: `status ${status} on ${targetEventId.slice(0, 8)}\u2026`,
|
|
1332
|
+
buildEvent: async (addr) => {
|
|
1333
|
+
const event = buildStatus(targetEventId, STATUS_KIND_BY_VALUE[status]);
|
|
1334
|
+
event.tags.push([
|
|
1335
|
+
"a",
|
|
1336
|
+
`${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
|
|
1337
|
+
]);
|
|
1338
|
+
return event;
|
|
1339
|
+
},
|
|
1340
|
+
sendDaemon: (client, addr) => client.gitStatus({ repoAddr: addr, targetEventId, status })
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/cli/rig.ts
|
|
1345
|
+
var USAGE = `rig \u2014 push git repos to TOON (pay-to-write Nostr + Arweave)
|
|
1346
|
+
|
|
1347
|
+
Usage: rig <command> [options]
|
|
1348
|
+
|
|
1349
|
+
Commands:
|
|
1350
|
+
push [refspecs...] plan, price, confirm, and execute a paid push
|
|
1351
|
+
issue create file an issue (kind:1621) against a repo
|
|
1352
|
+
comment <root-event-id> comment (kind:1622) on an issue or patch
|
|
1353
|
+
pr create publish a patch (kind:1617) with real
|
|
1354
|
+
\`git format-patch\` content
|
|
1355
|
+
status <event-id> <state> set an issue/patch status (kind:1630-1633):
|
|
1356
|
+
open | applied | closed | draft
|
|
1357
|
+
|
|
1358
|
+
Run \`rig <command> --help\` for the command's flags. All writes are paid,
|
|
1359
|
+
permanent, and non-refundable; each command quotes its fee and asks for
|
|
1360
|
+
confirmation before spending (--yes skips, --json emits machine output).`;
|
|
1361
|
+
function makeIo() {
|
|
1362
|
+
return {
|
|
1363
|
+
out: (line) => process.stdout.write(`${line}
|
|
1364
|
+
`),
|
|
1365
|
+
err: (line) => process.stderr.write(`${line}
|
|
1366
|
+
`),
|
|
1367
|
+
isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1368
|
+
confirm: async (question) => {
|
|
1369
|
+
const rl = createInterface({
|
|
1370
|
+
input: process.stdin,
|
|
1371
|
+
output: process.stderr
|
|
1372
|
+
});
|
|
1373
|
+
try {
|
|
1374
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
1375
|
+
return answer === "y" || answer === "yes";
|
|
1376
|
+
} finally {
|
|
1377
|
+
rl.close();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
async function main() {
|
|
1383
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
1384
|
+
const io = makeIo();
|
|
1385
|
+
const deps = {
|
|
1386
|
+
io,
|
|
1387
|
+
env: process.env,
|
|
1388
|
+
cwd: process.cwd(),
|
|
1389
|
+
fetchImpl: fetch
|
|
1390
|
+
};
|
|
1391
|
+
switch (command) {
|
|
1392
|
+
case "push":
|
|
1393
|
+
return runPush(rest, deps);
|
|
1394
|
+
case "issue":
|
|
1395
|
+
return runIssue(rest, deps);
|
|
1396
|
+
case "comment":
|
|
1397
|
+
return runComment(rest, deps);
|
|
1398
|
+
case "pr":
|
|
1399
|
+
return runPr(rest, deps);
|
|
1400
|
+
case "status":
|
|
1401
|
+
return runStatus(rest, deps);
|
|
1402
|
+
case "help":
|
|
1403
|
+
case "--help":
|
|
1404
|
+
case "-h":
|
|
1405
|
+
io.out(USAGE);
|
|
1406
|
+
io.out("");
|
|
1407
|
+
io.out(PUSH_USAGE);
|
|
1408
|
+
return 0;
|
|
1409
|
+
case void 0:
|
|
1410
|
+
io.err(USAGE);
|
|
1411
|
+
return 2;
|
|
1412
|
+
default:
|
|
1413
|
+
io.err(`unknown command: ${command}`);
|
|
1414
|
+
io.err(USAGE);
|
|
1415
|
+
return 2;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
main().then(
|
|
1419
|
+
(code) => {
|
|
1420
|
+
process.exitCode = code;
|
|
1421
|
+
},
|
|
1422
|
+
(err) => {
|
|
1423
|
+
process.stderr.write(
|
|
1424
|
+
`rig: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
1425
|
+
`
|
|
1426
|
+
);
|
|
1427
|
+
process.exitCode = 1;
|
|
1428
|
+
}
|
|
1429
|
+
);
|
|
1430
|
+
//# sourceMappingURL=rig.js.map
|